ヒープのデータを指す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); }
変数b
を定義して値5
を指すBox
の値があって、この値はヒープに確保されています。このプログラムは、
b = 5
と出力するでしょう; この場合、このデータがスタックにあるのと同じような方法でボックスのデータにアクセスできます。
あらゆる所有された値同様、b
がmain
の終わりでするようにボックスがスコープを抜けたら、
メモリから解放されます。メモリの解放は(スタックに格納されている)ボックスと(ヒープに格納されている)指しているデータに対して起きます。
ヒープに単独の値を置くことはあまり有用ではないので、このように単独でボックスを使用することはあまりありません。
単独の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,
}
注釈: この例のためだけに
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)));
}
最初の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`を表現可能にしてください)
エラーは、この型は「無限のサイズである」と表示しています。理由は、再帰的な列挙子を含む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::Move
はi32
値を2つ格納するのに十分な領域が必要などと確かめます。
ただ1つの列挙子しか使用されないので、Message
値一つが必要とする最大の領域は、
最大の列挙子を格納するのに必要になる領域です。
これをコンパイラがリスト15-2のList
enumのような再帰的な型が必要とする領域を決定しようとする時に起こることと比較してください。
コンパイラは、Cons
列挙子を見ることから始め、この列挙子には、型i32
値が一つと型List
の値が一つ保持されます。
故に、Cons
は1つのi32
とList
のサイズに等しい領域を必要とします。List
が必要とするメモリ量を計算するのに、
コンパイラはCons
列挙子から列挙子を観察します。Cons
列挙子は型i32
を1つと型List
の値1つを保持し、
この過程は無限に続きます。図15-1のようにですね。
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)))))); }
Cons
列挙子は、1つのi32
のサイズに加えてボックスのポインタデータを格納する領域を必要とするでしょう。
Nil
列挙子は、値を格納しないので、Cons
列挙子よりも必要な領域は小さいです。これで、
どんなList
値もi32
1つのサイズに加えてボックスのポインタデータのサイズを必要とすることがわかりました。
ボックスを使うことで、無限の再帰的な繰り返しを破壊したので、コンパイラは、List
値を格納するのに必要なサイズを計算できます。
図15-2は、Cons
列挙子の今の見た目を示しています。
ボックスは、間接参照とヒープメモリ確保だけを提供します; 他のスマートポインタ型で目撃するような、 他の特別な能力は何もありません。これらの特別な能力が招くパフォーマンスのオーバーヘッドもないので、 間接参照だけが必要になる唯一の機能であるコンスリストのような場合に有用になり得ます。 より多くのボックスのユースケースは第17章でもお見かけするでしょう。
Box<T>
型は、Deref
トレイトを実装しているので、スマートポインタであり、
このトレイトによりBox<T>
の値を参照のように扱うことができます。Box<T>
値がスコープを抜けると、
Drop
トレイト実装によりボックスが参照しているヒープデータも片付けられます。
これら2つのトレイトをより詳しく探究しましょう。これら2つのトレイトは、
この章の残りで議論する他のスマートポインタ型で提供される機能にとってさらに重要でしょう。