Ownership em Rust
Ownership (posse) é a ideia que faz o Rust ser Rust. É assim que a linguagem garante que a memória é usada com segurança, sem coletor de lixo (garbage collector) e sem você precisar liberar memória na mão. No começo parece estranho, mas a ideia central é simples. Vamos montá-la aos poucos.
O problema que o ownership resolve
Seção intitulada “O problema que o ownership resolve”Enquanto um programa roda, ele guarda dados na memória. Em algum momento essa memória precisa ser liberada. Se ninguém liberar, o programa vai acumulando memória que não usa mais (o famoso “vazamento de memória”) e, cedo ou tarde, trava. As linguagens costumam resolver isso de dois jeitos:
- Na mão (C/C++): você libera a memória. Esqueceu? Vazamento. Liberou cedo demais? O programa trava ou abre brechas de segurança.
- Com coletor de lixo (Java, Go, Python): um processo em segundo plano faz a limpeza para você. É prático, mas custa desempenho e deixa o programa menos previsível.
O Rust segue um terceiro caminho: ele decide, na hora de compilar, exatamente quando cada valor deve ser liberado. Para isso, ele segue um conjunto de regras de ownership. Nada de liberar memória na mão e nada de coletor de lixo.
As três regras do ownership
Seção intitulada “As três regras do ownership”Essas três regras são o sistema inteiro. Todo o resto vem delas:
- Cada valor tem um único dono (owner): a variável que “possui” esse valor.
- Só pode existir um dono por vez.
- Quando o dono sai do escopo, o valor é liberado automaticamente.
Vamos ver cada regra funcionando na prática.
Regra 3: o valor é liberado no fim do escopo
Seção intitulada “Regra 3: o valor é liberado no fim do escopo”Um escopo é o trecho entre { e }. Quando o escopo de uma variável termina, o Rust libera o valor dela automaticamente. Você nunca escreve free():
fn main() { { let message = String::from("olá"); // message é válida aqui println!("{message}"); } // <- o escopo termina e `message` é liberada automaticamente
// println!("{message}"); // ❌ erro: message não existe mais aqui}Você não precisou liberar nada. O Rust fez isso sozinho, no exato momento em que message saiu do escopo.
Regras 1 e 2: o “move”
Seção intitulada “Regras 1 e 2: o “move””Agora fica interessante. Veja o que acontece quando você atribui uma variável a outra:
fn main() { let s1 = String::from("olá"); let s2 = s1; // o ownership é MOVIDO de s1 para s2
println!("{s2}"); // ✅ funciona // println!("{s1}"); // ❌ erro: s1 foi movida}Quando você escreve let s2 = s1, o Rust não copia a string. Ele move o ownership de s1 para s2. Como a regra 2 diz que só pode haver um dono, s1 deixa de ser válida. Se você tentar usá-la, o compilador mostra:
error[E0382]: borrow of moved value: `s1`Traduzindo: “empréstimo de um valor que já foi movido: s1”. Ou seja, s1 já entregou o valor para s2 e não pode mais ser usada.
E se eu quiser duas cópias?
Seção intitulada “E se eu quiser duas cópias?”Se você realmente quer uma segunda cópia, independente da primeira, chame .clone():
fn main() { let s1 = String::from("olá"); let s2 = s1.clone(); // faz uma cópia completa
println!("s1 = {s1}, s2 = {s2}"); // ✅ agora as duas funcionam}O clone() é explícito de propósito: copiar pode sair caro, então o Rust faz você pedir a cópia com todas as letras.
Tipos simples são copiados, não movidos
Seção intitulada “Tipos simples são copiados, não movidos”Talvez você esteja se perguntando por que isto aqui não dá erro:
fn main() { let x = 5; let y = x; println!("x = {x}, y = {y}"); // ✅ as duas funcionam}Valores pequenos e de tamanho fixo, como inteiros, números de ponto flutuante, booleanos e char, são baratos de copiar. Por isso o Rust simplesmente os copia em vez de movê-los. Para quem está começando, vale esta regra prática:
- Valores simples (números,
bool,char) → são copiados. A variável original continua válida. - Dados maiores, como
String→ são movidos. A variável original deixa de valer.
Ownership e funções
Seção intitulada “Ownership e funções”Passar um valor para uma função também o move, igualzinho a atribuí-lo a outra variável:
fn take_ownership(s: String) { println!("Agora eu sou o dono de: {s}");} // s é liberada aqui
fn main() { let text = String::from("olá"); take_ownership(text); // println!("{text}"); // ❌ erro: text foi movida para dentro da função}É exatamente por isso que existe o próximo assunto: o borrowing (empréstimo). Com ele, uma função pode usar um valor sem virar dona dele, e você continua podendo usar esse valor depois.
Prática
Seção intitulada “Prática”Tente resolver cada exercício sozinho primeiro, no Rust Playground ou em um projeto local. Erros do compilador fazem parte do caminho: leia com atenção, porque eles costumam apontar direto para o move.
1. Mantenha o original
Seção intitulada “1. Mantenha o original”Este programa não compila. Corrija-o para que ele imprima rascunho -> rascunho v2, sem remover nenhuma das duas variáveis.
fn main() { let original = String::from("rascunho"); let mut edited = original; edited.push_str(" v2"); println!("{original} -> {edited}");}error[E0382]: borrow of moved value: `original` --> src/main.rs:5:16 |2 | let original = String::from("rascunho"); | -------- move occurs because `original` has type `String`, which does not implement the `Copy` trait3 | let mut edited = original; | -------- value moved here4 | edited.push_str(" v2");5 | println!("{original} -> {edited}"); | ^^^^^^^^ value borrowed here after moveO compilador avisa que original foi movido na linha 3 (“value moved here”) e depois usado na linha 5.
Dica: você precisa de duas strings independentes, para que mudar uma não afete a outra.
Ver solução
fn main() { let original = String::from("rascunho"); let mut edited = original.clone(); // uma cópia completa e separada edited.push_str(" v2"); println!("{original} -> {edited}");}Saída:
rascunho -> rascunho v2let mut edited = original; move a string, então original deixa de ser válida. O .clone() cria uma segunda string com a sua própria memória. Agora o push_str muda só edited, e original continua dizendo rascunho.
2. Devolva o valor
Seção intitulada “2. Devolva o valor”Escreva uma função add_signature que receba o ownership de uma String, acrescente " -- Ferris" no final e devolva a string. Chame essa função no main e imprima o resultado.
Dica: uma função pode retornar o valor que recebeu. Escreva -> String na assinatura e receba o parâmetro como mut text: String, para poder alterá-lo.
Ver solução
fn add_signature(mut text: String) -> String { text.push_str(" -- Ferris"); text // o ownership volta para quem chamou}
fn main() { let letter = String::from("Até logo!"); let letter = add_signature(letter); println!("{letter}");}Saída:
Até logo! -- FerrisO ownership entra na função com a chamada e sai de volta com o valor de retorno. Nós o recebemos em uma nova variável letter (fazendo shadowing da antiga). Isso funciona, mas ficar passando valores para dentro e para fora o tempo todo cansa. É exatamente esse o problema que o borrowing resolve.
3. Quem é o dono agora?
Seção intitulada “3. Quem é o dono agora?”Este programa não compila. Corrija-o para que ele imprima Ana entrou na lista e depois ["Ana"].
fn main() { let mut names = Vec::new(); let name = String::from("Ana"); names.push(name); println!("{name} entrou na lista"); println!("{:?}", names);}error[E0382]: borrow of moved value: `name` --> src/main.rs:5:16 |3 | let name = String::from("Ana"); | ---- move occurs because `name` has type `String`, which does not implement the `Copy` trait4 | names.push(name); | ---- value moved here5 | println!("{name} entrou na lista"); | ^^^^ value borrowed here after moveDica: o push recebe o ownership, igualzinho a uma chamada de função. Quem é o dono da string depois da linha 4? Use name antes do move, ou pergunte ao novo dono.
Ver solução
fn main() { let mut names = Vec::new(); let name = String::from("Ana"); println!("{name} entrou na lista"); // use enquanto ainda somos donos names.push(name); // agora o vetor é o dono println!("{:?}", names);}Saída:
Ana entrou na lista["Ana"]Depois de names.push(name), o vetor é o dono, e name não pode mais ser usada. Imprimir antes é uma forma de corrigir. Outra é manter a ordem original e imprimir names[0], porque o vetor ainda tem a string. Fazer um clone também funcionaria, mas aqui não há necessidade de copiar nada.
- O ownership permite que o Rust gerencie a memória com segurança, sem coletor de lixo.
- Três regras: cada valor tem um dono, só existe um dono por vez, e o valor é liberado quando o dono sai do escopo.
- Atribuir dados como uma
Stringa outra variável move esses dados: a variável original deixa de valer. - Use
.clone()para ter uma cópia independente. Tipos simples (números,bool,char) são copiados automaticamente. - Passar um valor para uma função também o move, e isso nos leva ao borrowing.
Teste o que aprendeu
Seção intitulada “Teste o que aprendeu”Teste o que aprendeu
1. Quantos donos um valor pode ter ao mesmo tempo em Rust?
2. Qual método você chama para fazer uma cópia independente de uma String em vez de movê-la?
3. Qual destes tipos é copiado automaticamente (e não movido) quando você o atribui a outra variável?
4. O que acontece quando você passa uma String para uma função que a recebe por valor (e não por referência)?
5. Segundo esta página, quando o Rust libera (faz o drop de) um valor automaticamente?
Pontuação: 0 / 5
Para saber mais
Seção intitulada “Para saber mais”- What Is Ownership? — The Rust Book (em inglês): o capítulo oficial, com desenhos de como uma
Stringfica na memória e de por que um move não a copia. - Ownership and moves — Rust by Example (em inglês): exemplos curtos que você pode rodar, mostrando moves, inclusive para dentro de funções.
- RAII — Rust by Example (em inglês): como os valores são liberados quando saem do escopo e onde o trait
Dropentra nessa história. Copy— documentação da biblioteca padrão (em inglês): quais tipos são copiados em vez de movidos, e a diferença entreCopyeClone.- Trilha de Rust — Exercism (em inglês): exercícios gratuitos com feedback, para quando você quiser praticar mais do que esta página oferece.