Sistema de Ownership do Rust

Photo by Gemma Evans on Unsplash

Sistema de Ownership do Rust

10 coisas que você precisa saber

Linguagens como C/C++ até dão liberdade pra gente controlar a memória diretamente. Mas isso é tiro no pé! É muito fácil cometer erros que causam crashs, brechas de segurança ou bugs chatíssimos.

Alguns pepinos clássicos que devs C/C++ encaram:

  • Tentar usar um pedaço de memória que já foi liberado. Aí é crash na certa ou bugs bizarros depois.

  • Esquecer de liberar memória depois de usar. O resultado é aquele vazamento de memória (memory leak) que vai deixando o programa lento.

  • Dois trechos de código tentando manipular a mesma memória ao mesmo tempo. Isso causa umas "corridas de dados" (race conditions) horrorosas.

Para evitar esses trampos, Rust usa um sistema de ownership (que dá pra traduzir como "propriedade"). São umas regrinhas que o compilador checa para garantir que a memória seja usada corretamente.

A ideia chave é que todo valor em Rust tem um "dono". O dono é responsável por esse valor - controla quando ele é criado e destruído, quem pode acessar, etc.

Com esse controle de ownership, o compilador do Rust garante que os valores são válidos quando usados, previne problemas de dados concorrentes, e libera memória automaticamente quando preciso. E tudo isso sem precisar de um coletor de lixo (garbage collector)!

Esse modelo de propriedade é o que permite o Rust ser seguro e veloz ao mesmo tempo. Seguindo algumas regras simples, seus programas em Rust vão estar protegidos de vários problemas de memória muito comuns.

Vamos ver agora os 10superpoderes de ownership que o Rust tem:


1- Todo valor tem um owner "dono" (que é uma variável)

Em Rust, todo valor – seja uma string, um número, qualquer coisa - tem um owner. O owner é a variável que está conectada a esse valor. Por exemplo:

let x = 10;

"Aqui, a variável x é a owner do valor 10. Ela é quem controla e gerencia esse 10.

Pensa nisto como se: o x ganhasse posse "ownership" desse valor 10 e ficasse responsável por ele. Ela é a chefona daquele 10 agora!

Esse sistema de ownership "propriedade ou posse" evita aquela confusão clássica de várias variáveis apontando pra mesma coisa. Com cada valor tendo só um owner, fica claro que o 5 pertence à x e ninguém mais.

2- Se o owner "sai da área" ou sai do escopo**, o valor é descartado**

Quando a variável que é owner "dona" do valor sai de escopo (aquela história de sair da chave {}), o Rust automaticamente chama uma função chamada drop e manda aquele valor embora (limpa ele da memória):

{ 
  let y = 10; // y se torna a owner do valor 10
} // y sai de escopo, e o valor 10 é "descartado" automaticamente pelo Rust

O "escopo" é o bloco de código onde a variável existe. No exemplo acima, a y só existe dentro das chaves {}. Quando o programa sai desse bloco, a y "some", e aí o valor 10 é liberado automaticamente.

Essa liberação automática de dados previne aqueles vazamentos chatos de memória. Assim que a ownery desaparece, Rust arruma a casa! Chega de se preocupar com ponteiros soltos ou memória enchendo sem parar!

3- Só pode existir um owner "dono" de cada vez

Rust garante que cada valor tenha só um dono, e não mais que isso. É por isso que não existe aquele esquema complicado de ficar contando referências a um mesmo valor:


let z = 10; // z é dona do 10
let x = z; // A "chave" de propriedade do valor 10 passa pra x
// Agora a z não é mais dona do 10!

Nesse exemplo, a transferência de ownership da z pra x é uma operação bem simples. Algumas linguagens usam o tal do "reference counting", onde várias variáveis podem apontar pra mesmo valor, mas isso tem um custo de processamento.

Com a regra de ownership, o Rust só precisa atualizar uma variável interna pra mover a propriedade de z pra x. Chega de ficar atualizando contadores toda hora! 😡

4- Quando o owner "dono" é "copiado", os dados se mudam

Quando você passa uma variável que é owner de alguma coisa pra outra, a propriedade muda:

let s1 = "tiago".to_string(); // s1 se torna a owner "dona" de "tiago"
let s2 = s1; // A propriedade passa pra s2
             // s1 não pode mais usar "tiago"

Aqui, a gente criou a string "tiago" e atribuiu pra s1. Depois, passamos s1 pra outra variável, s2.

Nesse momento, a ownership passa de s1 pra s2. A s1 não é mais owner da string! Os dados em si não foram copiados, só a propriedade que mudou de lugar.

Isso previne aquelas cópias sem querer que "pesam" no programa. Pra copiar os dados mesmo, você tem que usar o método clone() do Rust, aí fica claro que era essa a intenção.

5- Dá pra "emprestar" a propriedade com referências

Podemos criar variáveis com referências que pegam a propriedade emprestada "borrow ownership":

let s = "tiago".to_string(); // s é dona de "tiago"

let r = &s;  // r pega uma referência de s (pega emprestado)
             // s continua sendo dona de "tiago"

println!("{}", r); // mostra "tiago"

O operador & cria uma referência r, que pega a propriedade "emprestada" da variável s, mas só dentro desse bloco de código (escopo).

Pensa em r como um empréstimo temporário dos dados que s é dona. A s continua com a posse cheia dos dados. A r só está pegando pra dar uma lida na string "tiago".

6- Referências mutáveis não podem ser compartilhadas

Só pode existir uma única referência mutável pra um dado ao mesmo tempo:

let mut s = "tiago".to_string();  

let r1 = &mut s; // r1 pega s emprestada pra alterar

let r2 = &mut s; // Erro!

Isso é pra prevenir aquelas encrencas quando vários trechos de código tentam modificar as coisas ao mesmo tempo (race conditions). A referência mutável r1 tem acesso exclusivo pra escrever na string s, então nenhuma outra referência é permitida enquanto r1 não terminar.

Essa regra salva a gente daqueles bugs escondidos, porque simplesmente impossibilita que um acesso aconteça enquanto outro não tiver sido finalizado.

7- Referências não podem "viver" mais que seus donos

As referências precisam ter um tempo de vida menor do que o valor que estão pegando emprestado:

{
  let r; // variável sem valor
  let s = "tiago".to_string(); 

  r = &s; // Erro! A referência r não viveria por tempo suficiente  

} // a string s é descartada aqui

Nesse caso, a referência r sai de escopo antes da s. Isso faria a variável r apontar para um dado (s) que já foi descartado! O Rust previne esses bugs (usando o tal do "use after free** justamente por essa regra: as referências não podem durar mais que os dados a que se referem.

8- Structs podem ser passadas por cópia ou referência

Dá pra transferir ou emprestar a propriedade de dados dentro de uma struct (vou preparar um artigo sobre struct) também:


struct User {
  name: String,
  age: u32  
}

let user1 = User { ... }; // user1 é dono da struct

let user2 = user1; // Propriedade foi transferida pra user2
                   // user1 não pode mais usar a struct

let borrow = &user1; // Pega a struct emprestada com uma referência
                     // user1 continua sendo dono dos dados

As structs agrupam dados relacionados, mas as regras de propriedade continuam valendo pra tudo que tem dentro delas.

Podemos passar a estrutura pra funções ou outras threads como dono ou podemos emprestar só a referência imutável. As mesmas regras de dono único e empréstimo (owner/borrowing rules) valem, garantindo a segurança.

9- Ownership funciona igual com dados no heap

As regras de propriedade também valem pro heap! Veja este exemplo:

let s1 = String::from("tiago"); // a variável s1, que tá na pilha, 
                                // é dona do dado "tiago" que foi alocado
                                // no heap 

let s2 = s1.clone(); // os dados no heap foram copiados pra uma nova área
                     // s1 e s2 passam a ser donas de dados diferentes

let r = &s1; // r pega uma referência imutável aos dados que s1 possui no heap 
             // s1 continua sendo dona desses dados

Repare que a variável s1 fica na pilha, mas a string "tiago" fica no heap. Mesmo assim, as regras são as mesmas!

O Rust previne a liberação do mesmo trecho de memória várias vezes, ou que a memória seja usada depois de liberada, mesmo quando estão envolvidos dados no heap. O sistema de ownership garante a segurança das alocações no heap também.

10- Ownership é fundamental pra concorrência (ou paralelismo) segura

O sistema de ownership é o segredo que faz o Rust ser bom com concorrência:

use std::thread;

let v = vec![1, 2, 3]; 

let handle = thread::spawn(move || {
  println!("Aqui tá o vetor: {:?}", v);
});

handle.join().unwrap();

A palavra move força a transferência de propriedade do vetor v pra nova thread que criamos. Isso previne que essa variável seja acessada ao mesmo tempo por várias threads.

Por causa do sistema de ownership, concorrência no Rust é muito mais tranquila. Não precisa ficar gerenciando bloqueios (locks) porque o compilador cuida da regra de um dono por vez.


Conclusão

O sistema de ownership do Rust existe pra deixar seu código mais seguro e rápido. Umas poucas regras são a chave pra eliminar várias classes de bugs relacionados à memória!

Pontos importantes sobre ownership:

  • Todo valor em Rust tem uma variável "dona" que é responsável por cuidar dele.

  • Quando a variável dona desaparece (sai de escopo), o valor é automaticamente descartado. Chega de memory leaks!

  • Um valor só pode ter um dono por vez, pra evitar confusão.

  • Referências podem pegar a propriedade "emprestada" temporariamente, de maneira segura.

  • O compilador cuida pra todas as referências serem válidas e evitar aqueles ponteiros pra lugar nenhum!

  • As regras de propriedade previnem problemas com acessos simultâneos (race conditions) e facilitam o uso de concorrência (ou paralelismo).

Mesmo que o sistema de ownership te faça ficar pensando sobre gerenciamento de memória, é por um bom motivo – ele destrói vários bugs em potencial!

Ele é grande parte do que faz o Rust confiável e eficiente. Siga as regras, e seu código vai ficar seguro, mesmo com programas grandes.

É melhor pensar nos checks do compilador do Rust como um guia, não como restrições. No futuro, você vai se agradecer quando seu programa em Rust rodar lisinho sem crashes ou buracos de segurança!

Refêrencias

Rust's Ownership System: 10 Things You Should Know (Artigo Original)

Ownership & Borrowing | Aprenda Rust

Did you find this article valuable?

Support Tiago Neves by becoming a sponsor. Any amount is appreciated!