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:

  1. Cada valor em Rust tem um único dono (owner)
  2. Só pode existir um dono por vez
  3. 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