Pular para o conteúdo

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.

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: 2
contador forte de bob: 2
fim do main
▶ Experimente você mesmo

Repare 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.

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 é bob
alice: fortes = 1, fracos = 1
fim do main
liberando bob
liberando alice
▶ Experimente você mesmo

Desta 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 retorna None. É um valor inicial bem prático.
  • Rc::strong_count conta os donos. Rc::weak_count conta os links fracos. Só o contador forte decide quando o valor é descartado.
  • O Rc que você recebe de upgrade() é 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 home
caminho completo: home/fotos/viagens
home: fortes = 1, fracos = 1
fotos: fortes = 2, fracos = 1
▶ Experimente você mesmo

Leia 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.

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: 10
log recebeu: placar: 10
tela recebeu: placar: 20
(um listener sumiu, pulando)
listeners restantes: 1
▶ Experimente você mesmo

A 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().

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á uma String nova”.
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)
▶ Experimente você mesmo

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.

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: 3
começa com e: true
Boas-vindas, eve!
"eve" salvo na lista de usuários
▶ Experimente você mesmo

O 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.

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.

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` value
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
▶ Experimente você mesmo

A 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"),
}

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.

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 apagado
▶ Experimente você mesmo

O 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.

Escreva fn escape(text: &str) -> Cow<'_, str>, que troca cada < por &lt;. 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('<', "&lt;") no caso owned.

Ver solução
use std::borrow::Cow;
fn escape(text: &str) -> Cow<'_, str> {
if text.contains('<') {
Cow::Owned(text.replace('<', "&lt;"))
} 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 &lt; 2 (owned)
▶ Experimente você mesmo

Só o texto que realmente contém < paga por uma String nova. Todo o resto passa direto, intocado.

  • Dois valores Rc que apontam um para o outro nunca chegam a um contador forte de 0, então eles vazam e o Drop deles nunca roda.
  • Rc::downgrade cria um Weak<T>, que não mantém o valor vivo. upgrade() devolve Option<Rc<T>>.
  • Em árvores, os filhos são possuídos com Rc e os links para o pai são Weak. A posse desce, os links fracos apontam para cima.
  • Listas de observers e caches podem guardar Weak para nunca manter coisas vivas sem querer.
  • Cow<'_, str> devolve o texto emprestado quando nada mudou e uma String owned só quando mudou. Ele faz deref para &str, e .into_owned() dá uma String para você.

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