Rust ownership explicado para quem vem de linguagens com garbage collector
1. O Problema que o Ownership Resolve: Adeus ao GC
Se você vem de JavaScript, Python, Java ou Go, está acostumado com o garbage collector (GC) cuidando da memória para você. O GC é um processo que periodicamente varre o heap em busca de objetos que não são mais referenciados, liberando-os. Isso funciona, mas tem um custo: pausas imprevisíveis, latência extra e consumo de CPU que poderia estar sendo usado para sua lógica de negócio.
Rust adota uma abordagem radicalmente diferente: o sistema de ownership. Em vez de um coletor de lixo em tempo de execução, Rust usa regras verificadas em tempo de compilação que garantem que a memória seja liberada automaticamente quando não for mais necessária. O resultado é performance previsível, sem pausas e com consumo mínimo de recursos.
As três leis fundamentais do ownership são:
- Cada valor em Rust tem um único dono (owner)
- Só pode existir um dono por vez
- Quando o dono sai do escopo, o valor é descartado (drop)
// Exemplo: escopo e drop automático
{
let s = String::from("olá"); // s é o dono
println!("{}", s);
} // s sai do escopo, memória liberada automaticamente
// println!("{}", s); // ERRO: s não existe mais
2. Movimentação (Move) vs Cópia (Copy): O Fim das Referências Implícitas
Em linguagens com GC, você pode fazer isso sem problemas:
// JavaScript
let a = "hello";
let b = a;
console.log(a); // funciona
Em Rust, o comportamento depende do tipo. Para tipos que implementam a trait Copy (inteiros, booleanos, floats, char), a atribuição cria uma cópia real:
let a: i32 = 42;
let b = a; // cópia, não move
println!("{}", a); // funciona: 42
Para tipos que não implementam Copy (String, Vec, structs personalizadas), a atribuição move o ownership:
let a = String::from("hello");
let b = a; // ownership movido para b
// println!("{}", a); // ERRO: borrow of moved value
println!("{}", b); // funciona: "hello"
Essa é a armadilha clássica para quem vem de linguagens com GC. Em JavaScript, Python ou Java, variáveis de objetos são referências — você pode ter múltiplas "variáveis" apontando para o mesmo objeto. Em Rust, cada valor tem exatamente um dono.
3. Borrowing: Emprestando sem Perder a Propriedade
Se mover não é o que você quer, Rust oferece borrowing (empréstimo) através de referências. Você pode "emprestar" um valor sem transferir o ownership.
Referências imutáveis (&T): Você pode ter quantas quiser, todas apenas para leitura:
let s = String::from("dados");
let r1 = &s;
let r2 = &s;
println!("{} e {}", r1, r2); // múltiplos leitores: OK
Referências mutáveis (&mut T): Apenas uma por vez, garantindo exclusividade na escrita:
let mut s = String::from("dados");
let r = &mut s;
r.push_str(" extra");
// let r2 = &s; // ERRO: não pode ter ref imutável enquanto existe ref mutável
println!("{}", r);
A regra de ouro: Você não pode ter uma referência mutável e uma imutável ao mesmo tempo. Isso elimina data races em tempo de compilação — algo que em C++ ou Java só é detectado em execução (ou nunca).
4. Lifetimes: O Compilador como seu Parceiro de Memória
Lifetimes (tempos de vida) são anotações que garantem que referências não sobrevivam aos dados que apontam. O compilador usa isso para evitar dangling pointers.
Na maioria dos casos, o Rust consegue inferir os lifetimes sozinho (elisão de lifetimes):
fn primeiro_ou_segundo(x: &str, y: &str) -> &str {
if x.len() > y.len() { x } else { y }
}
Mas essa função não compila! O Rust não sabe se o retorno refere-se a x ou y, e não consegue determinar o lifetime correto. Precisamos anotar:
fn primeiro_ou_segundo<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() { x } else { y }
}
A anotação 'a diz: "o lifetime do retorno é o menor entre os lifetimes de x e y". Isso garante que a referência retornada nunca aponte para memória já liberada.
5. Ownership em Estruturas de Dados: Além dos Tipos Primitivos
Coleções como Vec, String e HashMap seguem as mesmas regras de ownership:
let mut v = vec![1, 2, 3];
let primeiro = v[0]; // i32 implementa Copy, então funciona
let segundo = &v[1]; // borrowing imutável
v.push(4); // ERRO: não pode mutar v enquanto existe referência imutável
Para structs, você pode ter ownership total ou referências:
struct Dados {
nome: String, // ownership
descricao: &'static str // referência com lifetime estático
}
let d = Dados {
nome: String::from("exemplo"),
descricao: "descrição fixa"
};
O pattern Cow (Clone-on-Write) é útil quando você quer flexibilidade: ele pode ser tanto um &T (emprestado) quanto um T (próprio), e só clona quando necessário:
use std::borrow::Cow;
fn processar<'a>(entrada: &'a str) -> Cow<'a, str> {
if entrada.contains(" ") {
Cow::Owned(entrada.replace(" ", "_"))
} else {
Cow::Borrowed(entrada)
}
}
6. Padrões Comuns para Quem Vem de GC: Adaptando sua Mente
Evitando clones desnecessários: A tentação inicial é clonar tudo para "resolver" problemas de borrow. Prefira usar referências:
// Ruim: clona sem necessidade
fn processar(dados: &Vec<String>) {
for item in dados.clone() { ... }
}
// Bom: usa referência
fn processar(dados: &Vec<String>) {
for item in dados.iter() { ... }
}
O padrão "take" com Option: Para extrair ownership temporariamente de uma struct:
struct Config {
nome: Option<String>,
}
impl Config {
fn consumir_nome(&mut self) -> String {
self.nome.take().unwrap_or_default()
}
}
Rc e Arc: Quando você realmente precisa de contagem de referências (como um GC manual):
use std::rc::Rc;
let dados = Rc::new(String::from("compartilhado"));
let a = Rc::clone(&dados);
let b = Rc::clone(&dados);
// dados só é liberado quando todos os Rc saírem de escopo
Rc é para single-thread; Arc (com Mutex ou RwLock) é para múltiplas threads.
7. Erros Clássicos e Como o Compilador te Salva
"Borrow of moved value": O erro mais comum. Correção: use referências ou clone quando necessário.
let s = String::from("teste");
let t = s; // move
// println!("{}", s); // ERRO
// Correção 1: clone
let s = String::from("teste");
let t = s.clone();
println!("{}", s);
// Correção 2: referência
let s = String::from("teste");
let t = &s;
println!("{}", s);
"Cannot borrow as mutable more than once": Comum em loops:
let mut v = vec![1, 2, 3];
for i in &v { // borrow imutável
v.push(4); // ERRO: tentando borrow mutável
}
Correção: iterar com índices ou coletar antes de modificar:
let mut v = vec![1, 2, 3];
let indices: Vec<_> = (0..v.len()).collect();
for i in indices {
v[i] += 1; // OK: borrow mutável exclusivo
}
"Lifetime mismatch": Ao trabalhar com structs que contêm referências:
struct Container<'a> {
item: &'a str,
}
fn criar_container() -> Container<'static> {
let local = String::from("temporário");
Container { item: &local } // ERRO: local não vive o suficiente
}
Use 'static com cautela — ele só funciona para literais de string ou dados que vivem toda a execução do programa.
O sistema de ownership de Rust pode parecer restritivo no início, mas é exatamente essa rigidez que garante segurança de memória sem garbage collector. Com prática, você passa a ver o compilador não como um obstáculo, mas como um parceiro que te ajuda a escrever código mais seguro e eficiente.
Referências
- The Rust Book: Ownership — Capítulo oficial sobre ownership, borrowing e lifetimes na documentação do Rust.
- Rust by Example: Ownership and Moves — Exemplos práticos de movimentação, cópia e borrowing com código executável.
- Rust Nomicon: Ownership — Aprofundamento técnico sobre as regras de ownership e o sistema de tipos.
- Common Rust Lifetime Misconceptions — Artigo detalhado sobre equívocos comuns com lifetimes, com exemplos claros.
- Learn Rust With Entirely Too Many Linked Lists — Tutorial prático que explora ownership, borrowing e lifetimes através da implementação de listas ligadas.