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 deb
b
é dropado no fim do bloco ea
fica com uma referência invalida
- Criamos uma variavel
- 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.