Entendendo o Sistema de Ownership e Borrowing
O Rust resolve o problema de gerenciamento de memória através de um sistema único: o ownership. Cada valor tem um proprietário, e quando esse proprietário sai do escopo, a memória é liberada. Porém, nem sempre queremos transferir propriedade de um dado. É aí que entra o borrowing: permite usar um valor sem tomar posse dele. Existem dois tipos de referências: &T (imutável) e &mut T (mutável). Entender a diferença é fundamental para escrever código Rust seguro.
O sistema é regido por três regras simples: você pode ter múltiplas referências imutáveis ou uma única referência mutável, mas nunca ambas simultaneamente. Isso previne data races em tempo de compilação, eliminando bugs sutis que causam problemas em outras linguagens.
fn main() {
let s = String::from("Rust");
// Referência imutável - podemos ter várias
let r1 = &s;
let r2 = &s;
println!("{}, {}", r1, r2); // OK: ambas usam s
// s ainda é proprietário dos dados
println!("{}", s);
}
Referências Imutáveis (&)
Uma referência imutável &T permite ler um valor sem modificá-lo e sem tomar posse. É como emprestar um livro para alguém ler, mas você continua sendo o dono. Você pode criar quantas referências imutáveis quiser do mesmo dado, pois ninguém está modificando nada. Quando você passa &s para uma função, apenas a referência é passada, não os dados.
O ponto crítico é que enquanto existem referências imutáveis ativas, você não pode modificar o valor original. O compilador garante isso. Funções que recebem &T não podem mudar o conteúdo, e o Rust força isso em tempo de compilação.
fn calcular_comprimento(s: &String) -> usize {
s.len()
} // s sai do escopo, mas como não é proprietário, nada acontece
fn main() {
let s1 = String::from("hello");
let len = calcular_comprimento(&s1);
println!("'{}' tem comprimento {}", s1, len); // s1 ainda é válido
}
Um exemplo prático: ao iterar sobre uma coleção em uma função, você frequentemente quer apenas ler os dados. Passar uma referência imutável evita cópia desnecessária:
fn processar_nomes(nomes: &Vec<String>) {
for nome in nomes {
println!("Nome: {}", nome);
}
}
fn main() {
let lista = vec![
String::from("Alice"),
String::from("Bob"),
];
processar_nomes(&lista);
processar_nomes(&lista); // Podemos chamar várias vezes
println!("{:?}", lista); // lista continua acessível
}
Referências Mutáveis (&mut)
Uma referência mutável &mut T permite modificar o valor emprestado. É como emprestar o livro, mas quem o recebe tem permissão para escrever nele. A restrição crucial é uma única referência mutável por escopo: você não pode ter duas &mut apontando para o mesmo dado simultaneamente, nem pode ter uma & e uma &mut juntas. Isso previne condições de corrida e modificações inesperadas.
Quando você passa &mut s, está dizendo "você pode modificar isso, mas é a única pessoa que pode". O compilador força essa exclusividade. Se você tentar violar as regras, terá um erro clara: "cannot borrow x as mutable more than once".
fn adicionar_exclamacao(s: &mut String) {
s.push_str("!");
}
fn main() {
let mut s = String::from("Hello");
adicionar_exclamacao(&mut s);
println!("{}", s); // Output: Hello!
}
Aqui está um exemplo que falha e por quê:
fn main() {
let mut x = 5;
let r1 = &mut x;
let r2 = &mut x; // ERRO: não pode ter duas referências mutáveis
println!("{}, {}", r1, r2);
}
O compilador rejeita isso porque r2 violaria a exclusividade. Porém, se você estruturar o código para que a primeira referência saia de escopo antes da segunda ser usada, funciona:
fn main() {
let mut x = 5;
let r1 = &mut x;
println!("{}", r1); // r1 usado aqui
let r2 = &mut x; // OK: r1 não está mais em uso
println!("{}", r2);
}
Mixing Referências (o que NÃO fazer)
fn main() {
let mut s = String::from("Hello");
let r1 = &s; // Referência imutável
let r2 = &s; // Outra referência imutável
let r3 = &mut s; // ERRO: não pode ter &mut enquanto há &
println!("{}, {}, {}", r1, r2, r3);
}
Boas Práticas e Padrões Comuns
Rust força boas práticas através de erros de compilação. Quando você está projetando uma API, prefira &T por padrão a menos que realmente precise modificar o valor. Use &mut T quando a função necessita alterar seus argumentos. Para tipos que implementam Copy (como inteiros), às vezes não vale a pena referenciar, pois a cópia é barata.
Um padrão comum é usar &[T] em vez de &Vec<T> em parâmetros de função, pois slices são mais genéricas e funcionam com arrays também:
fn somar_elementos(nums: &[i32]) -> i32 {
nums.iter().sum()
}
fn main() {
let vetor = vec![1, 2, 3, 4, 5];
let array = [10, 20, 30];
println!("Vetor: {}", somar_elementos(&vetor));
println!("Array: {}", somar_elementos(&array));
}
Para modificações, use &mut conscientemente. Se sua função modifica um vetor, deixe isso explícito na assinatura:
fn limpar_espacos(s: &mut String) {
s.retain(|c| !c.is_whitespace());
}
fn main() {
let mut texto = String::from("Hello World");
limpar_espacos(&mut texto);
println!("{}", texto); // Output: HelloWorld
}
Conclusão
O sistema de borrowing em Rust é seu maior trunfo para evitar bugs de memória. Lembre-se: uma única referência mutável ou múltiplas imutáveis, nunca ambas. Use &T para leitura e &mut T apenas quando necessário modificar. Essa aparente restrição é na verdade liberdade — liberdade de não se preocupar com data races, use-after-free ou double-frees. Dominando referências, você domina Rust.