Referências e Empréstimos
O problema com o código com a tupla da Listagem 4-5 é que precisamos retornar
a String para a função chamadora para que ainda possamos usá-la depois da
chamada para calculate_length, porque a String foi movida para dentro de
calculate_length. Em vez disso, podemos fornecer uma referência ao valor
String. Uma referência é parecida com um ponteiro no sentido de que é um
endereço que podemos seguir para acessar os dados armazenados naquele local;
esses dados pertencem a alguma outra variável. Diferentemente de um ponteiro,
uma referência tem a garantia de apontar para um valor válido de um tipo
específico durante toda a vida dessa referência.
Veja como você definiria e usaria uma função calculate_length que recebe como
parâmetro uma referência a um objeto, em vez de assumir o ownership do valor:
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, repare que todo o código envolvendo a 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 que, na definição da função, recebemos
&String em vez de String. Esses e comerciais (&) representam
referências, e elas permitem que você se refira a algum valor sem assumir seu
ownership. A Figura 4-6 ilustra esse conceito.
Figura 4-6: Um diagrama de &String s apontando para
String s1
Observação: o oposto de referenciar usando
&é desreferenciar, o que é feito com o operador de desreferência,*. Veremos alguns usos do operador de desreferência no Capítulo 8 e discutiremos os detalhes de desreferenciação no Capítulo 15.
Vamos observar mais de perto a chamada de função aqui:
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()
}
A sintaxe &s1 nos permite criar uma referência que se refere ao valor de
s1, mas não é dona dele. Como a referência não possui ownership, o valor
para o qual ela aponta não será desalocado quando a referência deixar 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 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 is a reference to a String
s.len()
} // Here, s goes out of scope. But because s does not have ownership of what
// it refers to, the String is not dropped.
O escopo em que a variável s é válida é igual ao escopo de qualquer
parâmetro de função, mas o valor apontado pela referência não é desalocado
quando s deixa de ser usado, porque s não tem ownership. Quando funções
recebem referências como parâmetros em vez dos próprios valores, não
precisamos devolver esses valores para transferir o ownership de volta, porque
nunca o tivemos.
Chamamos o ato de criar uma referência de borrowing ou empréstimo. Como na vida real: se uma pessoa é dona de algo, você pode pegar emprestado dela. Quando terminar, precisa devolver. Você não é o dono.
Então, o que acontece se tentarmos modificar algo que estamos pegando emprestado? Experimente o código da Listagem 4-6. Aviso de antemão: não funciona!
fn main() {
let s = String::from("hello");
change(&s);
}
fn change(some_string: &String) {
some_string.push_str(", world");
}
Este é o erro:
$ cargo run
Compiling ownership v0.1.0 (file:///projects/ownership)
error[E0596]: cannot borrow `*some_string` as mutable, as it is behind a `&` reference
--> src/main.rs:8:5
|
8 | some_string.push_str(", world");
| ^^^^^^^^^^^ `some_string` is a `&` reference, so the data it refers to cannot be borrowed as mutable
|
help: consider changing this to be a mutable reference
|
7 | fn change(some_string: &mut String) {
| +++
For more information about this error, try `rustc --explain E0596`.
error: could not compile `ownership` (bin "ownership") due to 1 previous error
Assim como variáveis são imutáveis por padrão, referências também são. Não temos permissão para modificar algo para o qual temos apenas uma referência.
Referências Mutáveis
Podemos corrigir o código da Listagem 4-6 para permitir modificar um valor emprestado com apenas alguns pequenos ajustes, usando, em vez disso, uma referência mutável:
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 mut. Depois, criamos uma referência mutável com
&mut s no ponto em que 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 vai mutar o valor que tomou
emprestado.
Referências mutáveis têm uma grande restrição: se você tem uma referência
mutável para um valor, não pode ter nenhuma outra referência para esse mesmo
valor. Este código, que tenta criar duas referências mutáveis para s, falha:
fn main() {
let mut s = String::from("hello");
let r1 = &mut s;
let r2 = &mut s;
println!("{r1}, {r2}");
}
Este é o erro:
$ cargo run
Compiling ownership v0.1.0 (file:///projects/ownership)
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
For more information about this error, try `rustc --explain E0499`.
error: could not compile `ownership` (bin "ownership") due to 1 previous error
Esse erro diz que o código é inválido porque não podemos pegar s emprestada
como mutável mais de uma vez ao mesmo tempo. O primeiro empréstimo mutável está
em r1 e precisa durar até ser 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 de r1.
A restrição que impede múltiplas referências mutáveis aos mesmos dados ao mesmo tempo permite mutação, mas de uma forma muito controlada. É algo com que novos rustaceanos costumam ter dificuldade, porque a maioria das linguagens permite mutar quando você quiser. A vantagem dessa restrição é que o Rust consegue evitar data races em tempo de compilação. Uma data race é parecida com uma condição de corrida e acontece quando estes três comportamentos ocorrem:
- Dois ou mais ponteiros acessam os mesmos dados ao mesmo tempo.
- Pelo menos um desses ponteiros está sendo usado para escrever nos dados.
- Não há nenhum mecanismo de sincronização sendo usado para coordenar o acesso.
Data races causam comportamento indefinido e podem ser difíceis de diagnosticar e corrigir quando você está tentando rastreá-las em tempo de execução; o Rust evita esse problema recusando-se a compilar código com data races.
Como sempre, podemos usar chaves para criar um novo escopo, permitindo múltiplas referências mutáveis, desde que não sejam simultâneas:
fn main() {
let mut s = String::from("hello");
{
let r1 = &mut s;
} // r1 goes out of scope here, so we can make a new reference with no problems.
let r2 = &mut s;
}
O Rust impõe uma regra semelhante para combinar referências mutáveis e imutáveis. Este código gera um erro:
fn main() {
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}");
}
Este é o erro:
$ cargo run
Compiling ownership v0.1.0 (file:///projects/ownership)
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
For more information about this error, try `rustc --explain E0502`.
error: could not compile `ownership` (bin "ownership") due to 1 previous error
Também não podemos ter uma referência mutável enquanto temos uma referência imutável ao mesmo valor.
Quem está usando uma referência imutável não espera que o valor mude de repente enquanto a referência ainda está em uso. No entanto, múltiplas referências imutáveis são permitidas, porque ninguém que esteja apenas lendo os dados consegue afetar a leitura dos demais.
Observe que o escopo de uma referência começa no ponto em que ela é
introduzida e continua até a última vez em que essa referência é usada. Por
exemplo, este código compila porque o último uso das referências imutáveis está
no println!, antes da introdução da referência mutável:
fn main() {
let mut s = String::from("hello");
let r1 = &s; // no problem
let r2 = &s; // no problem
println!("{r1} and {r2}");
// Variables r1 and r2 will not be used after this point.
let r3 = &mut s; // no problem
println!("{r3}");
}
Os escopos das referências imutáveis r1 e r2 terminam depois do println!,
onde são usadas pela última vez, o que acontece antes da criação da referência
mutável r3. Esses escopos não se sobrepõem, então esse código é permitido: o
compilador consegue perceber que a referência já não está mais sendo usada em
um ponto anterior ao fim do escopo.
Mesmo que erros de borrowing possam ser frustrantes às vezes, lembre-se de que é o compilador do Rust apontando um possível bug cedo, em tempo de compilação em vez de em tempo de execução, e mostrando exatamente onde está o problema. Assim, você não precisa sair procurando por que seus dados não são aquilo que você achava que fossem.
Referências Pendentes
Em linguagens com ponteiros, é fácil criar por engano um dangling pointer ou ponteiro pendente, isto é, um ponteiro que faz referência a um local da memória que talvez já tenha sido entregue a outra pessoa, ao liberar alguma memória enquanto ainda se preserva um ponteiro para ela. Em Rust, ao contrário, o compilador garante que referências jamais serão pendentes: se você tem uma referência para algum dado, o compilador garante que o dado não sairá de escopo antes da referência.
Vamos tentar criar uma referência pendente para ver como o Rust evita isso com um erro de compilação:
fn main() {
let reference_to_nothing = dangle();
}
fn dangle() -> &String {
let s = String::from("hello");
&s
}
Este é o erro:
$ cargo run
Compiling ownership v0.1.0 (file:///projects/ownership)
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, but this is uncommon unless you're returning a borrowed value from a `const` or a `static`
|
5 | fn dangle() -> &'static String {
| +++++++
help: instead, you are more likely to want to return an owned value
|
5 - fn dangle() -> &String {
5 + fn dangle() -> String {
|
For more information about this error, try `rustc --explain E0106`.
error: could not compile `ownership` (bin "ownership") due to 1 previous error
Essa mensagem de erro faz referência a um recurso que ainda não cobrimos: lifetimes. Discutiremos lifetimes em detalhe no Capítulo 10. Mas, se você ignorar as partes sobre lifetimes, a mensagem contém a chave para entender por que 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 observar com mais cuidado o que está acontecendo em cada etapa do código
de dangle:
fn main() {
let reference_to_nothing = dangle();
}
fn dangle() -> &String { // dangle returns a reference to a String
let s = String::from("hello"); // s is a new String
&s // we return a reference to the String, s
} // Here, s goes out of scope and is dropped, so its memory goes away.
// Danger!
Como s é criada dentro de dangle, quando o código de dangle termina, s
é desalocada. Mas tentamos retornar uma referência a ela. Isso significa que a
referência apontaria para uma String inválida. Nada bom! O Rust não nos
deixa fazer isso.
A solução aqui é retornar a String diretamente:
fn main() {
let string = no_dangle();
}
fn no_dangle() -> String {
let s = String::from("hello");
s
}
Isso funciona sem problemas. O ownership é movido para fora, e nada é desalocado.
As Regras das Referências
Vamos recapitular o que discutimos sobre referências:
- Em qualquer momento, você pode ter ou uma referência mutável ou qualquer quantidade de referências imutáveis.
- Referências sempre precisam ser válidas.
A seguir, veremos um tipo diferente de referência: fatias.