teoria

Regras de Ownership e Semântica de Movimento

Aprenda sobre regras de ownership e semântica de movimento

25 min
Aula 2 de 5

Regras de Ownership e Semântica de Movimento 🚀

Olá, futuros mestres do Rust! 👋

Nesta aula, mergulharemos no coração da promessa de segurança de memória do Rust: o Ownership System (Sistema de Propriedade). Este é um conceito fundamental e, embora possa parecer um pouco diferente no início, ele é o que permite ao Rust garantir a segurança de memória e a ausência de data races em concorrência, tudo sem a necessidade de um garbage collector. Prepare-se para desvendar o mistério por trás da "linguagem mais amada"!

1. Introdução: Por Que Ownership? 🤔

Em linguagens de programação que não possuem um garbage collector, gerenciar a memória é uma tarefa crucial. Erros comuns incluem:

  • Double Free: Tentar liberar a mesma memória duas vezes.
  • Use After Free: Acessar memória que já foi liberada.
  • Dangling Pointers: Ponteiros que apontam para memória que não é mais válida.

Rust resolve esses problemas com um conjunto de regras que o compilador verifica em tempo de compilação. Se você seguir essas regras, seu código estará livre desses bugs relacionados à memória. O cerne dessas regras é o conceito de Ownership.

Nesta aula, vamos explorar as regras de propriedade e como a semântica de movimento (move semantics) é a principal forma pela qual a propriedade é transferida em Rust.

2. Explicação Detalhada: As Regras de Ouro do Ownership ✨

O Rust tem um conjunto de regras simples que governam o Ownership:

  1. Cada valor em Rust tem uma variável que é chamada seu owner (proprietário).
  2. Pode haver apenas um owner por vez.
  3. Quando o owner sai do escopo, o valor será dropped (liberado).

Vamos entender cada uma delas com exemplos.

2.1. Escopo e o Fim da Vida de um Valor 🗑️

Um escopo é o intervalo dentro do qual um item é válido. Em Rust, isso geralmente é definido por chaves {}.

fn main() {
    { // s não é válido aqui, ainda não foi declarado
        let s = "hello"; // s é válido a partir deste ponto
        // faça algo com s
    } // Este escopo acabou, e s não é mais válido.
      // A memória associada a "hello" (se fosse um String alocado na heap)
      // seria liberada automaticamente aqui.
      // Para string literals (&str), eles são armazenados no binário e
      // não são alocados na heap, então não há liberação de memória.
}

Para tipos que alocam memória na heap (como String), Rust chama uma função especial drop automaticamente quando o owner sai do escopo. Isso é o que garante que a memória seja limpa sem a necessidade de um garbage collector.

2.2. Semântica de Movimento (Move Semantics) ➡️

Aqui é onde o Ownership realmente brilha e pode ser um pouco diferente de outras linguagens.

Considere o tipo String. Diferente de string literals (&str), que são imutáveis e armazenados diretamente no binário do programa, String é um tipo de dados que é alocado na heap e pode ser modificado.

fn main() {
    let s1 = String::from("hello"); // s1 é o owner da String "hello" na heap
    let s2 = s1; // 💡 O que acontece aqui?
                 // A propriedade de "hello" é MOVIDA de s1 para s2.
                 // s1 NÃO é mais válido após esta linha.
 
    // println!("s1: {}", s1); // ❌ ERRO DE COMPILAÇÃO!
                               // s1 foi movido e não pode ser usado.
    println!("s2: {}", s2); // ✅ Funciona! s2 agora é o owner.
}

Por que isso é importante? 🤔

Se Rust copiasse os dados da heap de s1 para s2 (uma "cópia profunda"), isso seria ineficiente para grandes strings. Além disso, se s1 e s2 apontassem para a mesma localização de memória na heap, quando s2 saísse do escopo, ele liberaria a memória. Então, quando s1 saísse do escopo, ele tentaria liberar a mesma memória novamente (um double free!), o que é um bug de segurança de memória.

Ao invalidar s1 após a atribuição a s2, Rust garante que:

  1. Apenas um owner (agora s2) está ativo por vez.
  2. A memória será liberada apenas uma vez quando s2 sair do escopo.

Este mecanismo é conhecido como semântica de movimento.

2.3. Tipos que Implementam o Trait Copy 🔄

Nem todos os tipos seguem a semântica de movimento. Alguns tipos, como inteiros (i32, u64), booleanos (bool), caracteres (char) e tuplas que contêm apenas tipos Copy, são considerados "simples" o suficiente para serem copiados em vez de movidos.

Quando um tipo implementa o trait Copy, uma cópia profunda é feita na stack (para os dados que estão na stack), e o valor original permanece válido.

fn main() {
    let x = 5; // x é um i32, que implementa Copy
    let y = x; // 💡 x é copiado para y. Ambos x e y são válidos.
 
    println!("x: {}, y: {}", x, y); // ✅ Funciona!
                                   // x e y são owners de cópias independentes.
 
    let s = String::from("world"); // String NÃO implementa Copy
    let s_copy = s.clone(); // Para copiar um String (deep copy), use .clone()
    println!("s: {}, s_copy: {}", s, s_copy); // ✅ Funciona!
                                             // s e s_copy são owners de cópias independentes.
}

Regra de Ouro do Copy Trait:

Um tipo pode implementar Copy se todos os seus componentes implementarem Copy. Se um tipo ou qualquer parte dele implementar Drop (como String faz para liberar sua memória da heap), ele não pode implementar Copy. Isso faz sentido, pois se ele pudesse ser copiado e o original ainda fosse válido, haveria dois drops para a mesma memória alocada na heap, levando a um double free.

2.4. Ownership e Funções 🤝

A passagem de valores para funções e o retorno de valores de funções também envolvem a transferência de propriedade.

Passando para Funções (Transferência de Ownership)

Quando você passa um valor para uma função, a propriedade é movida para a função.

fn takes_ownership(some_string: String) { // some_string entra no escopo
    println!("{}", some_string);
} // some_string sai do escopo e `drop` é chamado.
 
fn makes_copy(some_integer: i32) { // some_integer entra no escopo
    println!("{}", some_integer);
} // some_integer sai do escopo. Nada especial acontece aqui,
  // pois i32 é Copy e não aloca memória na heap.
 
fn main() {
    let s = String::from("hello Rust"); // s entra no escopo
    takes_ownership(s); // 💡 O valor de s é MOVIDO para takes_ownership.
                        // s NÃO é mais válido aqui.
 
    // println!("{}", s); // ❌ ERRO DE COMPILAÇÃO! s foi movido.
 
    let x = 5; // x entra no escopo
    makes_copy(x); // 💡 O valor de x é COPIADO para makes_copy.
                   // x AINDA é válido aqui.
 
    println!("{}", x); // ✅ Funciona!
}

Retornando Valores de Funções (Transferência de Ownership)

Quando uma função retorna um valor, a propriedade é movida da função para a variável que a recebe.

fn gives_ownership() -> String { // gives_ownership moverá seu valor de retorno para quem o chamar
    let some_string = String::from("yours"); // some_string entra no escopo
    some_string // some_string é retornado e sua propriedade é MOVIDA para a função chamadora.
}
 
fn takes_and_gives_back(a_string: String) -> String { // a_string entra no escopo
    a_string // a_string é retornado e sua propriedade é MOVIDA para a função chamadora.
}
 
fn main() {
    let s1 = gives_ownership(); // 💡 gives_ownership move seu retorno para s1.
    println!("s1: {}", s1);
 
    let s2 = String::from("hello"); // s2 entra no escopo
    let s3 = takes_and_gives_back(s2); // 💡 s2 é movido para takes_and_gives_back,
                                       // que então move seu retorno para s3.
                                       // s2 NÃO é mais válido.
    // println!("s2: {}", s2); // ❌ ERRO DE COMPILAÇÃO!
    println!("s3: {}", s3);
}

Essa constante transferência de propriedade pode parecer um pouco trabalhosa, especialmente se você apenas quer "usar" um valor sem se tornar seu owner. É aqui que o Borrowing (Empréstimo), nosso próximo tópico, entra em cena para nos salvar! 🦸‍♂️

3. Código de Exemplo Oficial (Adaptado da Documentação) 📚

Os exemplos acima são adaptados diretamente dos conceitos apresentados no Rust Book, Capítulo 4: Understanding Ownership. A documentação oficial é a sua melhor amiga para aprofundar qualquer conceito em Rust!

4. Exercício/Desafio Conceitual 🧠

Considerando as regras de Ownership e semântica de movimento:

  1. Explique por que o código abaixo não compila e sugira uma forma de corrigi-lo para que tanto v1 quanto v2 possam ser usados após a atribuição, sem usar .clone().

    fn main() {
        let v1 = vec![1, 2, 3];
        let v2 = v1; // Linha problemática
        println!("v1: {:?}", v1);
        println!("v2: {:?}", v2);
    }
    Clique para ver a resposta

    Explicação: O tipo Vec<T> (vetor) aloca seus dados na heap, assim como String. Portanto, ele não implementa o trait Copy. Quando v1 é atribuído a v2 (let v2 = v1;), a propriedade do vetor é movida de v1 para v2. Após essa linha, v1 é invalidado e não pode mais ser usado, pois o Rust impede um double free potencial.

    Sugestão de correção (sem .clone()): Para usar v1 e v2 independentemente, sem copiar os dados da heap, precisaríamos que v1 fosse um tipo Copy. Como Vec<T> não é Copy, a única forma de "usar" v1 após a atribuição a v2 sem .clone() seria se v1 nunca tivesse sido movido em primeiro lugar.

    No entanto, se o objetivo é ter dois owners independentes do mesmo conjunto de dados, clone() é a solução idiomática para tipos que alocam na heap.

    Outra forma de "usar" o valor sem ser o owner (e que veremos na próxima aula) é através de referências (borrowing). Por exemplo:

    fn main() {
        let v1 = vec![1, 2, 3];
        // Em vez de mover, passamos uma referência imutável
        let v2_ref = &v1; // v2_ref "empresta" v1. v1 ainda é o owner.
     
        println!("v1: {:?}", v1); // v1 ainda é válido
        println!("v2_ref: {:?}", v2_ref); // v2_ref pode ser usado
    }

    Isso nos leva perfeitamente ao próximo tópico: Borrowing!

5. Resumo e Próximos Passos 🏁

Nesta aula, desvendamos as regras fundamentais do Ownership em Rust:

  • Cada valor tem um único owner.
  • Quando o owner sai do escopo, o valor é liberado.
  • A propriedade é movida por padrão para tipos que alocam memória na heap (como String e Vec).
  • Tipos Copy são copiados, não movidos, mantendo o valor original válido.

Compreender a semântica de movimento é crucial para escrever código Rust eficiente e seguro.

No entanto, vimos que a constante transferência de propriedade pode ser inconveniente. Na próxima aula, exploraremos o Borrowing (Empréstimo), que nos permitirá usar valores sem tomar sua propriedade, uma técnica essencial para a flexibilidade do Rust sem comprometer a segurança.

Até lá, continue praticando e internalizando esses conceitos! 💪

© 2025 Escola All Dev. Todos os direitos reservados.

Regras de Ownership e Semântica de Movimento - Curso gratuito de Rust: A linguagem mais amada | escola.all.dev.br