Rc<T>
, o Ponteiro Inteligente com Contagem de Referências
Na maioria dos casos, a posse é clara: você sabe exatamente qual variável tem posse de um dado valor. Contudo, há casos onde um único valor pode ter múltiplos possuidores. Por exemplo, em uma estrutura de dados em grafo, múltiplas arestas podem apontar para o mesmo vértice, e esse vértice é conceitualmente possuído por todas as arestas que apontam para ele. Um vértice não deveria ser liberado a não ser que ele não tenha mais arestas apontando para ele.
Para permitir posse múltipla, o Rust tem um tipo chamado Rc<T>
. Seu nome é uma
abreviação para reference counting (contagem de referências) que, como o
nome diz, mantém registro do número de referências a um valor para saber se ele
ainda está em uso ou não. Se há zero referências a um valor, ele pode ser
liberado sem que nenhuma referência se torne inválida.
Imagine o Rc<T>
como uma TV numa sala de família. Quando uma pessoa entra para
assistir à TV, ela a liga. Outros podem entrar na sala e assistir à TV. Quando a
última pessoa sai da sala, ela desliga a TV porque essa não está mais em uso. Se
alguém desligasse a TV enquanto outros ainda estão assistindo, haveria revolta
entre os telespectadores restantes!
Nós usamos o tipo Rc<T>
quando queremos alocar algum dado no heap para que
múltiplas partes do nosso programa o leiam, e não conseguimos determinar em
tempo de compilação qual parte irá terminar de usar o dado por último. Se
soubéssemos qual parte terminaria por último, poderíamos simplesmente tornar
aquela parte a possuidora do dado e as regras normais de posse aplicadas em
tempo de compilação teriam efeito.
Note que o Rc<T>
serve apenas para cenários de thread única. Quando
discutirmos concorrência no Capítulo 16, cobriremos como fazer contagem de
referências em programas com múltiplas threads.
Usando Rc<T>
para Compartilhar Dados
Vamos retornar ao nosso exemplo de cons list da Listagem 15-5. Lembre-se de
que a definimos usando o Box<T>
. Desta vez, vamos criar duas listas que
compartilham ambas a posse de uma terceira lista, o que conceitualmente vai se
parecer com a Figura 15-3:
Vamos criar a lista a
que contém 5 e depois 10. Então criaremos mais duas
listas: b
, que começa com 3 e c
, que começa com 4. Ambas as listas b
e c
irão então continuar na lista a
contendo 5 e 10. Em outras palavras, ambas as
listas irão compartilhar a primeira lista contendo 5 e 10.
Tentar implementar esse cenário usando nossa definição de List
com Box<T>
não irá funcionar, como mostra a Listagem 15-17:
Arquivo: src/main.rs
enum List {
Cons(i32, Box<List>),
Nil,
}
use 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));
}
Quando compilamos esse código, recebemos este erro:
erro[E0382]: uso de valor movido: `a`
--> src/main.rs:13:30
|
12 | let b = Cons(3, Box::new(a));
| - valor movido para cá
13 | let c = Cons(4, Box::new(a));
| ^ valor usado aqui depois de movido
|
= nota: o valor é movido porque `a` tem tipo `List`, que não implementa
a trait `Copy`
As variantes Cons
têm posse dos dados que elas contêm, então quando criamos a
lista b
, a
é movida para dentro de b
, e b
toma posse de a
. Então,
quando tentamos usar a
de novo na criação de c
, não somos permitidos porque
a
foi movida.
Poderíamos mudar a definição de Cons
para guardar referências, mas aí teríamos
que especificar parâmetros de tempo de vida (lifetime parameters). Fazendo
isso, estaríamos especificando que cada elemento da lista devesse viver por pelo
menos tanto tempo quanto a lista inteira. O verificador de empréstimo (borrow
checker) não nos deixaria compilar let a = Cons(10, &Nil);
, por exemplo,
porque o valor temporário Nil
seria destruído antes que a
pudesse receber
uma referência a ele.
Em vez disso, vamos mudar nossa definição de List
para usar o Rc<T>
no lugar
do Box<T>
, como mostra a Listagem 15-18. Cada variante Cons
agora vai conter
um valor e um Rc<T>
apontando para uma List
. Quando criarmos b
, em vez de
tomar posse de a
, iremos clonar o Rc<List>
que a
está segurando, o que
aumenta o número de referências de uma para duas e permite com que a
e
b
compartilhem posse dos dados naquele Rc<List>
. Também vamos clonar a
quando criarmos c
, o que aumenta o número de referências de duas para três.
Cada vez que chamarmos Rc::clone
, a contagem de referências ao valor dentro do
Rc<List>
irá aumentar, e ele não será liberado até que haja zero referências a
ele:
Arquivo: src/main.rs
enum List { Cons(i32, Rc<List>), Nil, } use 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)); }
Precisamos adicionar uma declaração use
para trazer o Rc<T>
ao escopo porque
ele não está no prelúdio. Na main
, criamos a lista contendo 5 e 10 e a
armazenamos 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 ao Rc<List>
em a
como argumento.
Poderíamos ter chamado a.clone()
em vez de Rc::clone(&a)
, mas a convenção do
Rust é usar Rc::clone
neste caso. A implementação de Rc::clone
não faz uma
cópia profunda de todos os dados como faz a implementação 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.
Usando Rc::clone
para a contagem de referências, podemos distinguir
visualmente entre os clones de cópia profunda e os clones que incrementam a
contagem de referências. Quando estivermos procurando problemas de desempenho no
código, precisamos apenas considerar os clones de cópia profunda e podemos
ignorar as chamadas a Rc::clone
.
Clonar um Rc<T>
Aumenta a Contagem de Referências
Vamos mudar nosso exemplo de trabalho na Listagem 15-18 para podermos ver a
contagem de referências mudando conforme criamos e destruímos referências ao
Rc<List>
em a
.
Na Listagem 15-19, vamos mudar a main
para que tenha um escopo interno em
volta da lista c
; assim poderemos ver como a contagem de referências muda
quando c
sai de escopo. Em cada ponto do programa onde a contagem de
referências muda, iremos imprimir seu valor, que podemos obter chamando a função
Rc::strong_count
. Essa função se chama strong_count
(contagem das
referências fortes) em vez de count
(contagem) porque o tipo Rc<T>
também
tem uma weak_count
(contagem das referências fracas); veremos para que a
weak_count
é usada na seção "Evitando Ciclos de Referências".
Arquivo: src/main.rs
enum List { Cons(i32, Rc<List>), Nil, } use List::{Cons, Nil}; use std::rc::Rc; fn main() { let a = Rc::new(Cons(5, Rc::new(Cons(10, Rc::new(Nil))))); println!("contagem depois de criar a = {}", Rc::strong_count(&a)); let b = Cons(3, Rc::clone(&a)); println!("contagem depois de criar b = {}", Rc::strong_count(&a)); { let c = Cons(4, Rc::clone(&a)); println!("contagem depois de criar c = {}", Rc::strong_count(&a)); } println!("contagem depois que c sai de escopo = {}", Rc::strong_count(&a)); }
Esse código imprime o seguinte:
contagem depois de criar a = 1
contagem depois de criar b = 2
contagem depois de criar c = 3
contagem depois que c sai de escopo = 2
Podemos ver que o Rc<List>
em a
tem uma contagem de referências inicial de
um; depois, cada vez que chamamos clone
, a contagem aumenta em um. Quando c
sai de escopo, a contagem diminui em um. Nós não temos que chamar uma função
para decrementar a contagem de referências como temos que fazer com a
Rc::clone
para incrementá-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 da main
, a contagem se torna 0, e o Rc<List>
é
liberado por completo nesse ponto. O uso do Rc<T>
permite que um único valor
tenha múltiplos possuidores, e a contagem garante que o valor permaneça válido
enquanto algum dos possuidores ainda existir.
Por funcionar com referências imutáveis, o Rc<T>
nos permite compartilhar
dados entre diversas partes do nosso programa apenas para leitura. Se o
Rc<T>
nos deixasse ter múltiplas referências mutáveis também, nós poderíamos
violar uma das regras de empréstimo discutidas no Capítulo 4: múltiplos
empréstimos mutáveis do mesmo lugar podem causar corridas de dados (data
races) e inconsistências. Mas conseguir modificar dados é muito útil! Na
próxima seção, discutiremos a pattern de mutabilidade interior (interior
mutability) e o tipo RefCell<T>
que podemos usar junto com um Rc<T>
para
trabalhar com essa restrição de imutabilidade.