O Modelo de Propriedade do Rust
Todo valor em um programa Rust tem exatamente um proprietário em qualquer momento, e quando esse proprietário sai do escopo, o valor é limpo automaticamente. Essa única frase é a base sobre a qual todo o resto desta seção é construído: movimentos, empréstimos e tempos de vida são todas consequências de levar "um proprietário" a sério e aplicá-lo em tempo de compilação.
Este modelo é o motivo pelo qual Rust não precisa de um coletor de lixo nem de free manual. É também a maior mudança mental para desenvolvedores que vêm de linguagens com coleta de lixo ou gerenciamento manual, porque muda o que uma simples atribuição (let b = a;) realmente faz com a.
A Filosofia de Design do Rust explica por que a sintaxe da linguagem existe para suportar este modelo. Esta página é a âncora conceitual para a própria seção: Noções Básicas de Propriedade e as páginas seguintes abordam a mecânica na prática, baseando-se no modelo descrito aqui.
Resumo
- Todo valor tem um único proprietário responsável por sua limpeza; a propriedade pode se mover entre ligações, e outro código pode emprestar acesso temporariamente sem tomá-la.
- Por Que Importa: É assim que Rust obtém segurança de memória e ausência de corridas de dados sem um coletor de lixo ou
free/deletemanual - o compilador prova que as regras são seguidas antes que o programa seja executado. - Conceitos Chave: propriedade, semântica de movimento,
Copy, empréstimo, o verificador de empréstimos, tempos de vida. - Quando Usar Este Modelo: Para raciocinar sobre erros de "por que isso não compila", decidir se uma função deve tomar posse ou uma referência, projetar campos de struct que contenham dados emprestados e ler todas as páginas posteriores desta seção.
- Limitações / Compromissos: O modelo rejeita alguns padrões que são perfeitamente seguros na prática, mas que o compilador ainda não consegue provar como seguros (alguns deles estão sendo flexibilizados ao longo do tempo, mas não todos), e pede ao desenvolvedor para pensar explicitamente sobre o tempo de vida dos dados em vez de adiá-lo para um coletor.
- Tópicos Relacionados: semântica de movimento e
Copy, empréstimo e referências, anotações de tempo de vida, ponteiros inteligentes para propriedade compartilhada.
Fundamentos
Imagine a propriedade como um objeto físico com exatamente um detentor por vez. Se você entregar o objeto para outra pessoa, você não o tem mais - você não pode usá-lo e não é mais responsável por ele. Isso é um movimento em Rust: let b = a; transfere a responsabilidade pelo valor de a para b, e o compilador subsequentemente trata a como inválido.
Isso difere acentuadamente da maioria das linguagens. Em Java ou Python, b = a faz com que dois nomes se refiram ao mesmo objeto, ambos utilizáveis, com um coletor de lixo decidindo mais tarde quando o objeto não é mais alcançável de lugar nenhum. Em C, b = a pode copiar bytes ou copiar um ponteiro, e a linguagem não o impede de usar um ponteiro depois que seu alvo foi liberado em outro lugar. O modelo de movimento do Rust fecha essa lacuna por construção: nunca há um momento em que duas ligações ativas reivindiquem possuir a mesma alocação de heap.
Alguns tipos optam por isso implementando o trait Copy. Tipos pequenos e apenas de stack como i32, bool e char são Copy, o que significa que a atribuição duplica o valor em vez de movê-lo, e ambas as ligações permanecem válidas. String e Vec<T> não são Copy, porque eles possuem memória de heap e duplicar essa memória implicitamente em cada atribuição seria uma alocação oculta e potencialmente cara que a linguagem não quer esconder de você.
Propriedade não significa "ninguém mais pode olhar para isso". O empréstimo, via & e &mut, permite que outro código use um valor temporariamente sem tomar posse dele, o que torna a propriedade prática em vez de apenas segura.
Mecânicas e Interações
O verificador de empréstimos impõe uma regra que parece simples e tem consequências profundas: para qualquer valor dado, você pode ter qualquer número de referências compartilhadas (&T) ou exatamente uma referência exclusiva (&mut T), mas nunca ambos os tipos ao mesmo tempo. Isso às vezes é expresso como "aliasing xor mutabilidade" - um valor pode ser aliased (lido de vários lugares) ou mutado, nunca ambos simultaneamente.
Essa regra é o que impede corridas de dados em tempo de compilação, tanto em código single-threaded quanto multi-threaded. Uma corrida de dados requer pelo menos duas coisas acessando a mesma memória com pelo menos uma delas escrevendo, sem sincronização; se o compilador nunca permitir que uma referência compartilhada coexista com uma mutável, essa premissa simplesmente não pode ocorrer.
Tempos de vida são a contabilidade que torna o empréstimo verificável. Cada referência é válida por algum período do programa, e o compilador precisa provar que esse período nunca excede os dados para os quais aponta.
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() { x } else { y }
}O 'a aqui não muda como a função é executada; ele diz ao compilador "a referência retornada vive no máximo o tempo de vida mais curto de x e y", para que o compilador possa rejeitar qualquer chamador que permita que o resultado exceda os dados de origem. Tempos de vida são completamente apagados quando o programa executa - eles existem apenas para permitir que essa prova aconteça antecipadamente.
A maioria dos tempos de vida nunca precisa ser escrita. O compilador aplica um pequeno conjunto de regras previsíveis, chamadas elipse de tempo de vida, para inferi-los em assinaturas de função comuns, que é por que a maioria do código Rust que você lê tem poucas ou nenhuma anotação explícita de 'a. Tempos de vida explícitos aparecem principalmente quando a assinatura de uma função é ambígua sobre a qual entrada uma referência retornada se relaciona, ou quando uma struct armazena uma referência emprestada como um campo.
Movimentos, empréstimos e tempos de vida interagem constantemente. Um movimento termina imediatamente a propriedade de uma ligação; um empréstimo suspende o acesso total temporariamente e deve terminar antes que o proprietário seja usado de forma conflitante novamente; um tempo de vida é o registro do compilador de quanto tempo essa suspensão, ou essa propriedade em si, é permitida durar em relação a todo o resto no escopo.
Considerações Avançadas e Aplicações
A propriedade não é o único modelo para gerenciar memória com segurança, e saber contra o que ela se compara esclarece quando a abordagem do Rust vale seu atrito.
| Abordagem | Força | Fraqueza | Melhor Ajuste |
|---|---|---|---|
| Propriedade única + verificador de empréstimos (padrão Rust) | Sem pausa de GC; corridas de dados prevenidas em tempo de compilação | Alguns padrões legitimamente seguros são rejeitados até serem reestruturados | Software de sistemas críticos para desempenho ou de longa execução |
Propriedade compartilhada Rc/Arc | Múltiplos proprietários sem redesenhar o fluxo de dados | Sobrecarga de contagem de referências; ciclos/vazamentos possíveis | Estruturas semelhantes a grafos ou caches compartilhados com propriedade única incerta |
| Coleta de lixo | Nenhum raciocínio explícito de tempo de vida necessário | Pausa/sobrecarga em tempo de execução; corridas em estado mutável compartilhado ainda possíveis | Código de aplicativo onde a velocidade do desenvolvedor supera o controle granular |
| Gerenciamento manual de memória | Controle total, sem abstração imposta | Uso após liberação e corridas são inteiramente responsabilidade do desenvolvedor | Ambientes onde nem um GC nem um verificador de empréstimos estão disponíveis |
Na prática, a maioria do código Rust real não é puramente de propriedade única de ponta a ponta. Quando um valor genuinamente precisa de múltiplos proprietários - uma entrada de cache compartilhada, um nó em um grafo - o movimento idiomático é recorrer a Rc<T> (ou Arc<T> entre threads), que adiciona contagem de referências sobre a propriedade em vez de abandonar o modelo. Tipos de mutabilidade interna como Cell e RefCell permitem relaxar as regras de empréstimo em tempo de compilação para uma verificação em tempo de execução, nos casos restritos em que o compilador não consegue ver o suficiente para provar a segurança que você sabe que existe.
O retorno se acumula em código concorrente. Como estado mutável compartilhado sem sincronização simplesmente não compila, categorias inteiras de bugs de condição de corrida que são notoriamente difíceis de reproduzir e depurar em outras linguagens são pegas no momento em que você tenta escrevê-los, não depois que eles são lançados. É por isso que o Rust assíncrono também depende tanto da propriedade: um future que captura dados por propriedade, em vez de por referência ambiente, pode ser movido entre threads e consultado independentemente sem nenhum dos perigos de alias que afligem a concorrência baseada em callbacks em outros lugares.
O custo aparece como lutar contra o verificador de empréstimos - reestruturar o código porque o compilador não consegue provar um padrão como seguro, mesmo que um humano possa ver que ele é. Tempos de vida não lexicais (uma melhoria em como precisamente o compilador rastreia quando um empréstimo realmente termina) reduziram significativamente esse atrito desde os primeiros anos do Rust, mas ele não desapareceu, e aprender a projetar em torno dele (emprestar em vez de possuir, dividir structs, escolher Rc deliberadamente) é uma habilidade real e contínua.
Equívocos Comuns
- "Mover um valor o copia por baixo dos panos, apenas com regras extras." Um movimento é uma transferência superficial e barata da representação do valor (ponteiro, comprimento, capacidade para um
String); o compilador então torna a ligação antiga inutilizável, ele não duplica os dados subjacentes. - "Emprestar é apenas um tipo mais rigoroso de ponteiro, então funciona como um ponteiro C com rodinhas de apoio." Uma referência Rust é verificada em tempo de compilação para nunca exceder seu alvo e nunca aliasar uma referência mutável; um ponteiro C não oferece nenhuma dessas garantias.
- "Tempos de vida controlam quanto tempo um valor vive, como um temporizador manual." Tempos de vida descrevem por quanto tempo uma referência é válida para usar, derivada do escopo real dos dados - eles não estendem nem encurtam nada em tempo de execução.
- "Você precisa de
RcouArcsempre que um valor é usado em mais de um lugar." O empréstimo cobre a esmagadora maioria dos casos de "usado em mais de um lugar"; tipos de propriedade compartilhada são para quando nenhum proprietário único pode ser identificado, o que é menos comum do que parece inicialmente. - "O verificador de empréstimos ficou mais fácil com o tempo porque as regras ficaram mais flexíveis." As regras não ficaram mais flexíveis; a análise do compilador ficou mais inteligente (tempos de vida não lexicais rastreiam os fins de empréstimo com mais precisão), então mais código genuinamente seguro agora é aceito sem que as garantias subjacentes mudem.
FAQs
O que exatamente acontece quando eu escrevo `let b = a;` para um `String`?
A propriedade dos dados da string alocados no heap é transferida de a para b. O ponteiro, comprimento e capacidade são copiados para b na stack, mas a é então tratado pelo compilador como inválido - usá-lo depois é um erro de compilação, não um bug em tempo de execução.
Por que Rust não copia o valor em vez de movê-lo?
Copiar uma alocação de heap em cada atribuição seria uma operação implícita e potencialmente cara que a linguagem deliberadamente se recusa a esconder. Se você quiser uma duplicata, .clone() torna esse custo visível no código.
O que torna um tipo `Copy` em vez de apenas para movimento?
Tipos que são pequenos, de tamanho fixo e vivem inteiramente na stack (inteiros, floats, bool, char, e tuplas/arrays de tipos Copy) podem implementar Copy. Tipos que possuem memória de heap, como String ou Vec<T>, não podem, porque duplicá-los não é uma operação trivial de bit a bit.
Como o verificador de empréstimos realmente decide que um programa é inválido?
Ele rastreia, para cada referência, o trecho de código onde ela é usada e verifica esse trecho contra a regra de alias (muitos &T ou um &mut T, nunca ambos) e contra o tempo de vida dos dados para os quais aponta. Se qualquer uma das verificações falhar em qualquer ponto desse trecho, a compilação para com um erro identificando o conflito.
Os tempos de vida têm algum efeito no binário compilado?
Não. Tempos de vida são apagados antes da geração de código; eles existem puramente para permitir que o compilador prove a validade da referência antecipadamente, e não adicionam representação ou custo em tempo de execução.
Por que raramente vejo `'a` explícito em código Rust real?
As regras de elipse de tempo de vida permitem que o compilador infira tempos de vida nas formas comuns de assinaturas de função automaticamente. Anotações explícitas se tornam necessárias principalmente quando uma função tem múltiplas entradas de referência e uma relação ambígua com a saída, ou quando uma struct armazena um campo emprestado.
Qual é a diferença entre propriedade e empréstimo, na prática?
Possuir um valor significa que você é responsável por ele e pode movê-lo, mutá-lo ou descartá-lo; emprestar significa que você tem acesso temporário (somente leitura ou exclusivo) sem assumir essa responsabilidade, e o empréstimo deve terminar antes que certos usos conflitantes do proprietário ocorram.
Quando uma função deve tomar posse versus uma referência?
Tome posse quando a função precisar manter, consumir ou transformar o valor em algo novo que sobreviva à chamada; tome uma referência (&T ou &mut T) quando a função precisar apenas lê-la ou modificá-la durante a chamada e o chamador dever continuar a usá-la depois.
O modelo de propriedade é apenas sobre segurança de memória?
Não - as mesmas regras que previnem o uso após liberação também previnem corridas de dados, porque elas impedem que uma referência mutável coexista com qualquer outro acesso aos mesmos dados. Segurança de memória e segurança de thread vêm do mesmo mecanismo.
Por que "lutar contra o verificador de empréstimos" ainda acontece com desenvolvedores experientes?
Alguns padrões são seguros na prática, mas exigem informações que o compilador não consegue ver apenas pelo código (por exemplo, dois campos disjointes emprestados através de um limite de método). Estes geralmente requerem reestruturação - dividir empréstimos, mudar a forma de uma API ou introduzir um tipo explícito de propriedade compartilhada - em vez de indicar um erro.
Como `Rc` e `Arc` se encaixam em um modelo de proprietário único?
Eles adicionam contagem de referências sobre a propriedade em vez de substituí-la: o Rc/Arc em si é o único proprietário da contagem, e cloná-lo incrementa essa contagem em vez de fazer uma cópia profunda dos dados subjacentes. O valor é liberado apenas quando a contagem chega a zero.
A propriedade torna Rust mais lento por causa de toda essa verificação?
Não - tudo isso acontece em tempo de compilação e é apagado do binário. O benefício em tempo de execução é o oposto: sem pausas de coletor de lixo e sem sobrecarga de contagem de referências, a menos que você escolha explicitamente Rc/Arc.
Relacionados
- Noções Básicas de Propriedade - o passo a passo prático que esta página ancora
- Semântica de Movimento e Cópia - a distinção movimento versus cópia em profundidade
- Empréstimo e Referências - mecânicas de
&e&mut - Tempos de Vida Explicados - o modelo completo de tempo de vida
- Prévia de Propriedade - a primeira olhada sobre a qual esta seção se baseia
Versões de Stack: Esta página foi escrita para Rust 1.97.0 (edição 2024), Tokio 1.x, Axum 0.8, serde 1.0, sqlx 0.8, clap 4, e Polars 0.46+.