Программирование PC Rust: умные указатели Mon, September 16 2024  

Поделиться

Нашли опечатку?

Пожалуйста, сообщите об этом - просто выделите ошибочное слово или фразу и нажмите Shift Enter.

Rust: умные указатели Печать
Добавил(а) microsin   

Указатель (pointer) это основная концепция программирования, обозначающая переменную, в которой хранится адрес памяти. Этот адрес ссылается, или "указывает" (отсюда название) на какие-то другие данные (другую переменную или объект в памяти). Наиболее распространенным указателем в Rust является ссылка (reference), что мы изучили в главе 4 [2]. Ссылки обозначаются символом &, и они заимствуют (borrow) значение, на которое указывают. У ссылок нет каких-либо возможностей кроме того, чтобы ссылаться на данные, и это не вносит никаких дополнительных расходов, влияющих на производительность.

Интеллектуальные, или умные указатели (smart pointers), с другой стороны, это структуры данных, которые действуют наподобие указателя, однако содержат в себе дополнительные метаданные и возможности. Концепция умных указателей не уникальна для Rust: умные указатели появились в C++, и также существуют и в других языках программирования. Rust имеет множество умных smart-указателей, определенных в стандартной библиотеке, которые предоставляют функциональность, превышающую функционал обычных ссылок. Чтобы изучить эту основную концепцию, мы рассмотрим несколько примеров smart-указателей, включая тип smart-указателя для подсчета ссылок. Этот указатель позволяет владеть данными несколькими владельцами, отслеживая количество владельцев, чтобы данные были очищены, когда больше нет владельцев этих данных.

Rust, с его концепцией владения (ownership) и заимствования (borrowing) [2], реализует дополнительное отличие между ссылками и smart-указателями: в то время как ссылки только лишь заимствуют данные, smart-указатели во многих случаях осуществляют владение данными, на которые указывают.

Мы уже встречались с несколькими smart-указателями, хотя пока не называли их таковыми, например String и Vec< T> в главе 8 [3]. Оба этих типа считаются smart-указателями, потому что они владеют некоторой памятью, и позволяют вам ей манипулировать. У них есть также метаданные, и дополнительные возможности, или гарантии. Тип String, например, сохраняет размер в качестве метаданных, и имеет дополнительную возможность гарантии, что данные строки всегда будут в допустимой кодировке UTF-8.

Smart-указатели обычно реализованы с использованием структур. В отличие от обычной структуры, smart-указатели реализуют трейты Deref и Drop. Трейт Deref позволяет экземпляру структуры smart-указателя вести себя как ссылка, так что вы можете писать свой код так, что он будет нормально работать как с указателями, так и со smart-указателями. Трейт Drop позволит вам настроить код, запускаемый при входе smart-указателя за пределы области действия. В этой главе (перевод документации [1] главы 15) мы обсудим оба этих трейта, и продемонстрируем их важность для smart-указателей.

С учетом того, что шаблон smart-указателя является общим шаблоном дизайна и часто используется в Rust, эта глава не будет претендовать на рассмотрение всех существующих типов smart-указателей. Многие библиотеки имеют свои собственные smart-указателя, и вы можете даже писать свои smart-указатели. Мы рассмотрим далее наиболее распространенные smart-указатели стандартной библиотеки Rust:

Box< T> для выделения значений в куче.
Rc< T>, тип счетчика ссылок, который позволяет множественное владение (multiple ownership).
Ref< T> и RefMut< T>, доступные через RefCell< T>. Это тип, который принудительно применяет правила заимствования runtime вместо compile time.

Дополнительно мы рассмотрим шаблон внутренней изменчивости (interior mutability pattern), где immutable-тип предоставляет API для мутирования внутреннего значения. Мы также рассмотрим reference-циклы: как они могут привести к утечке памяти, и как это предотвратить.

[Использование Box< T> для указания на данные в куче]

Самый простой smart-указатель это box, тип которого Box< T>. Box-ы позволят вам сохранить данные в куче вместо стека. В стеке остается указатель указатель на данные кучи. См. главу 4 [2] для обзора различий между стеком и кучей.

Box-ы не вносят накладных расходов в плане потерь производительности, кроме сохранения своих данных в куче вместо стека. Но и дополнительных возможностей у них не так много. Вы будете использовать их чаще всего в следующих ситуациях:

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

Первую из этих ситуаций мы продемонстрируем в секции "Реализация рекурсивных типов на основе Box". Во втором случае передача владения большим количеством данных может занять много времени, потому что данные копируются в стеке. Для улучшения производительности в этой ситуации мы можем сохранить большое количество данных на куче с помощью box. Затем в стеке будет копироваться только маленькое количество данных для указателя, в то время как сами данные будут оставаться в одном им том же месте кучи. Третья ситуация известна как объект трейта, и глава 17 посвящена целому разделу "Using Trait Objects That Allow for Values of Different Types", только этой теме. Так что то, что вы узнаете здесь, можно будет снова применить в главе 17.

Использование Box< T> для сохранения данных в куче. Перед тем, как мы обсудим использование кучи в случае для Box< T>, давайте рассмотрим, как взаимодействовать со значениями, сохраненными в Box< T>.

Листинг 15-1 показывает, как использовать box для сохранения значения i32 в куче (файл src/main.rs):

fn main() {
    let b = Box::new(5);
    println!("b = {b}");
}

Листинг 15-1. Сохранение значения i32 в куче с использованием box.

Мы определяем здесь переменную b, чтобы у неё было значение Box, указывающее на значение 5, выделенное в куче. Эта программа напечатает print b = 5; в этом случае мы можем получить доступ к данным в box подобно тому, как если бы они были в стеке. Точно так же, как и с любым владеемым значением, когда box выйдет из области действия, как это происходит по окончании тела main, он будет освобожден из кучи. Освобождение произойдет как для box (сохраненного в стеке), так и для данных, на которые он указывает (сохраненных в стеке).

Размещение одиночного значения в куче не очень полезно, поэтому вы вряд ли будете часто использовать box-ы таким образом. В большинстве ситуаций уместнее будет разместить в стеке такие значения, как одиночное число i32, где они и размещаются по умолчанию. Давайте рассмотрим случай, когда box-ы позволяют нам определять такие типы, какие нам было бы не разрешено определять без box-ов.

Реализация рекурсивных типов на основе Box. Значение рекурсивного типа может иметь значение такого же типа как часть себя. Такие типы могут создавать проблему, потому что во время компиляции Rust-у нужно знать сколько места занимает тип. Однако вложенные значения рекурсивных типов теоретически могут продолжаться бесконечно, так что Rust не может знать, сколько нужно места для значения. Поскольку box-ы имеют известный размер, мы можем получить рекурсивные типы путем вставки box в определение рекурсивного типа.

В качестве примера рекурсивного типа рассмотрим список cons (cons list). Это тип данных, который можно обычно обнаружить в библиотеках функциональных языков программирования. Тип cons list мы определим простым образом, за исключением рекурсии; поэтому концепции в примере, с которым мы будем работать, будут полезны каждый раз, когда вы попадаете в более сложные ситуации, связанные с рекурсивными типами.

Что такое Cons List. Тип cons list это структура данных, которая пришла из из языка программирования Lisp и его диалектов. Этот тип состоит из вложенных пар, а также является Lisp-версией связанного списка (linked list). Имя cons list произошло от Lisp-функции cons (сокращение от "construct function"), которая конструирует новую пару из своих двух аргументов. Вызовом cons на паре, состоящей из значения и другой пары, мы можем конструировать списки cons, составленные из рекурсивных пар.

Например, вот представление на псевдокоде cons list, содержащий список 1, 2, 3, где каждая пара находится в скобках:

(1, (2, (3, Nil)))

Каждый элемент в cons list содержит два члена: значение и следующий элемент. Последний элемент в списке содержит только значение Nil без следующего элемента. Сам cons list генерируется рекурсивными вызовами функции cons. Каноническое имя для обозначения базы рекурсии Nil. Обратите внимание, что это не то же самое, что концепция "null" или "nil", что обсуждалось в главе 6 [4] ("null" или "nil" является недопустимым или отсутствующим значением).

Тип cons list не является часто используемой структурой данных в Rust. В большинстве случаев, когда у вас есть список элементов в Rust, лучшим выбором будет тип Vec< T>. Другие, более сложные рекурсивные типы данных полезны в различных ситуациях, но начав в этой главе с типа cons list, мы можем исследовать, как box-ы позволят нам определить рекурсивный тип данных без особого отвлечения на другие детали.

Листинг 15-2 содержит перечисление enum для cons list. Обратите внимание, что этот код пока не скомпилируется, потому что тип List не имеет известного размера, который мы продемонстрируем (файл src/main.rs).

enum List {
    Cons(i32, List),
    Nil,
}

Листинг 15-2. Первая попытка определить enum для представления структуры данных cons list со значениями i32.

Замечание: в целях этого примера мы реализуем cons list, который будет хранить только значения i32. Мы могли бы реализовать его, используя generic-типы, как обсуждалось в главе 10 [5], чтобы определить тип cons list, который мог бы хранить в себе любой тип.

Использование типа List для сохранения списка 1, 2, 3 в переменной list могло бы выглядеть как листинг 15-3 (файл src/main.rs, этот код не скомпилируется):

use crate::List::{Cons, Nil};

fn main() { let list = Cons(1, Cons(2, Cons(3, Nil))); }

Листинг 15-3. Использование перечисления List для сохранения списка 1, 2, 3 в переменной list.

Первое значение Cons содержит 1 и другое значение List. Это значение List представляет собой другое значение Cons, содержащее 2 и другое значение List. Это значение List представляет еще одно значение Cons, содержащее 3 и завершающее значение List, равное Nil, не рекурсивный вариант, сигнализирующий о конце списка.

Если мы попытаемся скомпилировать код листинга 15-3, то получим ошибку, показанную в листинге 15-4:

$ cargo run
   Compiling cons-list v0.1.0 (file:///projects/cons-list)
error[E0072]: recursive type `List` has infinite size
 --> src/main.rs:1:1
  |
1 | enum List {
  | ^^^^^^^^^
2 |     Cons(i32, List),
  |               ---- recursive without indirection
  |
help: insert some indirection (e.g., a `Box`, `Rc`, or `&`) to break the cycle
  |
2 |     Cons(i32, Box< List>),
  |               ++++    +
error[E0391]: cycle detected when computing when `List` needs drop --> src/main.rs:1:1 | 1 | enum List { | ^^^^^^^^^ | = note: ...which immediately requires computing when `List` needs drop again = note: cycle used when computing whether `List` needs drop = note: see https://rustc-dev-guide.rust-lang.org/overview.html#queries and
https://rustc-dev-guide.rust-lang.org/query.html for more information
Some errors have detailed explanations: E0072, E0391. For more information about an error, try `rustc --explain E0072`. error: could not compile `cons-list` (bin "cons-list") due to 2 previous errors

Листинг 15-4. Ошибка, которую мы получим при попытке определить рекурсивное enum.

Ошибка показывает, что у этого типа "бесконечное значение" (has infinite size). Причина в том, что мы определили List с вариантом, который рекурсивный: он хранит другое значение самого себя непосредственно. В результате Rust не может определить, сколько места нужно для сохранения значения List. Давайте разберемся, почему мы получили такую ошибку. Сначала рассмотрим, как Rust определяет, сколько нужно места для сохранения значения не рекурсивного типа.

Вычисление размера не рекурсивного типа. Вспомним перечисление Message, которое мы определили в листинге 6-2, когда обсуждали определения enum в главе 6 [4]:

enum Message {
    Quit,
    Move { x: i32, y: i32 },
    Write(String),
    ChangeColor(i32, i32, i32),
}

Чтобы определить, сколько места выделяется для значения Message, Rust просматривает каждый из вариантов перечисления, чтобы увидеть, какому из вариантов нужно больше всего места. Rust видит, что Message::Quit не требует никакого пространства, Message::Move требует достаточно места для сохранения двух значений i32, и так далее. Поскольку используется только один вариант, то наибольшее пространство, которое требуется для значения Message, это пространство, которое требуется для хранения наибольшего из его вариантов.

Сравните это с тем, что происходит, когда Rust пытается определить, сколько пространства требуется для рекурсивного типа наподобие перечисления List в листинге 15-2. Компилятор начинает просмотр с варианта Cons, который хранит значение типа i32 и значение типа List. Таким образом, Cons требует пространство, размеру i32 плюс размер List. Чтобы узнать, сколько памяти требуется типу List, компилятор просматривает его варианты, начиная с варианта Cons. Вариант Cons хранит значение типа i32 и значение типа List, и этот процесс продолжается бесконечно, как показано на рис. 15-1.

Rust smart pointers fig15 01

Рис. 15-1. Бесконечный List, состоящий из бесконечных вариантов Cons.

Использование Box< T> для получения рекурсивного типа известного размера. Поскольку Rust не может понять, сколько нужно места для рекурсивно определяемых типов, то компилятор выдает ошибку, в которой указывает полезный совет для решения проблемы:

help: insert some indirection (e.g., a `Box`, `Rc`, or `&`) to break the cycle
  |
2 |     Cons(i32, Box< List>),
  |               ++++    +

Здесь "indirection" означает, что вместо сохранения значения напрямую мы должны поменять структуру данных так, чтобы вместо этого хранить в ней значение косвенно, с помощью указателя.

Поскольку Box< T> это указатель, Rust всегда будет знать, сколько пространства нужно для Box< T>: размер указателя не зависит от количества данных, на которые он указывает. Это значит, что мы можем поместить Box< T> внутри варианта Cons вместо непосредственного сохранения значения List. Box< T> будет указывать на следующее значение List, которое находится в куче вместо того, чтобы быть внутри варианта Cons. Концептуально у нас все еще будет получаться список, где каждый список хранит ссылку на другой список, однако эта реализация теперь больше похожа на размещение элементов рядом друг с другом, а не внутри друг друга.

Поменяем определение List enum в листинге 15-2 и использование List в листинге 15-3 на код из листинга 15-5, который скомпилируется нормально (файл src/main.rs):

enum List {
    Cons(i32, Box< List>),
    Nil,
}

use crate::List::{Cons, Nil};

fn main() { let list = Cons(1, Box::new(Cons(2, Box::new(Cons(3, Box::new(Nil)))))); }

Листинг 15-5. Определение List, использующее Box< T>, чтобы иметь получить размер в перечислении для Cons.

Для варианта Cons требуется размер i32 плюс пространство для сохранения указателя на данные box. Вариант Nil не сохраняет значения, так что ему нужно меньше места, чем для варианта Cons. Теперь мы знаем, что любое значение List займет размер i32 плюс размер указателя на данные box. С использованием box мы разбили бесконечную, рекурсивную цепочку, так что компилятор может определить размер, требуемый для сохранения значения List. Рис. 15-2 показывает, как теперь выглядит вариант Cons.

Rust smart pointers fig15 02

Рис. 15-2. List, который не получает бесконечный размер, потому что Cons содержит Box.

Box-ы только предоставляют косвенное встраивание и выделение данных в куче; у них нет никаких других специальных возможностей, наподобие тех, которые мы видим в других типах smart-указателей. У них также нет накладных расходов, снижающих производительность, к которым приводят эти специальные возможности, так что box-ы могут быть полезны в таких случаях, как список cons, где требуется только косвенное встраивание в тип. Мы также рассмотрим другие варианты для использования box в главе 17.

Тип Box< T> это smart-указатель, потому что он реализует трейт Deref, который позволяет значениям Box< T> обрабатываться так же, как и ссылки. Когда значение Box< T> выходит из области действия, данные кучи, на которые указывает box, очищаются благодаря реализации трейту Drop. Эти два трейта будут еще более важными для функционала, предоставляемого другими типами smart-указателя, которые мы обсудим в остальной части этой главы. Давайте рассмотрим эти трейты более подробно.

[Трейт Deref: работа со smart-указателями как с обычными ссылками]

Реализация трейта Deref позволит вам настроить поведение оператора разыменования * (не путайте его с умножением или оператором glob). Путем реализации Deref таким образом можно работать со smart-указателями так же, как и с обычной ссылкой, т. е. вы можете писать код, который будет нормально работать как со ссылками, так и со smart-указателями.

Сначала мы рассмотрим, как оператор разыменования работает с обычными ссылками. Затем попробуем определить пользовательский тип, который ведет себя наподобие Box< T>, и посмотрим, почему оператор разыменования не работает с ним как со ссылкой. Далее мы рассмотрим, как реализовать для этого типа трейт Deref, который позволяет работать smart-указателю на него так же, как работают ссылки. Затем рассмотрим функционал deref coercion языка Rust, и как он позволяет нам работать либо со ссылками, либо со smart-указателями.

Замечание: существует большая разница между типом MyBox< T>, который мы создадим, и реальным Box< T>: наша версия не сохраняет свои данные в куче. Мы здесь фокусируемся на примере Deref, так что в контексте поведения smart-указателя как ссылки не так важно, где на самом деле хранятся данные.

Как по указателю получить значение. Обычная ссылка это тип указателя, и один из способов думать про указатель - это стрелка, указывающая на место, где хранится значение. В листинге 15-6, мы создали ссылку на значение i32, и затем используем оператор разыменования * для получения значения по этой ссылке (файл src/main.rs):

fn main() {
    let x = 5;
    let y = &x;
assert_eq!(5, x); assert_eq!(5, *y); }

Листинг 15-6. Использование оператора разыменования, чтобы следовать ссылке и получить значение i32.

Переменная x хранит значение 5 типа i32. Мы установили переменную y равной ссылке на x. Мы можем утверждать (assert), что x равно 5. Однако если мы захотим сделать то же самое со значением в y, то нужно использовать *y, чтобы следовать ссылке для получения значения, на которое она указывает (следовательно провести разыменование), чтобы компилятор мог провести сравнение с реальным значением. Если мы делаем разыменование y, то получаем доступ к integer-значению, которое мы можем сравнить с 5.

Если вместо этого мы попытаемся написать assert_eq!(5, y);, то получим ошибку компиляции:

$ cargo run
   Compiling deref-example v0.1.0 (file:///projects/deref-example)
error[E0277]: can't compare `{integer}` with `&{integer}`
 --> src/main.rs:6:5
  |
6 |     assert_eq!(5, y);
  |     ^^^^^^^^^^^^^^^^ no implementation for `{integer} == &{integer}`
  |
  = help: the trait `PartialEq< &{integer}>` is not implemented for `{integer}`
  = note: this error originates in the macro `assert_eq` (in Nightly builds, run 
    with -Z macro-backtrace for more info)
For more information about this error, try `rustc --explain E0277`. error: could not compile `deref-example` (bin "deref-example") due to 1 previous error

Сравнение числа и ссылки на число не дозволяется, потому что это разные типы. Мы должны использовать оператор разыменования *, чтобы следовать ссылке для получения значения, на которое она указывает.

Использование Box< T> в качестве ссылки. Мы можем переписать код в листинге 15-6 для использования Box< T> вместо ссылки; оператор разыменования, используемый на Box< T> в листинге 15-7 работает точно так же, как если бы он работал с обычной ссылкой в листинге 15-6:

fn main() {
    let x = 5;
    let y = Box::new(x);
assert_eq!(5, x); assert_eq!(5, *y); }

Листинг 15-7. Использование оператора разыменования на типе Box< i32>.

Основное отличие между листингом 15-7 и листингом 15-6 состоит в том, что мы устанавливаем y в экземпляр Box< T>, указывающий на копию значения x вместо того, чтобы установить y в указатель на значение x. В последнем assert мы можем использовать оператор разыменования, чтобы следовать указателю Box< T> таким же образом, как это делали со ссылкой. Далее мы рассмотрим особенность Box< T>, которая позволяет нам использовать с ним оператор разыменования, для этого мы создали свой собственный тип.

[Определение пользовательского smart-указателя]

Давайте создадим smart-указатель, подобный типу Box< T>, предоставляемому в стандартной библиотеке, чтобы понять, как по умолчанию smart-указатели ведут себя иначе, чем ссылки. Затем мы рассмотрим, как добавить в наш тип возможность использовать оператора разыменования.

Тип Box< T> в конечном счете определен как структура кортежа с одним элементом, поэтому листинг 15-8 определяет тип MyBox< T> таким же образом. Мы также определим функцию new, чтобы она соответствовала аналогичной функции в Box< T>.

struct MyBox< T>(T);

impl< T> MyBox< T> { fn new(x: T) -> MyBox< T> { MyBox(x) } }

Листинг 15-8. Определение типа MyBox< T> (файл src/main.rs).

Мы определили структуру с именем MyBox и декларировали generic-параметр T, потому что мы хотим, чтобы наш тип мог хранить значения любого типа. Тип MyBox это структура кортежа с одним элементом типа T. Функция MyBox::new принимает один параметр типа T и возвращает экземпляр MyBox, который хранит переданное в функцию new значение.

Давайте попробуем добавить функцию main из листинга 15-7 в листинг 15-8, и поменяем использование нашего типа MyBox< T> вместо типа Box< T>. Код в листинге 15-9 не скомпилируется, потому что Rust не знает, как выполнить разыменование MyBox.

fn main() {
    let x = 5;
    let y = MyBox::new(x);
assert_eq!(5, x); assert_eq!(5, *y); }

Листинг 15-9. Попытка использовать MyBox< T> так же, как мы используем ссылки и Box< T> (файл src/main.rs).

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

$ cargo run
   Compiling deref-example v0.1.0 (file:///projects/deref-example)
error[E0614]: type `MyBox< {integer}>` cannot be dereferenced
  --> src/main.rs:14:19
   |
14 |     assert_eq!(5, *y);
   |                   ^^
For more information about this error, try `rustc --explain E0614`. error: could not compile `deref-example` (bin "deref-example") due to 1 previous error

Наш тип не может быть разименован MyBox< T>, потому что мы не реализовали для него такую возможность. Чтобы разрешить разыменование оператором *, мы реализуем трейт Deref.

Определение трейта Deref, чтобы тип мог работать как ссылка. Как обсуждалось в секции "Реализация трейта на типе" главы 10 [5], для реализации трейта нам нужно предоставить реализации методов, необходимых для трейта. Трейт Deref, предоставляемый стандартной библиотекой, требует от нас реализации одного метода с именем deref, который делает заимствование self, и возвратит ссылку на внутренние данные. Листинг 15-10 содержит реализацию Deref для добавление в определение MyBox (файл src/main.rs):

use std::ops::Deref;

impl< T> Deref for MyBox< T> { type Target = T;
fn deref(&self) -> &Self::Target { &self.0 } }

Листинг 15-10. Реализация Deref на MyBox< T>.

Синтаксис type Target = T; определяет связанный тип для использования трейтом Deref. Связанные типы это несколько иной способ объявления generic-параметра, но пока что вам не нужно об этом беспокоиться, более подробно это будет рассмотрено в главе 19.

Мы заполнили тело метода deref выражением &self.0, чтобы deref возвратил ссылку на значение, к которому мы хотим обращаться через оператор *. Вспомните из секции "Использование структур кортежа без именованных полей для создания других типов" главы 5 [6], что .0 осуществляет доступ к первому значению в структуре кортежа. Функция main в листинге 15-9, которая вызывает * на значении MyBox< T>, теперь скомпилируется, и assert обработается нормально!

Без трейта Deref компилятор может только лишь разыменовывать & ссылки. Метод deref дает компилятору возможность получить значение любого типа, в котором реализован трейт Deref, и вызвать метод deref, чтобы получить & ссылку, которую понятно, как разыменовывать.

Когда мы ввели *y в листинге 15-9, внутри происходит на самом деле следующее:

*(y.deref())

Rust заменяет оператор * на вызов метода deref, и затем чистое разыменование, так что нам не нужно думать, что надо вызывать метод deref. Эта фича Rust позволяет нам писать код, который функционирует идентично как для обычных ссылок, так и для типа, в котором реализован Deref.

Причина, по которой метод deref возвращает ссылку на значение, и что простое разыменование вне скобок *(y.deref()) все еще необходимо, заключается в работе системы владения. Если метод deref возвращает значение напрямую вместо ссылки на значение, то значение будет перемещено из self. Мы не хотим брать во владение внутреннее значение в MyBox< T> как в этом случае, так и в большинстве случаев, когда используем оператор разыменования.

Обратите внимание, что оператор * заменяется на вызов метода deref, и затем происходит только однократное применение оператора *, каждый раз, когда мы используем * в своем коде. Поскольку замена оператора * не производит бесконечной рекурсии, мы завершаем получением данных типа i32, значение которого совпадет с 5 в assert_eq! листинга 15-9.

Неявные deref coercion с функциями и методами. Deref coercion преобразует ссылку на тип, который реализует трейт Deref, в ссылку на другой тип. Например, deref coercion может преобразовать &String в &str, потому что String реализует трейт Deref, который вернет &str. Deref coercion это действие удобства, которое Rust выполняет на аргументах для функций и методов, и это работает только на типах, которые реализуют трейт Deref. Это происходит автоматически, когда мы передаем ссылку на значение определенного типа в качестве аргумента функции или метода, который не соответствует типу параметра в определении функции или метода. Последовательность вызовов метода deref преобразует предоставленный нами тип тип, необходимый параметру.

Фича deref coercion была добавлена в Rust, чтобы программисты при написании функции и метода не должны были добавлять явные ссылки и разыменования с & и *. Фича deref coercion также позволяет нам писать код, который может одинаково работать как со ссылками, так и со smart-указателями.

Чтобы посмотреть на deref coercion в действии, давайте используем тип MyBox< T>, определенный в листинге 15-8, а также реализацию Deref, добавленную в листинге 15-10. Листинг 15-11 показывает определение функции, у которой параметр это слайс строки (файл src/main.rs):

fn hello(name: &str) {
    println!("Hello, {name}!");
}

Листинг 15-11. Функция hello, которая имеет параметр name типа &str.

Мы можем вызвать функцию hello со слайсом строки в качестве аргумента, как например hello("Rust");. Deref coercion делает это возможным для вызова hello со ссылкой на значение типа MyBox< String>, как показано в листинге 15-12 (файл src/main.rs):

fn main() {
    let m = MyBox::new(String::from("Rust"));
    hello(&m);
}

Листинг 15-12. Вызов hello со ссылкой на значение MyBox< String>, что работает благодаря deref coercion.

Здесь мы вызываем функцию hello с аргументом &m, которая является ссылкой на значение MyBox< String>. Поскольку мы реализовали трейт Deref на MyBox< T> в листинге 15-10, Rust может превратить &MyBox< String> в &String путем вызова deref. Стандартная библиотека предоставляет реализацию Deref на типе String, которая возвратит слайс строки, и это описано в API-документации для Deref. Rust снова вызовет deref, чтобы превратить &String в &str, который подойдет к определению функции hello.

Если бы в Rust не было реализации deref coercion, то нам надо было бы писать код в листинге 15-13 вместо кода в листинг 15-12, чтобы вызывать hello со значением типа &MyBox< String>.

fn main() {
    let m = MyBox::new(String::from("Rust"));
    hello(&(*m)[..]);
}

Листинг 15-13. Код, который мы должны были бы написать, если бы в Rust не было фичи deref coercion (файл src/main.rs).

Здесь (*m) разыменовывает MyBox< String> в String. Затем & и [..] берут слайс строки из String, равный всей строке, что будет соответствовать сигнатуре hello. Этот код без deref coercion труднее для чтения, написания и понимания, что происходит со всеми этими символами. Deref coercion позволяет Rust выполнить все эти преобразования автоматически.

Когда вовлекается использование трейта Deref для типов, Rust будет анализировать типы и использует Deref::deref столько раз, сколько это необходимо, чтобы получить ссылку, соответствующую типу параметра. Необходимое количество вставленных вызовов Deref::deref определяется во время компиляции, так что deref coercion не приводит к снижению производительности.

Как deref coercion взаимодействует с мутируемостью. Подобно тому, как вы используете трейт Deref для переназначения оператора * на немутируемых (immutable) ссылках, вы можете использовать трейт DerefMut для переназначения оператора * на мутируемых (mutable) ссылках.

Rust делает deref coercion, когда находит типы и реализации трейта в следующих трех случаях:

• От &T до &U, когда T: Deref< Target=U>.
• От &mut T до &mut U, когда T: DerefMut< Target=U>.
• От &mut T до &U, когда T: Deref< Target=U>.

Первые два случая совпадают, с единственным отличием, что второй реализует мутируемость. Первый случай устанавливает, что если у вас &T, и T реализует Deref для какого-то типа U, то вы можете прозрачно получить &U. Второй случай устанавливает, что то же самое deref coercion происходит для mutable-ссылок.

Третий случай хитрее: Rust будет также преобразовывать мутируемую ссылку в немутируемую. Но обратное провернуть невозможно: немутируемая ссылка никогда не может быть преобразована в мутируемую. Из-за правил заимствования, если у вас есть мутируемая ссылка, то эта мутируемая ссылка должна быть единственной для этих данных (иначе программа не скомпилируется). Преобразование одной мутируемой ссылки в одну немутируемую ссылку не нарушит правила заимствования. Преобразование немутируемой ссылки в мутируемую потребовало бы, чтобы исходная неизменяемая ссылка была единственной неизменяемой ссылкой на эти данные, но правила заимствования этого не гарантируют. Таким образом, Rust не может предположить, что возможно преобразование немутируемой ссылки в мутируемую.

[Трейт Drop: запуск кода при очистке]

Второй трейт, важный для использования шаблона smart-указателя, это Drop, который позволит вам настроить что-нибудь, когда значение типа выходит из области действия (т. е. выполнить действия по дополнительной очистке). Вы можете предоставить реализацию трейта Drop на любом типе, и его код может использоваться для освобождение ресурсов наподобие файлов или сетевых соединений.

Мы представляем Drop в контексте smart-указателей, потому что функциональность трейта Drop почти всегда используется, когда реализуется smart-указатель. Например, когда выбрасывается Box< T>, будет освобождаться память в куче, на которую указывал box.

В некоторых языках программирования, для некоторых типов программист должен вызвать код для освобождения памяти или ресурсов каждый раз, когда завершилось использование экземпляра этих типов. Примеры такого поведения включают дескрипторы файлов (file handles), сокеты (sockets) или блокировки (locks). Если забыть это сделать, то система может оказаться перегруженной или потерпеть сбой. В Rust вы можете указать, какой кусочек кода должен быть запущен всякий раз, когда значение выходит из области действия, и компилятор вставит этот код автоматически. В результате вам не нужно заботиться о том, чтобы вставлять специальный код очистки во всех местах, где завершена работа с каким-либо определенным типом, так что вы не столкнетесь с утечкой ресурсов!

Указать, какой код должен быть запущен, когда значение вышло из области действия, можно реализацией трейта Drop. Трейт Drop требует от вас реализации метода drop, который берет мутируемую ссылку на self. Чтобы увидеть, когда Rust вызовет drop, давайте сейчас реализуем с вызовами println!.

Листинг 15-14 показывает структуру CustomSmartPointer, у которой только единственный пользовательский функционал, который печатает "Очистка CustomSmartPointer", когда экземпляр выходит из области действия, чтобы показать, когда Rust запускает функцию drop.

struct CustomSmartPointer {
    data: String,
}

impl Drop for CustomSmartPointer { fn drop(&mut self) { println!("Очистка CustomSmartPointer с данными `{}`", self.data); } }

fn main() { let c = CustomSmartPointer { data: String::from("my stuff"), }; let d = CustomSmartPointer { data: String::from("other stuff"), }; println!("CustomSmartPointers created."); }

Листинг 15-14. Структура CustomSmartPointer, которая реализует трейт Drop, где мы могли бы поместить наш код очистки (файл src/main.rs).

Трейт Drop подключен в prelude, так что нам не нужно приводить его в область действия. Мы реализовали трейт Drop на CustomSmartPointer и предоставили реализацию метода drop, который вызывает println!. Тело функции drop это то место, где вы могли бы вставить любую логику, которую хотели бы запустить, когда экземпляр вашего типа выходит из области действия. Мы печатаем здесь некоторый текст, чтобы визуально демонстрировать, когда Rust вызовет drop.

В main мы создали два экземпляра CustomSmartPointer, и затем печатаем "CustomSmartPointers created". В конце main наши экземпляры выходят из области действия, и Rust вызовет код, который мы поместили в метод drop, что печатает наше завершающее сообщение. Обратите внимание, что нам не нужно явно вызывать метод drop.

Когда мы запустим эту программу, то увидим следующий вывод:

$ cargo run
   Compiling drop-example v0.1.0 (file:///projects/drop-example)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.60s
     Running `target/debug/drop-example`
CustomSmartPointers created.
Очистка CustomSmartPointer с данными `other stuff`
Очистка CustomSmartPointer с данными `my stuff`

Rust автоматически вызовет для нас drop, когда наши экземпляры выйдут из области действия. Переменные выбрасываются в порядке, обратном тому, в котором они были созданы, так что d будет выброшена перед c. Назначение этого примера - дать наглядное руководство, как работает метод drop; обычно вы указали бы в методе drop более полезный код, который делает очистку, вместо печати сообщения.

std::mem::drop: раннее отбрасывание значения. К сожалению, не существует простого способа запрета функционала автоматического drop. Запрет drop обычно не требуется; весь смысл трейта Drop состоит в том, чтобы делать очистку автоматически. Однако иногда может потребоваться ранняя очистка значения. Один из примеров - когда используются smart-указатели для управления блокировками: вам может понадобиться принудительно вызвать метод drop, который освободит блокировку, чтобы другой код в той же области действия мог бы взять блокировку. Rust не позволит вам вручную вызвать метод drop трейта Drop; вместо этого вы должны вызвать функцию std::mem::drop, предоставляемую стандартной библиотекой, если вы хотите принудительно выбросить значение до того, как оно выйдет из области действия.

Если мы попробуем вызвать метод drop трейта Drop вручную путем модификации функции main из листинга 15-14, как показано в листинге 15-15, то получим ошибку компилятора (файл src/main.rs):

fn main() {
    let c = CustomSmartPointer {
        data: String::from("some data"),
    };
    println!("CustomSmartPointer created.");
    c.drop();
    println!("CustomSmartPointer dropped before the end of main.");
}

Листинг 15-15. Попытка вручную вызвать метод drop из трейта Drop для выполнения ранней очистки.

Когда мы попытаемся скомпилировать этот код, то получим ошибку:

$ cargo run
   Compiling drop-example v0.1.0 (file:///projects/drop-example)
error[E0040]: explicit use of destructor method
  --> src/main.rs:16:7
   |
16 |     c.drop();
   |       ^^^^ explicit destructor calls not allowed
   |
help: consider using `drop` function
   |
16 |     drop(c);
   |     +++++ ~
For more information about this error, try `rustc --explain E0040`. error: could not compile `drop-example` (bin "drop-example") due to 1 previous error

Эта ошибка устанавливает, что нам не разрешено явно вызывать drop. Сообщение об ошибке использует термин destructor, который является общим термином в программировании для функции, которая делает очистку экземпляра. Деструктор это антипод конструктора, который создает экземпляр. Функция drop в Rust это один из конкретных деструкторов.

Rust не позволяет нам явно вызвать drop, потому что Rust все еще автоматически вызовет drop на значении, когда завершится main. Это вызвало бы ошибку двойного освобождения, потому что Rust попытался бы дважды освободить одно и то же значение.

Мы не можем запретить автоматическую вставку drop, когда значение выйдет из области действия, и не можем вызывать метод drop явно. Таким образом, если нам нужно принудительно выполнить раннюю очистку значения, то используется функция std::mem::drop.

Функция std::mem::drop отличается от метода drop в трейте Drop. Мы вызываем её путем передачи в качестве аргумента значение, которое хотим принудительно выбросить. Эта функция находится в prelude, так что мы можем изменить main в листинге 15-15, чтобы вызвать функцию drop, как показано в листинге 15-16 (файл src/main.rs):

fn main() {
    let c = CustomSmartPointer {
        data: String::from("some data"),
    };
    println!("CustomSmartPointer created.");
    drop(c);
    println!("CustomSmartPointer dropped before the end of main.");
}

Листинг 15-16. Вызов std::mem::drop для явного выбрасывания значения до того, как оно выйдет из области действия.

Запуск этого кода напечатает следующее:

$ cargo run
   Compiling drop-example v0.1.0 (file:///projects/drop-example)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.73s
     Running `target/debug/drop-example`
CustomSmartPointer created.
Dropping CustomSmartPointer with data `some data`!
CustomSmartPointer dropped before the end of main.

Текст "Dropping CustomSmartPointer with data `some data`!" напечатается между печатью "CustomSmartPointer created." и "CustomSmartPointer dropped before the end of main.", показывая, что код метода drop был вызван в этом месте для отбрасывания c.

Вы можете использовать код в реализации трейта Drop многими способами, чтобы сделать очистку удобной и безопасной: например, вы можете использовать это для создания собственного выделителя памяти! С трейтом Drop и системой владения Rust вам не нужно помнить про очитку потому что Rust сделает это автоматически.

Вам также не надо беспокоиться о проблемах, возникающих из-за случайной очистки значений, все еще находящихся в использовании: система владения, которая всегда гарантирует достоверность ссылок, также гарантирует, что drop вызовется только один раз, когда значение больше не используется.

Теперь, когда мы рассмотрели Box< T> и некоторые характеристики smart-указателей, давайте рассмотрим другие smart-указатели, определенные в стандартной библиотеке.

[Rc< T>: smart-указатель с подсчетом ссылок]

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

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

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

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

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

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

Rust smart pointers fig15 03

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

Создадим список, содержащий 5 и затем 10. Затем создадим еще 2 списка: b, который начинается с 3, и c, который начинается с 4. Оба списка b и c будут тогда продолжаться первым списком, содержащим 5 и 10. Другими словами, оба списка будут совместно использовать первый список, содержащий 5 и 10.

Попытка реализовать этот сценарий с использованием нашего определения List через Box< T>, как показано в листинге 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)))); let b = Cons(3, Box::new(a)); let c = Cons(4, Box::new(a)); }

Листинг 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 будет теперь хранить значение value и Rc< T>, указывающий на List. Когда мы создаем b, вместо получение во владение над a клонируем Rc< List>, который его содержит, увеличивая тем самым количество ссылок с одного до двух, и позволяя a и b совместно владеть данными в этом Rc< List>. Будем также клонировать a при создании c, увеличивая счетчик ссылок с двух до трех. Каждый раз, когда мы вызываем Rc::clone, счетчик ссылок на данные в Rc< List> будет увеличиваться, и данные не будут очищены, пока не достигнет нуля счетчик ссылок на них.

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

Листинг 15-18. Определение List, который использует Rc< T> (файл src/main.rs).

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

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

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

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

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

Листинг 15-19. Печать счетчика ссылок (файл src/main.rs).

В каждом месте программы, где меняется счетчик ссылок, мы его печатаем с помощью вызова функции Rc::strong_count. Эта функция носит имя strong_count вместо count, потому что тип Rc< T> также имеет weak_count; мы увидим, для чего используется weak_count в секции "Предотвращение циклов ссылок: превращение Rc< T> в 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 [2]: несколько изменяемых заимствований для одного и того же места могут привести к гонке данных (data races) и несоответствию (inconsistencies). Но возможность мутации данных очень полезна! В следующей секции мы обсудим шаблон внутренней мутируемости и тип RefCell< T>, который вы можете использовать совместно с Rc< T>, чтобы разобраться с этим ограничением не мутируемости.

[RefCell< T> и шаблон внутренней изменчивости]

Внутренняя изменчивость (interior mutability) это шаблон дизайна в Rust, который позволяет вам мутировать данные даже когда существуют немутируемые ссылки на эти данные; обычно это действие не разрешено согласно правилам заимствования (borrowing rules). Для мутирования данных шаблон использует небезопасный (unsafe) код внутри структуры данных, чтобы обойти обычные правила Rust, регулирующие мутацию и заимствование. Небезопасный код указывает компилятору, что мы проверяем правила вручную, а не полагаемся на компилятор, чтобы он проверял эти правила за нас; этот небезопасный код мы подробнее рассмотрим в главе 19.

Мы можем применять типы, которые используют шаблон interior mutability pattern только когда мы можем гарантировать, что правила заимствования будут соблюдаться runtime, хотя компилятор не может этого гарантировать. Небезопасный код затем упаковывается в безопасный API, а внешний тип все еще остается немутируемым.

Давайте рассмотрим эту концепцию на типе RefCell< T>, который следует шаблону interior mutability.

RefCell< T>: применение правил заимствования runtime. В отличие от Rc< T>, тип RefCell< T> представляет одиночное владение над хранящимися в нем данными. Итак, что отличает RefCell< T> от типа наподобие Box< T>? Вспомним правила заимствования, которым мы научились в главе 4 [2]:

• В любой момент времени вы можете либо (но не одновременно) иметь одну мутируемую ссылку, либо любое количество немутируемых ссылок.
• Ссылки должны быть всегда корректными.

При использовании ссылок и Box< T> инварианты правил заимствования применяются во время компиляции. При использовании RefCell< T> эти инварианты применяются во время выполнения (runtime). При использовании ссылок, если вы нарушите эти правила, то получите ошибку компилятора. При использовании RefCell< T>, если вы нарушите эти правила, то ваша программа сгенерирует panic и завершит работу (exit).

Достоинство проверки правил заимствования во время компиляции в том, что ошибки перехватываются на ранней стадии, в процессе разработки, и это не влияет на производительность кода, потому что весь анализ завершен заранее. По этим причинам проверка правил заимствования во время компиляции лучший выбор для большинства случаев, поэтому такой выбор применяется в Rust по умолчанию.

Достоинство проверки правил заимствования runtime состоит в том, что затем допускаются определенные сценарии, безопасные для памяти, где они были бы запрещены проверками времени компиляции. Статический анализ, как и компилятор Rust, по своей сути консервативен. Некоторые свойства кода невозможно обнаружить путем его анализа: самым известным примером может считаться Проблема Остановки [7], которая выходит за рамки нашего описания, хотя и остается интересной темой для исследования.

Поскольку не любой анализ возможен, если компилятор Rust не может быть уверен, что код соответствует правилам владения, он может отклонить успешное компилирование корректной программы; этот вариант поведения консервативен. Если бы Rust принял некорректную программу, пользователи не смогли бы доверять гарантиям, которые дает Rust. Однако если Rust отклонит корректную программу, программисту это будет неудобно, но ничего катастрофического не произойдет. Тип RefCell< T> полезен, когда ваш код следует правилам заимствования, однако компилятор не может этого понять и что-либо гарантировать.

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

Вот краткое описание причин выбора Box< T>, Rc< T> или RefCell< T>:

• Rc< T> разрешает нескольких владельцев над одними и теми же данными; Box< T> и RefCell< T> имеют одиночных владельцев.
• Box< T> позволяет проверять немутируемые или мутируемые заимствования во время компиляции; Rc< T> позволяет проверять только немутируемые заимствования во время компиляции; RefCell< T> позволяет проверять немутируемые или мутируемые заимствования во время выполнения (runtime).
• Поскольку RefCell< T> позволяет проверять мутируемые зависимости runtime, вы можете мутировать значение внутри RefCell< T>, даже когда RefCell< T> немутируемый.

Мутация значения внутри немутируемого значения это и есть шаблон interior mutability. Давайте рассмотрим ситуацию, в которой полезна interior mutability, и разберемся, как это стало возможным.

Interior Mutability: мутируемое заимствует немутируемое значение. Следствием правил заимствования является то, что когда у вас есть немутируемое значение, вы не можете выполнить её заимствование мутируемым способом. Например, следующий код не скомпилируется:

fn main() {
    let x = 5;
    let y = &mut x;
}

Если вы попытаетесь скомпилировать такой код, то получите следующую ошибку:

$ cargo run
   Compiling borrowing v0.1.0 (file:///projects/borrowing)
error[E0596]: cannot borrow `x` as mutable, as it is not declared as mutable
 --> src/main.rs:3:13
  |
3 |     let y = &mut x;
  |             ^^^^^^ cannot borrow as mutable
  |
help: consider changing this to be mutable
  |
2 |     let mut x = 5;
  |         +++
For more information about this error, try `rustc --explain E0596`. error: could not compile `borrowing` (bin "borrowing") due to 1 previous error

Однако есть ситуации, в которых было бы полезно, чтобы значение мутировало себя в своих методах, но оставалось немутируемым для другого кода. Код вне методов значения не может изменить (мутировать) значение. Использование RefCell< T> это один из способов получить возможность interior mutability, однако RefCell< T> не полностью обходит правила заимствования: проверка заимствования в компиляторе позволяет эту interior mutability, а правила заимствования проверяются вместо этого runtime. Если вы нарушите правила, по получите panic! программы вместо ошибки компилятора.

Давайте разберем практический пример, где мы можем использовать RefCell< T> для изменения немутируемого значения, и посмотрим, почему это полезно.

Случай использования Interior Mutability: фиктивные объекты. Иногда во время тестирования программист использует один тип вместо другого, чтобы наблюдать за определенным поведением и утверждать (assert), что оно реализовано правильно. Этот замещающий тип называют test double. Можно это представить себе как "двойной трюк" (stunt double) в кинематографе, когда дублер заменяет актера для выполнения какой-нибудь сложной сцены. Типы test double стоят за другими типами, когда мы запускаем тесты. Фиктивные объекты (mock objects) это специальные типы test double, которые записывают, что происходит во время теста, так что вы можете утверждать, что были осуществлены правильные действия.

Rust не имеет объектов в том же смысле, что и другие языки программирования, и Rust не имеет функционала mock-объекта, встроенного в стандартную библиотеку, как имеет место на других языках. Тем не менее вы определенно можете создать структуру, которая будет служить тем же целям, что и mock-объект.

Вот сценарий, который мы протестируем: создадим библиотеку, которая отслеживает значение на максимум, и отправляет сообщения на основе того, насколько близко к максимальному текущее значение. Эта библиотека может использоваться, например, для отслеживания квоты пользователя на количество вызовов API, которые ему разрешено выполнять.

Наша библиотека будет только предоставлять функциональность отслеживания того, насколько близко к максимальному значению текущее значение, и какими и в какой время должны быть сообщения. Подразумевается, что использующие нашу библиотеку приложения предоставят механизм отправки сообщений: приложение может поместить сообщение в очередь, отправить email, напечатать текст сообщения в консоли или сделать что-то подобное. Библиотеке знать эти детали не нужно. Все, что для неё необходимо - реализовать трейт Messenger. Листинг 15-20 показывает код этой библиотеки (файл src/lib.rs):

pub trait Messenger {
    fn send(&self, msg: &str);
}

pub struct LimitTracker< 'a, T: Messenger> { messenger: &'a T, value: usize, max: usize, }

impl< 'a, T> LimitTracker< 'a, T>
where T: Messenger, { pub fn new(messenger: &'a T, max: usize) -> LimitTracker< 'a, T> { LimitTracker { messenger, value: 0, max, } }
pub fn set_value(&mut self, value: usize) { self.value = value;
let percentage_of_max = self.value as f64 / self.max as f64;
if percentage_of_max >= 1.0 { self.messenger.send("Error: You are over your quota!"); } else if percentage_of_max >= 0.9 { self.messenger .send("Urgent warning: You've used up over 90% of your quota!"); } else if percentage_of_max >= 0.75 { self.messenger .send("Warning: You've used up over 75% of your quota!"); } } }

Листинг 15-20. Библиотека для отслеживания, насколько близко значение к максимуму, и выдачи предупреждения, когда значение достигнет определенных уровней.

Одна из важных частей этого кода - трейт Messenger с одним методом send, который принимает немутируемую ссылку на self и текст сообщения. Этот трейт является интерфейсом, который наш mock-объект должен реализовать, чтобы макет (mock) можно было использовать так же, как и реальный объект. Другая важная часть заключается в том, что мы хотим проверить поведение метода set_value на LimitTracker. Мы можем изменить то, что передаем в параметре value, но set_value ничего не возвращает нам, чтобы срабатывал assert. Мы хотим иметь возможность сказать, что если мы создаем LimitTracker с чем-то, что реализует трейт Messenger и конкретное значение для max, когда мы передаем различные числа для value, мессенджер посылает соответствующие сообщения.

Нам нужен mock-объект, который вместо отправки email или текстового сообщения, когда мы вызываем send, будет отслеживать только сообщения, которые ему было указано отправить. Мы можем создать новый экземпляр mock-объекта, создать LimitTracker, который использует mock-объект, вызывать метод set_value на LimitTracker, и затем проверить, что mock-объект имеет сообщения, которые мы ожидаем. Листинг 15-21 показывает попытку реализовать mock-объект для этой цели, но проверка правил заимствования не позволит это (файл src/lib.rs):

#[cfg(test)]
mod tests { use super::*;
struct MockMessenger { sent_messages: Vec< String>, }
impl MockMessenger { fn new() -> MockMessenger { MockMessenger { sent_messages: vec![], } } }
impl Messenger for MockMessenger { fn send(&self, message: &str) { self.sent_messages.push(String::from(message)); } }
#[test] fn it_sends_an_over_75_percent_warning_message() { let mock_messenger = MockMessenger::new(); let mut limit_tracker = LimitTracker::new(&mock_messenger, 100);
limit_tracker.set_value(80);
assert_eq!(mock_messenger.sent_messages.len(), 1); } }

Листинг 15-21. Попытка реализовать MockMessenger, проваленная из-за borrow checker.

Этот тест-код определяет структуру MockMessenger, в которой есть поле sent_messages со значениями вектора строк (Vec от String), чтобы отслеживать сообщения, которые мы говорим посылать. Мы также определили связанную функцию new, чтобы можно было удобно создавать новые значения MockMessenger, которые стартуют с пустым списком сообщений. Затем мы реализовали трейт Messenger для MockMessenger, чтобы можно было предоставить MockMessenger для LimitTracker. В определении метода send мы принимаем строку message, переданную в качестве параметра, и сохраняем её в списке sent_messages.

В тесте мы проверяем, что произойдет, когда для LimitTracker говорят установить значение, которое составляет более 75% от максимального значения. Сначала мы создаем новый MockMessenger, который запускается с пустым списком сообщений. Затем мы создаем новый LimitTracker и даем ему ссылку на новый MockMessenger и значение max 100. Мы вызываем метод set_value на LimitTracker со значением 80, которое больше 75 процентов от 100. Затем мы делаем assert, что в списке сообщений MockMessenger теперь должно быть одно сообщение.

Однако с этим тестом есть проблема, как показано в сообщении компилятора:

$ cargo test
   Compiling limit-tracker v0.1.0 (file:///projects/limit-tracker)
error[E0596]: cannot borrow `self.sent_messages` as mutable, as it is behind a `&` reference
  --> src/lib.rs:58:13
   |
58 |             self.sent_messages.push(String::from(message));
   |             ^^^^^^^^^^^^^^^^^^ `self` is a `&` reference, so the data it refers to cannot
| be borrowed as mutable | help: consider changing this to be a mutable reference | 2 | fn send(&mut self, msg: &str); | ~~~~~~~~~
For more information about this error, try `rustc --explain E0596`. error: could not compile `limit-tracker` (lib test) due to 1 previous error

Мы не можем модифицировать MockMessenger для отслеживания сообщений, потому что метод send принимает немутируемую ссылку на self. Мы также не можем принять совет из текста сообщения об ошибке, чтобы использовать вместо этого &mut self, потому что тогда сигнатура send не будет соответствовать сигнатуре в определении трейта Messenger (поэкспериментируйте и посмотрите, какое сообщение об ошибке вы при этом получите).

Это та ситуация, в которой может помочь interior mutability! Мы сохраним sent_messages внутри RefCell< T>, и тогда метод send сможет модифицировать sent_messages, чтобы сохранять сообщения, которые мы видели. Листинг 15-22 показывает, как это будет выглядеть (файл src/lib.rs):

#[cfg(test)]
mod tests { use super::*; use std::cell::RefCell;
struct MockMessenger { sent_messages: RefCell< Vec< String>>, }
impl MockMessenger { fn new() -> MockMessenger { MockMessenger { sent_messages: RefCell::new(vec![]), } } }
impl Messenger for MockMessenger { fn send(&self, message: &str) { self.sent_messages.borrow_mut().push(String::from(message)); } }
#[test] fn it_sends_an_over_75_percent_warning_message() { // -- вырезано --
assert_eq!(mock_messenger.sent_messages.borrow().len(), 1); } }

Листинг 15-22. Использование RefCell< T> для мутирования внутреннего значения, в то время как внешнее значение считается неизменяемым.

Поле sent_messages теперь типа RefCell< Vec< String>> вместо Vec< String>. В функции new мы создаем новый экземпляр RefCell< Vec< String>> вокруг пустого вектора.

Для реализации метода send, первый параметр по прежнему это немутируемое заимствование self, которое соответствует определению трейта. Мы вызываем borrow_mut на RefCell< Vec< String>> в self.sent_messages, чтобы получить мутируемую ссылку на значение внутри RefCell< Vec< String>>, которое является вектором. Затем мы вызываем push на мутируемой ссылке на вектор, чтобы отследить сообщения, отправленные во время теста.

Последнее изменение, что мы должны сделать, заключается в assert: чтобы увидеть, сколько элементов находится во внутреннем векторе, мы вызовем borrow на RefCell< Vec< String>>, чтобы получить немутируемую ссылку на вектор.

Давайте теперь разберемся, как это работает.

RefCell< T>: отслеживание заимствований runtime. Когда мы создаем немутируемые и мутируемые ссылки, то используем для этого синтаксис & и &mut соответственно. С типом RefCell< T>, мы используем методы borrow и borrow_mut, которые входят в safe API, принадлежащее RefCell< T>. Метод  borrow возвратит smart-указатель типа Ref< T>, а borrow_mut вернет smart-указатель типа RefMut< T>. Оба этих типа реализуют трейт Deref, так что мы можем работать с ними так же, как и с обычными ссылками.

RefCell< T> отслеживает, сколько smart-указателей Ref< T> и RefMut< T> в настоящий момент активно. Каждый раз, когда мы вызываем borrow, RefCell< T> увеличивает свой счетчик, соответствующий количеству активных немутируемых заимствований. Когда значение Ref< T> выходит из области действия, счетчик немутируемых заимствований уменьшается на единицу. Точно так же, как и с правилами заимствования времени компиляции, RefCell< T> позволяет нам получить множество немутируемых заимствований или одну мутируемую зависимость в любой момент времени.

Если мы попробуем нарушить эти правила, то вместо ошибки компилятора реализация RefCell< T> сгенерирует панику runtime. Листинг 15-23 показывает модификацию реализации send в листинге 15-22. Мы намеренно пытаемся создать два мутируемых заимствования, активных в одной и той же области действия, чтобы проиллюстрировать, что RefCell< T> не даст нам это сделать runtime (файл src/lib.rs).

impl Messenger for MockMessenger {
        fn send(&self, message: &str) {
            let mut one_borrow = self.sent_messages.borrow_mut();
            let mut two_borrow = self.sent_messages.borrow_mut();
one_borrow.push(String::from(message)); two_borrow.push(String::from(message)); } }

Листинг 15-23. Создание двух мутируемых ссылок в одной и той же области действия, чтобы увидеть, как RefCell< T> вызовет панику.

Мы создали переменную one_borrow для smart-указателя RefMut< T>, возвращаемого из borrow_mut. Затем мы создали мутируемое заимствование таким же способом в переменной two_borrow. Это делает две мутируемые ссылки в одной и той же области действия, что не допускается. Когда мы запустим тесты для нашей библиотеки, то код в листинге 15-23 скомпилируется без каких-либо ошибок, то test провалится:

$ cargo test
   Compiling limit-tracker v0.1.0 (file:///projects/limit-tracker)
    Finished `test` profile [unoptimized + debuginfo] target(s) in 0.91s
     Running unittests src/lib.rs (target/debug/deps/limit_tracker-e599811fa246dbde)
running 1 test test tests::it_sends_an_over_75_percent_warning_message ... FAILED
failures:
---- tests::it_sends_an_over_75_percent_warning_message stdout ---- thread 'tests::it_sends_an_over_75_percent_warning_message' panicked at src/lib.rs:60:53: already borrowed: BorrowMutError note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
failures: tests::it_sends_an_over_75_percent_warning_message
test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
error: test failed, to rerun pass `--lib`

Обратите внимание, что код паникует с сообщением already borrowed: BorrowMutError. Так RefCell< T> обрабатывает нарушения правил заимствования runtime.

Выбор runtime-перехвата ошибок заимствования вместо того, чтобы это делать во время компиляции, как было сделано в этом примере, потенциально чревато поздними обнаружениями ошибок в процессе разработки: возможны даже случаи, когда ошибка не будет обнаружена до выхода кода в релиз. Также ваш код будет подвержен небольшим снижением runtime-производительности, потому что заимствования теперь отслеживаются runtime, а не во время компиляции. Тем не менее использование RefCell< T> делает возможным писать код для mock-объекта, который имеет возможность модифицировать себя, чтобы отслеживать сообщения, которые он видел runtime, когда использовался в контексте, где разрешены только немутируемые значения. Вы можете использовать RefCell< T>, несмотря на его компромиссы, чтобы получить больше функциональных возможностей, чем предоставляют обычные ссылки.

Комбинация Rc< T> и RefCell< T> для нескольких владельцев мутируемых данных. Это общий метод использования RefCell< T> в комбинации с Rc< T>. Вспомним, что Rc< T> позволит вам получить нескольких владельцев каких-то данных, но при этом к этим данным будет только немутируемый доступ. Если у вас есть Rc< T>, который хранит RefCell< T>, то вы можете получить значение, у которого будет несколько владельцев, и вы при этом сможете это значение изменять!

Вспомним список cons в листинге 15-18, где мы использовали Rc< T>, чтобы несколько списков могли совместно использовать другой список. Поскольку Rc< T> хранит только немутируемые значения, мы не можем поменять любые значения в списке, как только его создали. Давайте добавим RefCell< T> для получения возможности менять значения в списках. Листинг 15-24 показывает использование RefCell< T> в определении Cons, таким способом мы можем модифицировать значение, сохраненное во всех списках (файл src/main.rs):

#[derive(Debug)]
enum List { Cons(Rc< RefCell< i32>>, Rc< List>), Nil, }

use crate::List::{Cons, Nil};
use std::cell::RefCell;
use std::rc::Rc;

fn main() { let value = Rc::new(RefCell::new(5));
let a = Rc::new(Cons(Rc::clone(&value), Rc::new(Nil)));
let b = Cons(Rc::new(RefCell::new(3)), Rc::clone(&a)); let c = Cons(Rc::new(RefCell::new(4)), Rc::clone(&a));
*value.borrow_mut() += 10;
println!("a after = {a:?}"); println!("b after = {b:?}"); println!("c after = {c:?}"); }

Листинг 15-24. Использование Rc< RefCell< i32>> для создания списка List, который мы можем мутировать.

Мы создаем переменную value, которая является экземпляром Rc< RefCell< i32>>, к которой сможем впоследствии напрямую обратиться. Затем мы создаем List в a с вариантом Cons, где хранится value. Нам нужно клонировать value, чтобы и a, и value имели владение над внутренним значением 5 вместо передачи права владения от value к a, или чтобы a заимствовало из value.

Мы обернули список a в Rc< T>, так что когда мы создадим списки b и c, они могли оба могли ссылаться на a, что мы и сделали в листинге 15-18.

После того, как мы создали списки в a, b и c, мы хотим добавить 10 в значение value. Это мы делаем вызовом borrow_mut на value, что использует фичу автоматического разыменования, которую мы обсуждали в главе 5 [6] (см. врезку "Где же оператор -> ?"), чтобы выполнить разыменование Rc< T> на внутреннее значение RefCell< T>. Метод borrow_mut вернет smart-указатель RefMut< T>, и мы используем на нем оператор разыменования, меняя тем самым внутреннее значение.

Когда мы печатаем a, b и c, можно увидеть, что у всех у них измененное значение 15 вместо 5:

$ cargo run
   Compiling cons-list v0.1.0 (file:///projects/cons-list)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.63s
     Running `target/debug/cons-list`
a after = Cons(RefCell { value: 15 }, Nil)
b after = Cons(RefCell { value: 3 }, Cons(RefCell { value: 15 }, Nil))
c after = Cons(RefCell { value: 4 }, Cons(RefCell { value: 15 }, Nil))

Эта техника довольно аккуратна! Используя RefCell< T>, мы получаем не изменяемое снаружи значение List value. Однако мы можем использовать методы на RefCell< T>, которые обеспечивают доступ к его внутренней изменчивости (interior mutability), чтобы можно было менять наши данные, когда это необходимо. Runtime-проверки правил заимствования защищают нас от рейсинга данных, и иногда резонным будет сместить компромисс в сторону незначительной потери производительности, чтобы получить эту гибкость в наших структурах данных. Обратите внимание, что RefCell< T> не работает в многопоточном коде! Mutex< T> это thread-safe версия RefCell< T>, и Mutex< T> мы обсудим в главе 16.

[Циклы ссылок могут привести к утечке памяти]

Функционал Rust, обеспечивающий гарантии безопасности памяти, делает сложным, но не невозможным случайное создание кода, когда память никогда не освобождается (что известно как утечка памяти, memory leak). Полное предотвращение утечек памяти не является одной из гарантий Rust, т. е. утечки памяти безопасны в Rust. Мы можем увидеть, что Rust позволяет утечки памяти при использовании Rc< T> и RefCell< T>: есть возможность создать ссылки, где элементы ссылаются друг на друга в цикле. Это создаст утечки памяти, потому что счетчик ссылок каждого элемента в цикле никогда не достигнет 0, и значения никогда не будут отброшены.

Создание цикла ссылок. Давайте рассмотрим, как может произойти ситуация с циклом ссылок, и как её предотвратить, начиная с определения перечисления List и метода tail в листинге 15-25 (файл src/main.rs):

use crate::List::{Cons, Nil};
use std::cell::RefCell;
use std::rc::Rc;

#[derive(Debug)]
enum List { Cons(i32, RefCell< Rc< List>>), Nil, }

impl List { fn tail(&self) -> Option< &RefCell< Rc< List>>> { match self { Cons(_, item) => Some(item), Nil => None, } } }

fn main() {}

Листинг 15-25. Определение списка cons, который хранит RefCell< T>, чтобы мы могли изменить то, на что ссылается вариант Cons.

Мы используем другую вариацию определения List из листинга 15-5. Второй элемент в варианте Cons теперь RefCell< Rc< List>>, т. е. вместо возможности менять значение i32, как мы это делали в листинге 15-24, мы хотим модифицировать значение List, на которое указывает вариант Cons. Мы также добавили метод tail для удобного доступа ко второму элементу, если у нас есть вариант Cons.

В листинге 15-26 мы добавляем функцию main, которая использует определения в листинге 15-25. Этот код создает список в a, и список a в b, который указывает на список в a. Затем модифицируется список в a, чтобы указывать на b, создавая тем самым цикл ссылок. Операторы println! в этом процессе показывают, как меняется счетчик ссылок в различных точках кода.

fn main() {
    let a = Rc::new(Cons(5, RefCell::new(Rc::new(Nil))));
println!("a initial rc count = {}", Rc::strong_count(&a)); println!("a next item = {:?}", a.tail());
let b = Rc::new(Cons(10, RefCell::new(Rc::clone(&a))));
println!("a rc count after b creation = {}", Rc::strong_count(&a)); println!("b initial rc count = {}", Rc::strong_count(&b)); println!("b next item = {:?}", b.tail());
if let Some(link) = a.tail() { *link.borrow_mut() = Rc::clone(&b); }
println!("b rc count after changing a = {}", Rc::strong_count(&b)); println!("a rc count after changing a = {}", Rc::strong_count(&a));
// Раскомментируйте следующую строчку, чтобы увидеть, что получился цикл; // это приведет к переполнению стека: // println!("a next item = {:?}", a.tail()); }

Листинг 15-26. Создание цикла ссылок, когда два значения List указывают друг на друга (файл src/main.rs).

Мы создали экземпляр Rc< List>, хранящий значение List в переменой a, где изначально был список 5, Nil. Затем мы создали экземпляр Rc< List>, хранящий другое значение List в переменной b, который содержит значение 10 и указывает на список в a.

Мы модифицировали a так, чтобы он указывал на b вместо Nil, чем создали цикл. Мы сделали это с помощью метода tail, чтобы получить ссылку на RefCell< Rc< List>> в a, которую мы поместили в переменную link. Затем мы использовали метод borrow_mut на RefCell< Rc< List>>, чтобы поменять значение внутри Rc< List> которое хранило значение Nil, на Rc< List> в b.

Когда мы запустим этот код, удерживая пока закомментированной последнюю строчку с println!, то получим следующий вывод:

$ cargo run
   Compiling cons-list v0.1.0 (file:///projects/cons-list)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.53s
     Running `target/debug/cons-list`
a initial rc count = 1
a next item = Some(RefCell { value: Nil })
a rc count after b creation = 2
b initial rc count = 1
b next item = Some(RefCell { value: Cons(5, RefCell { value: Nil }) })
b rc count after changing a = 2
a rc count after changing a = 2

Счетчик ссылок в экземплярах Rc< List> равны 2 для обоих a и b, после того как мы поменяли список в a, чтобы он указывал на b. В конце main, Rust выбрасывает переменную b, что уменьшает счетчик ссылок экземпляра b Rc< List> с 2 до 1. В этой точке память Rc< List> кучи не очищается, потому что счетчик ссылок 1, не 0. Затем Rust отбрасывает a, что также уменьшает счетчик ссылок экземпляра a Rc< List> с 2 до 1. Память этого экземпляра также не может быть очищена, потому что на него ссылается другой экземпляр Rc< List>. Память, выделенная для списка, остается востребованной навсегда. Этот цикл ссылок иллюстрирует рис. 15-4.

Rust smart pointers fig15 04

Рис. 15-4. Цикл ссылок списков, когда списки a и b указывают друг на друга.

Если вы раскомментируете последний println! и запустите program, Rust попытается напечатать этот цикл, где a указывает на b, указывающий на a, и так далее, пока не произойдет переполнение стека.

По сравнению с реальной программой, последствия создания цикла ссылок в этом примере не очень страшны: сразу после создания цикла ссылок программа завершится. Тем не менее если более сложная программа будет выделять память в цикле, и удерживать её в течение длительного времени, то программа будет использовать больше памяти, чем ей нужно, что может переполнить систему, что приведет к исчерпанию доступной памяти.

Создание цикла ссылок не так просто, но все-таки возможно. Если у вас есть значения RefCell< T>, которые содержат значения Rc< T> или подобные комбинации типов с функционалом interior mutability и подсчетом ссылок, то вы должны гарантировать, что не создали циклов взаимных ссылок; в перехвате подобных ошибок вы не сможете полагаться на Rust. Создание цикла ссылок было бы ошибкой логики вашей программы, которую должны выявлять автоматизированные тесты, ревизии кода и другие практики разработки ПО.

Другим решением, позволяющим избежать циклов ссылок, является реорганизация структур таким образом, чтобы некоторые ссылки выражали принадлежность, а некоторые нет. В результате могут существовать циклы, состоящие из некоторых отношений владения и некоторых отношений отсутствия владения, и только отношения владения влияют на то, может ли быть значение отброшено. В листинге 15-25 мы всегда хотим получить варианты Cons, чтобы они владели своим списком, поэтому реорганизация структуры данных невозможна. Давайте рассмотрим пример с использованием графов, состоящих из родительских узлов и дочерних узлов, чтобы увидеть, когда взаимоотношения отсутствия владения являются подходящим способом предотвращения циклов ссылок.

Предотвращение цикла ссылок: превращение Rc< T> в Weak< T>. До сих пор мы продемонстрировали, что вызов Rc::clone увеличивает strong_count экземпляра Rc< T>, а экземпляр Rc< T> очищается только если его strong_count равен 0. Вы также можете создать слабую (weak) ссылку на значение в экземпляре Rc< T> вызовом Rc::downgrade и передачей ссылки на Rc< T>. Сильные (strong) ссылки это способы совместного владения экземпляром Rc< T>. Weak-ссылки не выражают взаимосвязь владения, и их счетчик не влияет на то, когда очищается экземпляр Rc< T>. Они не вызовут ссылочный цикл, потому что любой цикл, включающий некоторые weak-ссылки, будет разбит, как только значения счетчиков задействованных strong-ссылок станут 0.

Когда вы вызвали Rc::downgrade, то получите smart-указатель типа Weak< T>. Вместо увеличения strong_count в экземпляре Rc< T> на 1, вызов Rc::downgrade увеличивает weak_count на 1. Тип Rc< T> использует weak_count для отслеживания, сколько существует ссылок Weak< T>, подобно strong_count. Разница между этими типами ссылок в том, что weak_count не должна быть 0 для того, чтобы был очищен экземпляр Rc< T>.

Поскольку значение, на котором могли быть отброшены ссылки Weak< T>, чтобы что-либо сделать со значением, на которое указывает Weak< T>, вы должны убедиться, что значение все еще существует. Для этого вызовите метод upgrade на экземпляре Weak< T>, который возвратит Option< Rc< T>>. Вы получите результат Some, если значение Rc< T> не отброшено, и результат None, если значение Rc< T> было отброшено. Поскольку upgrade возвратит Option< Rc< T>>, Rust будет гарантировать, что будут обработаны случай Some и случай None, и не будет недопустимого указателя.

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

Создание структуры данных дерева: узлы с дочерними узлами. Для начала мы построим дерево с узлами, которые знают о своих дочерних узлах. Мы создадим структуру с именем Node, которая хранит как собственное значение i32, так и ссылки на их дочерние значения Node (файл src/main.rs):

use std::cell::RefCell;
use std::rc::Rc;

#[derive(Debug)]
struct Node { value: i32, children: RefCell< Vec< Rc< Node>>>, }

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

Далее мы будем использовать наше определение структуры и создадим экземпляр Node с именем leaf (переводится как "лист"), значением 3 и без дочернего узла, и другой экземпляр с именем branch (переводится как "ветвь"), значением 5 и leaf как одним из своих дочерних элементов, как показано в листинге 15-27 (файл src/main.rs):

fn main() {
    let leaf = Rc::new(Node {
        value: 3,
        children: RefCell::new(vec![]),
    });

let branch = Rc::new(Node { value: 5, children: RefCell::new(vec![Rc::clone(&leaf)]), }); }

Листинг 15-27. Создание узла leaf без дочерних элементов и узла branch с leaf как с одним из своих дочерних элементов.

Мы клонировали Rc< Node> в leaf и сохранили его в branch, т. е. Node в leaf теперь имеет двух владельцев: leaf и branch. Мы можем обратиться к leaf от branch через branch.children, но нет никакого способа обратного доступа, от leaf к branch. Причина в том, что у leaf нет ссылки на branch и он не знает, что они связаны. Но мы хотим, чтобы leaf знал, что branch является его родителем. Этим займемся далее.

Добавление ссылки от дочернего элемента к его родителю. Чтобы сделать дочерний узел осведомленным о своем родителе, на нужно добавить поле parent в определение нашей структуры Node. Проблем в том, чтобы определить, каким должен быть тип поля parent. Мы знаем, что он не может содержать Rc< T>, поскольку это создало бы цикл ссылок с leaf.parent, указывающий на branch, и branch.children, указывающий на leaf, что приведет к тому, что их значения никогда не станут 0.

Если думать об этих взаимоотношениях по-другому, то узел parent должен владеть своими children: если узел parent отбрасывается, то также должны быть отброшены и его дочерние элементы. Однако дочерний элемент не владеет своим родителем: если мы отбросим дочерний узел, родитель остается. Это как раз случай для weak-ссылок!

Так что вместо Rc< T> мы сделаем тип parent использующим Weak< T>, а именно RefCell< Weak< Node>>. Теперь наше определение структуры Node будет выглядеть так (файл src/main.rs):

use std::cell::RefCell;
use std::rc::{Rc, Weak};

#[derive(Debug)]
struct Node { value: i32, parent: RefCell< Weak< Node>>, children: RefCell< Vec< Rc< Node>>>, }

Узел может ссылаться на свой родительский узел, но не владеет им. В листинге 15-28 мы обновили main для использования этого нового определения, так чтобы узел leaf имел способ обратиться к своему родителю branch (файл src/main.rs):

fn main() {
    let leaf = Rc::new(Node {
        value: 3,
        parent: RefCell::new(Weak::new()),
        children: RefCell::new(vec![]),
    });
println!("leaf parent = {:?}", leaf.parent.borrow().upgrade());
let branch = Rc::new(Node { value: 5, parent: RefCell::new(Weak::new()), children: RefCell::new(vec![Rc::clone(&leaf)]), });
*leaf.parent.borrow_mut() = Rc::downgrade(&branch);
println!("leaf parent = {:?}", leaf.parent.borrow().upgrade()); }

Листинг 15-28. Узел leaf с weak-ссылкой на свой родительский узел branch.

Создание узла leaf выглядит подобно листингу 15-27 с исключением поля parent: leaf запускается без parent, так что мы создаем новый, пустой экземпляр ссылки Weak< Node>.

На этой стадии, когда мы попытаемся получить ссылку на родителя leaf использованием метода upgrade, то получим значение None. Мы видим это в выводе из первого элемента println!:

leaf parent = None

Когда мы создаем узел branch, у него будет также новая ссылка Weak< Node> в поле parent, потому что у branch нет родительского узла. У нас все еще есть leaf как один из дочерних элементов branch. Как только у нас будет экземпляр Node в branch, мы сможем изменить leaf чтобы дать ему ссылку Weak< Node> на своего родителя. Мы используем метод borrow_mut на RefCell< Weak< Node>> в поле parent экземпляра leaf, и затем мы используем функцию Rc::downgrade, чтобы создать ссылку Weak< Node> на branch из Rc< Node> в branch.

Когда мы снова напечатаем parent для leaf, то на этот раз мы получим вариант Some, содержащий branch: теперь leaf может обратиться к своему родителю parent! Когда мы печатаем leaf, мы также избегаем цикла, который в конечном счете закончится переполнением стека, как у нас было в листинге 15-26; ссылки Weak< Node> печатаются как (Weak):

leaf parent = Some(Node { value: 5, parent: RefCell { value: (Weak) },
children: RefCell { value: [Node { value: 3, parent: RefCell { value: (Weak) },
children: RefCell { value: [] } }] } })

Отсутствие бесконечного вывода говорит о том, что этот код не создает цикла ссылок. Мы также можем сказать это, взгляну на значения, которые мы получаем из вызова Rc::strong_count и Rc::weak_count.

Визуализация изменений strong_count и weak_count. Давайте посмотрим, как меняются значения strong_count и weak_count экземпляров Rc< Node> путем создания новой внутренней области и перемещения создания branch в эту область. Если это сделать, то мы увидим, что произойдет, когда создается branch, а затем удалена, когда она выходит из области действия. Модификации показаны в листинге 15-29 (файл src/main.rs):

fn main() {
    let leaf = Rc::new(Node {
        value: 3,
        parent: RefCell::new(Weak::new()),
        children: RefCell::new(vec![]),
    });
println!( "leaf strong = {}, weak = {}", Rc::strong_count(&leaf), Rc::weak_count(&leaf), );
{ let branch = Rc::new(Node { value: 5, parent: RefCell::new(Weak::new()), children: RefCell::new(vec![Rc::clone(&leaf)]), });
*leaf.parent.borrow_mut() = Rc::downgrade(&branch);
println!( "branch strong = {}, weak = {}", Rc::strong_count(&branch), Rc::weak_count(&branch), );
println!( "leaf strong = {}, weak = {}", Rc::strong_count(&leaf), Rc::weak_count(&leaf), ); }
println!("leaf parent = {:?}", leaf.parent.borrow().upgrade()); println!( "leaf strong = {}, weak = {}", Rc::strong_count(&leaf), Rc::weak_count(&leaf), ); }

Листинг 15-29. Создание branch во внутренней области, и исследование счетчиков сильных и слабых ссылок.

После того, как создан leaf, в его Rc< Node> strong-счетчике 1, и weak-счетчике 0. Во внутренней области мы создаем branch, и связываем её с leaf, и в этой точке, когда мы печатаем счетчики, Rc< Node> в branch будет иметь strong-счетчик 1 и weak-счетчик 1 (для leaf.parent, указывающего на branch с помощью Weak< Node>). Когда мы печатаем счетчики в leaf, то видим, что его strong-счетчик 2, потому что branch теперь имеет клон Rc< Node> leaf, сохраненный в branch.children, но у него все еще weak-счетчик 0.

Когда внутренняя область заканчивается, branch выходит из области действия, и strong-счетчик Rc< Node> уменьшается до 0, так что этот Node отбрасывается. Weak-счетчик 1 из leaf.parent не имеет никакого отношения к тому, сброшен или нет Node, поэтому мы не получаем никаких утечек памяти!

Если мы попытаемся обратиться к родителю leaf после завершения области действия, то снова получим None. В конце программы Rc< Node> в leaf имеет strong-счетчик 1 и weak-счетчик 0, потому что переменная leaf теперь снова является единственной ссылкой на Rc< Node>.

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

[Общие выводы]

В этой главе рассматривалось, как использовать smart-указатели, чтобы получить различные гарантии и компромиссы из тех, которые Rust делает по умолчанию с обычными ссылками. Тип Box< T> имеет известный размер, и указывает на данные, размещенные в куче (heap). Тип Rc< T> отслеживает количество ссылок на данные в куче, так что у этих данных может быть несколько владельцев. Тип RefCell< T> с технологией внутренней изменчивости (interior mutability) дает нам тип, который мы можем использовать, когда нужно получить немутируемый тип, однако требуется менять внутреннее значение такого типа; это также применяет правила заимствования runtime вместо того, чтобы они применялись во время компиляции.

Также обсуждались трейты Deref и Drop, которые обеспечивают большую функциональность smart-указателей. Мы исследовали циклы ссылок, которые могут привести к утечкам памяти, и способы их предотвращения с помощью Weak< T>.

Если эта глава заинтересовала вас, и вы хотите реализовать свои собственные smart-указатели, то для дополнительной информации обратитесь к книге [8].

Далее мы поговорим о многопоточности (concurrency) в Rust. Вы изучите несколько новых smart-указателей.

[Ссылки]

1. Rust Smart Pointers site:rust-lang.org.
2. Rust: что такое Ownership.
3. Rust: коллекции стандартной библиотеки.
4. Rust: перечисления (enum) и совпадение шаблонов (match).
5. Rust: generic-типы, traits, lifetimes.
6. Rust: использование структуры для взаимосвязанных данных.
7. What is Halting Problem site:startup-house.com.
8. The Rustonomicon site:rust-lang.org.

 

Добавить комментарий


Защитный код
Обновить

Top of Page