Rustではゼロコスト抽象と安全なポインタ操作のために、ポインタ相当の型を非常に細かく分類しています。慣れないうちは適切なポインタ型を選ぶのが難しいであろうことを鑑みて、主要なスマートポインタ型(と参照型)を比較する表を作ってみました。
効率 | ライフタイム | 共有 | 書き換え | スレッドセーフ | |
---|---|---|---|---|---|
&mut T |
★★★★★ | 短命 | × | ○ | △ |
&T |
★★★★★ | 短命 | ○ | × | △ |
&RefCell<T> |
★★★★☆ | 短命 | ○ | ○ | × |
&Mutex<T> |
★★★☆☆ | 短命 | ○ | ○ | △ |
Box<T> |
★★★★☆ | 'static |
× | ○ | ○ |
Rc<T> |
★★★☆☆ | 'static |
○ | × | × |
Rc<RefCell<T>> |
★★☆☆☆ | 'static |
○ | ○ | × |
Arc<T> |
★★☆☆☆ | 'static |
○ | × | ○ |
Arc<Mutex<T>> |
★☆☆☆☆ | 'static |
○ | ○ | ○ |
以下補足です。
効率
トレードオフを強調するために、相対的な効率を星の個数で表しています。しかし、効率をことさら気にする必要はありません。他の多くのプログラミング言語では、Rustでいうところの Arc<Mutex<T>>
をデフォルトで使っているような状況です。要件にあったものを使うことが大事です。
ライフタイム
「短命」とついているものは基本的に、ある関数のスコープに紐付いた形でしか使えません。特に慣れないうちは、これらの型を構造体に入れて使うのはやめておいたほうが無難でしょう。
例外として、グローバル変数 (static
, lazy_static!
) は &'static
参照として扱うことができます
共有
「ポインタ」と聞くと、参照先が共有されている様子を思い浮かべる人が多いと思いますが、所有権の概念のあるRustでは必ずしもそのイメージは当てはまりません。 &mut T
と Box<T>
は共有のための仕組みとしては使えないことに留意するといいでしょう。
書き換え
Rustでは共有されたデータの書き換えは原則としてできません。これをオプトアウトするために Mutex
や RefCell
というコンテナを挟む必要があります。
スレッドセーフ
スレッドセーフかどうかはコンパイラが検査するので、あらかじめ注意しながら書く必要はありません。
"△" とついているものはスレッド安全ではありますが、短命であるため通常の方法で他のスレッドに送ることができません。rayonなどスコープドスレッドを使う処理では安全に使うことができます。
Cell, Atomic, RwLock
表を簡単にするために以下の型は除きました。
-
Cell<T>
はRefCell<T>
と似た特性を持ちます。ロック用の領域が省けるかわりに、限定的な操作しかできなくなります。T: Copy
であるときに便利です。 -
AtomicUsize
はMutex<usize>
と似た特性を持ちます。一般的にAtomic*
は対応するMutex<*>
とよく似ています。より効率的ですがロックはできません。 -
RwLock<T>
はMutex<T>
と似た特性を持ちますが、読み取り専用ロックをとれるため読み取りが多い場合はより効率的に使うことができます。
Option
表で挙げた型は全てnon-nullなポインタ型です。ポインタ型を Option
で囲めば nullable になります。
関数ポインタ
関数ポインタ型は fn(i32, &str) -> bool
のような形で書きます。 *const fn()
と書いてしまうと関数ポインタへのポインタになってしまうので注意が必要です。
ほとんどのユースケースでは関数ポインタではなくクロージャポインタである Box<dyn Fn(i32, &str) -> bool>
のほうが適切です。
生ポインタ
*const T
, *mut T
はFFI用、 NonNull<T>
はRust内でunsafeなコードを書くためのプリミティブとしてよく出てきます。これらを適切に扱うにはより高度な知識が必要なため本記事では省略します。
Box<T>
について
Box<T>
はしばしばRustの所有権が役に立つ代表例として挙げられますが、これはある意味誤解のもとです。確かに Box<T>
は優れていますが、これはこの型が多機能だからではなく、むしろ単機能だから優れていると考えられます。
実際、 T
を Box<T>
に変更しても、以下のような影響しかありません(そしてこれらの特徴は他の多くのポインタ型にも共通して成り立ちます)。
- サイズが一定 (1ワード~2ワード) になる。
- 単純コピーである
Copy
は使えなくなる。 (Clone
に置き換える必要がある) -
Pin
で包むことができるようになる。
3はより高度な使い方なので省略します。また2もデータ設計上重要ではないので基本的には1の**「サイズが一定になる」という恩恵のみが重要**ということになります。この1の恩恵が役に立つのは精々以下のようなケースくらいです。
- トレイトオブジェクト (
dyn Trait
) を包むため。 - 再帰的なデータを定義するため。
- 大きすぎる構造体のムーブコストが嵩む場合。
繰り返しになりますが、しばしばポインタに期待される「同じ領域を複数の所有者が共有する」という機能を Box<T>
は持っていません。このことに注意して必要なスマートポインタを選ぶのが肝要です。