Введение
Добро пожаловать в Использование Box
В этой лабораторной работе мы узнаем, как использовать умные указатели Box
This tutorial is from open-source community. Access the source code
💡 Этот учебник переведен с английского с помощью ИИ. Чтобы просмотреть оригинал, вы можете перейти на английский оригинал
Добро пожаловать в Использование Box
В этой лабораторной работе мы узнаем, как использовать умные указатели Box
<T>
{=html} для указания на данные в кучеСамый простой умный указатель - это коробка (box), чей тип записывается как Box<T>
. Коробки позволяют хранить данные в куче, а не на стеке. Что остается на стеке - это указатель на данные в куче. См. главу 4, чтобы освежить разницу между стеком и кучей.
Коробки не имеют накладных расходов на производительность, кроме хранения своих данных в куче вместо стека. Но они также не обладают многими дополнительными возможностями. Вы будете их чаще всего использовать в таких ситуациях:
Мы покажем первую ситуацию в разделе "Возможность рекурсивных типов с использованием коробок". Во втором случае передача владения большими объемами данных может занять много времени, потому что данные копируются по стеку. Чтобы повысить производительность в такой ситуации, мы можем хранить большие объемы данных в куче в коробке. Затем только небольшие объемы указательских данных копируются по стеку, в то время как данные, на которые они ссылаются, остаются в одном месте в куче. Третья ситуация известна как объект трейта (trait object), и раздел "Использование объектов трейтов, которые допускают значения разных типов" посвящен этой теме. Поэтому то, что вы здесь узнаете, вы снова примете в этом разделе!
<T>
{=html} для хранения данных в кучеПрежде чем мы обсудим сценарий хранения данных в куче для Box<T>
, рассмотрим синтаксис и способы взаимодействия с значениями, хранящимися внутри Box<T>
.
Листинг 15-1 показывает, как использовать коробку для хранения значения i32
в куче.
Имя файла: src/main.rs
fn main() {
let b = Box::new(5);
println!("b = {b}");
}
Листинг 15-1: Хранение значения i32
в куче с использованием коробки
Мы определяем переменную b
с значением Box
, которая указывает на значение 5
, которое выделяется в куче. Эта программа выведет b = 5
; в этом случае мы можем получить доступ к данным в коробке так же, как будто бы эти данные были на стеке. Как и любое собственное значение, когда коробка выходит из области видимости, как это происходит с b
в конце main
, она будет освобождена. Освобождение происходит как для коробки (хранящейся на стеке), так и для данных, на которые она ссылается (хранящихся в куче).
Разместить одно значение в куче не очень полезно, поэтому вы не будете часто использовать коробки именно таким образом. Хранить значения, такие как одиночное i32
, на стеке, где они хранятся по умолчанию, более подходяще в большинстве ситуаций. Рассмотрим случай, когда коробки позволяют нам определить типы, которые мы бы не могли определить, если бы не имели коробок.
Значение рекурсивного типа может содержать другое значение того же типа в качестве части самого себя. Рекурсивные типы представляют проблему, потому что на этапе компиляции Rust должен знать, сколько места занимает тип. Однако вложение значений рекурсивных типов теоретически может продолжаться бесконечно, поэтому Rust не может знать, сколько места требуется значению. Поскольку коробки имеют известный размер, мы можем позволить себе использовать рекурсивные типы, вставив коробку в определение рекурсивного типа.
В качестве примера рекурсивного типа рассмотрим список 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 в этой главе, мы можем изучить, как коробки позволяют нам определить рекурсивный тип данных без большого отвлечения.
Листинг 15-2 содержит определение перечисления для списка cons. Обратите внимание, что этот код еще не скомпилируется, потому что тип List
не имеет известного размера, что мы покажем.
Имя файла: src/main.rs
enum List {
Cons(i32, List),
Nil,
}
Листинг 15-2: Первая попытка определить перечисление для представления структуры данных списка cons из значений i32
Примечание: Мы реализуем список cons, который хранит только значения
i32
, для целей этого примера. Мы могли бы реализовать его с использованием обобщений, как мы обсуждали в главе 10, чтобы определить тип списка cons, который может хранить значения любого типа.
Использование типа List
для хранения списка 1, 2, 3
будет выглядеть как код в Листинге 15-3.
Имя файла: src/main.rs
--snip--
use crate::List::{Cons, Nil};
fn main() {
let list = Cons(1, Cons(2, Cons(3, Nil)));
}
Листинг 15-3: Использование перечисления List
для хранения списка 1, 2, 3
Первое значение Cons
содержит 1
и еще одно значение List
. Это значение List
- это еще одно значение Cons
, которое содержит 2
и еще одно значение List
. Это значение List
- это еще одно значение Cons
, которое содержит 3
и значение List
, которое в конце концов является Nil
, нерекурсивной вариантом, который сигнализирует о конце списка.
Если мы попытаемся скомпилировать код в Листинге 15-3, мы получим ошибку, показанную в Листинге 15-4.
error[E0072]: recursive type `List` has infinite size
--> src/main.rs:1:1
|
1 | enum List {
| ^^^^^^^^^ recursive type has infinite size
2 | Cons(i32, List),
| ---- recursive without indirection
|
help: insert some indirection (e.g., a `Box`, `Rc`, or `&`) to make `List`
representable
|
2 | Cons(i32, Box<List>),
| ++++ +
Листинг 15-4: Ошибка, которую мы получаем, пытаясь определить рекурсивное перечисление
Ошибка показывает, что этот тип "имеет бесконечный размер". Причина в том, что мы определили List
с вариантом, который является рекурсивным: он содержит непосредственно еще одно значение самого себя. В результате Rust не может понять, сколько места ему нужно для хранения значения List
. Разберём, почему мы получаем эту ошибку. Сначала мы рассмотрим, как Rust определяет, сколько места ему нужно для хранения значения нерекурсивного типа.
Помните перечисление Message
, которое мы определили в Листинге 6-2, когда обсуждали определения перечислений в главе 6:
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.
Рисунок 15-1: Бесконечный List
, состоящий из бесконечного количества вариантов Cons
<T>
{=html} для получения рекурсивного типа с известным размеромПоскольку Rust не может определить, сколько места нужно выделить для рекурсивно определенных типов, компилятор выдаёт ошибку с полезным советом:
help: insert some indirection (e.g., a `Box`, `Rc`, or `&`) to make `List`
representable
|
2 | Cons(i32, Box<List>),
| ++++ +
В этом совете индиректность означает, что вместо прямого хранения значения мы должны изменить структуру данных для хранения значения косвенно, сохраняя указатель на значение.
Поскольку Box<T>
- это указатель, Rust всегда знает, сколько места нужно для Box<T>
: размер указателя не зависит от количества данных, на которые он ссылается. Это означает, что мы можем поместить Box<T>
внутри варианта Cons
вместо другого значения List
напрямую. Box<T>
будет ссылаться на следующее значение List
, которое будет храниться в куче, а не внутри варианта Cons
. Концептуально мы по-прежнему имеем список, созданный из списков, содержащих другие списки, но данная реализация теперь более похожа на размещение элементов рядом с каждым другом, а не внутри друг друга.
Мы можем изменить определение перечисления List
в Листинге 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
требует размер i32
плюс место для хранения данных указателя коробки. Вариант Nil
не хранит никаких значений, поэтому он требует меньше места, чем вариант Cons
. Теперь мы знаем, что любое значение List
займет размер i32
плюс размер данных указателя коробки. Используя коробку, мы разбили бесконечную рекурсивную цепочку, поэтому компилятор может определить размер, необходимый для хранения значения List
. На рисунке 15-2 показано, как выглядит вариант Cons
теперь.
Рисунок 15-2: List
, не имеющий бесконечного размера, потому что Cons
содержит Box
Коробки обеспечивают только индиректность и выделение памяти в куче; у них нет других особых возможностей, таких как те, которые мы увидим с другими типами умных указателей. Они также не несут издержек в производительности, связанных с этими особыми возможностями, поэтому они могут быть полезны в случаях, таких как список cons, где индиректность - это единственная особенность, которую мы требуем. Мы рассмотрим больше случаев использования коробок в главе 17.
Тип Box<T>
является умным указателем, потому что он реализует трейт Deref
, который позволяет обрабатывать значения Box<T>
как ссылки. Когда значение Box<T>
выходит из области видимости, данные в куче, на которые указывает коробка, также освобождаются из-за реализации трейта Drop
. Эти два трейта будут еще более важны для функциональности, предоставляемой другими типами умных указателей, которые мы обсудим в остальной части этой главы. Давайте более подробно рассмотрим эти два трейта.
Поздравляем! Вы завершили лабораторную работу по использованию Box