Ponteiros Inteligentes como Extensões de Propriedade
A promessa central do Rust - um proprietário por valor, verificado inteiramente em tempo de compilação - é poderosa, mas ocasionalmente muito rígida para programas reais. Estruturas de dados recursivas, caches compartilhados, grafos com múltiplos pais e estado de callback desejam algo que o modelo estrito de proprietário único não permite diretamente. Ponteiros inteligentes são como o Rust responde a essa necessidade sem abandonar a propriedade: cada um é uma struct que gerencia a propriedade em seu nome, de acordo com regras diferentes, em vez de uma maneira de contornar o verificador de empréstimos.
Esta página é a âncora conceitual para a seção de ponteiros inteligentes. Onde Noções Básicas de Ponteiros Inteligentes percorre a sintaxe de Box, Rc, Arc e RefCell um por um, esta página explica qual problema cada um está resolvendo, por que eles formam um espectro em vez de um conjunto de tipos não relacionados, e o que a mutabilidade interior especificamente troca em troca de flexibilidade.
Resumo
- Ponteiros inteligentes são tipos que possuem um valor e controlam o acesso a ele de acordo com regras diferentes de "exatamente um proprietário, verificado em tempo de compilação" - indireção de heap, contagem de referências compartilhada ou mutabilidade verificada em tempo de execução.
- Por que é Importante: Algumas formas de dados - tipos recursivos, grafos compartilhados, estado mantido por callback - não podem ser expressas apenas com propriedade e empréstimo simples, e ponteiros inteligentes fornecem maneiras controladas e ainda seguras de relaxar restrições específicas.
- Conceitos Chave: indireção de heap (
Box), propriedade compartilhada (Rc/Arc), mutabilidade interior (RefCell/Cell), verificação de empréstimo em tempo de execução, coerçãoDeref. - Quando Usar: Tipos recursivos e objetos de trait (
Box); múltiplos proprietários de dados predominantemente de leitura (Rc/Arc); mutação através de um handle compartilhado (RefCell); qualquer um dos acima entre threads (Arc, com um lock interno). - Limitações / Compromissos: Cada relaxamento tem um custo concreto -
Boxcusta uma alocação e indireção,Rc/Arccustam sobrecarga de contagem de referências e o risco de vazamento de ciclos,RefCelltroca uma garantia de tempo de compilação por um pânico em tempo de execução em caso de conflito. - Tópicos Relacionados: propriedade e movimentos, empréstimo e tempos de vida,
Derefe objetos de trait, primitivas de concorrência (Mutex,RwLock).
Fundamentos
A propriedade em Rust, por padrão, é exclusiva e singular: um valor tem um proprietário, e quando esse proprietário sai do escopo, o valor é descartado. Referências (&T, &mut T) permitem que outro código empreste acesso temporariamente, mas o empréstimo não altera quem é o proprietário do valor, e o verificador de empréstimos garante que os empréstimos nunca sobrevivam ao que eles apontam.
Ponteiros inteligentes ficam uma camada acima disso. Eles são structs comuns - sem mágica especial do compilador além de implementar alguns traits padrão - que mantêm um valor e mediam como ele pode ser acessado. Box<T> é o caso mais simples: ele possui um T no heap em vez da stack, mas ainda obedece exatamente à regra de proprietário único. Nada sobre Box relaxa a propriedade; ele apenas realoca onde o valor vive.
Os outros ponteiros inteligentes relaxam restrições específicas, uma de cada vez. Rc<T> e Arc<T> relaxam "exatamente um proprietário" para "qualquer número de proprietários, rastreados por uma contagem de referências, compartilhando acesso de leitura". RefCell<T> e Cell<T> relaxam "mutação requer um empréstimo único, verificado em tempo de compilação" para "mutação é permitida através de um handle compartilhado, verificado em tempo de execução em vez disso". Cada um desses é um trade-off deliberado e estreito, não um bypass geral das garantias de segurança do Rust - o compilador ainda garante a segurança da memória, ele apenas impõe uma regra diferente da padrão.
Mecânicas e Interações
Uma maneira útil de organizar os quatro tipos principais é por qual regra de propriedade específica cada um está dobrando:
Box<T> -> ONDE o valor vive (heap, não stack); contagem de proprietários inalterada (1)
Rc<T> / Arc<T> -> QUANTOS proprietários um valor pode ter (muitos, contagem de referências, imutável por padrão)
RefCell<T> / Cell<T> -> QUANDO a mutação é verificada (tempo de execução, não tempo de compilação)
Box<T> não muda nada sobre a semântica de propriedade - ainda é exatamente um proprietário - apenas onde os dados vivem e como são acessados (através de um nível de indireção de ponteiro, resolvido via Deref). É por isso que Box é a ferramenta para tipos recursivos (enum List { Cons(i32, Box<List>), Nil }, onde uma struct de tamanho fixo não pode conter a si mesma diretamente) e para objetos de trait (Box<dyn Trait>, onde o compilador precisa de um handle de tamanho fixo para um valor cujo tipo concreto varia em tempo de execução).
Rc<T> responde a uma pergunta diferente: e se um valor realmente precisar de mais de um proprietário? Clonar um Rc não clona os dados subjacentes - ele incrementa uma contagem interna e devolve outro handle para a mesma alocação. O valor é descartado apenas quando o último Rc apontando para ele é descartado e a contagem chega a zero. Como múltiplos proprietários poderiam competir para mutar os mesmos dados, Rc<T> fornece apenas acesso compartilhado (&T) por padrão - ele resolve o problema da contagem de proprietários, mas deliberadamente não resolve a mutabilidade. Arc<T> é a mesma ideia com uma contagem de referências atômica, tornando seguro compartilhar entre threads ao custo da sobrecarga dessa atomicidade.
RefCell<T> resolve um terceiro problema ortogonal: e se um valor tiver um proprietário (ou for compartilhado via Rc), mas o código precisar mutá-lo através de uma referência que o compilador só consegue ver como compartilhada? A regra usual do Rust - muitos leitores ou um escritor, imposta em tempo de compilação - é às vezes muito grosseira para padrões como um cache atualizado de dentro de um método de outra forma somente leitura. RefCell move essa verificação para o tempo de execução: borrow() e borrow_mut() rastreiam empréstimos pendentes em um contador oculto e entram em pânico se você violar a regra "muitos leitores ou um escritor" em tempo de execução em vez de recusar a compilação.
use std::cell::RefCell;
struct Cache { hits: RefCell<u32> }
impl Cache {
// &self, não &mut self - mas isso muta. RefCell é o que torna isso
// legal: a verificação de empréstimo que normalmente ocorreria em tempo de compilação
// (via &mut self) ocorre aqui, em tempo de execução, por chamada.
fn hit(&self) {
*self.hits.borrow_mut() += 1;
}
}Essa combinação - Rc<RefCell<T>> para estado mutável compartilhado em single-thread, Arc<Mutex<T>> para o mesmo entre threads - é comum o suficiente para ser considerada um idiom próprio, precisamente porque empilha duas relaxações ortogonais: "mais de um proprietário" (Rc/Arc) e "mutar através de um handle compartilhado" (RefCell/Mutex).
Considerações Avançadas e Aplicações
Os custos de cada ponteiro inteligente são concretos e valem a pena ser nomeados explicitamente, porque "apenas envolva em Rc<RefCell<...>>" é um trade-off real, não uma conveniência gratuita.
Box custa uma alocação de heap e uma indireção de ponteiro por valor - barato, mas não grátis, e desnecessário para valores pequenos que ficariam bem na stack. Rc/Arc custam a memória para a contagem em si e o custo da CPU para incrementá-la/decrementá-la em cada clone e drop (uma operação atômica para Arc, que é mais cara que o incremento simples de Rc - é exatamente por isso que código single-thread deve preferir Rc em vez de Arc em vez de usar a versão thread-safe por precaução). RefCell custa uma verificação em tempo de execução em cada chamada de borrow/borrow_mut, e - mais seriamente - converte o que seria um erro de tempo de compilação em um pânico em tempo de execução se a regra "um escritor ou muitos leitores" for violada, o que aparece apenas quando o caminho de código violador realmente executa.
Ciclos de referência são o caso extremo mais agudo desta família. Dois Rcs que apontam um para o outro, direta ou através de uma cadeia, nunca atingirão uma contagem forte de zero, então o ciclo vaza memória pela vida do programa - o modelo de propriedade do Rust previne ponteiros pendentes e corridas de dados, mas não previne esse tipo específico de vazamento. Weak<T>, uma referência não proprietária que não contribui para a contagem forte, é a correção padrão: links pai-para-filho usam Rc, filho-para-pai (ou qualquer "referência de volta") usa Weak, então o ciclo é quebrado por construção.
| Abordagem | Força | Fraqueza | Melhor Encaixe |
|---|---|---|---|
Box<T> | Sobrecarga mínima, proprietário único e claro, habilita tipos recursivos e objetos de trait | Ainda proprietário único; não resolve compartilhamento ou mutação-através-de-referência-compartilhada | Enums recursivos, valores grandes na stack, armazenamento dyn Trait |
Rc<T> / Arc<T> | Múltiplos proprietários sem código unsafe; Arc é thread-safe | Sobrecarga de contagem de referências; ciclos vazam; predominantemente leitura por padrão | Grafos/árvores compartilhados, predominantemente imutáveis; dados compartilhados entre threads (Arc) |
RefCell<T> / Mutex<T> | Mutação através de um handle compartilhado, single-threaded ou cross-thread respectivamente | Risco de pânico em tempo de execução (RefCell) ou bloqueio (Mutex) em vez de segurança em tempo de compilação | Estado interno por trás de uma API de outra forma compartilhada (caches, contadores, estado de callback) |
A mutabilidade interior também é onde o modelo de segurança do Rust mostra seu limite real mais claramente: UnsafeCell<T> é o único primitivo por baixo de Cell e RefCell, e é a única maneira legal de mutar através de uma referência compartilhada em Rust - todo o resto descrito aqui são abstrações seguras construídas sobre essa única fundação unsafe, o que vale a pena saber ao depurar por que algum tipo é ou não Sync.
Concepções Erradas Comuns
- "Ponteiros inteligentes permitem contornar o verificador de empréstimos" - Eles não o desativam; cada um realoca ou relaxa uma regra específica (onde um valor vive, quantos proprietários ele tem, ou quando a mutação é verificada) enquanto o compilador ainda impõe todo o resto, incluindo marcadores de thread-safety como
Send/Sync. - "
Rc<T>permite mutar o valor compartilhado diretamente" -Rcapenas resolve o problema da contagem de proprietários e entrega acesso compartilhado (&T); mutar através dele requer emparelhá-lo comRefCell(ouCell), que é uma adição separada e deliberada. - "
RefCellé apenas uma versão mais lenta de&mut" - Não é apenas mais lento; muda quando a regra de empréstimo é verificada (tempo de execução em vez de tempo de compilação), o que significa que uma violação que seria um erro de compilação se torna um pânico possível durante a execução. - "
Arcé sempre o padrão seguro, então tudo bem usá-lo mesmo em código single-threaded" - A contagem atômica de referências doArcé sobrecarga pura quando nada cruza uma fronteira de thread;Rcé o padrão correto para compartilhamento single-threaded. - "Um ciclo de
Rcs será eventualmente limpo, como um coletor de lixo faria" - Rust não tem um coletor de lixo que coleta ciclos; um ciclo de referência forte nunca atinge uma contagem zero e vaza pela vida do programa, a menos que seja quebrado comWeak.
FAQs
Qual problema `Box<T>` realmente resolve, se ele ainda tem exatamente um proprietário?
Ele resolve onde um valor vive, não quantos proprietários ele tem. Colocar um valor no heap dá a ele um handle de tamanho fixo, independentemente do tamanho real do valor, o que é necessário para tipos recursivos e para objetos de trait (Box<dyn Trait>) onde o tipo concreto não é conhecido até o tempo de execução.
Por que `Rc<T>` só fornece referências compartilhadas, nunca mutáveis?
Como mais de um Rc pode apontar para os mesmos dados ao mesmo tempo, entregar &mut T através de qualquer um deles permitiria que dois proprietários mutassem concorrentemente, quebrando a garantia central de alias do Rust. Rc resolve apenas "quantos proprietários"; mutação através de um proprietário compartilhado requer um mecanismo separado como RefCell.
O que é realmente diferente entre `Rc<T>` e `Arc<T>`?
Eles rastreiam a mesma ideia - um proprietário compartilhado e com contagem de referências - mas Arc usa um contador atômico, tornando as operações de incremento/decremento seguras entre threads a um custo real de CPU, enquanto Rc usa um contador simples (não atômico) que é mais rápido, mas não é Send, portanto não pode cruzar limites de thread.
Por que `RefCell::borrow_mut()` entra em pânico em vez de retornar um erro?
borrow_mut() entra em pânico por padrão porque violar a regra de empréstimo é tratado como um erro de programação, a mesma categoria de um índice fora dos limites. Para chamadas que desejam lidar com um conflito graciosamente em vez de entrar em pânico, try_borrow_mut() retorna um Result em vez disso.
Como `RefCell` realmente impõe "um escritor ou muitos leitores" sem a ajuda do compilador?
Ele mantém um contador oculto de empréstimos pendentes, incrementado e decrementado à medida que os guards Ref/RefMut são criados e descartados. borrow_mut() verifica se o contador é exatamente zero antes de prosseguir, e borrow() verifica se nenhum empréstimo exclusivo está pendente - a mesma regra que o compilador impõe estaticamente, apenas verificada no momento da chamada em vez disso.
Por que `Rc<RefCell<T>>` aparece junto com tanta frequência?
Porque eles resolvem dois problemas independentes que ocorrem frequentemente juntos: Rc permite mais de um proprietário, e RefCell permite mutação através de um handle compartilhado (não &mut). Nenhum deles sozinho lhe dá "múltiplos proprietários que podem mutar", então a combinação é a maneira idiomática de obter ambos.
Um ciclo de referência de `Rc`s pode causar um vazamento de memória?
Sim - se Rc A aponta para Rc B e B aponta de volta para A, a contagem forte de nenhum deles nunca atinge zero, então nenhum deles é descartado, mesmo que nada fora do ciclo referencie nenhum deles. Este é um vazamento real que o modelo de propriedade do Rust não impede por si só.
Como `Weak<T>` previne esse tipo de ciclo?
Uma referência Weak<T> não incrementa a contagem forte, apenas uma contagem fraca separada, então ela não mantém o valor vivo por si só. Estruturar referências de volta (como o ponteiro de um filho para seu pai) como Weak em vez de Rc significa que o grafo de referência forte não tem ciclo, então a contagem normal recupera a memória corretamente.
`Cell<T>` é apenas um `RefCell<T>` mais simples?
Eles resolvem a mesma categoria de problema (mutação através de uma referência compartilhada) de maneiras diferentes: Cell requer T: Copy (ou usa replace/take para trocar valores inteiros) e nunca entrega uma referência para seu conteúdo, então não tem sobrecarga de rastreamento de empréstimos e não pode entrar em pânico - é um bom encaixe para tipos Copy pequenos como contadores ou flags.
Qual é a coisa real, de nível mais baixo, que torna a mutabilidade interior legal?
UnsafeCell<T>, o único primitivo na biblioteca padrão que tem permissão para entregar uma referência mutável através de uma referência compartilhada. Cell e RefCell são abstrações seguras construídas sobre ele - é código unsafe uma vez, na parte inferior, envolvido por APIs seguras acima.
Por que eu escolheria `Box` em vez de um parâmetro genérico?
Genéricos monomorfizam - o compilador gera uma cópia especializada por tipo concreto, o que é rápido, mas não pode conter uma mistura de tipos em uma única coleção. Box<dyn Trait> apaga o tipo concreto atrás de um ponteiro de tamanho fixo e uma vtable, o que custa uma alocação e despacho dinâmico, mas permite que você armazene tipos heterogêneos (como um Vec<Box<dyn Draw>> de formas diferentes) uniformemente.
Usar `Arc<Mutex<T>>` garante que meu código está livre de bugs de concorrência?
Garante que a classe específica de bug "mutação não sincronizada" é prevenida - Mutex garante que apenas uma thread muta o valor interno por vez - mas não previne corridas lógicas como deadlocks de ordenação de locks, ou leituras obsoletas se múltiplos locks protegerem invariantes relacionados de forma inconsistente.
Relacionado
- Noções Básicas de Ponteiros Inteligentes - um tour prático de
Box,Rc,Arc,RefCelleCowcom trechos executáveis. - Box<T> - alocação de heap, tipos recursivos e objetos de trait em profundidade.
- Rc & Arc - mecânicas de propriedade compartilhada,
strong_counte compartilhamento single- vs. multi-thread. - RefCell & Cell - verificação de empréstimo em tempo de execução e as condições de pânico em detalhes.
- Rc<RefCell<T>> & Arc<Mutex<T>> - o padrão combinado para estado compartilhado e mutável.
- Noções Básicas de Propriedade - as regras padrão de proprietário único que esses tipos relaxam deliberadamente.
Versões de Stack: Esta página é conceitual e não está vinculada a uma versão específica de stack.