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
- The Rust Programming Language, Cap. 4 — Understanding Ownership — https://doc.rust-lang.org/book/ch04-00-understanding-ownership.html
- Rust by Example — Ownership and moves — https://doc.rust-lang.org/rust-by-example/scope/move.html
- Visualizing Memory Layout of Rust's Data Types — Raph Levien — https://docs.google.com/presentation/d/1q-c7UAyrUlM-eZyTo1pd8SZ0qwA_wYxmPZVOQkoDmH4
- Jon Gjengset — Crust of Rust: Ownership — aula em vídeo aprofundada — https://www.youtube.com/watch?v=8M0QfLUDaaA
- Rustlings, seção
move_semantics— https://github.com/rust-lang/rustlings