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

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

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

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

Введение

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

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


Skills Graph

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

Использование Box<T>{=html} для указания на данные в куче

Самый простой умный указатель - это коробка (box), чей тип записывается как Box<T>. Коробки позволяют хранить данные в куче, а не на стеке. Что остается на стеке - это указатель на данные в куче. См. главу 4, чтобы освежить разницу между стеком и кучей.

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

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

Мы покажем первую ситуацию в разделе "Возможность рекурсивных типов с использованием коробок". Во втором случае передача владения большими объемами данных может занять много времени, потому что данные копируются по стеку. Чтобы повысить производительность в такой ситуации, мы можем хранить большие объемы данных в куче в коробке. Затем только небольшие объемы указательских данных копируются по стеку, в то время как данные, на которые они ссылаются, остаются в одном месте в куче. Третья ситуация известна как объект трейта (trait object), и раздел "Использование объектов трейтов, которые допускают значения разных типов" посвящен этой теме. Поэтому то, что вы здесь узнаете, вы снова примете в этом разделе!

Использование Box<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

Список 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

Использование Box<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 для указания на данные в куче. Вы можете практиковаться в более многих лабораторных работах в LabEx, чтобы улучшить свои навыки.