注意: 最新版のドキュメントをご覧ください。この第2版ドキュメントは古くなっており、最新情報が反映されていません。リンク先のドキュメントが現在の Rust の最新のドキュメントです。

ヒープのデータを指すBox<T>を使用する

最も素直なスマートポインタはボックスであり、その型はBox<T>と記述されます。 ボックスにより、スタックではなくヒープにデータを格納することができます。スタックに残るのは、 ヒープデータへのポインタです。スタックとヒープの違いを再確認するには、第4章を参照されたし。

ボックスは、データをスタックの代わりにヒープに格納する以外は、パフォーマンスのオーバーヘッドはありません。 しかし、多くのおまけの能力もありません。以下のような場面で最もよく使用するでしょう:

  • コンパイル時にはサイズを知ることができない型があり、正確なサイズを要求する文脈でその型の値を使用する時
  • 多くのデータがあり、所有権を転送したいが、その際にデータがコピーされないようにしたい時
  • 値を所有する必要があり、特定の型ではなく特定のトレイトを実装する型であることのみ気にかけている時

「ボックスで再帰的な型を可能にする」節で1つ目の場合について実際に説明します。 2番目の場合、多くのデータの所有権を転送するには、データがスタック上でコピーされるので、長い時間がかかり得ます。 この場面でパフォーマンスを向上させるには、多くのデータをヒープ上にボックスとして格納することができます。 そして、参照しているデータはヒープ上の1箇所に留まりつつ、少量のポインタのデータのみをスタック上でコピーするのです。 3番目のケースは、トレイトオブジェクトとして知られ、第17章の「トレイトオブジェクトで異なる型の値を許容する」の節は、 すべてその話題を説明するためだけのものです。 従って、ここで学ぶのと同じことが第17章においても適用するでしょう!

Box<T>を使ってヒープにデータを格納する

Box<T>のこのユースケースを議論する前に、Box<T>の記法と、Box<T>内に格納された値を読み書きする方法について講義しましょう。

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

ファイル名: src/main.rs

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

リスト15-1: ボックスを使用してi32の値をヒープに格納する

変数bを定義して値5を指すBoxの値があって、この値はヒープに確保されています。このプログラムは、 b = 5と出力するでしょう; この場合、このデータがスタックにあるのと同じような方法でボックスのデータにアクセスできます。 あらゆる所有された値同様、bmainの終わりでするようにボックスがスコープを抜けたら、 メモリから解放されます。メモリの解放は(スタックに格納されている)ボックスと(ヒープに格納されている)指しているデータに対して起きます。

ヒープに単独の値を置くことはあまり有用ではないので、このように単独でボックスを使用することはあまりありません。 単独のi32のような値は、既定で格納される場所であるスタックに置くことが、大多数の場合にはより適切です。 ボックスがなかったら定義することの叶わない型をボックスが定義させてくれる場合を見ましょう。

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

コンパイル時に、コンパイラは、ある型が取る領域を知る必要があります。コンパイル時にサイズがわからない型の1つは、 再帰的な型であり、これは、型の一部として同じ型の他の値を持つものです。この値のネストは、 理論的には無限に続く可能性があるので、コンパイラは再帰的な型の値が必要とする領域を知ることができないのです。 しかしながら、ボックスは既知のサイズなので、再帰的な型の定義にボックスを挟むことで再帰的な型を存在させることができるのです。

コンスリストは関数型プログラミング言語では一般的なデータ型ですが、これを再帰的な型の例として探究しましょう。 我々が定義するコンスリストは、再帰を除いて素直です; 故に、これから取り掛かる例の概念は、 再帰的な型が関わるもっと複雑な場面に遭遇したら必ず役に立つでしょう。

コンスリストについてもっと詳しく

コンスリストは、Lispプログラミング言語とその方言に由来するデータ構造です。Lispでは、 cons関数("construct function"の省略形です)が2つの引数から新しいペアを構成し、 この引数は通常、単独の値と別のペアからなります。これらのペアを含むペアがリストをなすのです。

cons関数の概念は、より一般的な関数型プログラミングの俗語にもなっています: "to cons x onto y"は、 俗に要素xをこの新しいコンテナの初めに置き、コンテナyを続けて新しいコンテナのインスタンスを生成することを意味します。

コンスリストの各要素は、2つの要素を含みます: 現在の要素の値と次の要素です。リストの最後の要素は、 次の要素なしにNilと呼ばれる値だけを含みます。コンスリストは、繰り返しcons関数を呼び出すことで生成されます。 繰り返しの規範事例を意味する標準的な名前は、Nilです。これは第6章の"null"や"nil"の概念とは異なることに注意してください。 "null"や"nil"は、無効だったり存在しない値です。

関数型プログラミング言語は、頻繁にコンスリストを使用するものの、Rustではあまり使用されないデータ構造です。 Rustで要素のリストがある場合はほとんどの場合、Vec<T>を使用するのがよりよい選択になります。 他のより複雑で再帰的なデータ型は、様々な場面で役に立ちますが、コンスリストから始めることで、 大して気を散らすことなく再帰的なデータ型をボックスが定義させてくれる方法を探究することができます。

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

ファイル名: src/main.rs

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

リスト15-2: i32値のコンスリストデータ構造を表すenumを定義する最初の試行

注釈: この例のためだけにi32値だけを保持するコンスリストを実装します。第10章で議論したように、 ジェネリクスを使用してどんな型の値も格納できるコンスリストを定義して実装することもできたでしょう。

このList型を使用してリスト1, 2, 3を格納すると、リスト15-3のコードのような見た目になるでしょう:

ファイル名: src/main.rs

use List::{Cons, Nil};

fn main() {
    let list = Cons(1, Cons(2, Cons(3, Nil)));
}

リスト15-3: List enumを使用してリスト1, 2, 3を格納する

最初のCons値は、1と別のList値を保持しています。このList値は、 2とまた別のList値を保持する別のCons値です。このList値は、 3と、ついにリストの終端を通知する非再帰的な列挙子のNilになるList値を保持するまたまた別のCons値です。

リスト15-3のコードをコンパイルしようとすると、リスト15-4に示したエラーが出ます:

error[E0072]: recursive type `List` has infinite size
(エラー: 再帰的な型`List`は無限のサイズです)
 --> src/main.rs:1:1
  |
1 | enum List {
  | ^^^^^^^^^ recursive type has infinite size
2 |     Cons(i32, List),
  |               ----- recursive without indirection
  |
  = help: insert indirection (e.g., a `Box`, `Rc`, or `&`) at some point to
  make `List` representable
  (助言: 間接参照(例: `Box`、`Rc`、あるいは`&`)をどこかに挿入して、`List`を表現可能にしてください)

リスト15-4: 再帰的なenumを定義しようとすると得られるエラー

エラーは、この型は「無限のサイズである」と表示しています。理由は、再帰的な列挙子を含むListを定義したからです: 自身の別の値を直接保持しているのです。結果として、コンパイラは、List値を格納するのに必要な領域が計算できないのです。 このエラーが得られた理由を少し噛み砕きましょう。まず、非再帰的な型の値を格納するのに必要な領域をどうコンパイラが決定しているかを見ましょう。

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

第6章でenum定義を議論した時にリスト6-2で定義したMessage enumを思い出してください:


# #![allow(unused_variables)]
#fn main() {
enum Message {
    Quit,
    Move { x: i32, y: i32 },
    Write(String),
    ChangeColor(i32, i32, i32),
}
#}

Message値一つにメモリを確保するために必要な領域を決定するために、コンパイラは、 各列挙子を見てどの列挙子が最も領域を必要とするかを確認します。コンパイラは、 Message::Quitは全く領域を必要とせず、Message::Movei32値を2つ格納するのに十分な領域が必要などと確かめます。 ただ1つの列挙子しか使用されないので、Message値一つが必要とする最大の領域は、 最大の列挙子を格納するのに必要になる領域です。

これをコンパイラがリスト15-2のList enumのような再帰的な型が必要とする領域を決定しようとする時に起こることと比較してください。 コンパイラは、Cons列挙子を見ることから始め、この列挙子には、型i32値が一つと型Listの値が一つ保持されます。 故に、Consは1つのi32Listのサイズに等しい領域を必要とします。Listが必要とするメモリ量を計算するのに、 コンパイラはCons列挙子から列挙子を観察します。Cons列挙子は型i32を1つと型Listの値1つを保持し、 この過程は無限に続きます。図15-1のようにですね。

無限のコンスリスト

図15-1: 無限のCons列挙子からなる無限のList

Box<T>で既知のサイズの再帰的な型を得る

コンパイラは、再帰的に定義された型に必要なメモリ量を計算できないので、リスト15-4ではエラーを返します。 しかし、エラーには確かにこの役に立つ提言が含まれています:

  = help: insert indirection (e.g., a `Box`, `Rc`, or `&`) at some point to
  make `List` representable

この提言において、「間接参照」は、値を直接格納する代わりに、データ構造を変更して値へのポインタを代わりに格納することで、 値を間接的に格納することを意味します。

Box<T>はポインタなので、コンパイラにはBox<T>が必要とする領域が必ずわかります: ポインタのサイズは、 指しているデータの量によって変わることはありません。つまり、別のList値を直接置く代わりに、 Cons列挙子の中にBox<T>を配置することができます。Box<T>は、 Cons列挙子の中ではなく、ヒープに置かれる次のList値を指します。概念的には、 それでも他のリストを「保持する」リストとともに作られたリストがありますが、 この実装は今では、要素はお互いの中にあるというよりも、隣り合って配置するような感じになります。

リスト15-2のList enumの定義とリスト15-3のListの使用をリスト15-5のコードに変更することができ、 これはコンパイルが通ります:

ファイル名: src/main.rs

enum List {
    Cons(i32, Box<List>),
    Nil,
}

use List::{Cons, Nil};

fn main() {
    let list = Cons(1,
        Box::new(Cons(2,
            Box::new(Cons(3,
                Box::new(Nil))))));
}

リスト15-5: 既知のサイズにするためにBox<T>を使用するListの定義

Cons列挙子は、1つのi32のサイズに加えてボックスのポインタデータを格納する領域を必要とするでしょう。 Nil列挙子は、値を格納しないので、Cons列挙子よりも必要な領域は小さいです。これで、 どんなList値もi321つのサイズに加えてボックスのポインタデータのサイズを必要とすることがわかりました。 ボックスを使うことで、無限の再帰的な繰り返しを破壊したので、コンパイラは、List値を格納するのに必要なサイズを計算できます。 図15-2は、Cons列挙子の今の見た目を示しています。

有限のコンスリスト

図15-2: ConsBoxを保持しているので、無限にサイズがあるわけではないList

ボックスは、間接参照とヒープメモリ確保だけを提供します; 他のスマートポインタ型で目撃するような、 他の特別な能力は何もありません。これらの特別な能力が招くパフォーマンスのオーバーヘッドもないので、 間接参照だけが必要になる唯一の機能であるコンスリストのような場合に有用になり得ます。 より多くのボックスのユースケースは第17章でもお見かけするでしょう。

Box<T>型は、Derefトレイトを実装しているので、スマートポインタであり、 このトレイトによりBox<T>の値を参照のように扱うことができます。Box<T>値がスコープを抜けると、 Dropトレイト実装によりボックスが参照しているヒープデータも片付けられます。 これら2つのトレイトをより詳しく探究しましょう。これら2つのトレイトは、 この章の残りで議論する他のスマートポインタ型で提供される機能にとってさらに重要でしょう。