Ownership — A Ideia que Muda Tudo

Chegamos ao artigo mais importante da série. Tudo que aprendemos até aqui — variáveis, tipos, funções, controle de fluxo — existe em outras linguagens com pequenas variações. Mas ownership é exclusivo de Rust. É o conceito que explica por que Rust é seguro sem coletor de lixo, e é o conceito que mais desafia quem vem de outras linguagens.

Reserve tempo para este artigo. Leia com calma. Volte quantas vezes precisar.


Por que gerenciamento de memória é difícil

Para entender ownership, precisamos primeiro entender o problema que ele resolve.

Todo programa usa memória. Existem basicamente duas regiões que nos interessam:

A stack é rápida, organizada, e funciona como uma pilha de pratos: o último a entrar é o primeiro a sair. Variáveis de tamanho conhecido em tempo de compilação vivem aqui — inteiros, booleanos, floats, tuplas de tamanho fixo. Quando uma função termina, toda a sua stack é liberada automaticamente.

O heap é uma região de memória maior e mais flexível, usada para dados cujo tamanho não se conhece em tempo de compilação — ou que precisam sobreviver além do escopo onde foram criados. Mas o heap tem um custo: você precisa gerenciá-lo manualmente ou ter um sistema que faça isso por você.

Em C, você gerencia manualmente com malloc e free. Esqueça o free e você tem um memory leak. Libere duas vezes e você tem um double free, que corrompe o programa. Use a memória depois de liberar e você tem um use-after-free — porta de entrada para ataques de segurança.

Em Java, Python e Go, um coletor de lixo monitora quais objetos ainda são referenciados e libera os que não são mais usados. Seguro — mas com custo: pausas imprevisíveis, overhead de CPU e memória.

Rust escolheu um terceiro caminho: ownership.


As três regras de Ownership

Ownership em Rust se resume a três regras. O compilador as verifica em tempo de compilação — sem custo em execução:

Regra 1: Cada valor em Rust tem exatamente um dono.

Regra 2: Só pode haver um dono por vez.

Regra 3: Quando o dono sai de escopo, o valor é destruído.

Simples de enunciar. Profundas nas consequências. Vamos explorar cada uma.


Escopo e destruição automática

Comece com o mais simples — a regra 3:

fn main() {
    {
        let s = String::from("olá"); // s entra em escopo
        println!("{s}");
    } // s sai de escopo aqui — memória liberada automaticamente

    // println!("{s}"); // ERRO: s não existe mais
}

Quando s sai do bloco, Rust chama automaticamente uma função especial chamada drop — que libera a memória do heap. Isso acontece deterministicamente, sempre no mesmo ponto, sem coletor de lixo.

Note que usamos String aqui, não &str. A diferença é importante:

  • &str é uma referência a uma string de tamanho fixo, geralmente armazenada no binário do programa. Vive na stack.
  • String é uma string dinâmica, alocada no heap, que pode crescer e mudar.

Move — a regra 2 em ação

Aqui está onde a maioria das pessoas tropeça pela primeira vez:

fn main() {
    let s1 = String::from("hello");
    let s2 = s1; // s1 é movido para s2

    println!("{s1}"); // ERRO: s1 foi movido!
}

Quando você escreve let s2 = s1, em outras linguagens esperaria uma cópia ou dois ponteiros para o mesmo dado. Em Rust, ocorre um move: a propriedade do valor é transferida de s1 para s2. Após isso, s1 não existe mais — o compilador o invalida.

Por quê? Porque se dois nomes apontassem para o mesmo dado no heap, quando ambos saíssem de escopo, o drop seria chamado duas vezes — o famoso double free. Rust simplesmente não permite que isso aconteça.

Visualmente, o que ocorre:

Antes do move:
s1 --> [ ptr | len=5 | cap=5 ] --> heap: "hello"

Após let s2 = s1:
s1 --> (inválido)
s2 --> [ ptr | len=5 | cap=5 ] --> heap: "hello"

Clone — quando você realmente quer uma cópia

Se você precisa de uma cópia independente do dado no heap, use .clone():

fn main() {
    let s1 = String::from("hello");
    let s2 = s1.clone(); // cópia profunda do heap

    println!("s1 = {s1}"); // funciona!
    println!("s2 = {s2}"); // funciona!
}

O clone cria uma segunda cópia completa dos dados no heap. Agora s1 e s2 são independentes, cada um com seu próprio dono. Ambos serão destruídos quando saírem de escopo.

O .clone() é explícito por design — em Rust, operações custosas nunca acontecem nas suas costas. Se você está clonando, está dizendo conscientemente: "Sei que isso tem um custo, e quero fazê-lo assim mesmo."


Tipos que copiam automaticamente

Mas espera — no artigo #02 fizemos isso sem problemas:

fn main() {
    let x = 5;
    let y = x;
    println!("{x} {y}"); // funciona!
}

Por que x não foi movido? Porque i32 é um tipo que vive inteiramente na stack, de tamanho fixo e conhecido. Para esses tipos, copiar é tão barato quanto mover — então Rust simplesmente copia. Não há risco de double free porque não há heap envolvido.

Tipos com essa propriedade implementam o trait Copy. São eles: todos os inteiros, floats, bool, char, e tuplas compostas apenas de tipos Copy. String não implementa Copy — ela tem dados no heap.


Ownership e funções

As mesmas regras se aplicam quando você passa valores para funções:

fn consumir(s: String) {
    println!("{s}");
} // s é destruído aqui

fn main() {
    let minha_string = String::from("mundo");
    consumir(minha_string); // ownership transferido para a função

    // println!("{minha_string}"); // ERRO: foi movido!
}

Passar uma String para uma função é um move. A função se torna a nova dona. Quando a função termina, a string é destruída.

E quando a função retorna um valor, o ownership é transferido de volta:

fn criar_string() -> String {
    let s = String::from("novo valor");
    s // ownership transferido para quem chamou
}

fn main() {
    let minha = criar_string();
    println!("{minha}");
} // minha é destruída aqui

O problema que isso cria

Imagine que você quer usar uma string em uma função mas ainda precisa dela depois:

fn tamanho(s: String) -> usize {
    s.len()
}

fn main() {
    let s = String::from("hello");
    let tam = tamanho(s);
    // println!("{s}"); // ERRO! s foi movido para tamanho()
    println!("Tamanho: {tam}");
}

Uma solução seria retornar a string de volta junto com o resultado:

fn tamanho(s: String) -> (String, usize) {
    let len = s.len();
    (s, len) // devolve a string junto com o resultado
}

fn main() {
    let s = String::from("hello");
    let (s, tam) = tamanho(s);
    println!("{s} tem {tam} caracteres");
}

Funciona — mas é verboso e inconveniente. Ter que devolver toda variável que você usa em uma função seria insuportável em programas reais.

É exatamente para resolver isso que Rust introduz o conceito de borrowing — referências que permitem usar um valor sem tomar posse dele.


Um vislumbre do próximo artigo

O borrowing usa o símbolo & para criar uma referência:

fn tamanho(s: &String) -> usize {
    s.len()
}

fn main() {
    let s = String::from("hello");
    let tam = tamanho(&s); // emprestamos s, não movemos
    println!("{s} tem {tam} caracteres"); // s ainda é válida!
}

Com &s, estamos emprestando a string para a função — ela pode usá-la, mas não é a dona. Quando a função termina, ela devolve o empréstimo automaticamente. s continua válida no main.

Essa é a essência do borrowing — e será o tema completo do Artigo #06.


Resumo visual das regras

// MOVE: ownership transferido
let s1 = String::from("a");
let s2 = s1;          // s1 inválido

// CLONE: cópia independente
let s1 = String::from("a");
let s2 = s1.clone();  // s1 e s2 válidos

// COPY: tipos simples copiam automaticamente
let x: i32 = 5;
let y = x;            // x e y válidos

// BORROW (preview): referência sem transferir ownership
let s1 = String::from("a");
let tam = tamanho(&s1); // s1 ainda válido

O compilador como professor

Ownership é a parte de Rust que mais gera erros nos primeiros dias. Mas cada erro do compilador é uma lição — ele te diz exatamente o que aconteceu e frequentemente sugere a correção. Não tente contornar os erros. Tente entendê-los.

Com o tempo, você vai internalizar as regras a ponto de escrevê-las naturalmente. E quando isso acontecer, vai perceber que está escrevendo código que simplesmente não tem certos bugs — não porque você é mais cuidadoso, mas porque o compilador tornou esses bugs impossíveis.


Fontes e leituras recomendadas