Ponteiros inteligentes em Rust
As regras de ownership (posse) e borrowing (empréstimo) dão conta da maioria das situações, mas alguns casos reais não cabem direitinho na ideia de “exatamente um dono”: um valor cujo tamanho só é conhecido em tempo de execução, dados que realmente precisam de mais de um dono, ou um valor que você precisa alterar através de uma referência compartilhada. Os smart pointers (ponteiros inteligentes) são a resposta do Rust para cada um desses casos.
Box<T>: colocando um valor na heap
Seção intitulada “Box<T>: colocando um valor na heap”Por padrão, os valores ficam na stack (pilha), e o compilador precisa saber o tamanho deles de antemão. O Box<T> move um valor para a heap e te dá um ponteiro de tamanho fixo para ele. Isso é útil quando o tamanho de um tipo não pode ser conhecido em tempo de compilação, principalmente em tipos recursivos:
fn main() { let boxed = Box::new(5); println!("{}", *boxed); // * para tirar o valor de dentro}Saída:
5Um exemplo clássico de quando você precisa do Box: um tipo que contém a si mesmo, como o nó de uma lista ligada:
enum List { Cons(i32, Box<List>), Nil,}
fn main() { let list = List::Cons(1, Box::new(List::Cons(2, Box::new(List::Nil))));
match list { List::Cons(value, _) => println!("Primeiro valor: {value}"), List::Nil => println!("Vazia"), }}Saída:
Primeiro valor: 1Sem o Box, Cons(i32, List) precisaria de espaço infinito para ser guardado: uma List contendo uma List contendo uma List, para sempre. Já Box<List> é um ponteiro de tamanho fixo, então o compilador consegue calcular quanto espaço precisa.
Rc<T>: vários donos
Seção intitulada “Rc<T>: vários donos”O ownership diz que um valor tem exatamente um dono. Na maior parte do tempo isso é exatamente o certo, mas às vezes você realmente precisa que várias partes do programa compartilhem os mesmos dados. O Rc<T> (“reference counted”, com contagem de referências) permite isso, mantendo a conta de quantos donos existem no momento:
use std::rc::Rc;
fn main() { let a = Rc::new(String::from("dados compartilhados")); let b = Rc::clone(&a); // não é uma cópia profunda, só mais um dono let c = Rc::clone(&a);
println!("{a} {b} {c}"); println!("Donos: {}", Rc::strong_count(&a));}Saída:
dados compartilhados dados compartilhados dados compartilhadosDonos: 3O Rc::clone não copia os dados: ele só aumenta um contador e devolve mais uma referência para o mesmo valor. Os dados só são liberados quando todos os donos saem de escopo.
RefCell<T>: alterando através de uma referência compartilhada
Seção intitulada “RefCell<T>: alterando através de uma referência compartilhada”Normalmente, as regras de borrowing são verificadas em tempo de compilação: ou várias referências só de leitura, ou uma mutável, nunca as duas coisas ao mesmo tempo. O RefCell<T> leva essa mesma verificação para o tempo de execução, o que permite alterar um valor mesmo através do que parece ser uma referência compartilhada, sem mut:
use std::cell::RefCell;
fn main() { let value = RefCell::new(5);
*value.borrow_mut() += 1;
println!("{}", value.borrow());}Saída:
6O RefCell costuma andar junto com o Rc: Rc<RefCell<T>> te dá um valor com vários donos que ainda pode ser alterado, coisa que o Rc<T> sozinho não permite.
Quando usar cada um de verdade
Seção intitulada “Quando usar cada um de verdade”Não comece pelos smart pointers. Comece com ownership e borrowing comuns e só recorra a eles quando o compilador estiver deixando claro que não vai dar certo: um tipo recursivo (Box), um valor que realmente precisa de vários donos (Rc) ou uma alteração através de uma referência compartilhada que as regras de borrowing não permitem (RefCell).
Prática
Seção intitulada “Prática”1. Somar uma lista ligada
Seção intitulada “1. Somar uma lista ligada”Use o enum List da seção sobre Box. Monte a lista 1, 2, 3 e escreva uma função fn sum(list: &List) -> i32 que some todos os valores. Dica: faça um match na lista. Para Cons(value, rest), a resposta é value mais a soma de rest. Para Nil, é 0.
Ver solução
enum List { Cons(i32, Box<List>), Nil,}
fn sum(list: &List) -> i32 { match list { List::Cons(value, rest) => value + sum(rest), List::Nil => 0, }}
fn main() { let list = List::Cons(1, Box::new(List::Cons(2, Box::new(List::Cons(3, Box::new(List::Nil)))))); println!("Soma: {}", sum(&list));}Saída:
Soma: 6A função chama a si mesma com o resto da lista até chegar ao Nil. Repare que dá para passar rest (um &Box<List>) onde se espera um &List. O Rust enxerga através do Box para você.
2. Uma lista de compras compartilhada
Seção intitulada “2. Uma lista de compras compartilhada”Crie uma lista de compras (um Vec<String>) que duas pessoas, alice e bob, sejam donas e possam alterar. Cada uma acrescenta um item. Depois imprima a lista e o número de donos. Dica: embrulhe o Vec em Rc<RefCell<...>>, dê um Rc::clone para cada pessoa e chame .borrow_mut().push(...) para acrescentar.
Ver solução
use std::cell::RefCell;use std::rc::Rc;
fn main() { let list = Rc::new(RefCell::new(Vec::new())); let alice = Rc::clone(&list); let bob = Rc::clone(&list);
alice.borrow_mut().push(String::from("leite")); bob.borrow_mut().push(String::from("pão"));
println!("{:?}", list.borrow()); println!("Donos: {}", Rc::strong_count(&list));}Saída:
["leite", "pão"]Donos: 3São três donos: list, alice e bob. Os três apontam para o mesmo Vec, então um item acrescentado por alice também aparece por bob e por list. Nenhuma das variáveis precisa de mut, porque quem cuida da alteração é o RefCell.
Box<T>guarda um valor na heap com um ponteiro de tamanho fixo; é necessário em tipos recursivos.Rc<T>permite vários donos para o mesmo valor em código de uma thread só, usando contagem de referências; entre threads, useArc<T>.RefCell<T>verifica as regras de borrowing em tempo de execução em vez de na compilação, permitindo alterar um valor através de uma referência compartilhada, mas entra em panic se as regras forem quebradas.- Use esses tipos só quando ownership e borrowing comuns não servirem para o seu caso.
Teste o que aprendeu
Seção intitulada “Teste o que aprendeu”Teste o que aprendeu
1. Por que um tipo recursivo, como o nó de uma lista ligada, precisa de Box<T>?
2. O que Rc::clone(&a) faz de verdade?
3. Quando o RefCell<T> verifica as regras de borrowing?
4. O que Rc<RefCell<T>> te dá que o Rc<T> sozinho não dá?
Pontuação: 0 / 4
Para saber mais
Seção intitulada “Para saber mais”- Using
Box<T>to Point to Data on the Heap — The Rust Book (em inglês): a história completa doBox, incluindo o exemplo da lista ligada (cons list) usado nesta página. Rc<T>, the Reference Counted Smart Pointer — The Rust Book (em inglês): ownership compartilhado passo a passo, acompanhando o contador subir e descer.RefCell<T>and the Interior Mutability Pattern — The Rust Book (em inglês): quando e por que levar as verificações de empréstimo para o tempo de execução, com um exemplo realista.std::cell— documentação da biblioteca padrão (em inglês): explicaCelleRefCell, e quando escolher cada um.- Rc — Rust by Example (em inglês): um exemplo curto e executável que imprime o contador de referências conforme os donos aparecem e somem.