Dominando o sistema de propriedade do Rust: o guia completo para segurança da memória sem coleta de lixo
1. Por que a propriedade é importante: o problema de US$ 2 trilhões
Bugs de segurança de memória custam à indústria de software cerca de US$ 2 trilhões anualmente. A Microsoft relata que 70% de suas vulnerabilidades de segurança são problemas de segurança de memória. A equipe do Google Chrome encontrou números semelhantes. Esses bugs incluem:
- Use-after-free: Acessando a memória que foi desalocada
- Double-free: Liberando memória duas vezes, causando corrupção
- Estouros de buffer: gravação além da memória alocada
- Corridas de dados: acesso simultâneo causando comportamento imprevisível
- Vazamentos de memória: Esquecimento de liberar memória alocada
A compensação tradicional
As linguagens de programação historicamente escolheram uma de duas abordagens:
Abordagem 1: gerenciamento manual de memória (C/C++)
// Código C - sujeito a erros
char* criar_mensagem() {
char* mensagem = malloc(100);
strcpy(mensagem, "Olá");
retornar mensagem; // O chamador deve se lembrar de liberar!
}
processo vazio() {
char* m = criar_mensagem();
printf("%s",m);
// Esqueci de free(m) - vazamento de memória!
}
Problemas: Requer disciplina perfeita, facilidade de cometer erros, vulnerabilidades de segurança.
Abordagem 2: Coleta de lixo (Java/Go/JavaScript)
// Código Java - seguro, mas com sobrecarga de tempo de execução
String createMessage() {
retorne "Olá"; // O GC irá limpar eventualmente
}
// Seguro, mas as pausas do GC afetam o desempenho
Problemas: pausas imprevisíveis, sobrecarga de memória, menos controle sobre o desempenho.
A solução revolucionária de Rust
Rust fornece uma terceira via: segurança de memória sem coleta de lixo por meio de verificação de propriedade em tempo de compilação. Você obtém:
- ✅ Segurança de memória garantida em tempo de compilação
- ✅ Sem sobrecarga de tempo de execução (abstrações de custo zero)
- ✅ Sem pausas no coletor de lixo
- ✅ Simultaneidade destemida (corridas de dados impossíveis)
- ✅ Desempenho previsível
O problema? Você deve aprender o sistema de propriedade. Este guia deixará isso bem claro.
2. As três regras de ouro da propriedade
Todo programa Rust segue estas três regras, aplicadas em tempo de compilação:
Regra 1: Cada valor tem um único proprietário
fn principal() {
deixe s = String::from("olá"); //s possui a String
// Apenas uma variável pode possuir esses dados por vez
} // s sai do escopo, a memória é liberada automaticamente
Regra 2: Quando o proprietário sai do escopo, o valor é eliminado
fn principal() {
{
deixe s = String::from("olá"); // s é válido daqui
//faz coisas com s
} // s sai do escopo e é descartado, memória liberada
//println!("{}",s); //ERRO: s não existe mais
}
Regra 3: Você pode ter uma referência mutável OU múltiplas referências imutáveis
fn principal() {
deixe mut s = String::from("olá");
// Múltiplas referências imutáveis - OK
seja r1 = &s;
seja r2 = &s;
println!("{} e {}", r1, r2);
// Uma referência mutável - OK (após r1, r2 não são mais usados)
seja r3 = &mut s;
r3.push_str("mundo");
println!("{}", r3);
}
Por que essas regras?
- Regra 1 e 2: Evita vazamentos de memória e erros de liberação dupla
- Regra 3: Evita corridas de dados em tempo de compilação
3. Mover Semântica: Compreendendo a Transferência de Propriedade
O problema: a cópia ingênua é cara
fn principal() {
deixe s1 = String::from("olá");
seja s2 = s1; // O que acontece aqui?
// println!("{}", s1); //ERRO: valor movido para s2
}
O que realmente acontece:
Pilha: Pilha:
s1 -> [ptr, len, cap] -> dados "olá"
|
| (mover)
v
s2 -> [ptr, len, cap] -> (mesmos dados de heap)
Rust muda propriedade em vez de copiar dados heap. Depois de let s2 = s1, apenas s2 é válido. Isso evita:
- Cópias profundas caras por padrão
- Dupla liberação de erros (somente s2 liberará memória)
Quando o Rust copia em vez de mover?
Os tipos que implementam a característica Copy são copiados em vez de movidos:
fn principal() {
// Inteiros, floats, bools, chars implementam Copy
seja x = 5;
seja y = x; // x é copiado para y
println!("x = {}, y = {}", x, y); // Ambos válidos!
// Tuplas de tipos Copy também são Copy
seja ponto = (3, 4);
deixe ponto2 = ponto; //copiado
println!("{:?} e {:?}", ponto, ponto2); // Ambos válidos!
}
Regra geral: Se um tipo armazena dados no heap ou possui recursos, ele não implementará Copy.
Clonagem explícita quando você precisar
fn principal() {
deixe s1 = String::from("olá");
deixe s2 = s1.clone(); // Cópia profunda explícita
println!("s1 = {}, s2 = {}", s1, s2); // Ambos válidos
}
Use clonar quando:
- Você precisa de cópias independentes
- O custo de desempenho é aceitável
- Deixa a intenção clara
Evite clonar quando:
- Você pode reestruturar para usar empréstimos
- O desempenho é crítico
- Trabalhando em loops quentes
4. Empréstimo: referências que não pertencem
O empréstimo permite que você faça referência a dados sem assumir a propriedade.
Referências imutáveis (&T)
fn principal() {
deixe s = String::from("olá");
deixe len = calcular_comprimento(&s); // Pegar emprestado
println!("O comprimento de '{}' é {}", s, len); // ainda é válido!
}
fn calcular_comprimento(s: &String) -> usar {
s.len()
} // s sai do escopo, mas não possui os dados, então nada acontece
Pontos-chave:
&scria uma referência para s- As referências são imutáveis por padrão
- Múltiplas referências imutáveis permitidas
- O proprietário original ainda pode ler os dados
Referências mutáveis (&mut T)
fn principal() {
deixe mut s = String::from("olá");
alterar(&mut s); // Pegar emprestado de forma mutável
println!("{}",s); // Imprime "olá, mundo"
}
fn mudança(s: &mut String) {
s.push_str(", mundo");
}
Restrição crítica: Apenas uma referência mutável por vez!
fn principal() {
deixe mut s = String::from("olá");
seja r1 = &mut s;
seja r2 = &mut s; // ERRO: não é possível emprestar como mutável mais de uma vez
println!("{}, {}", r1, r2);
}
Por quê? Evita corridas de dados em tempo de compilação:
// Isso é impossível em Rust (compilaria em C++)
deixe mut data = vec![1, 2, 3];
deixe ref1 = &mut dados;
deixe ref2 = &mut dados;
ref1.push(4); //Pode realocar
ref2.push(5); // Poderia causar uso após livre!
O segredo do verificador de empréstimos: tempos de vida não lexicais (NLL)
Modern Rust (edição 2018+) é mais inteligente sobre quando as referências terminam:
fn principal() {
deixe mut s = String::from("olá");
seja r1 = &s;
seja r2 = &s;
println!("{} e {}", r1, r2);
// r1 e r2 não são mais usados após este ponto
seja r3 = &mut s; // OK! r1 e r2 estão "mortos"
r3.push_str("mundo");
println!("{}", r3);
}
Antes do NLL (edição de 2015), isso não seria compilado. Agora o compilador rastreia onde as referências são realmente usadas, não apenas seu escopo léxico.
Padrão de empréstimo comum: leituras múltiplas, gravação única
fn principal() {
deixe mut data = vec![1, 2, 3, 4, 5];
// Fase de leitura: múltiplos empréstimos imutáveis
deixe primeiro = &data[0];
deixe último = &data[data.len() - 1];
println!("primeiro: {}, último: {}", primeiro, último);
// Fase de gravação: empréstimo mutável exclusivo
dados.push(6);
println!("Atualizado: {:?}", dados);
}
5. Vidas: ensinando o compilador sobre referências
A solução do problema ao longo da vida
fn mais longo(x: &str, y: &str) -> &str {
se x.len() > y.len() {
x // Qual tempo de vida o retorno deve ter?
} senão {
y // tempo de vida de x ou tempo de vida de y?
}
}
O compilador não pode determinar se a referência retornada é válida:
fn principal() {
deixe string1 = String::from("string longa");
deixe o resultado;
{
deixe string2 = String::from("curto");
resultado = mais longo(&string1, &string2);
} // string2 descartada aqui
// O resultado é válido? Depende de qual entrada foi retornada!
}
Anotações vitalícias: contratos explícitos
fn mais longo<'a>(x: &'a str, y: &'a str) -> &'a str {
se x.len() > y.len() {
x
} senão {
sim
}
}
Lendo isto: "A referência retornada será válida desde que ambos x e y sejam válidos."
fn principal() {
deixe string1 = String::from("string longa");
deixe o resultado;
{
deixe string2 = String::from("curto");
resultado = mais longo(&string1, &string2);
println!("{}", resultado); // OK: ambos ainda válidos
} // string2 descartada
// println!("{}", resultado); // ERRO: string2 pode ter sido retornado
}
Elisão vitalícia: quando você não precisa de anotações
O compilador pode inferir tempos de vida em padrões comuns:
// Nenhuma anotação necessária - tempo de vida de entrada única
fn primeira_palavra(s: &str) -> &str {
s.split_whitespace().next().unwrap_or("")
}
// O compilador vê isso como:
fn primeira_palavra<'a>(s: &'a str) -> &'a str {
s.split_whitespace().next().unwrap_or("")
}
Regras de Elisão:
- Cada referência de entrada tem seu próprio tempo de vida
- Se houver exatamente um tempo de vida de entrada, a saída obtém esse tempo de vida
- Se o método for
&self, a saída obtém o tempo de vida deself
Vidas em estruturas
Quando estruturas contêm referências, você deve anotar os tempos de vida:
struct ImportanteExcerpt<'a> {
parte: &'a str,
}
fn principal() {
let novel = String::from("Me chame de Ismael. Alguns anos atrás...");
deixe first_sentence = novel.split('.').next().unwrap();
deixe trecho = ImportanteExcerto {
parte: primeira_frase,
};
println!("{}", trecho.part);
} // trecho e romance descartados, tudo bem
Significado: Um ImportantExcerpt não pode sobreviver aos dados aos quais faz referência.
fn principal() {
deixe trecho;
{
let novel = String::from("Me chame de Ismael.");
trecho = ImportanteExcerto {
parte: &romance,
};
} // ERRO: romance descartado, excerpt.part ficaria pendurado
// println!("{}", trecho.part);
}
A 'vida estática
'static significa "vive durante toda a duração do programa":
// Literais de string têm 'tempo de vida estático
vamos: &'static str = "Estou armazenado no binário";
//Variáveis estáticas
estático GLOBAL: &str = "Também 'estático";
Erro comum: Não use 'static apenas para fazer com que os erros desapareçam!
// RUIM: Forçando 'estático para compilar
fn bad_function() -> &'static str {
deixe s = String::from("olá");
// &s // Não pode retornar - não vive o suficiente
// Vazamento de memória para obter 'estático está errado!
}
// BOM: Em vez disso, retorne os dados de propriedade
fn good_function() -> String {
String::from("olá")
}
6. Armadilhas comuns e como corrigi-las
Armadilha 1: Não é possível emprestar como mutável porque já foi emprestado
//ERRO
fn principal() {
deixe mut vec = vec![1, 2, 3];
deixe primeiro = &vec[0]; //Empréstimo imutável
vec.push(4); // ERRO: empréstimo mutável
println!("{}", primeiro);
}
Por que falha: push pode ser realocado, invalidando first.
Solução 1: Reestruturação para separar empréstimos
fn principal() {
deixe mut vec = vec![1, 2, 3];
deixe first_value = vec[0]; // Copie o valor
vec.push(4); // OK: nenhum empréstimo pendente
println!("{}", primeiro_valor);
}
Solução 2: clonar os dados antes da mutação
fn principal() {
deixe mut vec = vec![1, 2, 3];
deixe primeiro = vec.get(0).cloned(); // Opção<i32>, sem empréstimo
vec.push(4);
se deixar Some(val) = primeiro {
println!("{}", val);
}
}
Armadilha 2: Não é possível retornar referência à variável local
//ERRO
fn criar_string() -> &String {
deixe s = String::from("olá");
&s // ERRO: retorna referência aos dados pertencentes à função
} // s é descartado aqui, retornaria um ponteiro pendente!
Solução: retornar dados próprios
fn criar_string() -> String {
String::from("hello") // Propriedade transferida para o chamador
}
Armadilha 3: Não é possível sair do conteúdo emprestado
//ERRO
fn principal() {
deixe vec = vec![String::from("a"), String::from("b")];
deixe primeiro = &vec;
vamos levar = vec[0]; // ERRO: não é possível sair do conteúdo indexado
}
Solução 1: clonar o valor
fn principal() {
deixe vec = vec![String::from("a"), String::from("b")];
vamos levar = vec[0].clone();
println!("{}", obtido);
}
Solução 2: use métodos que transferem propriedade
fn principal() {
deixe mut vec = vec![String::from("a"), String::from("b")];
vamos levar = vec.swap_remove(0); // Assume a propriedade
println!("{}", obtido);
}
Armadilha 4: Empréstimos Simultâneos Mutáveis e Imutáveis
//ERRO
fn principal() {
deixe mut map = HashMap::new();
map.insert("chave", "valor");
deixe valor = map.get("chave");
map.insert("chave2", "valor2"); // ERRO: não é possível sofrer mutação enquanto estiver emprestado
println!("{:?}", valor);
}
Solução: use a API de entrada
fn principal() {
deixe mut map = HashMap::new();
map.insert("chave", "valor");
map.entry("chave2").or_insert("valor2"); // Sem empréstimos conflitantes
if let Some(value) = map.get("key") {
println!("{}", valor);
}
}
Armadilha 5: Incompatibilidade vitalícia em estruturas
//ERRO
estrutura Contêiner {
dados: &str, // ERRO: anotação de tempo de vida ausente
}
Solução: adicionar parâmetro de vida útil
estrutura Container<'a> {
dados: &'a str,
}
impl<'a> Contêiner<'a> {
fn new(texto: &'a str) -> Self {
Contêiner {dados: texto}
}
fn get_data(&self) -> &str {
auto.dados
}
}
Armadilha 6: Invalidação do Iterador
//ERRO
fn principal() {
deixe mut vec = vec![1, 2, 3, 4, 5];
para eu em &vec {
se *i % 2 == 0 {
vec.push(*i * 2); // ERRO: não é possível modificar durante a iteração
}
}
}
Solução: colete os índices primeiro
fn principal() {
deixe mut vec = vec![1, 2, 3, 4, 5];
deixe to_add: Vec<i32> = vec.iter()
.filtro(|&&x| x % 2 == 0)
.map(|&x| x * 2)
.coletar();
vec.extend(to_add);
println!("{:?}",vec);
}
7. Padrões avançados: além da propriedade básica
Padrão 1: Mutabilidade Interior com RefCell
Às vezes você precisa alterar dados apenas com uma referência imutável:
usar std::cell::RefCell;
estrutura Registrador {
logs: RefCell<Vec<String>>, // Pode sofrer mutação através de &self
}
impl Registrador {
fn novo() -> Próprio {
Registrador {
registros: RefCell::new(Vec::new()),
}
}
fn log(&self, message: &str) { // Leva &self, não &mut self
self.logs.borrow_mut().push(message.to_string());
}
fn print_logs(&self) {
para login self.logs.borrow().iter() {
println!("{}", registro);
}
}
}
fn principal() {
deixe logger = Logger::new();
logger.log("Primeira mensagem");
logger.log("Segunda mensagem");
logger.print_logs();
}
Quando usar:
- Implementação de caches ou loggers
- Estruturas gráficas ou de árvore com mutabilidade interior
- Simular objetos em testes
Cuidado: Verificação de empréstimo em tempo de execução – entra em pânico se as regras forem violadas!
deixe célula = RefCell::new(5);
deixe emprestado1 = cell.borrow();
deixe emprestado2 = cell.borrow_mut(); // PÂNICO: já peguei emprestado!
Padrão 2: Contagem de Referência com Rc
Compartilhe a propriedade dos dados com vários proprietários:
usar std::rc::Rc;
estrutura Nó {
valor: i32,
filhos: Vec<Rc<Nó>>,
}
fn principal() {
deixe folha = Rc::new(Node {
valor: 3,
crianças: vec![],
});
deixe branch1 = Rc::new(Nó {
valor: 1,
filhos: vec![Rc::clone(&leaf)], // Propriedade compartilhada
});
deixe branch2 = Rc::new(Nó {
valor: 2,
filhos: vec![Rc::clone(&leaf)], // Ambos os ramos possuem folha
});
println!("Contagem de referências de folhas: {}", Rc::strong_count(&leaf)); //3
}
Pontos-chave:
- Não é seguro para threads (use
Arcpara threads) - A contagem de referência tem sobrecarga
- Cria ciclos se não for cuidadoso (use
Weakpara quebrar ciclos)
Padrão 3: Combinando Rc e RefCell
Vários proprietários com mutação:
usar std::rc::Rc;
usar std::cell::RefCell;
#[derivar(Depurar)]
estrutura SharedCounter {
contagem: Rc<RefCell<i32>>,
}
impl SharedCounter {
fn novo() -> Próprio {
ContadorCompartilhado{
contagem: Rc::new(RefCell::new(0)),
}
}
fn incremento(&self) {
*self.count.borrow_mut() += 1;
}
fn get(&self) -> i32 {
*self.count.borrow()
}
}
fn principal() {
deixe contador1 = SharedCounter::new();
deixe counter2 = SharedCounter {
contagem: Rc::clone(&counter1.count),
};
contador1.incremento();
contador2.increment();
println!("Contagem: {}", contador1.get()); //2
}
Padrão 4: Padrão Construtor com Propriedade
estrutura Servidor {
anfitrião: Corda,
porta: u16,
tempo limite: u64,
}
estrutura ServerBuilder {
host: Opção<String>,
porta: Opção<u16>,
tempo limite: Opção<u64>,
}
impl ServerBuilder {
fn novo() -> Próprio {
Construtor de Servidores {
anfitrião: Nenhum,
porta: Nenhum,
tempo limite: Nenhum,
}
}
fn host(mut self, host: impl Into<String>) -> Self {
self.host = Some(host.into());
self // Move a propriedade de volta
}
porta fn(mut self, porta: u16) -> Self {
self.port = Some(porta);
eu mesmo
}
fn timeout(mut self, timeout: u64) -> Self {
self.timeout = Alguns(tempo limite);
eu mesmo
}
fn build(self) -> Resultado<Servidor, &'static str> {
Ok(Servidor {
host: self.host.ok_or("Host é necessário")?,
porta: self.port.unwrap_or(8080),
tempo limite: self.timeout.unwrap_or(30),
})
}
}
fn principal() {
deixe servidor = ServerBuilder::new()
.host("localhost")
.porta(3000)
.tempo limite(60)
.construir()
.desembrulhar();
println!("Servidor: {}:{}", servidor.host, servidor.port);
}
Padrão 5: RAII (aquisição de recursos é inicialização)
A propriedade permite a limpeza automática de recursos:
usar std::fs::Arquivo;
use std::io::{self, Write};
estrutura LogFile {
arquivo: Arquivo,
}
impl Arquivo de Log {
fn new(caminho: &str) -> io::Result<Self> {
Ok(ArquivoLog {
arquivo: Arquivo::criar(caminho)?,
})
}
fn write_log(&mut self, mensagem: &str) -> io::Result<()> {
writeln!(self.file, "{}", mensagem)
}
}
impl Soltar para LogFile {
fn drop(&mut self) {
println!("Fechando arquivo de log");
//Arquivo fechado automaticamente quando descartado
}
}
fn main() -> io::Result<()> {
{
deixe-o log = LogFile::new("app.log"?);
log.write_log("Aplicativo iniciado"?);
log.write_log("Processando dados"?);
} // Arquivo fechado automaticamente aqui via Drop
println!("Arquivo de log fechado automaticamente");
Ok(())
}
8. Estratégias de refatoração do mundo real
Cenário 1: passando dados para funções
Antes (lutando contra o verificador de empréstimo):
estruturar usuário {
nome: Corda,
e-mail: String,
}
fn process_user(usuário: Usuário) {
println!("Processando {}", usuário.nome);
}
fn principal() {
deixe usuário = Usuário {
nome: String::from("Alice"),
email: String::from("[email protected]"),
};
usuário_processo(usuário);
// println!("{}", usuário.nome); //ERRO: usuário movido
}
Depois (pegar emprestado em vez de mover):
fn process_user(user: &User) { // Em vez disso, peça emprestado
println!("Processando {}", usuário.nome);
}
fn principal() {
deixe usuário = Usuário {
nome: String::from("Alice"),
email: String::from("[email protected]"),
};
usuário_processo(&usuário);
println!("{}", usuário.nome); // OK: o usuário ainda pertence
}
Cenário 2: Trabalhando com Coleções
Antes:
fn get_first_name(usuários: Vec<Usuário>) -> Option<String> {
users.first().map(|u| u.name.clone()) // Clone desnecessário
}
fn principal() {
deixe usuários = vec![/* ... */];
deixe nome = get_first_name(usuários);
// Não é mais possível usar usuários - movido
}
Depois:
fn get_first_name(usuários: &[Usuário]) -> Opção<&str> {
usuários.first().map(|u| u.name.as_str())
}
fn principal() {
deixe usuários = vec![/* ... */];
deixe nome = get_first_name(&users);
// usuários ainda utilizáveis
}
Cenário 3: estrutura com vários campos de string
Antes (muita clonagem):
fn build_full_name(primeiro: String, último: String) -> String {
formato!("{} {}", primeiro, último)
}
fn principal() {
deixe primeiro = String::from("John");
deixe último = String::from("Doe");
deixe completo = build_full_name(first.clone(), last.clone());
println!("Primeiro: {}, Último: {}", primeiro, último);
}
Depois (use fatias de barbante):
fn build_full_name(primeiro: &str, último: &str) -> String {
formato!("{} {}", primeiro, último)
}
fn principal() {
deixe primeiro = String::from("John");
deixe último = String::from("Doe");
deixe completo = build_full_name(&primeiro, &último);
println!("Primeiro: {}, Último: {}", primeiro, último);
}
Cenário 4: Resultados de cache
Problema: precisa de cache mutável com métodos imutáveis
Solução: Mutabilidade interior
usar std::cell::RefCell;
usar std::collections::HashMap;
struct ExpensiveCalculator {
cache: RefCell<HashMap<i32, i32>>,
}
impl Calculadora Cara {
fn novo() -> Próprio {
Calculadora cara {
cache: RefCell::new(HashMap::new()),
}
}
fn calcular(&self, entrada: i32) -> i32 { // &self, não &mut self
//Verifica cache
se deixar Some(&cached) = self.cache.borrow().get(&input) {
retornar em cache;
}
// Cálculo caro
deixe resultado = entrada * entrada;
// Armazena em cache
self.cache.borrow_mut().insert(entrada, resultado);
resultado
}
}
fn principal() {
deixe calc = ExpensiveCalculator::new();
println!("{}", calc.calculate(5)); // Calculado
println!("{}", calc.calculate(5)); // Do cache
}
Cenário 5: Estruturas de árvore
Desafio: relacionamentos entre pais e filhos criam conflitos de empréstimos
Solução: Use índices ou Rc/Weak
usar std::rc::{Rc, Fraco};
usar std::cell::RefCell;
estrutura Nó {
valor: i32,
pai: RefCell<Weak<Node>>,
filhos: RefCell<Vec<Rc<Node>>>,
}
impl Nó {
fn novo(valor: i32) -> Rc<Self> {
Rc::new(Nó {
valor,
pai: RefCell::new(Fraco::new()),
filhos: RefCell::new(vec![]),
})
}
fn add_child(pai: &Rc<Nó>, filho: Rc<Nó>) {
*child.parent.borrow_mut() = Rc::downgrade(pai);
parent.children.borrow_mut().push(child);
}
}
fn principal() {
deixe root = Node::new(1);
deixe filho1 = Nó::novo(2);
deixe filho2 = Nó::novo(3);
Nó::add_child(&root, filho1);
Nó::add_child(&root, filho2);
println!("Root tem {} filhos", root.children.borrow().len());
}
9. Implicações de desempenho: abstrações de custo zero
Propriedade tem custo zero
O sistema de propriedade não tem sobrecarga de tempo de execução:
// Este código Rust:
processo fn(dados: Vec<i32>) -> i32 {
dados.iter().sum()
}
// Compila no mesmo assembly que:
// processo int(int* dados, tamanho_t len) {
// int soma = 0;
// for (tamanho_t i = 0; i < len; i++) {
// soma += dados[i];
// }
// retorna soma;
// }
Prova: Verifique a montagem com cargo build --release e ferramentas como cargo-asm.
Quando a clonagem tem custo
// Caro: cópia profunda
deixe vec1 = vec![1, 2, 3, 4, 5];
deixe vec2 = vec1.clone(); // Aloca nova memória heap, copia todos os elementos
// Grátis: Referência
deixe vec1 = vec![1, 2, 3, 4, 5];
seja vec2 = &vec1; // Sem alocação, apenas um ponteiro
Referência: Clonar vs Emprestar
use std::time::Instant;
fn process_by_value(dados: Vec<i32>) -> i32 {
dados.iter().sum()
}
fn process_by_reference(dados: &[i32]) -> i32 {
dados.iter().sum()
}
fn principal() {
deixe os dados: Vec<i32> = (0..1_000_000).collect();
//Clonar versão
vamos começar = Instantâneo::agora();
para _ em 0..1000 {
deixe resultado = process_by_value(data.clone()); //Clone cada vez
}
println!("Clone: {:?}", start.elapsed());
// Emprestar versão
vamos começar = Instantâneo::agora();
para _ em 0..1000 {
deixe resultado = process_by_reference(&data); //Sem clone
}
println!("Empréstimo: {:?}", start.elapsed());
}
// Resultados típicos:
//Clonar: 850ms
// Emprestar: 120ms
Ponteiro inteligente suspenso
// Rc tem pequena sobrecarga
usar std::rc::Rc;
use std::time::Instant;
fn with_rc(dados: Rc<Vec<i32>>) {
deixe _ = dados.len();
}
fn with_ref(dados: &Vec<i32>) {
deixe _ = dados.len();
}
// Rc tem 2 palavras (ponteiro + contagem de referências)
// A referência é uma palavra (apenas um ponteiro)
// Mas Rc permite propriedade compartilhada onde as referências não podem
Quando as despesas gerais são importantes:
- Hot loops com milhões de iterações
- Sistemas em tempo real
- Sistemas embarcados com recursos limitados
Quando a sobrecarga não importa:
- A maior parte do código do aplicativo
- Quando permite um melhor design
- Quando a clonagem seria mais cara
10. Guia de migração: de outros idiomas
Vindo de C++
Mentalidade C++:
std::string* createString() {
return new std::string("olá"); //O chamador deve excluir
}
processo vazio() {
std::string* s = createString();
std::cout << *s;
excluir; //Limpeza manual
}
Equivalente à ferrugem:
fn criar_string() -> String {
String::from("hello") // Propriedade transferida
}
processo fn() {
vamos s = create_string();
println!("{}",s);
} // Descartado automaticamente
Principais diferenças:
- Sem manual
novo/excluir - Sem ponteiros brutos em código seguro
- As referências têm verificação vitalícia
- Mover semântica por padrão
Vindo de Go
Vá mentalidade:
func processo(dados[]int) {
data[0] = 100 // Muda o original
}
função principal() {
números := []int{1, 2, 3}
processo (números)
fmt.Println(nums) // [100, 2, 3]
}
Rust requer mutabilidade explícita:
fn process(data: &mut [i32]) { // &mut explícito
dados[0] = 100;
}
fn principal() {
deixe mut nums = vec![1, 2, 3]; // palavra-chave mut necessária
process(&mut nums); // &mut explícito
println!("{:?}", números); // [100, 2, 3]
}
Principais diferenças:
- A mutabilidade deve ser explícita
- Sem corridas de dados ocultas
- As referências são explícitas (
&vs valor)
Vindo de Python
Mentalidade Python:
def modificar_lista(itens):
items.append(4) # Muda o original
números = [1, 2, 3]
lista_modificar(números)
imprimir(num) # [1, 2, 3, 4]
Equivalente à ferrugem:
fn modificar_lista(itens: &mut Vec<i32>) {
itens.push(4);
}
fn principal() {
deixe mut nums = vec![1, 2, 3];
modificar_list(&mut nums);
println!("{:?}", números); // [1, 2, 3, 4]
}
Principais diferenças:
- Tudo em Python é referência; Rust distingue valores e referências
- Python GC cuida da limpeza; Rust usa propriedade
- Python permite mutação livremente; A ferrugem requer
mut
Vindo de JavaScript
Mentalidade JavaScript:
function criarUsuário() {
return { nome: "Alice", email: "[email protected]" };
}
deixe usuário = criarUsuário();
deixe usuário2 = usuário; // Cópia superficial
user2.name = "Bob";
console.log(usuário.nome); // "Bob" - ambos referem-se ao mesmo objeto
Comportamento de ferrugem:
#[derivar(Clone)]
estruturar usuário {
nome: Corda,
e-mail: String,
}
fn create_user() -> Usuário {
Usuário {
nome: String::from("Alice"),
email: String::from("[email protected]"),
}
}
fn principal() {
deixe usuário = create_user();
deixe usuário2 = usuário; //Movido, não copiado
// println!("{}", usuário.nome); //ERRO: valor movido
// Se você quiser copiar o comportamento:
deixe usuário = create_user();
deixe user2 = user.clone(); //clone explícito
println!("{}", usuário.nome); // OK
}
Principais diferenças:
- Objetos JS são contados por referência; A ferrugem se move por padrão
- JS tem GC; A ferrugem tem propriedade
- A mutação JS é irrestrita; Rust impõe regras de empréstimo
Conclusão: adote o verificador de empréstimos
O sistema de propriedade parece restritivo no início, mas na verdade é libertador:
✅ Sem vazamentos de memória: Se compilar, a memória será gerenciada corretamente ✅ Sem corridas de dados: Bugs simultâneos são detectados em tempo de compilação ✅ Sem uso após livre: Impossível acessar a memória liberada ✅ Desempenho previsível: sem pausas no GC, sem alocações ocultas ✅ Refatoração destemida: o compilador detecta alterações significativas
Obtenha serviços profissionais →
A curva de aprendizado
Semana 1-2: Frustração. O verificador de empréstimo rejeita tudo. Semana 3-4: Compreensão. Você começa a pensar em propriedade. Mês 2: Fluência. Você projeta APIs que funcionam com propriedade. Mês 3+: Maestria. Você escreve Rust mais rápido do que sua linguagem antiga.
Dicas Práticas para Aprendizagem
- Leia os erros do compilador com atenção - eles são excelentes professores
- Comece com programas pequenos – domine o básico antes de construir sistemas grandes
- Use
clone()liberalmente no início - otimize mais tarde - Não lute contra o verificador de empréstimo - redesenhe se estiver com dificuldades
- Estude o código padrão da biblioteca – veja como os especialistas fazem isso
Próximas etapas
- Prática: Resolva problemas de Exercismo, LeetCode ou Advent of Code in Rust
- Leia: The Rust Book, Rust by Example, Rustonomicon (ferrugem insegura)
- Construir: Projetos reais forçam você a encontrar e resolver problemas reais
- Contribuir: Projetos Rust de código aberto dão as boas-vindas aos recém-chegados
Recursos
- A linguagem de programação Rust (O livro)
- Rust por exemplo
- Rustlings - Pequenos exercícios
- Fórum de usuários do Rust - Faça perguntas
- Esta semana em Rust - Fique atualizado