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>, умный указатель с подсчётом ссылок

В большинстве случаев владение понятно: вы точно знаете, какая переменная владеет определённым значением. Однако бывают случаи, когда одно значение может иметь нескольких владельцев. Например, в графовых структурах данных несколько рёбер могут указывать на одну вершину, и эта вершина концептуально принадлежит всем рёбрам, которые на неё указывают. Вершина не должна удаляться, пока на неё не останется ссылок, то есть пока у неё не останется владельцев.

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

Представьте Rc<T> как телевизор в гостиной. Когда один человек заходит посмотреть телевизор, он включает его. Другие могут зайти в комнату и смотреть телевизор. Когда последний человек выходит из комнаты, он выключает телевизор, потому что он больше не используется. Если бы кто-то выключил телевизор, пока другие ещё смотрят, это вызвало бы возмущение оставшихся зрителей!

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

Обратите внимание, что Rc<T> предназначен только для однопоточных сценариев. Когда мы обсудим конкурентность в главе 16, мы рассмотрим, как выполнять подсчёт ссылок в многопоточных программах.

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

Вернёмся к нашему примеру списка cons в листинге 15-5. Напомним, что мы определили его с помощью Box<T>. На этот раз мы создадим два списка, которые оба разделяют владение третьим списком. Концептуально это похоже на рисунок 15-3.

Два списка, разделяющие владение третьим списком

Рисунок 15-3: Два списка, b и c, разделяющие владение третьим списком, a

Мы создадим список a, содержащий 5 и затем 10. Затем создадим ещё два списка: b, начинающийся с 3, и c, начинающийся с 4. И список b, и список c затем продолжат первый список a, содержащий 5 и 10. Другими словами, оба списка разделят первый список, содержащий 5 и 10.

Попытка реализовать этот сценарий, используя наше определение List с Box<T>, не сработает, как показано в листинге 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: Демонстрация того, что двум спискам с Box<T> не разрешено разделять владение третьим списком

При компиляции этого кода мы получаем следующую ошибку:

$ 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

For more information about this error, try `rustc --explain E0382`.
error: could not compile `cons-list` (bin "cons-list") due to 1 previous error

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

Мы могли бы изменить определение Cons так, чтобы оно хранило ссылки вместо значений, но тогда нам пришлось бы указать параметры времени жизни. Указав параметры времени жизни, мы бы определяли, что каждый элемент списка будет жить не меньше, чем весь список. Это верно для элементов и списков в листинге 15-17, но не для каждого сценария.

Вместо этого мы изменим наше определение List на использование Rc<T> вместо Box<T>, как показано в листинге 15-18. Каждый вариант Cons теперь будет хранить значение и Rc<T>, указывающий на List. При создании b вместо того, чтобы принимать владение a, мы клонируем Rc<List>, который хранится в a, тем самым увеличивая количество ссылок с одного до двух и позволяя a и b разделять владение данными в этом Rc<List>. Мы также клонируем a при создании c, увеличивая количество ссылок с двух до трёх. Каждый раз при вызове Rc::clone количество ссылок на данные внутри Rc<List> увеличивается, и данные не будут удалены, пока на них не останется нулевых ссылок.

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: Определение List, использующее Rc<T>

Нам нужно добавить оператор use, чтобыBring Rc<T> в область видимости, потому что он не находится в прелюдии. В main мы создаём список, содержащий 5 и 10, и сохраняем его в новом Rc<List> в a. Затем при создании b и c мы вызываем функцию Rc::clone и передаём ссылку на Rc<List> в a в качестве аргумента.

Мы могли бы вызвать a.clone() вместо Rc::clone(&a), но соглашение Rust в этом случае — использовать Rc::clone. Реализация Rc::clone не создаёт глубокую копию всех данных, как это делают реализации clone у большинства типов. Вызов Rc::clone только увеличивает счётчик ссылок, что не требует много времени. Глубокие копии данных могут занять много времени. Используя Rc::clone для подсчёта ссылок, мы можем визуально различать виды клонов, создающих глубокие копии, и виды клонов, увеличивающих счётчик ссылок. При поиске проблем с производительностью в коде нам нужно рассматривать только глубокие копии и игнорировать вызовы Rc::clone.

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

Изменим наш рабочий пример из листинга 15-18, чтобы мы могли видеть, как меняются счётчики ссылок при создании и удалении ссылок на Rc<List> в a.

В листинге 15-19 мы изменим main так, чтобы у списка c была внутренняя область видимости; тогда мы увидим, как меняется счётчик ссылок, когда c выходит из области видимости.

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)))));
    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: Вывод счётчика ссылок

В каждой точке программы, где счётчик ссылок меняется, мы выводим счётчик ссылок, получая его вызовом функции Rc::strong_count. Эта функция называется strong_count (сильный счётчик), а не просто count, потому что тип Rc<T> также имеет weak_count (слабый счётчик); мы увидим, для чего используется weak_count, в разделе “Предотвращение циклов ссылок с помощью Weak<T>.

Этот код выводит следующее:

$ 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

Мы видим, что у Rc<List> в a начальный счётчик ссылок равен 1; затем каждый раз при вызове clone счётчик увеличивается на 1. Когда c выходит из области видимости, счётчик уменьшается на 1. Нам не нужно вызывать функцию для уменьшения счётчика ссылок, как мы вызываем Rc::clone для его увеличения: реализация типажа Drop автоматически уменьшает счётчик ссылок, когда значение Rc<T> выходит из области видимости.

То, что мы не видим в этом примере, — это то, что когда b, а затем и a выходят из области видимости в конце main, счётчик становится 0, и Rc<List> полностью удаляется. Использование Rc<T> позволяет одному значению иметь нескольких владельцев, и счётчик гарантирует, что значение остаётся действительным, пока любой из владельцев всё ещё существует.

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