Borrowing e referências
Esta aula explica o sistema de borrowing e referências em Rust, incluindo referências imutáveis (&) e mutáveis (&mut), as regras do borrow checker para garantir segurança de memória, e exemplos práticos que ilustram como evitar data races e dangling pointers.
Em Rust, o conceito de borrowing (empréstimo) permite que você acesse um valor sem tomar posse dele, usando referências. Isso é fundamental para escrever código eficiente e seguro, pois evita cópias desnecessárias e garante que as regras de ownership sejam respeitadas. Nesta aula, exploraremos como criar e usar referências, a diferença entre referências mutáveis e imutáveis, e como o borrow checker do compilador impõe regras rígidas para prevenir erros comuns de memória.
Referências (&)
Uma referência é um ponteiro que aponta para um valor sem possuí-lo. Em Rust, usamos o operador & para criar uma referência a um valor. Por exemplo, &x cria uma referência imutável a x. Referências são úteis quando queremos passar dados para uma função sem transferir a posse, permitindo que o valor original continue válido após a chamada.
As referências em Rust são sempre válidas (não nulas) e o compilador garante que não haja dangling pointers (ponteiros soltos) através do sistema de lifetimes. Veja um exemplo básico:
fn main() {
let s1 = String::from("hello");
let len = calculate_length(&s1);
println!("The length of '{}' is {}.", s1, len);
}
fn calculate_length(s: &String) -> usize {
s.len()
}
Note que calculate_length recebe uma referência a String, não a String em si. Dentro da função, s é uma referência, e não podemos modificá-la (a menos que seja mutável). Após a chamada, s1 ainda é dona do valor e pode ser usada novamente.
Mutável vs imutável
Referências podem ser imutáveis (&T) ou mutáveis (&mut T). Uma referência imutável permite apenas leitura do valor, enquanto uma referência mutável permite modificar o valor emprestado. A regra fundamental é: você pode ter um número ilimitado de referências imutáveis ou exatamente uma referência mutável, mas nunca ambos ao mesmo tempo. Isso evita condições de corrida (data races) em tempo de compilação.
Exemplo com referência mutável:
fn main() {
let mut s = String::from("hello");
change(&mut s);
println!("{}", s);
}
fn change(some_string: &mut String) {
some_string.push_str(", world");
}
Aqui, change recebe uma referência mutável e pode modificar a String original. A variável s deve ser declarada como mut para que possamos criar uma referência mutável a ela.
Regras do borrow checker
O borrow checker é a parte do compilador Rust que verifica as regras de borrowing durante a compilação. As principais regras são:
- Você pode ter uma ou mais referências imutáveis (
&T) para um valor, mas não pode ter uma referência mutável (&mut T) enquanto existir qualquer referência imutável. - Você pode ter exatamente uma referência mutável para um valor, e nenhuma referência imutável pode coexistir.
- As referências devem sempre ser válidas (não podem durar mais que o valor referenciado).
Essas regras são aplicadas para garantir a segurança de memória sem a necessidade de um garbage collector. Vamos a um exemplo que viola a regra de referências mutáveis e imutáveis simultâneas:
fn main() {
let mut s = String::from("hello");
let r1 = &s; // referência imutável
let r2 = &s; // outra referência imutável - OK
let r3 = &mut s; // referência mutável - ERRO! r1 e r2 ainda estão em escopo
println!("{} {} {}", r1, r2, r3);
}
O compilador emitirá um erro como: cannot borrow `s` as mutable because it is also borrowed as immutable. Para corrigir, podemos limitar o escopo das referências imutáveis antes de criar a mutável:
fn main() {
let mut s = String::from("hello");
{
let r1 = &s;
let r2 = &s;
println!("{} {}", r1, r2);
} // r1 e r2 saem de escopo aqui
let r3 = &mut s; // OK
println!("{}", r3);
}
Exemplos
Vamos ver mais exemplos que demonstram o borrowing em ação:
Exemplo 1: Referências imutáveis múltiplas
fn main() {
let s = String::from("hello");
let r1 = &s;
let r2 = &s;
println!("r1: {}, r2: {}", r1, r2); // OK
}
Aqui, duas referências imutáveis coexistem pacificamente.
Exemplo 2: Referência mutável única
fn main() {
let mut s = String::from("hello");
let r = &mut s;
r.push_str(" world");
println!("{}", r);
// println!("{}", s); // ERRO: s foi emprestado como mutável
}
Após criar uma referência mutável, não podemos usar a variável original até que a referência não seja mais usada.
Exemplo 3: Dangling reference (ponteiro solto)
fn main() {
let reference_to_nothing = dangle();
}
fn dangle() -> &String {
let s = String::from("hello");
&s // ERRO: s sai de escopo, a referência se torna inválida
}
O compilador impede isso com um erro: missing lifetime specifier. A solução é retornar a própria String (transferir a posse) ou usar um lifetime explícito (tópico avançado).
Boas práticas
- Prefira referências imutáveis sempre que possível, pois são mais flexíveis e seguras.
- Use referências mutáveis apenas quando precisar modificar o valor.
- Mantenha o escopo das referências o menor possível para evitar conflitos.
- Lembre-se de que o borrow checker é seu aliado: ele previne erros em tempo de compilação.
Referências
- The Rust Programming Language - References and Borrowing
- Rust by Example - Borrowing
- The Rustonomicon - Borrow Splitting
- Rust Book (First Edition) - Ownership
- Rust Standard Library - Deref trait
Exercícios
-
O que é uma referência em Rust e qual operador é usado para criá-la?
✓ Resposta: Uma referência é um ponteiro que permite acessar um valor sem possuí-lo. O operador&é usado para criar uma referência. Por exemplo,&xcria uma referência imutável ax. -
Qual é a diferença entre uma referência imutável e uma mutável? Dê um exemplo de cada.
✓ Resposta: Uma referência imutável (&T) permite apenas leitura, enquanto uma mutável (&mut T) permite modificar o valor. Exemplo imutável:let r = &s;. Exemplo mutável:let r = &mut s;. -
O código abaixo compila? Se não, explique o erro e corrija.
fn main() { let mut s = String::from("hello"); let r1 = &s; let r2 = &mut s; println!("{} {}", r1, r2); }✓ Resposta: Não compila. O erro é: não se pode ter uma referência mutável enquanto existir uma referência imutável ativa. Para corrigir, podemos limitar o escopo der1antes de criarr2:fn main() { let mut s = String::from("hello"); { let r1 = &s; println!("{}", r1); } let r2 = &mut s; println!("{}", r2); } -
O que é um dangling reference? Como o Rust impede que isso ocorra?
✓ Resposta: Um dangling reference é uma referência que aponta para uma região de memória que já foi liberada. Rust impede isso através do borrow checker e do sistema de lifetimes: o compilador verifica que toda referência é válida durante todo o seu uso, e não permite que uma referência sobreviva ao valor que ela referencia. -
Escreva uma função que receba uma referência mutável a uma String e adicione um ponto de exclamação ao final. Depois, chame-a no
main.✓ Resposta:fn add_exclamation(s: &mut String) { s.push('!'); } fn main() { let mut s = String::from("Hello"); add_exclamation(&mut s); println!("{}", s); // imprime "Hello!" }