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

Użycie Box<T> do wskazywania na dane na stercie

Najprostszym inteligentnym wskaźnikiem jest pudełko, którego typ zapisuje się jako Box<T>. Pudełka pozwalają przechowywać dane na stercie zamiast na stosie. To, co pozostaje na stosie, to wskaźnik do danych na stercie. Odwołaj się do Rozdziału 4, aby zapoznać się z różnicą między stosem a stertą.

Pudełka nie mają narzutu wydajnościowego, poza przechowywaniem danych na stercie zamiast na stosie. Ale nie mają też wielu dodatkowych możliwości. Będziesz ich używać najczęściej w następujących sytuacjach:

  • Kiedy masz typ, którego rozmiar nie może być znany w czasie kompilacji, i chcesz użyć wartości tego typu w kontekście, który wymaga dokładnego rozmiaru
  • Kiedy masz dużą ilość danych i chcesz przenieść własność, ale upewnić się, że dane nie zostaną skopiowane, gdy to zrobisz
  • Kiedy chcesz być właścicielem wartości, a zależy ci tylko na tym, aby była to typ, który implementuje określoną cechę, zamiast być konkretnym typem

Pokażemy pierwszą sytuację w sekcji „Włączanie typów rekurencyjnych za pomocą pudełek”. W drugim przypadku przeniesienie własności dużej ilości danych może zająć dużo czasu, ponieważ dane są kopiowane na stosie. Aby poprawić wydajność w tej sytuacji, możemy przechowywać dużą ilość danych na stercie w pudełku. Wtedy tylko mała ilość danych wskaźnika jest kopiowana na stosie, podczas gdy dane, do których wskazuje, pozostają w jednym miejscu na stercie. Trzeci przypadek jest znany jako obiekt cechy, a sekcja „Używanie obiektów cech do abstrakcji nad wspólnym zachowaniem” w Rozdziale 18 jest poświęcona temu tematowi. Więc to, czego się tutaj nauczysz, zastosujesz p ponownie w tej sekcji!

Przechowywanie danych na stercie

Zanim omówimy przypadek użycia Box<T> do przechowywania na stercie, powiemy o składni i sposobie interakcji z wartościami przechowywanymi w Box<T>.

Listing 15-1 pokazuje, jak użyć pudełka do przechowywania wartości i32 na stercie.

Nazwa pliku: src/main.rs
fn main() {
    let b = Box::new(5);
    println!("b = {b}");
}
Listing 15-1: Przechowywanie wartości i32 na stercie za pomocą pudełka

Definiujemy zmienną b z wartością Box, która wskazuje na wartość 5, alokowaną na stercie. Ten program wyświetli b = 5; w tym przypadku możemy dostęp do danych w pudełku podobnie jak wtedy, gdyby te dane znajdowały się na stercie. Podobnie jak każda posiadana wartość, gdy pudełko wyjdzie poza zakres, tak jak b na końcu main, zostanie zdealokowane. Dealokacja odbywa się zarówno dla pudełka (przechowywanego na stosie), jak i dla danych, do których wskazuje (przechowywanych na stercie).

Umieszczenie pojedynczej wartości na stercie nie jest zbyt użyteczne, więc rzadko będziesz używać pudełek w ten sposób. Posiadanie wartości, takich jak pojedyncze i32 na stosie, gdzie są domyślnie przechowywane, jest bardziej odpowiednie w większości sytuacji. Spójrzmy na przypadek, w którym pudełka pozwalają nam definiować typy, których nie moglibyśmy zdefiniować, gdybyśmy nie mieli pudełek.

Włączanie typów rekurencyjnych za pomocą pudełek

Wartość typu rekurencyjnego może zawierać inną wartość tego samego typu jako swoją część. Typy rekurencyjne stanowią problem, ponieważ Rust musi wiedzieć w czasie kompilacji, ile miejsca zajmuje dany typ. Jednak zagnieżdżanie wartości typów rekurencyjnych mogłoby teoretycznie trwać w nieskończoność, więc Rust nie może wiedzieć, ile miejsca potrzebuje wartość. Ponieważ pudełka mają znany rozmiar, możemy włączyć typy rekurencyjne, wstawiając pudełko do definicji typu rekurencyjnego.

Jako przykład typu rekurencyjnego, przeanalizujmy listę cons. Jest to typ danych powszechnie spotykany w językach programowania funkcyjnego. Typ listy cons, który zdefiniujemy, jest prosty, z wyjątkiem rekurencji; dlatego koncepcje w przykładzie, z którym będziemy pracować, będą przydatne zawsze, gdy znajdziesz się w bardziej złożonych sytuacjach związanych z typami rekurencyjnymi.

Zrozumienie listy cons

Lista cons to struktura danych pochodząca z języka programowania Lisp i jego dialektów, składająca się z zagnieżdżonych par, i jest lispońską wersją listy składającej się z połączonych elementów. Jej nazwa pochodzi od funkcji cons (skrót od construct function) w Lispie, która konstruuje nową parę z dwóch swoich argumentów. Wywołując cons na parze składającej się z wartości i innej pary, możemy konstruować listy cons składające się z rekurencyjnych par.

Na przykład, oto pseudokodowa reprezentacja listy cons zawierającej listę 1, 2, 3, gdzie każda para jest w nawiasach:

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

Każdy element listy cons zawiera dwa elementy: wartość bieżącego elementu i następnego elementu. Ostatni element listy zawiera tylko wartość zwaną Nil bez następnego elementu. Lista cons jest tworzona przez rekurencyjne wywoływanie funkcji cons. Kanoniczna nazwa oznaczająca przypadek bazowy rekurencji to Nil. Zauważ, że to nie to samo, co koncepcja „null” lub „nil” omówiona w Rozdziale 6, która oznacza nieprawidłową lub brakującą wartość.

Lista cons nie jest powszechnie używaną strukturą danych w Rust. W większości przypadków, gdy masz listę elementów w Rust, Vec<T> jest lepszym wyborem. Inne, bardziej złożone rekurencyjne typy danych użyteczne w różnych sytuacjach, ale zaczynając od listy cons w tym rozdziale, możemy zbadać, jak pudełka pPozwalają nam definiować rekurencyjny typ danych bez zbytniego rozpraszania.

Listing 15-2 zawiera definicję wyliczenia dla listy cons. Zauważ, że ten kod jeszcze się nie skompiluje, ponieważ typ List nie ma znanego rozmiaru, co pokażemy.

Nazwa pliku: src/main.rs
enum List {
    Cons(i32, List),
    Nil,
}

fn main() {}
Listing 15-2: Pierwsza próba zdefiniowania wyliczenia do reprezentowania struktury danych listy cons z wartościami i32

Uwaga: Implementujemy listę cons, która przechowuje tylko wartości i32 dla celów tego przykładu. Moglibyśmy zaimplementować ją za pomocą generyków, jak omówiliśmy w Rozdziale 10, aby zdefiniować typ listy cons, który mógłby przechowywać wartości dowolnego typu.

Użycie typu List do przechowywania listy 1, 2, 3 wyglądałoby jak kod w Listingu 15-3.

Nazwa pliku: src/main.rs
enum List {
    Cons(i32, List),
    Nil,
}

// --snip--

use crate::List::{Cons, Nil};

fn main() {
    let list = Cons(1, Cons(2, Cons(3, Nil)));
}
Listing 15-3: Użycie wyliczenia List do przechowywania listy 1, 2, 3

Pierwsza wartość Cons przechowuje 1 i inną wartość List. Ta wartość List to inna wartość Cons, która przechowuje 2 i inną wartość List. Ta wartość List to jeszcze jedna wartość Cons, która przechowuje 3 i wartość List, która jest w końcu Nil, nierrekurencyjnym wariantem, który sygnalizuje koniec listy.

Jeśli spróbujemy skompilować kod z Listingu 15-3, otrzymamy błąd pokazany w Listingu 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
Listing 15-4: Błąd, który otrzymujemy, próbując zdefiniować rekurencyjne wyliczenie

Błąd pokazuje, że ten typ „ma nieskończony rozmiar”. Powodem jest to, że zdefiniowaliśmy List z wariantem, który jest rekurencyjny: przechowuje inną wartość siebie bezpośrednio. W rezultacie Rust nie potrafi określić, ile miejsca potrzebuje do przechowywania wartości List. Rozłóżmy na czynniki pierwsze, dlaczego otrzymujemy ten błąd. Najpierw przyjrzymy się, jak Rust decyduje, ile miejsca potrzebuje do przechowywania wartości typu nierrekurencyjnego.

Obliczanie rozmiaru typu nierrekurencyjnego

Przypomnijmy wyliczenie Message, które zdefiniowaliśmy w Listingu 6-2, kiedy omawialiśmy definicje wyliczeń w Rozdziale 6:

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

fn main() {}

Aby określić, ile miejsca należy przeznaczyć na wartość Message, Rust przechodzi przez każdy z wariantów, aby sprawdzić, który wariant potrzebuje najwięcej miejsca. Rust widzi, że Message::Quit nie potrzebuje miejsca, Message::Move potrzebuje wystarczająco dużo miejsca na przechowanie dwóch wartości i32 i tak dalej. Ponieważ używany będzie tylko jeden wariant, najwięcej miejsca, jakie będzie potrzebować wartość Message, to miejsce potrzebne na przechowanie największego z jej wariantów.

Porównaj to z tym, co dzieje się, gdy Rust próbuje określić, ile miejsca potrzebuje rekurencyjny typ, taki jak wyliczenie List w Listingu 15-2. Kompilator zaczyna od wariantu Cons, który przechowuje wartość typu i32 i wartość typu List. Dlatego Cons potrzebuje tyle miejsca, ile wynosi rozmiar i32 plus rozmiar List. Aby dowiedzieć się, ile pamięci potrzebuje typ List, kompilator patrzy na warianty, zaczynając od wariantu Cons. Wariant Cons przechowuje wartość typu i32 i wartość typu List, i ten proces trwa w nieskończoność, jak pokazano na Rysunku 15-1.

Nieskończona lista Cons: prostokąt oznaczony 'Cons' podzielony na dwa mniejsze prostokąty. Pierwszy mniejszy prostokąt zawiera etykietę 'i32', a drugi mniejszy prostokąt zawiera etykietę 'Cons' i mniejszą wersję zewnętrznego prostokąta 'Cons'. Prostokąty 'Cons' nadal zawierają coraz mniejsze wersje samych siebie, aż najmniejszy, wygodnie rozmiarowy prostokąt zawiera symbol nieskończoności, wskazując, że to powtórzenie trwa wiecznie.

Rysunek 15-1: Nieskończona lista List składająca się z nieskończonych wariantów Cons

Uzyskiwanie rekurencyjnego typu o znanym rozmiarze

Ponieważ Rust nie jest w stanie określić, ile miejsca należy przydzielić dla rekurencyjnie zdefiniowanych typów, kompilator zwraca błąd z tą pomocną sugestią:

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

W tej sugestii pośrednictwo oznacza, że zamiast bezpośrednio przechowywać wartość, powinniśmy zmienić strukturę danych, aby przechowywać wartość pośrednio, przechowując zamiast tego wskaźnik do wartości.

Ponieważ Box<T> jest wskaźnikiem, Rust zawsze wie, ile miejsca potrzebuje Box<T>: rozmiar wskaźnika nie zmienia się w zależności od ilości danych, na które wskazuje. Oznacza to, że możemy umieścić Box<T> w wariancie Cons zamiast bezpośrednio innej wartości List. Box<T> będzie wskazywać na kolejną wartość List, która będzie znajdować się na stercie, a nie wewnątrz wariantu Cons. Koncepcyjnie, nadal mamy listę, utworzoną z list przechowujących inne listy, ale ta implementacja jest teraz bardziej podobna do umieszczania elementów obok siebie, a nie wewnątrz siebie.

Możemy zmienić definicję wyliczenia List w Listingu 15-2 i użycie List w Listingu 15-3 na kod w Listingu 15-5, który się skompiluje.

Nazwa pliku: 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))))));
}
Listing 15-5: Definicja List, która używa Box<T>, aby mieć znany rozmiar

Wariant Cons potrzebuje rozmiaru i32 plus miejsca na przechowywanie danych wskaznika pudełka. Wariant Nil nie przechowuje żadnych wartości, więc potrzebuje mniej miejsca na stosie niż wariant Cons. Wiemy teraz, że każda wartość List zajmie rozmiar i32 plus rozmiar danych wskaźnika pudełka. Używając pudełka, przerwaliśmy nieskończony, rekurencyjny łańcuch, więc kompilator może określić rozmiar potrzebny do przechowywania wartości List. Rysunek 15-2 pokazuje, jak wygląda teraz wariant Cons.

Prostokąt oznaczony 'Cons' podzielony na dwa mniejsze prostokąty. Pierwszy mniejszy prostokąt zawiera etykietę 'i32', a drugi mniejszy prostokąt zawiera etykietę 'Box' z jednym wewnętrznym prostokątem, który zawiera etykietę 'usize', reprezentującą skończony rozmiar wskaźnika pudełka.

Rysunek 15-2: List, która nie ma nieskończonego rozmiaru, ponieważ Cons zawiera Box

Pudełka zapewniają jedynie pośrednictwo i alokację na stercie; nie mają żadnych innych specjalnych możliwości, takich jak te, które zobaczymy w przypadku innych typów inteligentnych wskaźników. Nie mają również narzutu wydajnościowego, który wiąże się z tymi specjalnymi możliwościami, więc mogą być przydatne w przypadkach, takich jak lista cons, gdzie pośrednictwo jest jedyną potrzebną nam funkcją. Więcej przypadków użycia pudełek omówimy w Rozdziale 18.

Typ Box<T> jest inteligentnym wskaźnikiem, ponieważ implementuje cechę Deref, która pozwala traktować wartości Box<T> jak referencje. Kiedy wartość Box<T> wychodzi poza zakres, dane na stercie, na które wskazuje pudełko, również są czyszczone ze względu na implementację cechy Drop. Te dwie cechy będą jeszcze ważniejsze dla funkcjonalności zapewnianej przez inne typy inteligentnych wskaźników, które omówimy w pozostałej części tego rozdziału. Przeanalizujmy te dwie cechy bardziej szczegółowo.