Ownership é o sistema de gerenciamento de memória do Rust que elimina a necessidade de um garbage collector. Ele garante segurança de memória em tempo de compilação através de um conjunto de regras que controlam como os valores são criados, movidos e destruídos. Nesta aula, vamos explorar as regras de ownership, a diferença entre stack e heap, os mecanismos de move e drop, e por que isso é tão importante para programação segura e eficiente.

Entender ownership é fundamental para dominar Rust, pois afeta como você escreve código, gerencia recursos e evita erros comuns como uso após liberação e vazamentos de memória. Vamos começar com as regras básicas.

Regras de ownership

O sistema de ownership em Rust segue três regras simples, mas poderosas:

  • Cada valor em Rust tem uma variável que é sua dona (owner).
  • Só pode haver um dono por vez.
  • Quando o dono sai de escopo, o valor é descartado (drop).

Vamos ver um exemplo prático. Quando você declara uma variável, ela se torna a dona do valor. Se você atribuir essa variável a outra, a ownership é transferida (move), e a variável original não pode mais ser usada. Isso evita duplicação acidental de dados e garante que a memória seja liberada corretamente.

let s1 = String::from("hello");
let s2 = s1; // move: s1 não é mais válida
// println!("{}", s1); // erro: s1 foi movida
println!("{}", s2); // ok

Outro aspecto importante é que tipos simples como inteiros implementam a trait Copy, então eles são copiados em vez de movidos. Isso ocorre porque esses tipos são armazenados na stack e são baratos de copiar.

let x = 5;
let y = x; // cópia, x ainda é válido
println!("x = {}, y = {}", x, y);

Stack vs heap

Para entender ownership, é crucial saber a diferença entre stack e heap. A stack é uma estrutura de dados LIFO (last in, first out) que armazena valores de tamanho fixo conhecido em tempo de compilação. O heap, por outro lado, armazena dados de tamanho dinâmico, alocados em tempo de execução.

Em Rust, tipos como String e Vec são alocados no heap. A variável na stack contém um ponteiro para o heap, além do comprimento e capacidade. Quando você move um valor heap, o Rust copia o ponteiro (e metadados) para a nova variável, mas não copia os dados do heap. Isso é eficiente, mas significa que apenas um dono existe para evitar dupla liberação.

let s1 = String::from("hello");
let s2 = s1; // move: s1 é inválido
// s1 não pode mais ser usado

Move e drop

O move é a transferência de ownership de um valor. Quando você atribui um valor a outra variável, passa como argumento para uma função ou retorna de uma função, a ownership pode ser movida. Após o move, a variável original não pode mais ser usada, evitando referências pendentes.

fn take_ownership(s: String) {
    println!("{}", s);
} // s é dropado aqui

let s = String::from("world");
take_ownership(s); // s é movido para a função
// println!("{}", s); // erro: s foi movido

O drop é a liberação da memória quando o dono sai de escopo. Rust chama automaticamente a função drop da trait Drop para limpar recursos. Você pode implementar Drop para tipos personalizados, mas geralmente o compilador cuida disso para tipos padrão.

{
    let s = String::from("temporary");
    // s é usado aqui
} // s sai de escopo e é dropado

Por que isso importa

Ownership resolve problemas clássicos de gerenciamento de memória sem a necessidade de um garbage collector. Ele previne:

  • Uso após liberação: como o dono único garante que o valor não seja acessado após ser dropado.
  • Dupla liberação: porque apenas um dono pode liberar a memória.
  • Vazamentos de memória: o drop automático no fim do escopo garante que a memória seja liberada.

Além disso, ownership permite que Rust seja tão rápido quanto C/C++ em muitas tarefas, pois não há overhead de garbage collector. Para o programador, isso significa mais controle sobre recursos e previsibilidade de desempenho. Combinado com o sistema de empréstimos (borrowing) e lifetimes, ownership torna o código seguro por construção.

Boas práticas

  • Prefira tipos que implementam Copy para valores pequenos e imutáveis.
  • Use referências (&T) para emprestar dados sem transferir ownership.
  • Evite clonar desnecessariamente; clone apenas quando precisar de uma cópia independente.

Referências

Exercícios

  1. Explique o que acontece com a variável s1 após let s2 = s1; se s1 é uma String. O que ocorreria se s1 fosse um i32?

    ✓ Resposta: Se s1 é String, a ownership é movida para s2 e s1 se torna inválida, não podendo mais ser usada. Se s1 é i32, como implementa Copy, o valor é copiado e ambas as variáveis permanecem válidas.
  2. O que o código a seguir imprime? Justifique.

    let x = 10;
    let y = x;
    println!("{}", x);
    

    ✓ Resposta: Imprime 10. i32 implementa Copy, então y = x faz uma cópia, e x permanece válido.
  3. Considere a função fn foo(s: String) { }. O que acontece com a String passada? Como evitar a perda de ownership?

    ✓ Resposta: A String é movida para a função, e após a execução, é dropada. Para evitar a perda, pode-se retornar a String da função ou passar uma referência &String.
  4. Escreva um código que demonstre a transferência de ownership ao retornar um valor de uma função.

    ✓ Resposta:
    fn give_ownership() -> String {
        let s = String::from("hello");
        s // ownership é movida para quem chamou
    }
    
    let my_string = give_ownership();
    println!("{}", my_string);
    
  5. Explique a diferença entre mover e clonar um valor heap. Dê um exemplo onde clonar é necessário.

    ✓ Resposta: Mover transfere ownership sem copiar os dados do heap; clonar cria uma cópia profunda, duplicando os dados. Clonar é necessário quando se quer manter a variável original e ter uma cópia independente. Exemplo: let s1 = String::from("hello"); let s2 = s1.clone(); — ambas válidas.