スマートポインタ
スマートポインタとは
Rustにおけるスマートポインタとは内部に値を持ち、C言語のポインタのような振る舞いをもつデータ構造であり、唯一ヒープ上で値が確保される。
その他のRustの値は全てスタック上で確保される。
以下の2つのトレイトが実装されている必要がある。
Deref
トレイトDrop
トレイト
Deref トレイト
内部で所持している値を返す。
このトレイトを実装すると、自動的に* 演算子が実装される。
NOTE:
参照型における *演算子
参照元の値を表す。
let a = [0, 1, 2]; let a = [0, 1, 2]; let iter = a.iter().map(|x| *x < 2).filter(|x| *x == true);
NOTE: 比較演算子は T型
と &T型
の比較ができない
したがって、*
演算子(参照外し演算子をつけなければコンパイルエラーとなる。
error[E0308]: mismatched types | 14 | let iter = a.iter().map(|x| x < 2) | ^ | | | expected reference, found integer | help: consider borrowing here: `&2` | = note: expected reference `&_` found type `{integer}`
スマートポインタにおける *演算子
内部で所持している値(スライス)を返す。
let vec = vec![1,2,3,4,5]; let _arr = &*vec; // &[i32] let string = "Hello World".to_string(); let _str = &*string; // &str
NOTE: &
を付けている理由
Rustでは必ずコンパイル時にサイズが決まっている必要があるため。
&
を付けないとコンパイルエラーになる。
error[E0277]: the size for values of type `[{integer}]` cannot be known at compilation time | 5 | let _arr = *vec; // [i32] | ^^^^ doesn't have a size known at compile-time | = help: the trait `std::marker::Sized` is not implemented for `[{integer}]`
Dropトレイト
自身のスコープが切れた際に、内部で保持している値のクリーンナップ処理などを行う。
代表的なスマートポインタ
標準ライブラリーで実装されている代表的なスマートポインタ
- String
- Vec<T>
- Box<T> ヒープ領域への値の確保
- Rc<T> 参照カウントによる複数の所有者を実現
- RefCell<T> 借用ルールをランタイム時に強制させる
Box<T>
値をヒープ領域に格納するためのスマートポインタ
これによって以下を実現することができる。
- 再帰データ型の実現
- 所有権の移動による巨大データのコピー回避
- トレイトオブジェクトへの所有権の付与
1. 再帰データ型の実現
Rustではコンパイル時に型のサイズが決まっている必要があるので、 Consリストのような再帰型をそのままでは実現できない。
enum List { Cons(i32, List), Nil, } error[E0072]: recursive type `List` has infinite size | 2 | enum List { | ^^^^^^^^^ recursive type has infinite size 3 | Cons(i32, List), | ---- recursive without indirection | = help: insert indirection (e.g., a `Box`, `Rc`, or `&`) at some point to make `List` representable
そこで Box\<T> を間に挟むことで、型のサイズを固定にすることができる
enum List { Cons(i32, Box<List>), Nil, } let item1 = Box::new(List::Nil); let item2 = Box::new(List::Cons(1, item1)); let item3 = Box::new(List::Cons(2, item2)); let item4 = Box::new(List::Cons(3, item3));
2. 所有権の移動による巨大データのコピー回避
巨大なデータをもつ値をBox<T>に包み込むことで、所有権の移動でも値そのもののコピーを回避できる
let box1 = Box::new(big_data); .... let box2 = box1; // 所有権が移っても big_data のコピーは発生しない (ポインタがコピーされるだけ)
3. 所有権の管理
具体的な型情報が消去された状態でトレイトオブジェクトの所有権を管理できる。
fn print_if_string(value: Box<dyn Any + Send>) { if let Ok(string) = value.downcast::<String>() { println!("String ({}) : {}", string.len(), string); } } // 値をBox<_>型として保持できる let my_string= Box::new("hello".to_string()); let my_u8 = Box::new(5u8); print_if_string(my_string); print_if_string(my_u8);
dyn Any + Send
何らかのstruct型であることを表すBox<T>::downcast()
Result<Box<T>, Box<dyn Any + 'static>>
を返すdowncast
: トレイトオブジェクトから型への変換
トレイトオブジェクトとの違い
トレイトオブジェクトは、動的ディスパッチになるため、ランタイムでコストが発生する。 Box<T>インスタンスは、静的ディスパッチであるため、型を変換するオーバーヘッドが発生しない。
関連
参照
Rust における `From<T>` とか `Into<T>` とかの考え方 - Qiita コメント
ボクシングされたトレイトオブジェクトの利点は、具体的な型情報が消去された状態でトレイトオブジェクトの所有権を管理できるという点です。 なので、逆に言えば所有権を管理する必要がなければボクシングは必要ありません。