Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Використання Box<T> для вказування на дані в купі (heap)

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

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

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

Ми продемонструємо першу ситуацію в “Увімкнення рекурсивних типів за допомогою Boxes”. У другому випадку передавання власності на велику кількість даних може зайняти багато часу, тому що дані копіюються по стеку. Щоб поліпшити продуктивність у цій ситуації, ми можемо зберігати велику кількість даних у купі в box. Тоді лише невелика кількість даних вказівника копіюється по стеку, тоді як дані, на які він посилається, залишаються в одному місці в купі. Третій випадок відомий як трейт-об’єкт, і “Використання трейт-об’єктів для абстрагування над спільною поведінкою” у розділі 18 присвячений цій темі. Отже, те, що ви дізнаєтеся тут, ви знову застосуєте в тому розділі!

Зберігання даних у купі

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

У переліку 15-1 показано, як використовувати box для зберігання значення i32 у купі.

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

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

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

Увімкнення рекурсивних типів за допомогою Boxes

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

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

Розуміння Cons List

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

Наприклад, ось псевдокодове представлення cons list, що містить список 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 у цьому розділі, ми можемо дослідити, як boxes дають нам змогу визначати рекурсивний тип даних без особливих відволікань.

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

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

fn main() {}

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

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

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

// --snip--

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 і значення 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

У повідомленні про помилку показано, що цей тип “has infinite size”. Причина полягає в тому, що ми визначили 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.

An infinite Cons list: a rectangle labeled 'Cons' split into two smaller rectangles. The first smaller rectangle holds the label 'i32', and the second smaller rectangle holds the label 'Cons' and a smaller version of the outer 'Cons' rectangle. The 'Cons' rectangles continue to hold smaller and smaller versions of themselves until the smallest comfortably sized rectangle holds an infinity symbol, indicating that this repetition goes on forever.

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

Отримання рекурсивного типу з відомим розміром

Оскільки 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))))));
}

Варіант Cons потребує розміру i32 плюс місце для зберігання даних вказівника box. Варіант Nil не зберігає жодних значень, тому йому потрібно менше місця у стеку, ніж варіанту Cons. Тепер ми знаємо, що будь-яке значення List займатиме розмір i32 плюс розмір даних вказівника box. Використовуючи box, ми розірвали нескінченний, рекурсивний ланцюг, тож компілятор може визначити розмір, потрібний для зберігання значення List. Рисунок 15-2 показує, як тепер виглядає варіант Cons.

A rectangle labeled 'Cons' split into two smaller rectangles. The first smaller rectangle holds the label 'i32', and the second smaller rectangle holds the label 'Box' with one inner rectangle that contains the label 'usize', representing the finite size of the box's pointer.

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

Boxes надають лише непряме зберігання та виділення в купі; вони не мають жодних інших особливих можливостей, як ті, які ми побачимо в інших типах розумних вказівників. Вони також не мають накладних витрат на продуктивність, які спричиняють ці особливі можливості, тому вони можуть бути корисними у випадках, подібних до cons list, де непряме зберігання є єдиною потрібною нам можливістю. Ми розглянемо більше варіантів використання boxes у розділі 18.

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