A Rust surgiu pelo quinto ano consecutivo como linguagem de programação mais amada em uma pesquisa de desenvolvedor realizada por Stack Overflow. Existem vários motivos pelos quais os desenvolvedores amam o Rust, um deles é a garantia de segurança da memória.

O Rust garante a segurança da memória com um recurso chamado propriedade. A propriedade funciona de maneira diferente de um coletor de lixo em outras linguagens, porque consiste simplesmente em um conjunto de regras que o compilador precisa verificar em tempo de execução.

Em algumas linguagens que requerem um coletor de lixo, os desenvolvedores precisam alocar explicitamente o espaço de trabalho e a memória livre. Isso pode se tornar rapidamente tedioso e desafiador quando envolve grandes quantidades de alocação de memória.

Felizmente, lidar com a alocação de memória é o propósito do recurso de propriedade no Rust. Para entender como funciona a propriedade, vamos começar com uma compreensão mais profunda da pilha e do heap.

O que são pilha e heap?

A pilha e o heap são funções de armazenamento de memória que estão disponíveis para seu código usar em tempo de execução. Para a maioria das linguagens de programação, os desenvolvedores geralmente não se preocupam com a forma como a alocação de memória vai para a pilha e para o heap. No entanto, como Rust é uma linguagem de programação do sistema , como os valores são armazenados (na pilha ou heap) é essencial para como o a linguagem se comporta.

Aqui está um exemplo de como a memória é armazenada em uma pilha: vamos pensar em uma pilha de livros em uma mesa. Esses livros são organizados de forma que o último livro seja colocado no topo da pilha e o primeiro livro na parte inferior. Idealmente, não gostaríamos de deslizar o livro de baixo para fora da pilha, seria mais fácil escolher um livro em cima para ler.

É exatamente assim que a memória é armazenada na pilha; ele usa o método último a entrar, primeiro a sair. Aqui, ele armazena valores na ordem em que os obtém, mas os remove na ordem oposta. Também é importante observar que todos os dados armazenados na pilha têm um tamanho conhecido.

A alocação de memória na pilha é diferente de como a memória é alocada na pilha. Pense em comprar uma camisa para um amigo. Você não sabe o tamanho exato da camisa que seu amigo usa, mas ao vê-lo com frequência, você acha que ele pode ser médio ou grande. Embora você não tenha certeza absoluta, você compra o grande porque ele ainda será capaz de caber fisicamente nele, mesmo que seja um médium. É assim que funciona a alocação de memória no heap. Quando você tem um valor (seu amigo) para o qual não sabe a quantidade exata de memória que vai exigir (tamanho da camiseta), você solicita uma quantidade específica de espaço para o valor. O alocador encontra um ponto na pilha que é grande o suficiente e marca esse ponto como em uso. Esta é uma diferença importante entre a pilha e o heap: não precisamos saber o tamanho exato do valor que está sendo armazenado no heap.

Não há organização na pilha em comparação com a pilha. É fácil empurrar dados para dentro e para fora da pilha, porque tudo é organizado e segue um processo. O sistema entende que quando você coloca um valor na pilha, ele permanece no topo e quando você precisa retirar um valor da pilha, está recuperando o último valor armazenado.

Este não é, entretanto, o caso na pilha. A alocação na pilha envolve a busca por um espaço vazio grande o suficiente para corresponder à quantidade de memória solicitada e o retorno de um endereço para o local que será armazenado na pilha. Recuperar um valor do heap requer que você siga um ponteiro para o local onde o valor está armazenado no heap.

Alocar na pilha se parece com a indexação de livros, onde um ponteiro para um valor armazenado na pilha é armazenado na pilha. No entanto, o alocador também precisa procurar um espaço vazio que seja grande o suficiente para conter o valor.

O recurso de propriedade gerencia como a alocação de memória é feita na pilha, bem como no heap, para que você não tenha que passar por esse complicado processo de alocação. No entanto, seu programa se comportará de maneira inesperada se você não compreender os fundamentos da alocação de memória na pilha e no heap e como a propriedade pode assumir o controle deles.

Regras de propriedade

A propriedade tem três regras básicas que prevêem como a memória é armazenada na pilha e no heap:

  1. Cada valor de Rust tem uma variável chamada de “dono”:
     seja x=5;//x é o proprietário do valor"5"
  2. Cada valor só pode ter um proprietário de cada vez
  3. Quando o proprietário sai do escopo, o valor é descartado:
     fn main () {
    {//o escopo começa
    deixe s=String:: from ("olá");//s entra no escopo
    }//o valor de s é descartado neste ponto, está fora do escopo
    } 

Como funciona a propriedade

Em nossa introdução, estabelecemos o fato de que a propriedade não é como o sistema coletor de lixo e, de fato, Rust não lida com um sistema coletor de lixo. A maioria das linguagens de programação usa um coletor de lixo ou exige que o desenvolvedor aloque e libere memória por conta própria.

Na propriedade, solicitamos memória para nós mesmos e, quando o proprietário sai do escopo, o valor é descartado e a memória liberada. Isso é exatamente o que a terceira regra de propriedade explica. Para entender melhor como isso funciona, vejamos um exemplo:

 

Este exemplo é bastante direto; é assim que funciona a alocação de memória na pilha. Um blob de memória é alocado para a , pois sabemos o espaço exato que seu valor 5 ocupará. No entanto, nem sempre é esse o caso. Às vezes, você precisará alocar espaço de memória para um valor expansível para o qual você não sabe seu tamanho no momento da compilação.

Para este caso, a memória é alocada na pilha e você primeiro deve solicitar a memória, conforme mostrado no exemplo abaixo:

 fn main () { { deixe mut s=String:: from ("olá");//s é válido deste ponto em diante s.push_str (", mundo!");//push_str () anexa um literal a uma String println! ("{}", s);//Isso imprimirá `hello, world!` }//s não é mais válido aqui
}

Podemos anexar tanto da string quanto quisermos a s porque ela é mutável, tornando difícil saber o tamanho exato necessário no momento da compilação. Portanto, vamos exigir um espaço de memória do tamanho de uma string em nosso programa:

 let mut s=String:: from ("hello")//solicitando espaço na pilha, o tamanho de uma String.

Quando a variável sai do escopo, o recurso de propriedade Rust permite que a memória seja retornada (liberada).

Clonar e copiar

Nesta seção, veremos como a propriedade afeta certos recursos do Rust, começando com o recurso clone e copiar .

Para valores que têm um tamanho conhecido como inteiros , é mais fácil copiar o valor para outro valor:

 fn main () { deixe a="5"; deixe b=a;//copia o valor a para b println! ("{}", a)//5 println! ("{}", b)//5
}

Como a é armazenado na pilha, é mais fácil copiar seu valor para fazer outra cópia para b . Este não é o caso de um valor armazenado no heap:

 fn main () { deixe a=String:: from ("olá"); deixe b=a;//copia o valor a para b println! ("{}", a)//Isso gerará um erro porque um foi esquecido println! ("{}", b)//olá
}

Ao executar o comando, você obterá um erro erro [E0382] : empréstimo do valor movido:"a". Anteriormente, expliquei como os valores no heap são armazenados como um processo de indexação, onde o ponteiro é armazenado na pilha.

Quando você copia um valor armazenado no heap, o sistema copia automaticamente apenas o ponteiro, deixando de fora os dados do heap.

Isso faz com que Rust render a não seja mais válido, então o double free error não ocorrerá. O erro ocorre quando você tenta liberar uma memória que já foi liberada. Uma vez que a e b estão acessando uma memória no heap com seus ponteiros, é provável que quando a sai do escopo e limpa a memória , b deseja limpar a mesma memória também, portanto, double free error .

Para acessar a e b , você deve usar um recurso chamado método clone :

 fn main () { deixe a=String:: from ("olá"); deixe b=a.clone (); println! ("a={}, b={}", a, b);//a=olá, b=olá
}

Propriedade e funções

A atribuição de valores a funções segue as mesmas regras de propriedade, o que significa que elas podem ter apenas um proprietário por vez e liberar memória quando estiver fora do escopo. Vejamos este exemplo na documentação do Rust :

 fn main () { deixe s1=GiveOwnership ();//dá os movimentos de propriedade seu retorno //valor em s1 deixe s2=String:: from ("olá");//s2 entra no escopo deixe s3=takesAndGivesBack (s2);//s2 é movido para //takesAndGivesBack, que também //move seu valor de retorno para s3
}//Aqui, s3 sai do escopo e é descartado. s2 sai do escopo, mas estava //mudou, então nada acontece. s1 sai do escopo e é descartado. fn giveOwnership ()-> String {//giveOwnership irá mover seu //retorna o valor para a função //isso chama deixe someString=String:: from ("olá");//someString entra no escopo someString//someString é retornado e //passa para a chamada //função
} //takesAndGivesBack pegará uma String e retornará um
fn takesAndGivesBack (aString: String)-> String {//aString entra em //alcance aString//aString é retornado e segue para a função de chamada
}

A segunda regra de propriedade (cada valor pode ter apenas um proprietário por vez) torna a escrita de funções muito prolixa, já que você precisa retornar a propriedade das funções sempre que quiser usá-las, conforme mostrado no exemplo acima.

Para retornar vários valores, os desenvolvedores do Rust usam tupla , mas isso tende a levar muito tempo. O melhor método é usar o recurso de referências Rust.

Referências e empréstimos

Com referências, você pode usar uma função que tem uma referência a um objeto como parâmetro em vez de assumir a propriedade do valor.
Com o e comercial ( & ), você pode se referir a um valor sem se apropriar dele. Nossa função de exemplo agora pode ser escrita desta forma:

 fn main () { deixe s1=& giveOwnership ();//move seu valor de retorno para s1 deixe s2=String:: from ("olá");//s2 entra no escopo deixe s3=takesAndGivesBack (s2);//s2 é movido para s3 println! ("{}", s1); println! ("{}", s3);//takesAndGivesBack, que move seu valor de retorno para s3
}//Aqui, s3 sai do escopo e é descartado. s2 sai do escopo, mas estava //mudou, então nada acontece. s1 sai do escopo e é descartado. fn giveOwnership ()-> String {//giveOwnership irá mover seu //retorna o valor para a função //isso chama deixe someString=String:: from ("olá");//someString entra no escopo someString//someString é retornado e //passa para a chamada //função
} //takesAndGivesBack pegará uma String e retornará um
fn takesAndGivesBack (aString: String)-> String {//aString entra em //alcance aString//aString é retornado e segue para a função de chamada
}

Outro bom exemplo de como usar referências é este exemplo mostrado em Rust documentação :

 fn main () { deixe s1=String:: from ("olá"); deixe len=calcular_comprimento (& s1); println! ("O comprimento de'{}'é {}.", s1, len);
} fn calcule_length (s: & String)-> usize { s.len ()
}

Fatia

Em vez de fazer referência a uma coleção inteira, você pode fazer referência a elementos que estão próximos uns dos outros em uma sequência. Para fazer isso, você pode usar o tipo de fatia no Rust. No entanto, esse recurso não tem propriedade como referência e empréstimo.

Vejamos o exemplo abaixo. Neste exemplo, usaremos o tipo de fatia para fazer referência a elementos de um valor que está em uma sequência contígua:

 fn main () { deixe s=String:: from ("Nigerian"); deixe a=& s [0..4];//não transfere propriedade, mas faz referência às primeiras quatro letras. deixe b=& s [4..8];//não transfere propriedade, mas faz referência às últimas quatro letras. println! ("{}", a);//imprime Nige println! ("{}", b);//imprime rian
}

Conclusão

A propriedade é uma característica importante do Rust. Quanto mais um desenvolvedor Rust entende a propriedade, mais fácil se torna para ele escrever código escalável. A razão pela qual muitos desenvolvedores amam o Rust é por causa desse recurso, e depois de dominá-lo, você pode escrever um código eficiente e prever o resultado sem que o Rust puxe um rápido para você!

Neste artigo, vimos os princípios básicos de propriedade, suas regras e como aplicá-las em nossos programas. Também vimos alguns dos recursos do Rust que não têm propriedade e como usá-los na perfeição. Para obter mais dicas sobre o recurso de propriedade do Rust, verifique a documentação .

A postagem Dominando a propriedade em Rust apareceu primeiro em LogRocket Blog .

Source link