Borrowing: referências imutáveis e mutáveis
1. Introdução ao Borrowing
Em Rust, o sistema de ownership resolve problemas de gerenciamento de memória sem um garbage collector, mas copiar valores grandes repetidamente seria ineficiente. O borrowing (empréstimo) surge como solução: em vez de transferir a posse de um valor, você pode "emprestá-lo" temporariamente.
A diferença fundamental entre ownership e borrowing é clara: no ownership, você é o dono do valor e responsável por liberá-lo; no borrowing, você apenas pega emprestado, sem assumir a responsabilidade final pela memória.
A sintaxe básica utiliza &T para referências imutáveis (apenas leitura) e &mut T para referências mutáveis (leitura e escrita).
let x = 42;
let ref_imutavel = &x; // &i32
let mut y = 10;
let ref_mutavel = &mut y; // &mut i32
2. Regras Fundamentais do Borrowing
O borrow checker impõe duas regras de ouro que todo desenvolvedor Rust precisa memorizar:
- Você pode ter uma ou mais referências imutáveis OU exatamente uma referência mutável.
- As referências devem sempre ser válidas (nunca apontar para memória liberada).
Essas regras derivam diretamente do sistema de ownership: quando você empresta um valor, o dono original mantém a posse, mas cede acesso temporário. O empréstimo termina quando a referência sai de escopo.
fn main() {
let mut numero = 5;
{
let r = &mut numero; // empréstimo começa
*r += 1;
} // empréstimo termina aqui
println!("{}", numero); // 6 - dono original intacto
}
3. Referências Imutáveis (&T)
Referências imutáveis permitem ler dados sem modificá-los. Você pode criar quantas quiser simultaneamente:
fn main() {
let dados = vec![1, 2, 3, 4, 5];
let ref1 = &dados;
let ref2 = &dados;
let ref3 = &dados;
println!("ref1: {:?}, ref2: {:?}, ref3: {:?}", ref1, ref2, ref3);
// Todas as três referências coexistem pacificamente
}
A limitação principal: você não pode modificar o valor através de uma referência imutável. Tentar fazer isso gera erro de compilação:
fn main() {
let valor = 10;
let ref_imut = &valor;
// *ref_imut = 20; // ERRO: cannot assign to `*ref_imut`, which is behind a `&` reference
}
4. Referências Mutáveis (&mut T)
Referências mutáveis permitem alterar o valor emprestado. A regra de exclusividade é rigorosa: apenas uma referência mutável pode existir em um determinado escopo.
fn main() {
let mut texto = String::from("Olá");
let ref_mut = &mut texto;
ref_mut.push_str(", mundo!");
println!("{}", ref_mut); // "Olá, mundo!"
}
Tentar criar duas referências mutáveis no mesmo escopo causa erro:
fn main() {
let mut valor = 42;
let a = &mut valor;
let b = &mut valor; // ERRO: cannot borrow `valor` as mutable more than once at a time
println!("{} {}", a, b);
}
5. Interação entre Referências Imutáveis e Mutáveis
O conflito mais comum ocorre quando tentamos misturar referências imutáveis e mutáveis no mesmo escopo:
fn main() {
let mut dados = vec![1, 2, 3];
let imutavel = &dados[0]; // referência imutável
let mutavel = &mut dados; // ERRO: cannot borrow `dados` as mutable because it is also borrowed as immutable
println!("{} {}", imutavel, mutavel[1]);
}
A mensagem do compilador é clara: você não pode emprestar como mutável algo que já está emprestado como imutável. A solução é reorganizar o escopo:
fn main() {
let mut dados = vec![1, 2, 3];
{
let imutavel = &dados[0];
println!("{}", imutavel);
} // empréstimo imutável termina aqui
let mutavel = &mut dados;
mutavel.push(4);
println!("{:?}", mutavel);
}
6. Escopo e Lifetime das Referências
O escopo determina quando um empréstimo termina. Tradicionalmente, o escopo de uma referência seguia o bloco onde foi criada, mas o Rust moderno implementa NLL (Non-Lexical Lifetimes), que permite que o borrow checker seja mais inteligente:
fn main() {
let mut x = 5;
let y = &x; // empréstimo imutável começa
println!("{}", y); // último uso de y
// Com NLL, o empréstimo termina aqui, mesmo dentro do mesmo bloco
let z = &mut x; // agora permitido!
*z += 1;
println!("{}", z);
}
Sem NLL, o código acima não compilaria. O NLL analisa onde a referência é realmente usada pela última vez, permitindo que empréstimos terminem antes do final do bloco léxico.
7. Boas Práticas e Padrões Comuns
Quando usar referência imutável vs mutável:
- Use
&Tquando você só precisa ler dados (funções de consulta, exibição) - Use
&mut Tquando você precisa modificar o valor (funções de atualização)
Evitando referências penduradas (dangling references):
fn cria_referencia_pendurada() -> &i32 {
let x = 5;
&x // ERRO: `x` é liberado quando a função termina
}
O compilador impede isso com o lifetime checker. A solução é retornar o valor por ownership ou usar um lifetime explícito com dados que vivem mais tempo.
Exemplo prático: função que recebe &str vs &mut String
fn exibir_texto(texto: &str) {
println!("Texto: {}", texto);
}
fn adicionar_saudacao(texto: &mut String) {
texto.push_str(", bem-vindo!");
}
fn main() {
let mut mensagem = String::from("Olá");
exibir_texto(&mensagem); // referência imutável
adicionar_saudacao(&mut mensagem); // referência mutável
exibir_texto(&mensagem); // "Olá, bem-vindo!"
}
Padrão comum: split borrows
Quando você precisa emprestar diferentes partes de uma estrutura simultaneamente:
struct Pessoa {
nome: String,
idade: u32,
}
fn main() {
let mut p = Pessoa {
nome: String::from("Alice"),
idade: 30,
};
let nome_ref = &mut p.nome;
let idade_ref = &mut p.idade; // Permitido! São campos diferentes
nome_ref.push_str(" Silva");
*idade_ref += 1;
println!("{} tem {} anos", p.nome, p.idade);
}
O borrow checker entende que campos diferentes de uma struct são independentes, permitindo múltiplas referências mutáveis para partes diferentes do mesmo dado.
Dominar o borrowing é essencial para escrever Rust seguro e eficiente. As regras podem parecer restritivas no início, mas elas previnem toda uma classe de bugs comuns em outras linguagens, como data races e uso após liberação.
Referências
- The Rust Programming Language - Chapter 4: References and Borrowing — Capítulo oficial do livro sobre borrowing, com exemplos detalhados e explicações das regras fundamentais.
- Rust Reference - Type System: Reference types — Documentação técnica completa sobre tipos de referência em Rust, incluindo regras de coerção e representação em memória.
- Rust by Example - Borrowing — Exemplos práticos interativos de borrowing, mutabilidade e aliasing em Rust.
- Non-Lexical Lifetimes (NLL) RFC 2094 — Proposta e explicação técnica do sistema NLL que relaxa as regras de escopo de empréstimos.
- The Rustonomicon - References and Borrowing — Guia avançado sobre referências, dangling pointers e garantias de segurança em baixo nível no Rust.
- Common Rust Lifetime Misconceptions — Artigo técnico que esclarece equívocos comuns sobre lifetimes e borrowing, com exemplos práticos.