Introdução
Bem-vindo ao Rc
Neste laboratório, exploraremos o uso de Rc
Rc<T>, o Ponteiro Inteligente com Contagem de Referências
Na maioria dos casos, a propriedade é clara: você sabe exatamente qual variável possui um determinado valor. No entanto, existem casos em que um único valor pode ter múltiplos proprietários. Por exemplo, em estruturas de dados de grafos, 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 proprietários.
Você deve habilitar a propriedade múltipla explicitamente usando o tipo Rust Rc<T>, que é uma abreviação para contagem de referências (reference counting). O tipo Rc<T> acompanha o número de referências a um valor para determinar se o valor ainda está em uso. Se houver zero referências a um valor, o valor 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 não está mais sendo usada. Se alguém desligar a TV enquanto outros ainda estão assistindo, haveria uma comoção dos telespectadores restantes!
Usamos o tipo Rc<T> quando queremos alocar alguns dados no heap (heap) para que várias partes do nosso programa leiam e não podemos 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 com que essa parte fosse a proprietária dos dados, e as regras normais de propriedade aplicadas em tempo de compilação entrariam em vigor.
Observe que Rc<T> é apenas para uso em cenários de thread único. Quando discutirmos concorrência no Capítulo 16, abordaremos como fazer contagem de referências em programas multithreaded.
Usando Rc<T> para Compartilhar Dados
Vamos retornar ao nosso exemplo de lista cons em Listing 15-5. Lembre-se de que o definimos usando Box<T>. Desta vez, criaremos duas listas que compartilham a propriedade de uma terceira lista. Conceitualmente, isso se assemelha à Figura 15-3.
Figura 15-3: Duas listas, b e c, compartilhando a propriedade de uma terceira lista, a
Criaremos a lista a que contém 5 e depois 10. Em seguida, faremos mais duas listas: b que começa com 3 e c que começa com 4. Ambas as listas b e c continuarão para a primeira lista a contendo 5 e 10. Em outras palavras, ambas as listas compartilharão a primeira lista contendo 5 e 10.
Tentar implementar este cenário usando nossa definição de List com Box<T> não funcionará, como mostrado no Listing 15-17.
Nome do arquivo: src/main.rs
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))));
1 let b = Cons(3, Box::new(a));
2 let c = Cons(4, Box::new(a));
}
Listing 15-17: Demonstrando que não podemos ter duas listas usando Box<T> que tentam compartilhar a propriedade de uma terceira lista
Quando compilamos este código, obtemos este erro:
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
As variantes Cons possuem os dados que contêm, então, quando criamos a lista b [1], a é movido para b e b possui a. Então, quando tentamos usar a novamente ao criar c [2], não podemos porque a foi movido.
Poderíamos alterar a definição de Cons para conter referências em vez disso, mas então teríamos que especificar parâmetros de tempo de vida. Ao especificar parâmetros de tempo de vida, estaríamos especificando que cada elemento na lista viverá pelo menos tanto tempo quanto a lista inteira. Este é o caso dos elementos e listas no Listing 15-17, mas não em todos os cenários.
Em vez disso, mudaremos nossa definição de List para usar Rc<T> em vez de Box<T>, como mostrado no Listing 15-18. Cada variante Cons agora conterá um valor e um Rc<T> apontando para um List. Quando criamos b, em vez de assumir a propriedade de a, clonaremos o Rc<List> que a está contendo, aumentando assim o número de referências de um para dois e permitindo que a e b compartilhem a propriedade 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 chamamos Rc::clone, a contagem de referência para os dados dentro do Rc<List> aumentará, e os dados não serão limpos a menos que haja zero referências a eles.
Nome do arquivo: src/main.rs
enum List {
Cons(i32, Rc<List>),
Nil,
}
use crate::List::{Cons, Nil};
1 use std::rc::Rc;
fn main() {
2 let a = Rc::new(Cons(5, Rc::new(Cons(10, Rc::new(Nil)))));
3 let b = Cons(3, Rc::clone(&a));
4 let c = Cons(4, Rc::clone(&a));
}
Listing 15-18: Uma definição de List que usa Rc<T>
Precisamos adicionar uma instrução use para trazer Rc<T> para o escopo [1] porque não está no preâmbulo. Em main, criamos a lista contendo 5 e 10 e a armazenamos em um novo Rc<List> em a [2]. Então, quando criamos b [3] e c [4], chamamos a função Rc::clone e passamos uma referência ao Rc<List> em a como um 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 a maioria das implementações de clone dos tipos fazem. A chamada para Rc::clone apenas incrementa a contagem de referência, o que não leva muito tempo. Cópias profundas de dados podem levar muito tempo. Ao usar Rc::clone para contagem de referência, podemos distinguir visualmente entre os tipos de clones de cópia profunda e os tipos de clones que aumentam a contagem de referência. Ao procurar problemas de desempenho no código, só precisamos considerar os clones de cópia profunda e podemos desconsiderar as chamadas para Rc::clone.
Clonar um Rc<T> Aumenta a Contagem de Referências
Vamos alterar nosso exemplo de trabalho no Listing 15-18 para que possamos ver as contagens de referência mudando à medida que criamos e descartamos referências ao Rc<List> em a.
No Listing 15-19, mudaremos main para que ele tenha um escopo interno em torno da lista c; então podemos ver como a contagem de referência muda quando c sai do escopo.
Nome do arquivo: src/main.rs
--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)
);
}
Listing 15-19: Imprimindo a contagem de referência
Em cada ponto do programa onde a contagem de referência muda, imprimimos a contagem de referência, que obtemos chamando a função Rc::strong_count. Esta função é nomeada strong_count em vez de count porque o tipo Rc<T> também tem um weak_count; veremos para que weak_count é usado em "Prevenindo Ciclos de Referência Usando Weak<T>".
Este código imprime o seguinte:
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 de referência inicial de 1; então, cada vez que chamamos clone, a contagem aumenta em 1. Quando c sai do escopo, a contagem diminui em 1. Não precisamos chamar uma função para diminuir a contagem de referência como precisamos chamar Rc::clone para aumentar a contagem de referência: a implementação do trait Drop diminui a contagem de referência automaticamente quando um valor Rc<T> sai do escopo.
O que não podemos ver neste exemplo é que quando b e então a saem do escopo no final de main, a contagem é então 0, e o Rc<List> é limpo completamente. Usar Rc<T> permite que um único valor tenha múltiplos proprietários, e a contagem garante que o valor permaneça válido enquanto qualquer um dos proprietários ainda existir.
Por meio de referências imutáveis, Rc<T> permite que você compartilhe dados entre várias partes do seu programa apenas para leitura. Se Rc<T> permitisse que você também tivesse múltiplas referências mutáveis, você poderia violar uma das regras de empréstimo discutidas no Capítulo 4: múltiplos empréstimos mutáveis para o mesmo local podem causar condições de corrida de dados e inconsistências. Mas ser capaz de mutar dados é muito útil! Na próxima seção, discutiremos o padrão de mutabilidade interna e o tipo RefCell<T> que você pode usar em conjunto com um Rc<T> para trabalhar com essa restrição de imutabilidade.
Resumo
Parabéns! Você concluiu o laboratório Rc