Использование Box<T> для работы с данными в куче
Самый простой умный указатель — это Box, тип которого обозначается как Box<T>. Box позволяет хранить данные в куче, а не в стеке. То, что остаётся в стеке, — это указатель на данные в куче. Обратитесь к Главе 4, чтобы повторить разницу между стеком и кучей.
Box не добавляет накладных расходов на производительность, кроме хранения данных в куче вместо стека. Но у него и нет многих дополнительных возможностей. Вы будете использовать его чаще всего в следующих ситуациях:
- Когда у вас есть тип, размер которого неизвестен на этапе компиляции, и вы хотите использовать значение этого типа в контексте, требующем точного размера
- Когда у вас большой объём данных, и вы хотите передать владение, но гарантировать, что данные не будут скопированы при этом
- Когда вы хотите владеть значением, и для вас важно только, чтобы это был тип, реализующий определённый типаж, а не конкретный тип
Мы продемонстрируем первую ситуацию в «Включении рекурсивных типов с помощью Box». Во втором случае передача владения большим объёмом данных может занять много времени, потому что данные копируются в стеке. Чтобы улучшить производительность в этой ситуации, мы можем хранить большой объём данных в куче внутри Box. Тогда только небольшой объём данных указателя копируется в стеке, в то время как данные, на которые он ссылается, остаются в одном месте в куче. Третий случай известен как типаж-объект, и «Использование типажей-объектов, позволяющих значениям разных типов,» в Главе 18 посвящена этой теме. Так что то, что вы узнаете здесь, вы примените снова в том разделе!
Использование Box<T> для хранения данных в куче
Прежде чем обсуждать использование Box<T> для хранения в куче, мы рассмотрим синтаксис и взаимодействие со значениями, хранящимися внутри Box<T>.
Листинг 15-1 показывает, как использовать Box для хранения значения i32 в куче.
fn main() { let b = Box::new(5); println!("b = {b}"); }
i32 в куче с помощью boxМы определяем переменную b со значением Box, указывающим на значение 5, размещённое в куче. Эта программа выведет b = 5; в этом случае мы можем получить доступ к данным в Box так же, как если бы они были в стеке. Как и любое владеющее значение, когда Box выходит из области видимости, как b в конце main, он будет освобождён. Освобождение происходит как для Box (хранящегося в стеке), так и для данных, на которые он указывает (хранящихся в куче).
Размещение одного значения в куче не очень полезно, поэтому вы не будете часто использовать Box таким образом. Хранение значений, таких как один i32, в стеке, где они хранятся по умолчанию, более уместно в большинстве ситуаций. Давайте рассмотрим случай, где Box позволяют нам определить типы, которые мы не могли бы определить без Box.
Включение рекурсивных типов с помощью Box
Значение рекурсивного типа может содержать другое значение того же типа как часть себя. Рекурсивные типы создают проблему, потому что Rust должен знать на этапе компиляции, сколько места занимает тип. Однако вложение значений рекурсивных типов может теоретически продолжаться бесконечно, поэтому Rust не может определить, сколько места нужно значению. Поскольку Box имеет известный размер, мы можем включить рекурсивные типы, вставив Box в определение рекурсивного типа.
В качестве примера рекурсивного типа рассмотрим список cons. Это тип данных, часто встречающийся в функциональных языках программирования. Список cons, который мы определим, прост, кроме рекурсии; поэтому понятия в этом примере будут полезны каждый раз, когда вы сталкиваетесь с более сложными ситуациями, связанными с рекурсивными типами.
Дополнительная информация о списке cons
Список cons — это структура данных, происходящая из языка Lisp и его диалектов, состоит из вложенных пар и является версией связного списка в Lisp. Его название происходит от функции cons (сокращение от construct function) в Lisp, которая создаёт новую пару из двух аргументов. Вызывая cons на паре, состоящей из значения и другой пары, мы можем создавать списки cons из рекурсивных пар.
Например, вот псевдокодное представление списка cons, содержащего список 1, 2, 3 с каждой парой в скобках:
(1, (2, (3, Nil)))
Каждый элемент в списке cons содержит два элемента: значение текущего элемента и следующий элемент. Последний элемент в списке содержит только значение Nil без следующего элемента. Список cons создаётся рекурсивным вызовом функции cons. Каноническое имя для обозначения базового случая рекурсии — Nil. Обратите внимание, что это не то же самое, что концепция “null” или “nil”, обсуждавшаяся в Главе 6, которая является недопустимым или отсутствующим значением.
Список cons не является часто используемой структурой данных в Rust. В большинстве случаев, когда у вас есть список элементов в Rust, Vec<T> — лучший выбор. Другие, более сложные рекурсивные типы данных полезны в различных ситуациях, но начиная со списка cons в этой главе, мы можем исследовать, как Box позволяют определить рекурсивный тип данных без больших отвлечений.
Листинг 15-2 содержит определение enum для списка cons. Обратите внимание, что этот код пока не скомпилируется, потому что тип List не имеет известного размера, что мы и продемонстрируем.
enum List {
Cons(i32, List),
Nil,
}
fn main() {}
i32Примечание: Мы реализуем список cons, содержащий только значения
i32, для целей этого примера. Мы могли бы реализовать его с использованием обобщений, как мы обсуждали в Главе 10, чтобы определить тип списка cons, который мог бы хранить значения любого типа.
Использование типа List для хранения списка 1, 2, 3 будет выглядеть как код в Листинге 15-3.
enum List {
Cons(i32, List),
Nil,
}
use crate::List::{Cons, Nil};
fn main() {
let list = Cons(1, Cons(2, Cons(3, Nil)));
}
List для хранения списка 1, 2, 3Первое значение 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
Ошибка показывает, что этот тип “имеет бесконечный размер.” Причина в том, что мы определили List с вариантом, который рекурсивен: он напрямую содержит другое значение самого себя. В результате Rust не может определить, сколько места нужно для хранения значения List. Давайте разберем, почему мы получаем эту ошибку. Сначала мы посмотрим, как Rust решает, сколько места нужно для хранения значения нерекурсивного типа.
Вычисление размера нерекурсивного типа
Вспомните enum Message, который мы определили в Листинге 6-2, когда обсуждали определения enum в Главе 6:
enum Message { Quit, Move { x: i32, y: i32 }, Write(String), ChangeColor(i32, i32, i32), } fn main() {}
Чтобы определить, сколько места выделить для значения Message, Rust перебирает все варианты, чтобы увидеть, какой вариант требует больше всего места. Rust видит, что Message::Quit не требует места, Message::Move требует достаточно места для хранения двух значений i32, и так далее. Поскольку будет использован только один вариант, максимальное место, которое потребуется значению Message, — это место, необходимое для хранения самого большого из его вариантов.
Сравните это с тем, что происходит, когда Rust пытается определить, сколько места нужно рекурсивному типу, такому как enum List в Листинге 15-2. Компилятор начинает с варианта Cons, который содержит значение типа i32 и значение типа List. Следовательно, Cons требует пространства, равного размеру i32 плюс размер List. Чтобы выяснить, сколько памяти нужно типу List, компилятор смотрит на варианты, начиная с варианта Cons. Вариант Cons содержит значение типа i32 и значение типа List, и этот процесс продолжается бесконечно, как показано на Рисунке 15-1.
Рисунок 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. Концептуально мы всё ещё имеем список, созданный из списков, содержащих другие списки, но эта реализация теперь больше похожа на размещение элементов рядом друг с другом, а не один внутри другого.
Мы можем изменить определение enum List в Листинге 15-2 и использование List в Листинге 15-3 на код в Листинге 15-5, который скомпилируется.
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)))))); }
List, использующее Box<T> для получения известного размераВариант Cons требует размера i32 плюс пространство для хранения данных указателя Box. Вариант Nil не хранит значений, поэтому ему нужно меньше места, чем варианту Cons. Теперь мы знаем, что любое значение List займёт размер i32 плюс размер данных указателя Box. Используя Box, мы разорвали бесконечную рекурсивную цепочку, поэтому компилятор может определить размер, необходимый для хранения значения List. Рисунок 15-2 показывает, как выглядит вариант Cons теперь.
Рисунок 15-2: List, который не бесконечного размера, потому что Cons содержит Box
Box предоставляет только перенаправление и выделение в куче; у него нет других специальных возможностей, таких как те, которые мы увидим с другими типами умных указателей. Он также не имеет накладных расходов на производительность, которые влекут эти специальные возможности, поэтому он может быть полезен в случаях, таких как список cons, где перенаправление — это единственная необходимая функция. Мы рассмотрим больше случаев использования Box в Главе 18.
Тип Box<T> является умным указателем, потому что он реализует типаж Deref, который позволяет значениям Box<T> обращаться как ссылкам. Когда значение Box<T> выходит из области видимости, данные в куче, на которые указывает Box, также очищаются благодаря реализации типажа Drop. Эти два типажа будут ещё более важны для функциональности, предоставляемой другими типами умных указателей, которые мы обсудим в остальной части этой главы. Давайте изучим эти два типажа более подробно.