Contents

Rust - Ownership - Parte 1

Contents

Ownership - O problema

Aqui esta um conceito/funcionalidade super interessante no RUST e é um conceito que precisa ser muito bem entendido.

Para explicar ‘ownership’ é necessário um pouco de abstração.

Uma grande vantagem que o C possuí sobre o JAVA, Javascript e muitas outras linguagens é o poder que o programador possuí sobre o seu código.

Em C um desses poderes é o de alocação/desalocação de memória. Quando programando em C o programador pode alocar memória para uma variável quando bem entender e o poder é tão grande que nem a quantidade de memória alocada é controlada. Esse tipo de trás alguns problemas e o mais comum deles é quando o programador esquece que o C não faz nada para desalocar aquela área de memória que ele alocou. Outro problema também é que em códigos muito grandes essa transferência de endereços de memória alocados por programadores pode ficar confuso ao ponto de um ponteiro ser perdido e quando um ponteiro esta perdido nada mais pode ser feito para liberar aquele espaço alocado antes.

Algumas linguagens modernas trabalham com um conceito conhecido como “Garbage Collection”. Nessas linguagens o programador não é o responsável pela desalocação da memória que ele utiliza. Nesse caso existe uma “entidade” que de tempos em tempos toma o controle e limpa a “bagunça deixada para trás”.

Mas um dos problemas desse conceito é a complexidade envolvido nele. Não é fácil definir que área da memória pode ou não ser liberado em um momento do tempo e além disso o “Garbage collector” necessita de tempo de processador e memória para se manter e ser executado. E existem momentos em que processamento e memória são extremamente importantes e não podemos nos dar ao luxo de entregá-los para “terceiros”.

O time do RUST procurou desenvolver uma nova forma de trabalhar o problema para que não fosse necessário um “Garbage collector” e para, também, tentar driblar a realidade de que programadores vão esquecer algo para trás quando o código é muito grande ou desenvolvido por muitos.

Diferença entre alocação na pilha e alocação na heap

Quando criamos variáveis em nossas funções estamos por padrão alocando memória na pilha.

1
2
3
4
5
6
7
   // codigo em C
   int main() {
       int var_a, var_b;
       double var_d_a, var_d_b;
       char var_c_a;
       return 0;
   }

Todas as variáveis declaradas acima estão alocadas no espaço de memória conhecido como ‘stack’.

Utilizar a pilha trás diversas vantagens para o seu programa e comodidades para o programador. Vantagens e comodidades como o do acesso aos dados na pilha ser mais rápido e de que o programador não precisa se preocupar com a desalocação dessa memória pois tudo é desalocado automaticamente quando uma função termina.

Mas existem momentos onde a pilha não é suficiente. Existem variáveis de tamanhos indefinidos. Estruturas de lista ligada são um exemplo. O espaço ocupado por elas é algo que pode fugir do controle do programador e existem momentos também em que queremos estender o tempo de vida de uma variáveis. Momentos em que não queremos desalocar a memória mesmo que a função termine.

Para situações como essas usamos a memória heap porem o problema em C com a memória heap é o problema descrito na sessão anterior. Qualquer alocação feita na heap pelo programador é de responsabilidade do próprio programador.

O espeço na heap é limitado pelo sistema operacional que por si limita esse espaço baseado na quantidade de memória física presente na maquina.

1
2
3
4
5
6
7
8
9
    // codigo em C
    #include <stdlib.h>

    int main() {
        int *var_heap = malloc(sizeof(int));
        *var_heap = 5;
        free(var_heap);
        return 0;
    }

Ownership - Regras para a solução

Para resolver o problemas referentes a alocação/manipulação/desalocação de memória o RUST possuí algumas regras:

  • Cada valor em RUST possuí uma variável chamada ‘owner’
  • Só pode existir um proprietário (‘onwer’) por vez
  • Assim que o ‘owner’ sair de escopo o valor sera desalocado

Escoco de variável

Cada variável declarada possuí seu próprio escopo. Uma variável global faz parte do escopo global e todas as outras variáveis fazem parte do escopo da função, classe ou método na qual foram declaradas.

Em C por exemplo, o tempo de vida da variável é limitado ao tempo de vida do escopo ao qual ela pertence. Assim que uma função terminal sua execução todas as variáveis declaradas dentro dela morrem junto com ela. Ainda em C, se eu termino uma função e não liberei alguma área de memória alocada dinamicamente, a variável que guardava o endereço desse endereço de memória sera desalocado e nosso acesso àquele trecho sera perdido e só sera liberado quando o programa finalizar sua execução ou quando for finalizado pelo Sistema Operacional.

Em RUST as coisas funcionam da mesma maneira que em C. Assim que uma função termina a sua execução todas as variáveis alocadas na stack serão desalocadas automaticamente porem, se o programador transferir o ‘ownership’ de uma variável de um espoco para uma variável em outro escopo a área de memória que pertence àquele ‘owner’ não sera desalocada.

Ownership - A solução

1
2
3
4
5
6
7
8
    fn main() {
        let minha_variavel = String::from("Ola");
        transfere_ownership(minha_variavel);
    }

    fn transfere_ownership(variavel_transferida: String) {
        println!("{}", variavel_transferida);
    }

No exemplo acima criamos uma variável chamada ‘minha_variavel’ logo após transferimos seu ‘owner’ para a variável ‘variavel_transferida’ na funçao ‘transfere_ownership’. Nesse momento, a função ‘transfere_ownership’ é “dona” da variável que foi transferida.

Nesse mesmo exemplo. A função ‘transfere_ownership’ tem um tempo de vida menor que o tempo de vida da funcao ‘main’. Sendo assim, o espaço de memória alocado para ‘minha_variavel’ sera automaticamente desalocado quando o fim de ‘transfere_ownership’ for atingido.

Agora, se tentarmos acessar a variável ‘minha_variavel’ no escopo de ‘main’ o compilador RUST vai gerar um erro pois aquela variável já não pertence a ninguém mais e é bem provável que já tenha sido desalocada.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
    fn main() {
        let s1 = recebe_ownership();
        let s2 = String::from("Ola");
        set s3 = transfere_e_recebe_ownership(s2);
    }

    fn recebe_ownership() -> String {
        let var1 = 5;
        let var2 = 1.0;
        let s4 = String::from("String de outra funcao");
        s4
    }

    fn transfere_e_recebe_ownership(s5: String) -> String {
        s5
    }

No exemplo acima estamos alocando um espaço em memória na função ‘recebe_ownership’ e logo após estamos transferindo por retorno de função o ‘ownership’ dessa variável que agora pertence a função ‘main’.

Vale lembrar que a transferência é feita no ponto de retorno da função e nesse momento todas as variáveis que pertencem àquele escopo são desalocadas (var1 e var2).

De volta a execução da função main criamos uma nova variável com o nome de s2 que inicialmente pertence a própria main. Logo depois passamos a responsabilidade dessa variável para a função transfere_e_recebe_ownership que por sua vez transfere de volta para main.

No final desse exemplo:

  • S1 é a variável criada na função recebe_ownership.
  • S2 não é mais acessível em main pois ao transfeir seu ‘ownership’ para ‘transfere_e_recebe_ownership’ deixou de possuír um ‘ownwer’
  • S3 é dona do espaço de memória que uma vez pertenceu a S2 pois recebeu essa responsabilidade da função ‘transfere_e_recebe_ownership’