Borrowing e referências
Esta aula explica o sistema de borrowing em Rust, incluindo referências imutáveis e mutáveis, as regras do borrow checker e exemplos práticos. O objetivo é entender como o Rust garante segurança de memória em tempo de compilação sem necessidade de coleta de lixo.
O borrowing é um dos conceitos fundamentais de Rust que permite acessar dados sem transferir a propriedade (ownership). Em vez de passar o valor inteiro para uma função, podemos emprestar uma referência a ele. Isso é essencial para escrever código eficiente e seguro, evitando cópias desnecessárias e garantindo que não haja data races ou uso inválido de memória.
Nesta aula, vamos explorar como criar referências com o operador &, a diferença entre referências mutáveis e imutáveis, e as regras que o borrow checker impõe para garantir que todas as referências sejam válidas e não conflitem entre si.
Referências (&)
Uma referência em Rust é como um ponteiro que aponta para um valor sem possuí-lo. O operador & é usado para criar uma referência a um valor, e o operador * é usado para dereferenciar (acessar o valor apontado). O tipo de uma referência imutável é &T, onde T é o tipo do valor.
As referências são não-copiáveis por padrão, mas podem ser copiadas se o tipo subjacente implementar a trait Copy. No entanto, o mais comum é que as referências sejam emprestadas temporariamente. O Rust garante que a referência nunca seja nula ou inválida, desde que o borrow checker aprove o código.
fn main() {
let x = 42;
let ref_x = &x; // referência imutável a x
println!("Valor de x: {}", *ref_x); // dereferenciando
println!("Valor de x (auto-deref): {}", ref_x); // auto-dereferencia
}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 modificação. O Rust impõe uma regra importante: você pode ter várias referências imutáveis simultaneamente, mas apenas uma referência mutável de cada vez, e ela não pode coexistir com referências imutáveis.
Essa regra previne data races em tempo de compilação. Se você tem uma referência mutável, nenhuma outra referência (imutável ou mutável) pode acessar o mesmo dado ao mesmo tempo. Isso garante que não haja conflitos de leitura/escrita.
fn main() {
let mut s = String::from("hello");
let ref1 = &s; // imutável
let ref2 = &s; // imutável, ok
// let ref3 = &mut s; // erro: não pode ter ref mutável junto com imutáveis
println!("{}, {}", ref1, ref2);
}fn main() {
let mut s = String::from("hello");
let ref_mut = &mut s;
ref_mut.push_str(" world");
println!("{}", ref_mut);
}Regras do borrow checker
O borrow checker é o componente do compilador Rust que verifica as regras de empréstimo. 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) ao mesmo tempo. - Você pode ter exatamente uma referência mutável (
&mut T) para um valor, mas não pode ter nenhuma referência imutável ao mesmo tempo. - A referência deve sempre apontar para um valor válido (não pode ser dangling). O compilador garante que o valor referenciado não seja destruído enquanto a referência existir.
Essas regras são verificadas em tempo de compilação, sem custo em tempo de execução. Se o código violar alguma regra, o compilador emite um erro e o programa não compila. Isso elimina toda uma classe de bugs comuns em C/C++.
fn main() {
let mut v = vec![1, 2, 3];
let first = &v[0]; // referência imutável
v.push(4); // erro: mutação enquanto referência imutável existe
println!("{}", first);
}Exemplos
Vamos ver alguns exemplos práticos que ilustram o uso correto de borrowing.
Exemplo 1: Função que toma referência
fn calcular_comprimento(s: &String) -> usize {
s.len()
}
fn main() {
let s = String::from("hello");
let len = calcular_comprimento(&s);
println!("O comprimento de '{}' é {}.", s, len);
}Exemplo 2: Modificando via referência mutável
fn adicionar_exclamacao(s: &mut String) {
s.push_str("!");
}
fn main() {
let mut s = String::from("hello");
adicionar_exclamacao(&mut s);
println!("{}", s);
}Exemplo 3: Múltiplas referências imutáveis
fn main() {
let s = String::from("hello");
let r1 = &s;
let r2 = &s;
println!("{} e {}", r1, r2);
// r1 e r2 saem de escopo aqui, então podemos ter ref mutável depois
let r3 = &mut s; // erro se r1 ou r2 ainda estivessem vivos
}Boas práticas
Sempre prefira referências imutáveis quando não precisar modificar o valor. Use referências mutáveis apenas quando necessário e mantenha o escopo delas o mais curto possível para evitar conflitos. Lembre-se de que o borrow checker é seu aliado: ele previne bugs em tempo de compilação.
Referências
- The Rust Programming Language - References and Borrowing
- Rust by Example - Borrowing
- Rust Reference - Borrow Operators
- The Rustonomicon - Borrow Splitting
- First Edition - References and Borrowing
Exercícios
Escreva uma função que recebe uma referência imutável a uma String e retorna o número de caracteres (use
chars().count()).✓ Resposta:fn count_chars(s: &String) -> usize { s.chars().count() } fn main() { let s = String::from("hello"); println!("{}", count_chars(&s)); }Escreva uma função que recebe uma referência mutável a uma String e adiciona um ponto de exclamação no final.
✓ Resposta:fn add_exclamation(s: &mut String) { s.push_str("!"); } fn main() { let mut s = String::from("hello"); add_exclamation(&mut s); println!("{}", s); }O código abaixo compila? Se não, corrija-o para que compile, mantendo a intenção de imprimir o primeiro elemento e depois modificar o vetor.
fn main() { let mut v = vec![1, 2, 3]; let first = &v[0]; v.push(4); println!("{}", first); }✓ Resposta:Não compila. Uma solução é imprimir antes de modificar:fn main() { let mut v = vec![1, 2, 3]; let first = &v[0]; println!("{}", first); v.push(4); }O que há de errado com o código a seguir? Corrija-o.
fn main() { let mut s = String::from("hello"); let r1 = &s; let r2 = &mut s; println!("{}, {}", r1, r2); }✓ Resposta:Não pode ter referência imutável e mutável ao mesmo tempo. Corrigir removendo uma delas ou alterando a ordem:fn main() { let mut s = String::from("hello"); let r1 = &s; println!("{}", r1); let r2 = &mut s; println!("{}", r2); }Escreva um programa que declara uma variável mutável do tipo
i32, cria uma referência imutável para ela, imprime o valor, depois cria uma referência mutável, modifica o valor para 10 e imprime novamente.✓ Resposta:fn main() { let mut x = 5; let ref_imm = &x; println!("{}", ref_imm); let ref_mut = &mut x; *ref_mut = 10; println!("{}", x); }