Счётчик указателей Rc<T>

В большинстве случаев владение очень понятно: вы знаете какие переменные владеют значениями. Но это не всегда так бывает. Иногда необходимо, чтобы переменные имели нескольких владельцев. Для этого существует тип Rc<T>. Это счётчик ссылок, который следит за количеством ссылок на значение для того, чтобы понять используется ли данное значение или уже нет. Если количество ссылок равно нулю, то тогда мы можем удалить данные без каких-либо внешних негативных эффектов.

Это как просмотр ТВ. Когда какой-либо человек входит и смотрит ТВ оно включает его. Другие люди также могут смотреть ТВ. Когда же последний человек покидает комнату, он должен выключить ТВ, т.к. его использование никому уже не нужно. Если же выключить ТВ раньше, люди, которые смотрят ТВ будут недовольны.

Rc<T> используется тогда, в куче необходимо разместить данные для общего пользования (для чтения) и нельзя определить в момент компиляции время, за которое части использующие программу закончат её использование. Если же вы знаете, какая часть закончит последней, мы можем сделать эту часть владельцем данных и обычные правила владения сделают всё техническую работу по освобождению ресурсов.

Обратите внимание, что Rc<T> используется только в однопоточном сценарии. В последующих главах мы рассмотрим многопоточные программы. Если вы попытаетесь использовать Rc<T> в различных потоках, вы не сможете скомпилировать такую программу (сработает защита компилятора).

Использование Rc<T> для совместного использования данных

Рассмотрим уже знакомый нам пример (15-5). В коде 15-11 мы попытаемся использовать List используя Box<T>. Сначала мы создадим экземпляр списка, который содержит 5 и затем 10. Далее, мы хотим создать ещё два листа. Один начинается с 3 и продолжается списком содержащий 5 и 10. Другой начинается с 4 и также продолжается списком с 5 и 10. Т.е. мы хотим, чтобы два списка разделяли бы владение третьим списком, как показано на иллюстрации 15-10:

Two lists that share ownership of a third list

рисунок 15-10: два списка, b и c совместно используют (владеют) списком a

Попытка реализовать это используя определение List с Box<T> не будет работать (15-11):

Filename: 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));
}

код 15-11: создание экземпляров используя Box<T>, с помощью которых мы пытаемся совместно использовать одну переменную

Описание ошибки:

error[E0382]: use of moved value: `a`
  --> src/main.rs:13:30
   |
12 |     let b = Cons(3, Box::new(a));
   |                              - value moved here
13 |     let c = Cons(4, Box::new(a));
   |                              ^ value used here after move
   |
   = note: move occurs because `a` has type `List`, which does not
   implement the `Copy` trait

Переменные Cons владеют данными, которые хранят в себе. Когда мы создаём b, то он получает во владение a. Когда же мы пытаемся использовать a снова, когда создаём c, нам это не разрешено, т.к. a уже перемещено.

Мы можем изменить определение Cons для хранения ссылок, но когда мы должны определить параметры времени жизни и должны конструировать элементы таким образом, чтобы каждый элемент жил по крайней мере столько же, сколько список, в котором он содержится. В противном случае, код не будет скомпилирован. Всё это весьма сложно для реализации.

Лучшим вариантом будет использовать Rc<T> (15-12):

Filename: 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, a.clone());
    let c = Cons(4, a.clone());
}

код 15-12: определение List, который использует Rc<T>

Обратите внимание на необходимость использовать выражение use, т.к. Rc не подгружается по умолчанию. В main мы создаём список содержащий 5 и 10 и сохраняем его в новом Rc в a. Далее, мы создаём b и c. Мы клонируем a.

Клонирование Rc<T> увеличивает количество ссылок

Мы уже использовали метод clone раньше, для копирования элемента полностью. Совместно с Rc<T> полная копия не делается. Rc<T> хранить количество ссылок. Изменим код в main, как показано в 15-13:

Filename: 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!("rc = {}", Rc::strong_count(&a));
    let b = Cons(3, a.clone());
    println!("rc after creating b = {}", Rc::strong_count(&a));
    {
        let c = Cons(4, a.clone());
        println!("rc after creating c = {}", Rc::strong_count(&a));
    }
    println!("rc after c goes out of scope = {}", Rc::strong_count(&a));
}

код 15-13: печать количества ссылок

Вывод на консоль:

rc = 1
rc after creating b = 2
rc after creating c = 3
rc after c goes out of scope = 2

Такое решение помогает нам иметь нескольких владельцев.

В начале этой секции мы сказали, что Rc<T> позволяет делить данные между несколькими частями программы для чтения неизменяемых ссылок значений T, которые содержаться в Rc<T>. Если бы Rc<T> позволяла бы иметь изменяемые ссылки, это бы нарушило бы целостность данных, о котором мы говорили в Главе 4.

В следующей секции мы поговорим о шаблоне внутренней изменчивости и типе RefCell<T>, который мы можем использовать совместно с Rc<T> для преодоления ограничений ссылочной неизменяемости.