Використання Box<T>
для Вказування на Дані в Купі
Найбільш простий розумний вказівник це box, тип якого записано в Box<T>
. Box дозволяє зберігати дані в heap, а не на стеку. На стеку залишається вказівник на значення в Heap. Перегляньте Розділ 4, щоб побачити різницю між стеком та купою.
Коробки не мають накладних витрат, окрім як зберігання даних в heap замість того, щоб розміщувати дані на стеку. Але у них також немає багато додаткових можливостей. Найчастіше ви будете використовувати їх у таких ситуаціях:
- Якщо у вас є тип, розмір якого не може бути відомий при компілюванні і ви хочете використати значення цього типу в контексті, який вимагає точного розміру
- Якщо у вас є велика кількість даних і ви хочете передати володіння, але хочете гарантії, що дані не будуть скопійовані після виконання
- Коли ви бажаєте володіти значенням та вам важливо лише, що тип реалізує певний трейт, а не є певним типом
Ми продемонструємо першу ситуацію в "Рекурсивні типи з Box" секції. У другому випадку передача володіння великої кількості даних може зайняти багато часу тому, що дані копіюються зі стеку. Щоб підвищити продуктивність в такій ситуації, ми можемо зберігати велику кількість даних в Heap в box. Копіюється тільки невелика кількість даних вказівника у стеку, в той час як дані, на яких він посилається, залишається в одному місці в Heap. Третій випадок - відомий як трейт об'єкт **, займає весь розділ 17, "Використання трейт об'єктів, які допускають значення різних типів", Отже, що ви знаєте тут, ви будете використовувати ще раз в Розділі 17!
Використання Box<T>
для Зберігання Даних в Купі
Перед тим як обговорити випадок зберігання даних в Heap в Box<T>
ми розглянемо синтаксис і як взаємодіяти зі значеннями, що зберігаються в Box<T>
.
Блок коду 15-1 показує, як використовувати Box для збереження значення типу i32
у купі:
Файл: src/main.rs
fn main() { let b = Box::new(5); println!("b = {}", b); }
Ми визначили змінну 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() {}
Примітка: Ми реалізуємо 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)));
}
Перший 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>),
| ++++ +
For more information about this error, try `rustc --explain E0072`.
error: could not compile `cons-list` due to previous error
Помилка показує, що цей тип "має нескінченний розмір." Причина в тому, що ми визначили 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.
Використання 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)))))); }
Cons
потрібно мати розмір i32
плюс пам'ять для зберігання даних вказівника box. Варіант Nil
не зберігає значення, тому йому потрібно менше місця, ніж Cons
. Ми тепер знаємо, що будь-яке значення Cons
займе розмір i32
плюс розмір вказівника box. Використовуючи box, ми зламали нескінченний, рекурсивний ланцюжок, таким чином, компілятор може визначити розмір, який йому потрібно щоб зберегти List
. Рисунок 15-2 показує як зараз виглядає варіант Cons
.
Box забезпечує лише розміщення в Heap; у них немає жодних інших спеціальних можливостей, які ми побачимо в інших розумних вказівниках. Також вони не мають накладних витрат на ці спеціальні можливості, тож вони можуть бути корисні у випадках як cons list, де розміщення в іншому місці для того, щоб мати вказівник відомого розміру - все що нам потрібно. Ми також розглянемо застосування box в Розділі 17.
Box<T>
є розумним вказівником, оскільки реалізує трейт Deref
що дозволяє Box<T>
застосовувати як посилання. Коли значення Box<T>
виходить з області видимості, дані в купі, на які вказує box, видаляться через реалізацію трейту Drop
. Ці трейти будуть ще важливіші для функціональності, які надають інші розумні вказівники, які ми обговоримо в інших главах цього розділу. Розгляньмо ці трейти детальніше.