Використання Box<T> для Вказування на Значення в Heap

Найбільш простий розумний вказівник це box, тип якого записано в Box<T>. Box дозволяє зберігати дані в heap, а не на стеку. На стеку залишається вказівник на значення в Heap. Перегляньте Розділ 4, щоб побачити різницю між стеком та купою.

Коробки не мають накладних витрат, окрім як зберігання даних в heap замість того, щоб розміщувати дані на стеку. Але у них також немає багато додаткових можливостей. Найчастіше ви будете використовувати їх у таких ситуаціях:

  • Якщо у вас є тип, розмір якого не може бути відомий при компілюванні і ви хочете використати значення цього типу в контексті, який вимагає точного розміру
  • Якщо у вас є велика кількість даних і ви хочете передати володіння, але хочете гарантії, що дані не будуть скопійовані після виконання
  • Коли ви бажаєте володіти значенням та вам важливо лише, що тип реалізує певний трейт, а не є певним типом

Ми продемонструємо першу ситуацію в "Рекурсивні типи з Box" секції. У другому випадку передача володіння великої кількості даних може зайняти багато часу тому, що дані копіюються зі стеку. Щоб підвищити продуктивність в такій ситуації, ми можемо зберігати велику кількість даних в Heap в box. Копіюється тільки невелика кількість даних вказівника у стеку, в той час як дані, на яких він посилається, залишається в одному місці в Heap. Третій випадок - відомий як трейт об'єкт **, займає весь розділ 17, "Використання трейт об'єктів, які допускають значення різних типів", Отже, що ви знаєте тут, ви будете використовувати ще раз в Розділі 17!

Використання Box<T> для зберігання Значення в Heap

Перед тим як обговорити випадок зберігання даних в Heap в Box<T>ми розглянемо синтаксис і як взаємодіяти зі значеннями, що зберігаються в Box<T>.

Блок коду 15-1 показує, як використовувати Box для збереження значення типу i32 у купі:

Файл: src/main.rs

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

Блок коду 15-1: Збереження i32 значення в Heap з використанням box

Ми визначили змінну b що має значення Box, яке вказує на значення 5, яке виділяється в heap. Ця програма виведе на екран b = 5; в цьому випадку, ми можемо отримати доступ до даних у box, аналогічно до того, як ми могли б зробити так, якби дані були на стеку. Будь-яке значення, наприклад коли box виходить за scope, як це робить b в кінці main, буде звільнено. Звільнення відбувається як для коробки (на стеці), так і для значення на яке вказує (зберігаються в heap).

Розміщення одного значення в heap не дуже ефективно, тому таким чином ви будете робити не часто. Мати значення як один i32 на стеку, де вони зберігаються за замовчуванням, більш підходить для більшості ситуацій. Розглянемо випадок, де box дозволяють нам використати типи які ми б не могли застосувати без box.

Рекурсивні типи з Box

Складовою рекурсивного типу може бути значення того цього ж типу. Реалізація рекурсивного типу може бути проблемою, бо при компіляції Rust потрібно знати скільки місця займає тип. Однак вкладеність значень рекурсивних типів теоретично може тривати нескінченно, тому Rust не може знати, скільки пам'яті потребує значення. Оскільки box має відомий розмір, ми можемо реалізувати рекурсивні типи вставленням box у визначення рекурсивного типу.

Як приклад рекурсивного типу, давайте дослідимо cons list. Це тип даних, який зазвичай зустрічається у функціональних мовах програмування. Тип cons list ми визначимо його напряму, за винятком рекурсії. Концепції у прикладі, з якими ми працюватимемо, будуть корисними при потраплянні у складніші ситуації, що стосуються рекурсивних типів.

Більше інформації про cons list

Cons list — це структура даних, що прийшла із мови програмування Lisp та його діалектів. Структура складається з вкладених пар, і є різновидом зв'язаного списку в Lisp. Ця назва походить з cons функції (коротко для функції "construct function") в Lisp, яка формує нову пару з двох аргументів. Викликанням cons до пари зі значенням та іншою парою, ми можемо створювати зв'язані списки з рекурсивних пар.

Наприклад, ось набір псевдокоду, що представляє зв'язаний список з 1, 2, 3 з кожною парою в дужках:

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

Кожен елемент у cons list містить два елементи: значення поточного елемента і наступного елементу. Останній елемент списку містить значення Nil, що означає відсутність наступного елемента. Cons list створюєтся рекурсивним викликанням функції cons. Канонічне ім'я для позначення загального випадку рекурсії Nil. Зверніть увагу, що це не те саме, що "null" або "nil" концепція у Розділі 6, яке є недійсним або відсутнім значенням.

Cons list не є загально вживаною структурою даних в Rust. В більшості випадків, коли у вас є список елементів в Rust, Vec<T> є кращим варіантом для використання. Більш складні типи рекурсивних даних корисні в різних ситуаціях, але починаючи з cons list у цьому розділі, ми можемо ясніше дослідити, як box дає змогу визначити тип рекурсивних даних.

Блок коду 15-2 містить визначення енума для cons list. Зверніть увагу, що цей код не скомпілюється, тому що тип List не має відомого розміру, що ми продемонструємо.

Файл: src/main.rs

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

fn main() {}

Блок коду 15-2: Перша спроба визначення енума що представляє cons list значень типу i32

Примітка: Ми реалізуємо cons list, який містить лише значення i32 як приклад. Ми могли б реалізувати її за допомогою generic, які ми розглянули у розділі 10, щоб визначити cons list для збереження значення будь-якого типу.

Використовування типу List, щоб зберегти список з 1, 2, 3 буде виглядати як код в Блоці коду 15-3:

Файл: src/main.rs

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

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 і Cons який нарешті 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 {
  | ^^^^^^^^^ 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>),
  |               ++++    +

error[E0391]: cycle detected when computing drop-check constraints for `List`
 --> src/main.rs:1:1
  |
1 | enum List {
  | ^^^^^^^^^
  |
  = note: ...which immediately requires computing drop-check constraints for `List` again
  = note: cycle used when computing dropck types for `Canonical { max_universe: U0, variables: [], value: ParamEnvAnd { param_env: ParamEnv { caller_bounds: [], reveal: UserFacing, constness: NotConst }, value: List } }`

Some errors have detailed explanations: E0072, E0391.
For more information about an error, try `rustc --explain E0072`.
error: could not compile `cons-list` due to 2 previous errors

Блок коду 15-4: Помилка, яку ми отримуємо при спробі визначити рекурсивний енум

Помилка показує, що цей тип "має нескінченний розмір." Причина в тому, що ми визначили List з варіантом, який рекурсивний: він складається зі значення свого ж типу. Як результат, Rust не може визначити скільки місця йому потрібно для List. Розберімось, чому ми отримуємо цю помилку. Спочатку ми подивимось на те, як Rust вирішує, скільки місця їй потрібно зберегти значення не рекурсивного типу.

Обчислення Розміру Нерекурсивного Типу

Розглянемо повторно Message енум з Розділу 6-2 коли ми дізнались про енум в Розділі 6:

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

fn main() {}

Щоб визначити скільки займати місця для Message Rust проходить через кожен з варіантів, щоб побачити, який варіант потребує найбільше місця. Rust бачить що Message::Quit не потрібно місця. Message::Move достатньо місця як для зберігання 2 i32 значень, і так далі. Тому що використовуватиметься лише один варіант, Message буде займати як найбільший з можливих своїх варіантів.

Порівняйте це з тим, що відбувається, коли Rust намагається визначити скільки місця займає рекурсивний тип Cons в Роздруку 15-2. Компілятор дивиться на варіант Cons який містить значення типу i32 та значення типу Cons. Відповідно, Cons потребує пам'яті, що дорівнює розміру i32 плюс розмір Cons. Щоб дізнатись скільки пам'яті потребує List, компілятор дивиться на варіанти, починаючи з Cons. Cons є значенням типу i32 і значення типу Cons, і цей процес нескінченно продовжується як показано на Рисунку 15-1.

Нескінченних список з Cons

Рисунок 15-1: Нескінченний List що складається з безлічі Cons варіантів

Використання Box<T> для Рекурсивного типу з Відомим Розміром

Оскільки Rust не може визначити скільки пам'яті для рекурсивно визначених типів, компілятор надає помилку з корисною пропозицією:

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

У цій пропозиції "indirection" (опосередкованість) означає, що замість того, щоб зберігати значення безпосередньо, ми повинні змінити структуру даних, щоб зберігати значення опосередковано, зберігаючи замість нього вказівник на значення.

Тому що Box<T> є вказівником, Rust завжди знає, скільки потрібно пам'яті для Box<T>: розмір вказівника не залежить від розміру типу даних, на які вказує. Це означає, що ми можемо розмістити Cons в Box<T> замість напряму Cons. Box<T> вказує на наступний List, яке буде в Heap, а не в 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 плюс пам'ять для зберігання даних вказівника box. Варіант Nil не зберігає значення, тому йому потрібно менше місця, ніж Cons. Ми тепер знаємо, що будь-яке значення Cons займе розмір i32 плюс розмір вказівника box. Використовуючи box, ми зламали нескінченний, рекурсивний ланцюжок, таким чином, компілятор може визначити розмір, який йому потрібно щоб зберегти List. Рисунок 15-2 показує як зараз виглядає варіант Cons.

Скінченний список з Cons

Рисунок 15-2: List який не є нескінченним розміром, тому що List містить Box

Box забезпечує лише розміщення в Heap; у них немає жодних інших спеціальних можливостей, які ми побачимо в інших розумних вказівниках. Також вони не мають накладних витрат на ці спеціальні можливості, тож вони можуть бути корисні у випадках як cons list, де розміщення в іншому місці для того, щоб мати вказівник відомого розміру - все що нам потрібно. Ми також розглянемо застосування box в Розділі 17.

Box<T> є розумним вказівником, оскільки реалізує трейт Deref що дозволяє Box<T> застосовувати як посилання. Коли значення Box<T> виходить з області видимості, дані в купі, на які вказує box, видаляться через реалізацію трейту Drop. Ці трейти будуть ще важливіші для функціональності, які надають інші розумні вказівники, які ми обговоримо в інших главах цього розділу. Розгляньмо ці трейти детальніше.