Weak e Cow em Rust: ciclos de Rc e clone-on-write
Na página de Ponteiros inteligentes, você conheceu Box, Rc e RefCell. Esta página traz mais duas ferramentas que resolvem dois problemas bem reais. O Weak<T> deixa você apontar para um valor sem mantê-lo vivo. O Cow<T> deixa uma função devolver dados emprestados quando possível, e só fazer uma cópia quando for preciso.
Pense no Rc como a chave de uma casa: enquanto alguém tiver uma chave, a casa fica de pé. O Weak é mais como o endereço anotado num papel. Você pode usá-lo para achar a casa, mas, se todo mundo devolver as chaves e a casa deixar de existir, o endereço simplesmente não leva a lugar nenhum. Ninguém fica preso pagando por uma casa da qual se esqueceu.
O problema: ciclos de Rc nunca são limpos
Seção intitulada “O problema: ciclos de Rc nunca são limpos”Um valor Rc é descartado (dropped) quando o seu contador forte (strong count) chega a 0. Isso funciona muito bem, até que dois valores guardem um Rc um do outro. Aí cada um mantém o outro vivo, para sempre.
Para ver isso acontecer, o tipo Person abaixo implementa Drop, então ele imprime uma mensagem quando é liberado. Repare em quais mensagens aparecem:
use std::cell::RefCell;use std::rc::Rc;
struct Person { name: String, friend: RefCell<Option<Rc<Person>>>,}
impl Drop for Person { fn drop(&mut self) { println!("liberando {}", self.name); }}
fn main() { let alice = Rc::new(Person { name: String::from("alice"), friend: RefCell::new(None) }); let bob = Rc::new(Person { name: String::from("bob"), friend: RefCell::new(None) });
// alice aponta para bob, e bob aponta de volta para alice *alice.friend.borrow_mut() = Some(Rc::clone(&bob)); *bob.friend.borrow_mut() = Some(Rc::clone(&alice));
println!("contador forte de alice: {}", Rc::strong_count(&alice)); println!("contador forte de bob: {}", Rc::strong_count(&bob)); println!("fim do main");}Saída:
contador forte de alice: 2contador forte de bob: 2fim do mainRepare no que está faltando: não aparece nenhum liberando alice nem liberando bob. Quando o main termina, as variáveis alice e bob vão embora, então cada contador cai de 2 para 1. Mas ele nunca chega a 0, porque cada pessoa ainda é segurada pelo campo friend da outra. Essa memória vazou (memory leak).
Não se sinta mal se isso pegou você de surpresa. O compilador não consegue detectar esse problema, e ele não derruba o programa. É só memória que nunca volta. Num programa que roda por muito tempo e vive criando ciclos, isso vai se acumulando.
Weak<T>: um ponteiro que não mantém nada vivo
Seção intitulada “Weak<T>: um ponteiro que não mantém nada vivo”A solução é tornar um dos links, ou os dois, fraco (weak). Você obtém um Weak a partir de um Rc com Rc::downgrade. Um link fraco não conta como dono, então ele não impede que o valor seja descartado.
Como o valor pode já ter sumido, você não pode usar um Weak diretamente. Você chama .upgrade(), que devolve um Option<Rc<T>>: Some se o valor ainda está vivo, None se não está.
use std::cell::RefCell;use std::rc::{Rc, Weak};
struct Person { name: String, friend: RefCell<Weak<Person>>,}
impl Drop for Person { fn drop(&mut self) { println!("liberando {}", self.name); }}
fn main() { let alice = Rc::new(Person { name: String::from("alice"), friend: RefCell::new(Weak::new()) }); let bob = Rc::new(Person { name: String::from("bob"), friend: RefCell::new(Weak::new()) });
*alice.friend.borrow_mut() = Rc::downgrade(&bob); *bob.friend.borrow_mut() = Rc::downgrade(&alice);
if let Some(friend) = alice.friend.borrow().upgrade() { println!("o amigo de alice é {}", friend.name); }
println!("alice: fortes = {}, fracos = {}", Rc::strong_count(&alice), Rc::weak_count(&alice)); println!("fim do main");}Saída:
o amigo de alice é bobalice: fortes = 1, fracos = 1fim do mainliberando bobliberando aliceDesta vez, as duas linhas liberando aparecem. A Alice tem um dono forte (a variável alice) e um link fraco (vindo do Bob). Quando o main termina, o contador forte chega a 0, e o Rust a libera. O Bob vai primeiro porque as variáveis são descartadas na ordem inversa da criação.
Algumas coisas para lembrar:
Weak::new()cria um ponteiro fraco vazio, que não aponta para nada.upgrade()nele sempre retornaNone. É um valor inicial bem prático.Rc::strong_countconta os donos.Rc::weak_countconta os links fracos. Só o contador forte decide quando o valor é descartado.- O
Rcque você recebe deupgrade()é um dono de verdade, só que temporário. Quando ele sai de escopo, o contador forte volta a diminuir.
Uma árvore de pastas: os filhos são donos, os pais são fracos
Seção intitulada “Uma árvore de pastas: os filhos são donos, os pais são fracos”Árvores são o lugar clássico para o Weak. Uma pasta é dona das suas subpastas, então children guarda Rc. Uma subpasta também quer saber quem é a pasta pai, mas não deveria ser dona dela. Se as duas direções usassem Rc, você teria um ciclo de novo. Por isso, parent é um Weak:
use std::cell::RefCell;use std::rc::{Rc, Weak};
struct Folder { name: String, parent: RefCell<Weak<Folder>>, children: RefCell<Vec<Rc<Folder>>>,}
fn new_folder(name: &str) -> Rc<Folder> { Rc::new(Folder { name: name.to_string(), parent: RefCell::new(Weak::new()), children: RefCell::new(Vec::new()), })}
fn add_child(parent: &Rc<Folder>, child: Rc<Folder>) { *child.parent.borrow_mut() = Rc::downgrade(parent); parent.children.borrow_mut().push(child);}
fn full_path(folder: &Folder) -> String { match folder.parent.borrow().upgrade() { Some(parent) => format!("{}/{}", full_path(&parent), folder.name), None => folder.name.clone(), }}
fn main() { let home = new_folder("home"); let photos = new_folder("fotos"); let trips = new_folder("viagens");
add_child(&home, Rc::clone(&photos)); add_child(&photos, Rc::clone(&trips));
match photos.parent.borrow().upgrade() { Some(parent) => println!("fotos fica dentro de {}", parent.name), None => println!("fotos não tem pasta pai"), }
println!("caminho completo: {}", full_path(&trips)); println!("home: fortes = {}, fracos = {}", Rc::strong_count(&home), Rc::weak_count(&home)); println!("fotos: fortes = {}, fracos = {}", Rc::strong_count(&photos), Rc::weak_count(&photos));}Saída:
fotos fica dentro de homecaminho completo: home/fotos/viagenshome: fortes = 1, fracos = 1fotos: fortes = 2, fracos = 1Leia os contadores com calma. photos tem dois donos fortes: a variável photos e a lista children dentro de home. Ela tem um link fraco: o campo parent de trips. home tem só um dono forte, a própria variável, porque ninguém é dono da sua pasta pai. Esse é exatamente o formato que você quer: a posse flui para baixo na árvore, e os links que vão para cima são fracos.
O RefCell está ali porque você define parent e coloca itens em children depois que as pastas já estão dentro de um Rc, e o Rc sozinho só dá acesso compartilhado, somente leitura.
Listas fracas que não mantêm nada vivo
Seção intitulada “Listas fracas que não mantêm nada vivo”O Weak também é ótimo para listas do tipo “fique de olho nestes, mas não segure eles”: listeners (ouvintes), observers ou um cache de coisas que pertencem a outra parte do programa. Aqui, o placar de um jogo manda atualizações para quem ainda estiver ouvindo. Quando um listener é descartado, o link fraco dele simplesmente para de funcionar:
use std::rc::{Rc, Weak};
struct Listener { name: String,}
fn notify(listeners: &[Weak<Listener>], event: &str) { for weak in listeners { match weak.upgrade() { Some(listener) => println!("{} recebeu: {event}", listener.name), None => println!("(um listener sumiu, pulando)"), } }}
fn main() { let screen = Rc::new(Listener { name: String::from("tela") }); let logger = Rc::new(Listener { name: String::from("log") });
let mut listeners: Vec<Weak<Listener>> = Vec::new(); listeners.push(Rc::downgrade(&screen)); listeners.push(Rc::downgrade(&logger));
notify(&listeners, "placar: 10");
drop(logger); // o dono fecha o log
notify(&listeners, "placar: 20");
// limpa os links que não apontam para nada listeners.retain(|weak| weak.upgrade().is_some()); println!("listeners restantes: {}", listeners.len());}Saída:
tela recebeu: placar: 10log recebeu: placar: 10tela recebeu: placar: 20(um listener sumiu, pulando)listeners restantes: 1A lista nunca impediu o log de ser descartado. Se ela guardasse Rc em vez de Weak, o log continuaria vivo dentro da lista, recebendo mensagens que ninguém queria. Com Weak, quem decide quando algo vai embora é o dono, e todo o resto só confere com upgrade().
Cow: empreste quando puder, copie quando precisar
Seção intitulada “Cow: empreste quando puder, copie quando precisar”Agora, um problema diferente. Digamos que você escreva uma função que limpa nomes de usuário. A maioria dos nomes já está certinha. Alguns têm espaços sobrando ou letras maiúsculas. Se a sua função sempre retornar uma String nova, você faz uma cópia de cada nome, até dos que você não mudou.
O Cow (“clone on write”, ou “clonar ao escrever”, de std::borrow::Cow) resolve isso. Um Cow<'_, str> é um enum com duas variantes:
Cow::Borrowed(&str): “aqui está o texto original, intocado”. Nenhuma alocação.Cow::Owned(String): “precisei mudar, então aqui está umaStringnova”.
use std::borrow::Cow;
fn normalize(name: &str) -> Cow<'_, str> { let trimmed = name.trim(); let has_upper = name.chars().any(|c| c.is_uppercase());
if trimmed.len() == name.len() && !has_upper { Cow::Borrowed(name) // já está limpo: devolve o original } else { Cow::Owned(trimmed.to_lowercase()) // precisa de ajuste: cria uma String nova }}
fn main() { for input in ["alice", " Bob ", "CAROL", "dave"] { match normalize(input) { Cow::Borrowed(name) => println!("{input:?} -> {name:?} (emprestado, sem cópia)"), Cow::Owned(name) => println!("{input:?} -> {name:?} (owned, String nova)"), } }}Saída:
"alice" -> "alice" (emprestado, sem cópia)" Bob " -> "bob" (owned, String nova)"CAROL" -> "carol" (owned, String nova)"dave" -> "dave" (emprestado, sem cópia)Dois dos quatro nomes não custaram nada. Num programa real em que 99% da entrada já vem limpa, são muitas cópias que você nunca faz. O '_ em Cow<'_, str> só diz “isto pode pegar emprestado da entrada”. Se os lifetimes ainda parecem confusos, a página de Lifetimes explica.
Usando um Cow como uma string normal
Seção intitulada “Usando um Cow como uma string normal”Normalmente você nem precisa fazer match em um Cow. Ele faz deref para &str, então você pode chamar métodos de string nele e passar &name para qualquer função que receba &str. Quando você realmente precisar de uma String sua, chame .into_owned(). Ele tira a String de dentro, se já houver uma, e só copia se o Cow estava emprestado.
use std::borrow::Cow;
fn normalize(name: &str) -> Cow<'_, str> { let trimmed = name.trim(); if trimmed.len() == name.len() && !name.chars().any(|c| c.is_uppercase()) { Cow::Borrowed(name) } else { Cow::Owned(trimmed.to_lowercase()) }}
fn greet(name: &str) { println!("Boas-vindas, {name}!");}
fn main() { let name = normalize(" Eve ");
println!("tamanho: {}", name.len()); // métodos de str funcionam direto println!("começa com e: {}", name.starts_with('e')); greet(&name); // &Cow<str> vira &str
let saved: String = name.into_owned(); // agora é uma String comum println!("{saved:?} salvo na lista de usuários");}Saída:
tamanho: 3começa com e: trueBoas-vindas, eve!"eve" salvo na lista de usuáriosO nome " Eve " ficou em inglês de propósito: o exemplo mostra name.len(), e letras com acento ocupam 2 bytes em UTF-8, o que mudaria o resultado.
O Cow também funciona com outros tipos, como Cow<'_, [i32]> para uma slice que talvez precise mudar. Mas, para texto, o Cow<'_, str> é o que você mais vai ver.
Quando usar cada um
Seção intitulada “Quando usar cada um”Aqui vai uma colinha com todos os tipos “meio ponteiro” que você viu até agora:
| Tipo | O que ele oferece | Use quando |
|---|---|---|
Box<T> |
Um dono, valor no heap | Tipos recursivos, ou um valor grande que você quer mover sem custo |
Rc<T> |
Vários donos, uma thread | Várias partes do programa compartilham os mesmos dados |
Arc<T> |
Vários donos, entre threads | Igual ao Rc, mas compartilhado entre threads (veja Estado compartilhado) |
Weak<T> |
Um link que não é dono | Links para o pai, listas de observers, caches, quebrar ciclos de Rc |
RefCell<T> |
Mutação através de uma referência compartilhada, verificada em tempo de execução | Você precisa mudar algo que vive dentro de um Rc |
Cow<'_, T> |
Emprestado ou owned, decidido em tempo de execução | Uma função que normalmente devolve a entrada sem mudanças |
Na dúvida, comece com ownership (posse) e borrowing (empréstimo) comuns. Só acrescente esses tipos quando o compilador, ou um vazamento de memória, mostrar que você precisa deles.
Erros comuns
Seção intitulada “Erros comuns”Dar unwrap em um upgrade depois que o valor sumiu
Seção intitulada “Dar unwrap em um upgrade depois que o valor sumiu”O upgrade() retorna um Option por um motivo. Se você der .unwrap() por costume, o programa vai entrar em panic assim que o valor tiver sido descartado:
use std::rc::Rc;
fn main() { let session = Rc::new(String::from("user-42")); let link = Rc::downgrade(&session);
drop(session); // a sessão termina
let current = link.upgrade().unwrap(); println!("ainda logado como {current}");}Quando você roda, aparece:
thread 'main' (12345) panicked at src/main.rs:9:34:called `Option::unwrap()` on a `None` valuenote: run with `RUST_BACKTRACE=1` environment variable to display a backtraceA mensagem diz que o programa chamou Option::unwrap() em um valor None. A correção é tratar “sumiu” como um caso normal, com match ou if let:
match link.upgrade() { Some(current) => println!("ainda logado como {current}"), None => println!("a sessão terminou"),}Usar Rc no link que aponta de volta
Seção intitulada “Usar Rc no link que aponta de volta”Se um filho guarda Rc<Parent> e o pai guarda Rc<Child>, você montou o ciclo da primeira seção. Nada quebra e nada avisa você; a memória simplesmente nunca volta. Uma boa regra: decida quem é dono de quem, use Rc nessa direção e Weak em todo link que vai no sentido contrário.
struct Folder { parent: RefCell<Rc<Folder>>, // errado: o filho mantém o pai vivo children: RefCell<Vec<Rc<Folder>>>,}Troque parent por RefCell<Weak<Folder>> e o ciclo desaparece.
Prática
Seção intitulada “Prática”1. Comentários em um post apagado
Seção intitulada “1. Comentários em um post apagado”Crie um Post com um title, guardado em um Rc. Crie um Comment com um text e um post: Weak<Post>. Escreva uma função describe(comment: &Comment) que imprima "<texto>" em <título> se o post ainda existir, ou "<texto>" em um post apagado se não existir. Chame a função, descarte o post e chame de novo. Dica: use match comment.post.upgrade().
Ver solução
use std::rc::{Rc, Weak};
struct Post { title: String,}
struct Comment { text: String, post: Weak<Post>,}
fn describe(comment: &Comment) { match comment.post.upgrade() { Some(post) => println!("\"{}\" em {}", comment.text, post.title), None => println!("\"{}\" em um post apagado", comment.text), }}
fn main() { let post = Rc::new(Post { title: String::from("Aprendendo Rust") }); let comment = Comment { text: String::from("Ótimo post!"), post: Rc::downgrade(&post) };
describe(&comment); drop(post); describe(&comment);}Saída:
"Ótimo post!" em Aprendendo Rust"Ótimo post!" em um post apagadoO comentário nunca mantém o post vivo. Assim que o único Rc é descartado, upgrade() retorna None, e o comentário lida com isso com calma em vez de entrar em panic.
2. Escapar texto só quando precisa
Seção intitulada “2. Escapar texto só quando precisa”Escreva fn escape(text: &str) -> Cow<'_, str>, que troca cada < por <. Se o texto não tiver nenhum <, devolva-o emprestado. Teste com "olá mundo" e "1 < 2", e imprima qual variante você recebeu. Dica: confira primeiro com text.contains('<') e depois use text.replace('<', "<") no caso owned.
Ver solução
use std::borrow::Cow;
fn escape(text: &str) -> Cow<'_, str> { if text.contains('<') { Cow::Owned(text.replace('<', "<")) } else { Cow::Borrowed(text) }}
fn main() { for text in ["olá mundo", "1 < 2"] { match escape(text) { Cow::Borrowed(s) => println!("{s} (emprestado)"), Cow::Owned(s) => println!("{s} (owned)"), } }}Saída:
olá mundo (emprestado)1 < 2 (owned)Só o texto que realmente contém < paga por uma String nova. Todo o resto passa direto, intocado.
- Dois valores
Rcque apontam um para o outro nunca chegam a um contador forte de 0, então eles vazam e oDropdeles nunca roda. Rc::downgradecria umWeak<T>, que não mantém o valor vivo.upgrade()devolveOption<Rc<T>>.- Em árvores, os filhos são possuídos com
Rce os links para o pai sãoWeak. A posse desce, os links fracos apontam para cima. - Listas de observers e caches podem guardar
Weakpara nunca manter coisas vivas sem querer. Cow<'_, str>devolve o texto emprestado quando nada mudou e umaStringowned só quando mudou. Ele faz deref para&str, e.into_owned()dá umaStringpara você.
Teste o que aprendeu
Seção intitulada “Teste o que aprendeu”Teste o que aprendeu
1. Dois valores guardam, cada um, um Rc do outro. O que acontece quando o main termina?
2. O que weak.upgrade() retorna?
3. Em uma árvore de pastas, qual link deve ser Weak?
4. Quando uma função que retorna Cow<'_, str> evita criar uma String nova?
Pontuação: 0 / 4
Para saber mais
Seção intitulada “Para saber mais”- Reference Cycles Can Leak Memory — The Rust Book (em inglês): o passo a passo oficial sobre ciclos de Rc e uma árvore com links
Weakpara o pai. std::rc::Weak— documentação da biblioteca padrão (em inglês): todos os métodos deWeak, incluindoupgrade,strong_counteWeak::new.std::rc— documentação da biblioteca padrão (em inglês): como os contadores forte e fraco funcionam juntos, com um exemplo mais longo de como quebrar ciclos.std::borrow::Cow— documentação da biblioteca padrão (em inglês): o enumCow,to_mut,into_ownede exemplos além de strings.