Executando verificação de segurança...
15

Entendendo gerenciamento de memoria do rust

Bom dia! Esse é o meu segundo post sobre rust e nesse post vou tentar descomplicar alguns conceitos que pessoas que vem de outras linguagens como javascript, ruby ou python tem dificuldade de entender como o rust gerencia memoria em tempo de compilação usando os conceitos de propriedade (ownership), emprestimo (borrowing) e tempo de vida (lifetimes).

Ownership & Borrowing

Vamos começar com um exemplo, digamos que queremos passar uma struct para outra função multiplas vezes:

struct Sound(String);
fn make_sound(sound: Sound) {
  println!("{}", &sound.0);
}
fn main() {
    let sound = Sound("QUACK".to_string());
    make_sound(sound); // OK 
    make_sound(sound); // Error: `sound` foi movido
}

Ue?
Para quem vem de outras linguagens isso pode parecer confuso, o que está acontecendo que a nossa variavel sound foi movida para o parametro sound pra dentro da função make_sound quando chamamos pela primeira vez, isso significa que perdemos a propriedade da variavel, ela foi transferida pra dentro da função, quando a função make_sound termina, o valor de sound é deletado/dropado, Então não conseguimos usar a variavel sound mais porque ela foi dropada.
Uma solução seria retorna a propriedade de sound de volta:

struct Sound(String);
fn make_sound(sound: Sound) -> Sound {
  println!("{}", &sound.0);
  sound
}
fn main() {
    let sound = Sound("QUACK".to_string());
    let sound1 = make_sound(sound); // OK 
    let sound2 = make_sound(sound1); // OK
}

Mas isso é muito tedioso, imagina precisar de criar uma nova variavel a cada vez que usamos a função, por isso existe uma funcionalidade do rust que se chama Empréstimo.

Empréstimo

Assim como na vida real, emprestar seria dar alguma coisa que seja de nossa propriedade a outra pessoa temporariamente. No rust fazemos isso criando uma referencia, uma referencia é nada mais que um ponteiro que é verificado pelo compilador, um ponteiro é um endereço de memoria que diz que nessa endereço tem a nossa struct.

A grande diferença do C/C++ é que o rust verifica SEMPRE que a referencia é valida e que não está apontando para lixo na memoria em tempo de compilação, assim os erros sempre vão aparecer na nossa maquina, e não em produção ás 3:00 da manha.

Vamos usar referencias para o nosso exemplo:

struct Sound(String);
fn make_sound(sound: &Sound) {
  println!("{}", &sound.0);
}
fn main() {
    let sound = Sound("QUACK".to_string());
    make_sound(&sound);
    make_sound(&sound);
}

Note que referencias são IMUTAVEIS por padrão, isso significa que não podem ser modificadas e podem ter varias referencias para um mesmo objeto.

O "Alfabeto" da informação é simples:

  • Informação só pode ter 1 dono
  • Pode ter varias referencias imutaveis ou apenas 1 referencia mutavel
  • Referencias devem ter um tempo de vida MENOR ou IGUAL a propria informação
    Essas 3 regras são o que trazem a segurança de memoria do rust, e acidentalmente concertam tudo!

Tempo de vida

Tempos de vida ou lifetimes é o tempo que uma variavel vive, e que é sempre igual ao seu escopo, por exemplo:

fn main() {
    struct A(u8);
    let a;
    {
        let b = A(2);
        /*
          error[E0597]: `b` does not live long enough
          --> src/lib.rs:6:13
           |
        6  |         a = &b;
           |             ^^ borrowed value does not live long enough
        7  |     }
           |     - `b` dropped here while still borrowed
        8  |     println!("{} {}", a.0);
           |                       --- borrow later used here
        */
        a = &b;
    }
    println!("{} {}", a.0);
}

Vamos ativar nosso cerebro agora:

  • Primeiro criamos uma variavel chamada a não inicializada
  • Criamos um bloco
    • Criamos uma variavel b
    • Tentamos inicializar a com uma referencia de b
    • b é dropado no fim do bloco e a fica com uma referência invalida
  • Printamos a apontando pra um valor inexistente

O problema aqui é que b é dropado no fim do bloco, o tempo de vida de b é limitado ao bloco que criamos, e depois tentamos criar uma referencia pra b e armazenar no a, mas isso quebra a regra que referencias devem ter um tempo de vida menor ou igual ao valor, ou seja esse codigo é invalido.

Conclusão

Com essas regras e ótimos erros de compilação, rust tem vindo a se tornar uma das melhores linguagens de programação de sistemas. Finalmente conseguimos construir aplicações sem nos preocupar se vai ter alguma vulnerabilidade, data races, e conseguir programar multi-threading sem medo de a aplicação cair as 3 da manha. Porque rust, é uma linguagem para os proximos 40 anos, rust é uma linguagem para ficar, e finalmente o nosso código pode ser perfeito.

Carregando publicação patrocinada...
4

No JavaScript, objetos são tratados como referência, mas tipos primitivos não (números e strings, por exemplo). No Rust, qualquer variável que eu utilize preciso tomar esse cuidado com ownership e borrowing ou apenas caso a variável seja uma struct?

Ótimo artigo, consegui entender bem mesmo sem nunca ter programado em Rust 🤝.

5

esqueci de mencionar que tipos que implementam Copy como referencias e numeros, ou você implementando usando o macro derive, ele copia o valor inves de mover. Mas se não implementar Copy ele vai mover a variavel por padrão, se tiver a sorte de implementar Clone, vocề pode usar o metodo clone, mas ai é explicito. Copy e Clone são traits, traits são como interfaces, se você implementar para alguma uma struct você precisa implementar todos os metodos da trait sem uma implementação padrão para a struct, ou se tiver um derive pode usar o macro #[derive(...)], bibliotecas normalmente tem macros derive para gerar implementações padrão, como o serde pra serializar e deserializar em qualquer formato, ou o bevy pra criar componentes e entidades.
E tambem se quiser você pode fazer referencias pra qualquer tipo, como str, ints e arrays/slices.
Só pra esclarecer, o rust tem 2 tipos de strings, tem a struct String, e &str.
String tem o ownership da string dentro, já &str é uma referencia pra uma string, o tipo primitivo str poderia ser utilizado sozinho, se ele tivesse um tamanho fixo conhecido em tempo de compilação, o que não é o caso, por isso "escondemos" atrás de uma referencia.

structs são parecidas com classes, com a diferença que não suportam herança, ele reforça polimorfismo sem herança, e as structs em rust podem ser uma tupla ou uma lista de chaves e valores como um obj no js, eu usei structs de tupla no exemplo para ser mais curto.

1
1