Rc<T>, o Ponteiro Inteligente com Contagem de Referências
Na maioria dos casos, o ownership é claro: você sabe exatamente qual variável tem ownership de determinado valor. No entanto, há casos em que um único valor pode ter múltiplos donos. Por exemplo, em estruturas de dados em grafo, múltiplas arestas podem apontar para o mesmo nó, e esse nó é conceitualmente possuído por todas as arestas que apontam para ele. Um nó não deve ser limpo a menos que não tenha nenhuma aresta apontando para ele e, portanto, não tenha donos.
Você precisa habilitar ownership múltiplo explicitamente usando o tipo Rc<T>
de Rust, que é uma abreviação de reference counting (contagem de
referências). O tipo Rc<T> registra o número de referências a um valor para
determinar se o valor ainda está em uso ou não. Se houver zero referências a um
valor, ele pode ser limpo sem que nenhuma referência se torne inválida.
Imagine Rc<T> como uma TV em uma sala de estar. Quando uma pessoa entra para
assistir TV, ela a liga. Outras pessoas podem entrar na sala e assistir à TV.
Quando a última pessoa sai da sala, ela desliga a TV porque ela já não está
sendo usada. Se alguém desligasse a TV enquanto outras pessoas ainda estivessem
assistindo, haveria protestos dos espectadores restantes!
Usamos o tipo Rc<T> quando queremos alocar alguns dados no heap para que
múltiplas partes do nosso programa os leiam e não conseguimos determinar, em
tempo de compilação, qual parte terminará de usar os dados por último. Se
soubéssemos qual parte terminaria por último, poderíamos simplesmente fazer
dela a dona dos dados, e as regras normais de ownership aplicadas em tempo de
compilação entrariam em ação.
Observe que Rc<T> deve ser usado apenas em cenários de thread única. Quando
discutirmos concorrência no Capítulo 16, veremos como fazer contagem de
referências em programas multithread.
Compartilhando Dados
Vamos retornar ao exemplo da cons list da Listagem 15-5. Lembre-se de que a
definimos usando Box<T>. Desta vez, criaremos duas listas que compartilham o
ownership de uma terceira lista. Conceitualmente, isso se parece com a Figura
15-3.
Figura 15-3: Duas listas, b e c, compartilhando
ownership de uma terceira lista, a
Criaremos a lista a, que contém 5 e depois 10. Em seguida, criaremos mais
duas listas: b, que começa com 3, e c, que começa com 4. Tanto a lista
b quanto a lista c continuarão na primeira lista a, que contém 5 e
10. Em outras palavras, as duas listas compartilharão a primeira lista que
contém 5 e 10.
Tentar implementar esse cenário usando nossa definição de List com Box<T>
não funcionará, como mostrado na Listagem 15-17.
enum List {
Cons(i32, Box<List>),
Nil,
}
use crate::List::{Cons, Nil};
fn main() {
let a = Cons(5, Box::new(Cons(10, Box::new(Nil))));
let b = Cons(3, Box::new(a));
let c = Cons(4, Box::new(a));
}
Box<T> que tentam compartilhar ownership de uma terceira listaQuando compilamos esse código, recebemos este erro:
$ cargo run
Compiling cons-list v0.1.0 (file:///projects/cons-list)
error[E0382]: use of moved value: `a`
--> src/main.rs:11:30
|
9 | let a = Cons(5, Box::new(Cons(10, Box::new(Nil))));
| - move occurs because `a` has type `List`, which does not implement the `Copy` trait
10 | let b = Cons(3, Box::new(a));
| - value moved here
11 | let c = Cons(4, Box::new(a));
| ^ value used here after move
|
note: if `List` implemented `Clone`, you could clone the value
--> src/main.rs:1:1
|
1 | enum List {
| ^^^^^^^^^ consider implementing `Clone` for this type
...
10 | let b = Cons(3, Box::new(a));
| - you could clone this value
For more information about this error, try `rustc --explain E0382`.
error: could not compile `cons-list` (bin "cons-list") due to 1 previous error
As variantes Cons têm ownership dos dados que armazenam, então, quando
criamos a lista b, a é movida para dentro de b, e b passa a ter
ownership de a. Depois, quando tentamos usar a novamente ao criar c, isso
não é permitido porque a foi movida.
Poderíamos mudar a definição de Cons para armazenar referências, mas então
teríamos que especificar parâmetros de lifetime. Ao especificar parâmetros de
lifetime, estaríamos dizendo que cada elemento da lista viverá pelo menos tanto
quanto a lista inteira. Esse é o caso para os elementos e listas da Listagem
15-17, mas não em todos os cenários.
Em vez disso, mudaremos nossa definição de List para usar Rc<T> no lugar de
Box<T>, como mostrado na Listagem 15-18. Cada variante Cons agora armazenará
um valor e um Rc<T> apontando para uma List. Quando criarmos b, em vez de
tomar ownership de a, clonaremos o Rc<List> que a está armazenando,
aumentando o número de referências de um para dois e permitindo que a e b
compartilhem ownership dos dados nesse Rc<List>. Também clonaremos a ao
criar c, aumentando o número de referências de dois para três. Toda vez que
chamarmos Rc::clone, a contagem de referências aos dados dentro do Rc<List>
aumentará, e os dados só serão limpos quando houver zero referências a eles.
enum List {
Cons(i32, Rc<List>),
Nil,
}
use crate::List::{Cons, Nil};
use std::rc::Rc;
fn main() {
let a = Rc::new(Cons(5, Rc::new(Cons(10, Rc::new(Nil)))));
let b = Cons(3, Rc::clone(&a));
let c = Cons(4, Rc::clone(&a));
}
List que usa Rc<T>Precisamos adicionar uma instrução use para trazer Rc<T> para o escopo,
porque ele não está no prelude. Em main, criamos a lista que armazena 5 e
10 e a guardamos em um novo Rc<List> em a. Então, quando criamos b e
c, chamamos a função Rc::clone e passamos uma referência para o Rc<List>
em a como argumento.
Poderíamos ter chamado a.clone() em vez de Rc::clone(&a), mas a convenção
em Rust é usar Rc::clone nesse caso. A implementação de Rc::clone não faz
uma cópia profunda de todos os dados como fazem as implementações de clone da
maioria dos tipos. A chamada a Rc::clone apenas incrementa a contagem de
referências, o que não leva muito tempo. Cópias profundas de dados podem levar
muito tempo. Ao usar Rc::clone para contagem de referências, conseguimos
distinguir visualmente os tipos de clone que fazem cópia profunda dos tipos de
clone que aumentam a contagem de referências. Ao procurar problemas de
desempenho no código, só precisamos considerar os clones de cópia profunda e
podemos desconsiderar chamadas a Rc::clone.
Clonando para Aumentar a Contagem de Referências
Vamos alterar nosso exemplo de trabalho da Listagem 15-18 para que possamos ver
as contagens de referências mudarem à medida que criamos e descartamos
referências ao Rc<List> em a.
Na Listagem 15-19, mudaremos main para que tenha um escopo interno ao redor
da lista c; assim, poderemos ver como a contagem de referências muda quando
c sai de escopo.
enum List {
Cons(i32, Rc<List>),
Nil,
}
use crate::List::{Cons, Nil};
use std::rc::Rc;
// --snip--
fn main() {
let a = Rc::new(Cons(5, Rc::new(Cons(10, Rc::new(Nil)))));
println!("count after creating a = {}", Rc::strong_count(&a));
let b = Cons(3, Rc::clone(&a));
println!("count after creating b = {}", Rc::strong_count(&a));
{
let c = Cons(4, Rc::clone(&a));
println!("count after creating c = {}", Rc::strong_count(&a));
}
println!("count after c goes out of scope = {}", Rc::strong_count(&a));
}
Em cada ponto do programa em que a contagem de referências muda, imprimimos a
contagem de referências, que obtemos chamando a função Rc::strong_count. Essa
função se chama strong_count, em vez de count, porque o tipo Rc<T> também
tem uma weak_count; veremos para que weak_count é usada em “Prevenindo
Ciclos de Referência Usando Weak<T>”.
Esse código imprime o seguinte:
$ cargo run
Compiling cons-list v0.1.0 (file:///projects/cons-list)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.45s
Running `target/debug/cons-list`
count after creating a = 1
count after creating b = 2
count after creating c = 3
count after c goes out of scope = 2
Podemos ver que o Rc<List> em a tem uma contagem inicial de referências de
1; então, cada vez que chamamos clone, a contagem aumenta em 1. Quando c
sai de escopo, a contagem diminui em 1. Não precisamos chamar uma função para
diminuir a contagem de referências como precisamos chamar Rc::clone para
aumentá-la: a implementação da trait Drop diminui a contagem automaticamente
quando um valor Rc<T> sai de escopo.
O que não conseguimos ver nesse exemplo é que, quando b e depois a saem de
escopo no final de main, a contagem chega a 0, e o Rc<List> é limpo por
completo. Usar Rc<T> permite que um único valor tenha múltiplos donos, e a
contagem garante que o valor permaneça válido enquanto qualquer um dos donos
ainda existir.
Por meio de referências imutáveis, Rc<T> permite compartilhar dados entre
múltiplas partes do programa apenas para leitura. Se Rc<T> permitisse também
múltiplas referências mutáveis, poderíamos violar uma das regras de borrowing
discutidas no Capítulo 4: múltiplos empréstimos mutáveis para o mesmo lugar
podem causar data races e inconsistências. Mas poder modificar dados é muito
útil! Na próxima seção, discutiremos o padrão de mutabilidade interior e o tipo
RefCell<T>, que você pode usar em conjunto com Rc<T> para trabalhar com essa
restrição de imutabilidade.