Propriedade e empréstimo em Rust: um guia abrangente para gerenciamento de memória
Rust é uma linguagem de programação de alto nível focada em desempenho e segurança, especialmente simultaneidade segura. Ele incorpora um sistema de propriedade com um conjunto de regras que o compilador verifica em tempo de compilação. Aqui, vamos nos aprofundar em como o Rust gerencia a memória, especialmente por meio dos conceitos de propriedade, empréstimo e fatias.
O Stack (pilha) e o Heap (monte)
Entender como o Rust lida com a memória começa com a compreensão dos conceitos de stack e heap. Estas são partes da memória do seu computador onde os dados são armazenados quando um programa é executado.
No entanto, antes de nos aprofundarmos nisso, vamos primeiro dar um passo para trás e pensar sobre o que acontece quando você executa um programa em seu computador.
Como funcionam os computadores?
Em um nível muito alto, seu computador possui três componentes principais envolvidos na execução de programas:
- A CPU (Unidade Central de Processamento): Isto é como o cérebro do computador. Ele executa as operações que seu programa diz para ele fazer.
- Memória (RAM — Memória de acesso aleatório): é aqui que seu programa e os dados em que ele está trabalhando são armazenados enquanto o programa está em execução. A memória é como um grande espaço vazio onde seu programa pode armazenar e recuperar dados.
- Armazenamento (disco rígido, SSD): é aqui que seu programa e seus dados são armazenados quando não estão em execução. O armazenamento é como um grande arquivo onde seu programa e seus arquivos ficam quando não estão sendo usados no momento.
Para obter mais informações sobre como funcionam os computadores Confira este vídeo
Agora, vamos dar um zoom na memória (RAM). Existem duas partes principais da memória onde seu programa pode armazenar dados: stack e heap.
Stack e heap são partes da memória que podem ser usadas pelo seu programa para armazenar dados, mas são usadas de maneiras diferentes.
Stack
Pense no stack como uma pilha de livros. Ao adicionar um novo livro (alguns dados), você o coloca no topo da pilha. Quando você deseja remover um livro, também o retira do topo da pilha. Você não pode remover um livro de baixo sem remover todos os de cima primeiro. É por isso que a pilha é chamada de “Last-In, First-Out” ou LIFO (o último a entrar é o primeiro a sair).
No Rust, os dados armazenados no stack devem ter um tamanho fixo conhecido. Isso ocorre porque o Rust precisa saber quanta memória alocar quando for colocado no stack.
fn main() {
let x = 10; // x é armazenado no stack
}
No exemplo acima, x é um número inteiro, que tem um tamanho fixo, por isso é armazenado no stack. Quando o main
(principal) termina de executar, x sai do escopo e é retirado do stack.
Heap
O heap, por outro lado, é mais como um quarto bagunçado. Quando você quer guardar algo, basta encontrar um lugar vazio onde caiba e colocar lá. Não há ordem inerente e você pode fazer as coisas na ordem que quiser.
No Rust, os dados armazenados no heap podem ter um tamanho dinâmico. Mas como o heap está desorganizado, o acesso é mais lento. Você tem que seguir um ponteiro para chegar aos dados que deseja.
fn main() {
let x = Box::new(10); // x é armazenado no monte
}
No exemplo acima, x
é uma Box, que é um tipo que aloca memória no heap.O número inteiro real é armazenado no heap, e um ponteiro para o inteiro é armazenado na pilha. Quando o main
termina de executar, x
sai do escopo, a memória no monte é liberada e o ponteiro é retirado da pilha.
Para referência de imagem sobre como o stack e heap funcionam, eu realmente gosto desta imagem simples do artigo Pilha, Monte, Tipo de Valor e Tipo de Referência em C#
Espero que isso esclareça a diferença entre a stack e heap e como eles se encaixam no quadro geral da execução de um programa em um computador. Nas próximas seções, discutiremos como o Rust usa esses conceitos para reforçar a segurança da memória por meio de suas regras de propriedade.
Regras de propriedade
As regras de propriedade em Rust são fundamentais para a linguagem e são as seguintes:
- Cada valor em Rust tem uma variável que é sua proprietária.
- Só pode haver um proprietário por vez.
- Quando o proprietário sair do escopo, o valor será descartado.
Vamos considerar um exemplo:
fn main() {
// s torna-se o dono da memória contendo "hello"
let s = String::from("hello");
// o valor de s se move para a função e não é mais válido aqui
takes_ownership(s);
// x é uma variável simples e entra no escopo
let x = 5;
// x iria para a função, mas i32 é Copy, então
// não há problema em usar x depois
makes_copy(x);
}
// Aqui, x sai do escopo, então s. Mas porque o valor de s foi movido,
// nada de especial acontece.
fn takes_ownership(some_string: String) {
// some_string entra no escopo
println!("{}", some_string);
}
// Aqui, some_string sai do escopo e `drop` é chamado.
// A memória de apoio é liberada.
fn makes_copy(some_integer: i32) {
// some_integer entra no escopo
println!("{}", some_integer);
} // Aqui, some_integer sai do escopo. Nada de especial acontece.
Neste exemplo, a string s
é passada para a funçãotakes_ownership
, então s
não é mais válida na função main
. Isto é porque s
foi movida. Por outro lado, a integral x
ainda é válida após ser passada para a função makes_copy
porque as integrais têm a característica Copy
, o que significa que elas são copiadas em vez de movidas.
Empréstimos e Referências
O empréstimo é o mecanismo que permite que uma função use um valor sem se apropriar dele. O empréstimo é feito usando referências, que são criadas usando o símbolo &
.
Considere o seguinte exemplo:
fn main() {
let s = String::from("hello");
let len = calculate_length(&s);
println!("O comprimento de '{}' é {}.", s, len);
}
fn calculate_length(s: &String) -> usize {
s.len()
}
Neste exemplo, passamos uma referência para s
em calculate_length
. Como calculate_length
não possui s, a string não será descartada quando calculate_length
sair do escopo.
Rust também possui referências mutáveis para casos em que você precisa modificar o valor emprestado. Mas há uma grande restrição: você pode ter apenas uma referência mutável para um determinado dado em um determinado escopo. Isso impede corridas de dados no tempo de compilação.
Aqui está um exemplo:
fn main() {
let mut s = String::from("olá");
change(&mut s);
}
fn change(some_string: &mut String) {
some_string.push_str(", mundo");
}
Aqui,s
é uma string mutável. Passamos uma referência mutável de s
para change
, que acrescenta ", mundo" à string.
Esses conceitos de propriedade, empréstimo e referências são fundamentais para o Rust, permitindo que ele faça garantias de segurança poderosas sem sacrificar o desempenho.
O tipo Slice (fatia)
Um slice é uma visão de um bloco de memória representado como um ponteiro e um comprimento. Slices _permitem que você trabalhe com uma parte de uma coleção de itens em vez de toda a coleção. A coleção pode ser um _array (matriz), uma string, um vetor ou qualquer outra estrutura de dados que contenha uma sequência de elementos.
Considere as fatias como um objeto de duas palavras, a primeira palavra é um ponteiro para os dados e a segunda palavra é o comprimento da fatia. Os dados apontados por um slice são garantidos como válidos pelo comprimento especificado. Você pode pegar um slice de um array, uma string, uma Vec
e outras coleções usando a sintaxe do intervalo.
Vamos nos aprofundar nas fatias no contexto de strings e arrays:
Slices de Strings
Uma fatia de string é uma referência da parte de uma String
. Criamos uma fatia de string especificando um intervalo entre colchetes, [starting_index..ending_index]
. O índice inicial é inclusivo, enquanto o índice final é exclusivo.
let s = String::from("olá, mundo!");
let hello = &s[0..5]; // 'olá'
let world = &s[7..13]; // 'mundo'
Neste exemplo, olá
e mundo
são slices de s
. Eles não contêm nenhum dado de string. Em vez disso, são referências aos dados da String
original.
Slices de Array
Da mesma forma, também podemos criar um slice a partir de um array:
let arr = [1, 2, 3, 4, 5];
let slice = &arr[1..3]; // [2, 3]
Nesse caso, slice
é uma fatia de arr
. É uma janela para arr
do índice 1 para 2. Como com fatias de string, slice
não possui os dados para os quais aponta.
Tempos de vida e slices
Um conceito importante a ser entendido com fatias é o "tempo de vida" em Rust. Como uma fatia contém uma referência a um elemento, ela está vinculada às regras de empréstimo do Rust. O tempo de vida da fatia não pode exceder o tempo de vida da coisa à qual ela faz referência. Quando esses dados saem do escopo, sua fatia não é mais válida.
{
let s = String::from("olá");
let t = &s[0..2];
} // <- s sai do escopo aqui, então t não é mais válido
Neste código, quando s
sai do escopo, t
também se torna inválido. Isto é porque t
detém uma referência a s
, e s
não é mais válido.
Compreender e usar fatias de forma eficaz pode ajudar a melhorar a eficiência da memória de seus programas, permitindo acesso a dados mais precisos sem transferências de propriedade desnecessárias. Em conclusão, Rust oferece um sistema poderoso e exclusivo para gerenciamento de memória. Por meio da propriedade, o Rust garante que haja um proprietário claro para cada parte dos dados e que esses dados sejam devidamente limpos quando não forem mais necessários. Isso reduz significativamente o risco de vazamentos de memória.
Conclusão
O empréstimo permite flexibilidade no acesso e uso de dados sem assumir a propriedade, enquanto as regras de empréstimo evitam corridas de dados. As referências imutáveis permitem várias leituras simultaneamente e as referências mutáveis permitem uma única gravação sem que nenhuma leitura ocorra simultaneamente.
Por fim, os tipos de fatia são uma ferramenta flexível e poderosa que permite fazer referência a uma variedade de elementos em uma coleção sem se apropriar de toda a coleção. Isso pode ser particularmente útil para trabalhar com subconjuntos de dados.
Entender esses conceitos é fundamental para dominar o Rust, pois eles não são apenas essenciais para o gerenciamento de memória, mas também têm profundas implicações no design e na estrutura de seus programas. O compromisso da Rust com abstrações de custo zero significa que, quando usados corretamente, esses recursos podem criar código eficiente e seguro, fornecendo o melhor dos dois mundos.
Obrigado por ler este artigo!
Em nossa jornada coletiva para o futuro, seus pensamentos e contribuições são inestimáveis. Para conversas mais envolventes sobre blockchain, IA, programação e biohacking, vamos nos manter conectados nessas plataformas:
- Leia mais dos meus artigos no Medium 📚
- Siga-me no Twitter 🐦 para pensamentos e discussões sobre IA, biohacking e blockchain.
- Confira meus projetos e contribua no GitHub 👩💻.
- E junte-se a mim no YouTube 🎥 para conversas mais aprofundadas.
Vamos continuar moldando o futuro juntos, uma criação de cada vez. 🚀
Este artigo foi escrito por Aurora Poppyseed e traduzido por Diogo Jorge. O artigo original pode ser encontrado aqui.
Latest comments (0)