Introdução
Bem-vindo a Referências e Empréstimos (References and Borrowing). Este laboratório faz parte do Livro do Rust. Você pode praticar suas habilidades em Rust no LabEx.
Neste laboratório, aprenderemos como usar referências em Rust para emprestar valores em vez de assumir a propriedade (ownership), permitindo-nos passar e manipular dados sem precisar retornar a propriedade à função chamadora.
Referências e Empréstimos (References and Borrowing)
O problema com o código da tupla no Listing 4-5 é que precisamos retornar a String para a função chamadora para que possamos continuar usando a String após a chamada para calculate_length, porque a String foi movida para calculate_length. Em vez disso, podemos fornecer uma referência ao valor String. Uma referência é como um ponteiro, pois é um endereço que podemos seguir para acessar os dados armazenados nesse endereço; esses dados são de propriedade de alguma outra variável. Ao contrário de um ponteiro, uma referência tem a garantia de apontar para um valor válido de um tipo específico durante a vida útil dessa referência.
Aqui está como você definiria e usaria uma função calculate_length que tem uma referência a um objeto como um parâmetro em vez de assumir a propriedade do valor:
Nome do arquivo: src/main.rs
fn main() {
let s1 = String::from("hello");
let len = calculate_length(&s1);
println!("The length of '{s1}' is {len}.");
}
fn calculate_length(s: &String) -> usize {
s.len()
}
Primeiro, observe que todo o código da tupla na declaração da variável e no valor de retorno da função desapareceu. Segundo, observe que passamos &s1 para calculate_length e, em sua definição, pegamos &String em vez de String. Esses "e comerciais" representam referências e permitem que você se refira a algum valor sem assumir a propriedade dele. A Figura 4-5 ilustra esse conceito.
Figura 4-5: Um diagrama de &String s apontando para String s1
Nota: O oposto de referenciar usando
&é dereferenciar (dereferencing), que é realizado com o operador de desreferência,*. Veremos alguns usos do operador de desreferência no Capítulo 8 e discutiremos os detalhes da desreferenciação no Capítulo 15.
Vamos analisar mais de perto a chamada da função aqui:
let s1 = String::from("hello");
let len = calculate_length(&s1);
A sintaxe &s1 nos permite criar uma referência que se refere ao valor de s1, mas não o possui. Como não o possui, o valor para o qual aponta não será descartado quando a referência parar de ser usada.
Da mesma forma, a assinatura da função usa & para indicar que o tipo do parâmetro s é uma referência. Vamos adicionar algumas anotações explicativas:
fn calculate_length(s: &String) -> usize { // s é uma referência a uma String
s.len()
} // Aqui, s sai do escopo. Mas como não tem a propriedade do que
// se refere, a String não é descartada
O escopo em que a variável s é válida é o mesmo que o escopo de qualquer parâmetro de função, mas o valor apontado pela referência não é descartado quando s para de ser usado, porque s não tem propriedade. Quando as funções têm referências como parâmetros em vez dos valores reais, não precisaremos retornar os valores para devolver a propriedade, porque nunca tivemos a propriedade.
Chamamos a ação de criar uma referência de empréstimo (borrowing). Como na vida real, se uma pessoa possui algo, você pode emprestá-lo dela. Quando terminar, você tem que devolvê-lo. Você não é o dono.
Então, o que acontece se tentarmos modificar algo que estamos emprestando? Experimente o código no Listing 4-6. Alerta de spoiler: não funciona!
Nome do arquivo: src/main.rs
fn main() {
let s = String::from("hello");
change(&s);
}
fn change(some_string: &String) {
some_string.push_str(", world");
}
Listing 4-6: Tentando modificar um valor emprestado
Aqui está o erro:
error[E0596]: cannot borrow `*some_string` as mutable, as it is behind a `&`
reference
--> src/main.rs:8:5
|
7 | fn change(some_string: &String) {
| ------- help: consider changing this to be a mutable
reference: `&mut String`
8 | some_string.push_str(", world");
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ `some_string` is a `&` reference, so
the data it refers to cannot be borrowed as mutable
Assim como as variáveis são imutáveis por padrão, as referências também são. Não podemos modificar algo ao qual temos uma referência.
Referências Mutáveis (Mutable References)
Podemos corrigir o código do Listing 4-6 para nos permitir modificar um valor emprestado com apenas alguns pequenos ajustes que usam, em vez disso, uma referência mutável:
Nome do arquivo: src/main.rs
fn main() {
let mut s = String::from("hello");
change(&mut s);
}
fn change(some_string: &mut String) {
some_string.push_str(", world");
}
Primeiro, mudamos s para ser mut. Em seguida, criamos uma referência mutável com &mut s onde chamamos a função change e atualizamos a assinatura da função para aceitar uma referência mutável com some_string: &mut String. Isso deixa muito claro que a função change irá mutar o valor que ela empresta.
Referências mutáveis têm uma grande restrição: se você tem uma referência mutável a um valor, você não pode ter outras referências a esse valor. Este código que tenta criar duas referências mutáveis a s falhará:
Nome do arquivo: src/main.rs
let mut s = String::from("hello");
let r1 = &mut s;
let r2 = &mut s;
println!("{r1}, {r2}");
Aqui está o erro:
error[E0499]: cannot borrow `s` as mutable more than once at a time
--> src/main.rs:5:14
|
4 | let r1 = &mut s;
| ------ first mutable borrow occurs here
5 | let r2 = &mut s;
| ^^^^^^ second mutable borrow occurs here
6 |
7 | println!("{r1}, {r2}");
| -- first borrow later used here
Este erro diz que este código é inválido porque não podemos emprestar s como mutável mais de uma vez por vez. O primeiro empréstimo mutável está em r1 e deve durar até que seja usado no println!, mas entre a criação dessa referência mutável e seu uso, tentamos criar outra referência mutável em r2 que empresta os mesmos dados que r1.
A restrição que impede múltiplas referências mutáveis aos mesmos dados ao mesmo tempo permite a mutação, mas de uma forma muito controlada. É algo com o qual os novos Rustaceans lutam porque a maioria das linguagens permite que você mute sempre que quiser. O benefício de ter essa restrição é que o Rust pode evitar condições de corrida de dados (data races) em tempo de compilação. Uma condição de corrida de dados (data race) é semelhante a uma condição de corrida (race condition) e acontece quando esses três comportamentos ocorrem:
- Dois ou mais ponteiros acessam os mesmos dados ao mesmo tempo.
- Pelo menos um dos ponteiros está sendo usado para escrever nos dados.
- Não há nenhum mecanismo sendo usado para sincronizar o acesso aos dados.
Condições de corrida de dados causam comportamento indefinido e podem ser difíceis de diagnosticar e corrigir quando você está tentando rastreá-las em tempo de execução; Rust impede esse problema recusando-se a compilar código com condições de corrida de dados!
Como sempre, podemos usar chaves para criar um novo escopo, permitindo múltiplas referências mutáveis, mas não simultâneas:
let mut s = String::from("hello");
{
let r1 = &mut s;
} // r1 sai do escopo aqui, então podemos fazer uma nova referência sem problemas
let r2 = &mut s;
Rust impõe uma regra semelhante para combinar referências mutáveis e imutáveis. Este código resulta em um erro:
let mut s = String::from("hello");
let r1 = &s; // no problem
let r2 = &s; // no problem
let r3 = &mut s; // BIG PROBLEM
println!("{r1}, {r2}, and {r3}");
Aqui está o erro:
error[E0502]: cannot borrow `s` as mutable because it is also borrowed as
immutable
--> src/main.rs:6:14
|
4 | let r1 = &s; // no problem
| -- immutable borrow occurs here
5 | let r2 = &s; // no problem
6 | let r3 = &mut s; // BIG PROBLEM
| ^^^^^^ mutable borrow occurs here
7 |
8 | println!("{r1}, {r2}, and {r3}");
| -- immutable borrow later used here
Ufa! Também não podemos ter uma referência mutável enquanto temos uma imutável para o mesmo valor.
Os usuários de uma referência imutável não esperam que o valor mude repentinamente sob eles! No entanto, múltiplas referências imutáveis são permitidas porque ninguém que está apenas lendo os dados tem a capacidade de afetar a leitura dos dados por outra pessoa.
Observe que o escopo de uma referência começa de onde ela é introduzida e continua até a última vez que essa referência é usada. Por exemplo, este código será compilado porque o último uso das referências imutáveis, o println!, ocorre antes que a referência mutável seja introduzida:
let mut s = String::from("hello");
let r1 = &s; // no problem
let r2 = &s; // no problem
println!("{r1} and {r2}");
// variáveis r1 e r2 não serão usadas após este ponto
let r3 = &mut s; // no problem
println!("{r3}");
Os escopos das referências imutáveis r1 e r2 terminam após o println! onde são usadas pela última vez, que é antes que a referência mutável r3 seja criada. Esses escopos não se sobrepõem, então este código é permitido: o compilador pode dizer que a referência não está mais sendo usada em um ponto antes do final do escopo.
Embora os erros de empréstimo possam ser frustrantes às vezes, lembre-se de que é o compilador Rust que aponta um possível bug no início (em tempo de compilação, em vez de em tempo de execução) e mostra exatamente onde está o problema. Então você não precisa rastrear por que seus dados não são o que você pensava que eram.
Referências Pendentes (Dangling References)
Em linguagens com ponteiros, é fácil criar erroneamente um ponteiro pendente (dangling pointer) — um ponteiro que referencia um local na memória que pode ter sido dado a outra pessoa — liberando alguma memória enquanto preserva um ponteiro para essa memória. Em Rust, por outro lado, o compilador garante que as referências nunca serão referências pendentes: se você tem uma referência a alguns dados, o compilador garantirá que os dados não sairão do escopo antes que a referência aos dados o faça.
Vamos tentar criar uma referência pendente para ver como o Rust as impede com um erro em tempo de compilação:
Nome do arquivo: src/main.rs
fn main() {
let reference_to_nothing = dangle();
}
fn dangle() -> &String {
let s = String::from("hello");
&s
}
Aqui está o erro:
error[E0106]: missing lifetime specifier
--> src/main.rs:5:16
|
5 | fn dangle() -> &String {
| ^ expected named lifetime parameter
|
= help: this function's return type contains a borrowed value,
but there is no value for it to be borrowed from
help: consider using the `'static` lifetime
|
5 | fn dangle() -> &'static String {
| ~~~~~~~~
Esta mensagem de erro se refere a um recurso que ainda não abordamos: tempos de vida (lifetimes). Discutiremos os tempos de vida em detalhes no Capítulo 10. Mas, se você desconsiderar as partes sobre tempos de vida, a mensagem contém a chave para o porquê esse código é um problema:
this function's return type contains a borrowed value, but there
is no value for it to be borrowed from
Vamos analisar mais de perto exatamente o que está acontecendo em cada estágio do nosso código dangle:
// src/main.rs
fn dangle() -> &String { // dangle retorna uma referência a uma String
let s = String::from("hello"); // s é uma nova String
&s // nós retornamos uma referência à String, s
} // Aqui, s sai do escopo e é descartada, então sua memória desaparece
// Perigo!
Como s é criado dentro de dangle, quando o código de dangle é finalizado, s será desalocado. Mas tentamos retornar uma referência a ela. Isso significa que essa referência estaria apontando para uma String inválida. Isso não é bom! Rust não nos deixará fazer isso.
A solução aqui é retornar a String diretamente:
fn no_dangle() -> String {
let s = String::from("hello");
s
}
Isso funciona sem problemas. A propriedade é movida para fora e nada é desalocado.
As Regras das Referências
Vamos recapitular o que discutimos sobre referências:
- A qualquer momento, você pode ter ou uma referência mutável ou qualquer número de referências imutáveis.
- As referências devem sempre ser válidas.
Em seguida, veremos um tipo diferente de referência: fatias (slices).
Resumo
Parabéns! Você concluiu o laboratório de Referências e Empréstimos (Borrowing). Você pode praticar mais laboratórios no LabEx para aprimorar suas habilidades.