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> を使ってヒープ上のデータを指す

最も単純なスマートポインタはボックスで、その型は Box<T> と書きます。ボックス を使うと、データをスタックではなくヒープに格納できます。 スタックに残るのは、ヒープ上のデータへのポインタです。スタックとヒープの違いを振り返るには、第4章を参照してください。

ボックスには、データをスタックではなくヒープに格納すること以外の パフォーマンス上のオーバーヘッドはありません。しかし、追加の機能も それほど多くはありません。最もよく使うのは、次のような場合です。

  • サイズをコンパイル時に知ることができない型があり、その型の値を 正確なサイズが必要なコンテキストで使いたい場合
  • 大量のデータがあり、所有権を移動したいが、 その際にデータがコピーされないようにしたい場合
  • 値を所有したいが、具体的な型であることではなく、 特定のトレイトを実装している型であることだけが重要な場合

最初のケースは 「ボックスで再帰型を可能にする」 で示します。2番目の ケースでは、大量のデータの所有権を移動するのに長い時間がかかることが あります。これは、データがスタック上でコピーされるためです。この 状況でパフォーマンスを改善するには、大量のデータをボックスでヒープ上に 格納できます。そうすれば、スタック上でコピーされるのは少量のポインタ データだけで、そのポインタが参照するデータはヒープ上の同じ場所に とどまります。3番目のケースは トレイトオブジェクト として知られており、 第18章の「トレイトオブジェクトを使って共有された振る舞いを抽象化する」 でこの話題を扱います。 ですから、ここで学ぶことはその節でもまた活用することになります。

ヒープにデータを格納する

Box<T> をヒープに格納する用途について説明する前に、構文と、 Box<T> の中に格納された値をどのように扱うかを見ていきます。

リスト15-1は、ボックスを使って i32 値をヒープに格納する方法を示しています。

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

b 変数は、ヒープに確保された値 5 を指す Box の値を持つように 定義しています。このプログラムは b = 5 を出力します。この場合、 ボックス内のデータには、そのデータがスタック上にある場合と同様に アクセスできます。他のあらゆる所有権を持つ値と同様に、bmain の 末尾でそうなるように、ボックスがスコープを抜けると、それは解放されます。 解放は、ボックス自体(スタックに格納)と、それが指すデータ (ヒープに格納)の両方に対して行われます。

単一の値をヒープに置くだけではあまり有用ではないので、このような形で ボックス単体を使うことはあまりありません。i32 のような単一の値は、 デフォルトで格納されるスタック上に置いておくほうが、大半の状況では より適切です。次に、ボックスがあることで、ボックスがなければ 定義できない型を定義できるケースを見てみましょう。

ボックスで再帰型を可能にする

再帰型 の値は、自身の一部として同じ型の別の値を持つことができます。 再帰型は、Rust が型の占有する領域の大きさをコンパイル時に知っておく必要が あるため、問題になります。しかし、再帰型の値の入れ子は理論上無限に 続く可能性があるため、Rust はその値にどれだけの領域が必要かを知ることが できません。ボックスはサイズが既知なので、再帰型の定義にボックスを 挿入することで再帰型を可能にできます。

再帰型の例として、コンスリストを見ていきましょう。これは関数型 プログラミング言語でよく見られるデータ型です。これから定義する コンスリスト型は、再帰であることを除けば単純です。そのため、 これから扱う例の概念は、再帰型が関わるより複雑な状況に取り組むときにも 役立ちます。

コンスリストを理解する

コンスリスト は Lisp プログラミング言語とその方言に由来するデータ構造で、 入れ子になったペアから構成される、Lisp 版の連結リストです。その名前は、 2つの引数から新しいペアを構築する Lisp の cons 関数(construct function の略)に由来します。値と別のペアからなるペアに対して cons を呼び出すことで、再帰的なペアからなるコンスリストを 構築できます。

たとえば、各ペアを括弧で囲んだ、リスト 1, 2, 3 を含むコンスリストの 疑似コード表現は次のようになります。

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

コンスリストの各要素には2つの要素があります。現在の要素の値と、 次の要素です。リストの最後の要素には、次の要素を持たず、 Nil と呼ばれる値だけが含まれます。コンスリストは、cons 関数を再帰的に呼び出すことで作られます。再帰のベースケースを表す 慣例的な名前が Nil です。これは第6章で議論した「null」や「nil」の 概念、つまり無効な値や値が存在しないことを表すものとは同じではないことに 注意してください。

コンスリストは、Rust では一般的に使われるデータ構造ではありません。 Rust で項目のリストを扱う場合、たいていは Vec<T> を使うほうが 適切です。ほかの、より複雑な再帰データ型はさまざまな状況で 実際に 有用ですが、この章ではコンスリストから始めることで、余計なことに 気を取られずに、ボックスによって再帰データ型をどのように定義できるかを 探れます。

リスト15-2には、コンスリストの enum 定義が含まれています。このコードは まだコンパイルできないことに注意してください。というのも、List 型の サイズが既知ではないためで、それをこれから示します。

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

fn main() {}

注: この例の都合上、i32 値だけを保持するコンスリストを実装しています。 第10章で議論したようにジェネリクスを使って実装し、任意の型の値を格納できる コンスリスト型を定義することもできました。

リスト 1, 2, 3 を格納するために List 型を使うと、リスト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 値は 2 と別の List 値を保持する別の Cons 値です。この List 値は さらにもう1つの Cons 値で、3List 値を保持しており、その 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

このエラーは、この型が「無限のサイズを持つ」ことを示しています。その理由は、List を再帰的なバリアントを持つように定義しているからです。つまり、そのバリアントは自分自身の別の値を直接保持しています。その結果、Rust は List 型の値を格納するのにどれだけの領域が必要かを判断できません。なぜこのエラーが発生するのかを分解して見ていきましょう。まず、Rust が非再帰型の値を格納するのにどれだけの領域が必要かをどのように決めるのかを見ていきます。

非再帰型のサイズを計算する

enum の定義について第 6 章で説明したときに、Listing 6-2 で定義した Message enum を思い出してください。

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 値を格納するのに十分な領域が必要であり、という具合に判断します。使われるバリアントは 1 つだけなので、Message 型の値が必要とする最大の領域は、そのバリアントのうちもっとも大きいものを格納するのに必要な領域になります。

これに対して、Rust が Listing 15-2 の List enum のような再帰型にどれだけの領域が必要かを判断しようとすると、事情は異なります。コンパイラはまず Cons バリアントを調べます。このバリアントは、型 i32 の値と型 List の値を保持しています。したがって、Cons に必要な領域の大きさは、i32 のサイズと List のサイズの合計に等しくなります。List 型にどれだけのメモリが必要かを調べるために、コンパイラはバリアントを調べますが、そこでも Cons バリアントから始まります。Cons バリアントは、型 i32 の値と型 List の値を保持しており、この過程は図 15-1 に示すように無限に続きます。

無限の Cons リスト: 'Cons' とラベル付けされた長方形が 2 つの小さな長方形に分割されている。最初の小さな長方形には 'i32' というラベルがあり、2 つ目の小さな長方形には 'Cons' というラベルと、外側の 'Cons' 長方形を小さくしたものが入っている。'Cons' 長方形は、自分自身をさらに小さくしたものを保持し続け、最後に十分小さい長方形には無限大記号が入っており、この繰り返しが永遠に続くことを示している。

図 15-1: 無限の Cons バリアントから成る無限の List

既知のサイズを持つ再帰型を得る

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> にどれだけの領域が必要かを常に把握しています。ポインタのサイズは、それが指しているデータ量に応じて変化しないからです。つまり、Cons バリアントには別の List の値を直接入れる代わりに、その中に Box<T> を入れることができます。Box<T> は、Cons バリアントの内側ではなく、ヒープ上に置かれる次の List の値を指します。概念的には、依然としてリストであり、ほかのリストを保持するリストによって作られていますが、この実装は、要素を互いの内側に入れるというより、互いの隣に配置するのに近くなります。

Listing 15-2 にある List enum の定義と、Listing 15-3 にある List の使い方を、Listing 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 のサイズと、ボックスのポインタデータを格納するための領域が必要です。Nil バリアントは値を一切格納しないため、スタック上で必要な領域は Cons バリアントよりも小さくなります。これで、どの List 型の値も i32 のサイズとボックスのポインタデータのサイズを合わせた大きさになることがわかります。ボックスを使うことで、無限に続く再帰的な連鎖を断ち切ったため、コンパイラは List 型の値を格納するのに必要なサイズを判断できるようになります。図 15-2 は、現在の Cons バリアントがどのようになっているかを示しています。

'Cons' とラベル付けされた長方形が 2 つの小さな長方形に分割されている。最初の小さな長方形には 'i32' というラベルがあり、2 つ目の小さな長方形には 'Box' というラベルと、その内側に 'usize' というラベルを含む 1 つの長方形があり、ボックスのポインタの有限なサイズを表している。

図 15-2: ConsBox を保持するため、サイズが無限ではない List

ボックスが提供するのは、間接参照とヒープ割り当てだけです。これから見るほかのスマートポインタ型が持つような、ほかの特別な機能はありません。また、そうした特別な機能に伴うパフォーマンスオーバーヘッドもありません。そのため、間接参照だけが必要な cons リストのようなケースでは役に立ちます。ボックスのさらに多くのユースケースについては、第 18 章で見ていきます。

Box<T> 型はスマートポインタです。これは、Box<T> の値を参照のように扱えるようにする Deref トレイトを実装しているからです。Box<T> の値がスコープを抜けると、そのボックスが指しているヒープ上のデータも、Drop トレイトの実装によってクリーンアップされます。これら 2 つのトレイトは、この章の残りで説明するほかのスマートポインタ型が提供する機能において、さらに重要になります。では、これら 2 つのトレイトをもう少し詳しく見ていきましょう。