Rc<T>, умный указатель с подсчетом ссылок

RustRustBeginner
Практиковаться сейчас

This tutorial is from open-source community. Access the source code

💡 Этот учебник переведен с английского с помощью ИИ. Чтобы просмотреть оригинал, вы можете перейти на английский оригинал

Введение

Добро пожаловать в Rc, умный указатель с подсчетом ссылок. Эта лабораторная работа является частью Rust Book. Вы можете практиковать свои навыки Rust в LabEx.

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


Skills Graph

%%%%{init: {'theme':'neutral'}}%%%% flowchart RL rust(("Rust")) -.-> rust/BasicConceptsGroup(["Basic Concepts"]) rust(("Rust")) -.-> rust/DataTypesGroup(["Data Types"]) rust(("Rust")) -.-> rust/FunctionsandClosuresGroup(["Functions and Closures"]) rust(("Rust")) -.-> rust/DataStructuresandEnumsGroup(["Data Structures and Enums"]) rust(("Rust")) -.-> rust/AdvancedTopicsGroup(["Advanced Topics"]) rust/BasicConceptsGroup -.-> rust/variable_declarations("Variable Declarations") rust/DataTypesGroup -.-> rust/integer_types("Integer Types") rust/FunctionsandClosuresGroup -.-> rust/function_syntax("Function Syntax") rust/FunctionsandClosuresGroup -.-> rust/expressions_statements("Expressions and Statements") rust/DataStructuresandEnumsGroup -.-> rust/method_syntax("Method Syntax") rust/AdvancedTopicsGroup -.-> rust/operator_overloading("Traits for Operator Overloading") subgraph Lab Skills rust/variable_declarations -.-> lab-100434{{"Rc, умный указатель с подсчетом ссылок"}} rust/integer_types -.-> lab-100434{{"Rc, умный указатель с подсчетом ссылок"}} rust/function_syntax -.-> lab-100434{{"Rc, умный указатель с подсчетом ссылок"}} rust/expressions_statements -.-> lab-100434{{"Rc, умный указатель с подсчетом ссылок"}} rust/method_syntax -.-> lab-100434{{"Rc, умный указатель с подсчетом ссылок"}} rust/operator_overloading -.-> lab-100434{{"Rc, умный указатель с подсчетом ссылок"}} end

Rc<T>{=html}, умный указатель с подсчетом ссылок

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

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

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

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

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

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

Вернемся к нашему примеру связного списка в Listing 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> не сработает, как показано в Listing 15-17.

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

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

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

Варианты Cons владеют данными, которые они хранят, поэтому когда мы создаем список b [1], a перемещается в b, и b становится владельцем a. Затем, когда мы пытаемся снова использовать a при создании c [2], мы не имеем права hacerlo, потому что a уже перемещено.

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

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

Имя файла: 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: Определение List, которое использует Rc<T>

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

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

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

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

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

Имя файла: 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: Вывод счетчика ссылок

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

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

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

Резюме

Поздравляем! Вы завершили лабораторную работу по умному указателю с подсчетом ссылок Rc. Вы можете практиковать в LabEx еще больше лабораторных работ, чтобы улучшить свои навыки.