Ownership é o conceito mais distintivo e fundamental do Rust. Ele permite que o Rust garanta segurança de memória sem a necessidade de um garbage collector, através de um conjunto de regras que o compilador verifica em tempo de compilação. Nesta aula, vamos explorar as regras de ownership, como a memória é gerenciada entre stack e heap, os mecanismos de move e drop, e entender por que tudo isso é importante para escrever código eficiente e seguro.

Dominar ownership é essencial para programar em Rust de forma eficaz, pois influencia diretamente como você estrutura seus dados e funções. Vamos começar com as regras básicas.

Regras de ownership

Em Rust, cada valor tem um único dono (owner) a cada momento. Quando o dono sai de escopo, o valor é descartado. Existem três regras principais:

  • Cada valor em Rust tem uma variável que é seu dono.
  • Só pode haver um dono por vez.
  • Quando o dono sai de escopo, o valor é liberado.

Essas regras são verificadas em tempo de compilação, prevenindo erros como uso após liberação, dupla liberação e ponteiros soltos. Vejamos um exemplo:

fn main() {
    let s1 = String::from("hello"); // s1 é dono da String
    let s2 = s1; // ownership é movido para s2
    // println!("{}", s1); // erro! s1 não é mais válido
    println!("{}", s2); // ok
} // s2 sai de escopo, a String é liberada

Note que quando atribuímos s1 a s2, o ownership é movido. Se tentarmos usar s1 depois, o compilador impede, evitando acesso a memória inválida.

Stack vs heap

Em Rust, os dados podem ser armazenados na stack ou no heap. A stack é rápida e organizada (LIFO), usada para dados de tamanho fixo conhecido em tempo de compilação, como inteiros e booleanos. O heap é mais flexível para dados de tamanho dinâmico, como String e Vec, mas requer alocação e liberação explícitas, que o Rust gerencia via ownership.

Quando você cria uma String, o conteúdo (os caracteres) fica no heap, enquanto um ponteiro, tamanho e capacidade ficam na stack. O ownership garante que quando a variável dona sai de escopo, o Rust chama drop automaticamente para liberar a memória do heap. Exemplo:

fn main() {
    let s = String::from("hello"); // s é dono da String
    // s está na stack, apontando para o heap
    println!("{}", s);
} // s sai de escopo, drop é chamado, memória do heap liberada

Sem ownership, seria fácil esquecer de liberar o heap (vazamento) ou liberar duas vezes. O Rust evita isso com as regras de ownership.

Move e drop

O movimento (move) ocorre quando um valor é atribuído a outra variável ou passado para uma função, transferindo o ownership. O drop é a liberação automática da memória quando o dono sai de escopo. Vamos ver exemplos:

fn main() {
    let s1 = String::from("hello");
    let s2 = s1; // s1 é movido para s2
    // s1 não é mais válido
    
    let s3 = s2.clone(); // clone copia dados do heap, s2 continua válido
    println!("s2 = {}, s3 = {}", s2, s3);
} // s2 e s3 saem de escopo, ambos liberados

Passar um valor para uma função também move:

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

fn main() {
    let s = String::from("hello");
    take_ownership(s); // s é movido para a função
    // println!("{}", s); // erro! s não é mais válido
}

Para evitar o move, podemos usar referências (empréstimo), que serão abordadas em aulas futuras. O drop é implementado via trait Drop, que você pode customizar.

Por que isso importa

O sistema de ownership é o que torna Rust único: ele elimina toda uma classe de bugs de memória (uso após liberação, dupla liberação, vazamentos) sem sacrificar performance. Em linguagens como C++, você precisa gerenciar manualmente a memória; em Java, há garbage collector com pausas. Rust oferece o melhor dos dois mundos: segurança e desempenho, com custo zero de abstração.

Além disso, ownership permite que o Rust tenha concorrência sem data races, como veremos em aulas futuras. Entender ownership é o primeiro passo para dominar Rust e escrever código confiável e eficiente.

Boas práticas

  • Prefira usar referências (&T) quando não precisar de ownership, para evitar moves desnecessários.
  • Use clone() apenas quando realmente precisar de uma cópia independente, pois é custoso.
  • Lembre-se que tipos como i32 implementam Copy, então eles são copiados em vez de movidos.

Referências

Exercícios

  1. O que acontece se você tentar usar uma variável após mover seu valor para outra variável? Dê um exemplo.

    ✓ Resposta: O compilador gera um erro de compilação, pois a variável original não é mais válida. Exemplo:
    let s1 = String::from("hello");
    let s2 = s1;
    println!("{}", s1); // erro: use of moved value: `s1`
  2. Explique a diferença entre stack e heap em Rust. Dê exemplos de tipos que vão para cada um.

    ✓ Resposta: A stack armazena dados de tamanho fixo conhecido em tempo de compilação, como i32, bool, tuplas de tipos fixos. O heap armazena dados de tamanho dinâmico, como String e Vec. A stack é rápida e organizada, enquanto o heap requer alocação dinâmica e é gerenciado pelo ownership.
  3. O que é o trait Drop e quando ele é chamado?

    ✓ Resposta: O trait Drop permite customizar o que acontece quando um valor sai de escopo. É chamado automaticamente pelo Rust no final do escopo do dono. Exemplo:
    struct Custom;
    impl Drop for Custom {
        fn drop(&mut self) {
            println!("Custom dropped");
        }
    }
    fn main() {
        let c = Custom;
    } // imprime "Custom dropped"
  4. Considere o código: let x = 5; let y = x; println!("{}", x);. Por que isso funciona, ao contrário de String?

    ✓ Resposta: Porque i32 implementa o trait Copy, que faz com que a atribuição copie o valor em vez de movê-lo. Portanto, x permanece válido. Tipos como String não implementam Copy por serem alocados no heap.
  5. Escreva uma função que receba uma String e retorne o seu comprimento, sem consumir a String (ou seja, permitindo que a String continue sendo usada após a chamada). Dica: use referência.

    ✓ Resposta: Use uma referência:
    fn len(s: &String) -> usize {
        s.len()
    }
    fn main() {
        let s = String::from("hello");
        let l = len(&s);
        println!("{} length {}", s, l); // s continua válido
    }