Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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.

Uma lista ligada com o rótulo 'a' apontando para três elementos. O primeiro elemento contém o inteiro 5 e aponta para o segundo elemento. O segundo elemento contém o inteiro 10 e aponta para o terceiro elemento. O terceiro elemento contém o valor 'Nil', que significa o fim da lista; ele não aponta para nada. Uma lista ligada com o rótulo 'b' aponta para um elemento que contém o inteiro 3 e aponta para o primeiro elemento da lista 'a'. Uma lista ligada com o rótulo 'c' aponta para um elemento que contém o inteiro 4 e também aponta para o primeiro elemento da lista 'a', de modo que as caudas das listas 'b' e 'c' são ambas a lista 'a'.

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.

Filename: 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))));
    let b = Cons(3, Box::new(a));
    let c = Cons(4, Box::new(a));
}
Listing 15-17: Demonstrando que não temos permissão para ter duas listas usando Box<T> que tentam compartilhar ownership de uma terceira lista

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

Filename: src/main.rs
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));
}
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, 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.

Filename: src/main.rs
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));
}
Listing 15-19: Imprimindo a contagem de referências

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.