保証を選ぶ

Rustの重要な特長の1つは、プログラムのコストと保証を制御することができるということです。

Rustの標準ライブラリには、様々な「ラッパ型」の抽象があり、それらはコスト、エルゴノミクス、保証の間の多数のトレードオフをまとめています。 それらのトレードオフの多くでは実行時とコンパイル時のどちらかを選ばせてくれます。 このセクションでは、いくつかの抽象を選び、詳細に説明します。

先に進む前に、Rustにおける 所有権借用 について読んでおくことを強く推奨します。

基本的なポインタ型

Box<T>

Box<T> は「所有される」ポインタ、すなわち「ボックス」です。 ボックスは中身のデータへの参照を渡すことができますが、ボックスだけがそのデータの唯一の所有者です。 特に、次のことを考えましょう。

fn main() { let x = Box::new(1); let y = x; // x no longer accessible here // ここではもうxにアクセスできない }
let x = Box::new(1);
let y = x;
// ここではもうxにアクセスできない

ここで、そのボックスは yムーブ されました。 x はもはやそれを所有していないので、これ以降、コンパイラはプログラマが x を使うことを許しません。 同様に、ボックスはそれを返すことで関数の にムーブさせることもできます。

(ムーブされていない)ボックスがスコープから外れると、デストラクタが実行されます。 それらのデストラクタは中身のデータを解放して片付けます。

これは動的割当てのゼロコスト抽象化です。 もしヒープにメモリを割り当てたくて、そのメモリへのポインタを安全に取り回したいのであれば、これは理想的です。 コンパイル時にチェックされる通常の借用のルールに基づいてこれへの参照を共有することが許されているだけだということに注意しましょう。

&T&mut T

参照にはイミュータブルな参照とミュータブルな参照がそれぞれあります。 それらは「読み書きロック」パターンに従います。それは、あるデータへのミュータブルな参照を1つだけ持つこと、又は複数のイミュータブルな参照を持つことはあり得るが、その両方を持つことはあり得ないということです。 この保証はコンパイル時に強制され、目に見えるような実行時のコストは発生しません。 多くの場合、それら2つのポインタ型は低コストの参照をコードのセクション間で共有するには十分です。

それらのポインタを関連付けられているライフタイムを超えて有効になるような方法でコピーすることはできません。

*const T*mut T

関連付けられたライフタイムや所有権を持たない、C的な生ポインタがあります。 それらはメモリのある場所を何の制約もなく単に指示します。 それらの提供する唯一の保証は、 unsafe であるとマークされたコードの外ではそれらが参照を外せないということです。

それらは Vec<T> のような安全で低コストな抽象を構築するときには便利ですが、安全なコードの中では避けるべきです。

Rc<T>

これは、本書でカバーする中では初めての、実行時にコストの発生するラッパです。

Rc<T> は参照カウンタを持つポインタです。 言い換えると、これを使えば、あるデータを「所有する」複数のポインタを持つことができるようになるということです。そして、全てのポインタがスコープから外れたとき、そのデータは削除されます(デストラクタが実行されます)。

内部的には、それは共有「参照カウント」(「refcount」とも呼ばれます)を持っています。それは、 Rc がクローンされる度に1増加し、 Rc がスコープから外れる度に1減少します。 Rc<T> の主な役割は、共有データのデストラクタが呼び出されることを保証することです。

ここでの中身のデータはイミュータブルで、もし循環参照が起きてしまったら、そのデータはメモリリークを起こすでしょう。 もし循環してもメモリリークを起こさないデータを求めるのであれば、ガーベジコレクタが必要です。

保証

ここで提供される主な保証は、それに対する全ての参照がスコープから外れるまではデータが破壊されないということです。

これは(読込専用の)あるデータを動的に割り当て、プログラムの様々な部分で共有したいときで、どの部分が最後にポインタを使い終わるのかがはっきりしないときに使われるべきです。 それは &T が正しさを静的にチェックすることが不可能なとき、又はプログラマがそれを使うために開発コストを費やすことを望まないような極めて非エルゴノミックなコードを作っているときに、 &T の有望な代替品です。

このポインタはスレッドセーフでは ありません 。Rustはそれを他のスレッドに対して送ったり共有したりはさせません。 これによって、それらが不要な状況でのアトミック性のためのコストを省くことができます。

これの姉妹に当たるスマートポインタとして、 Weak<T> があります。 これは所有せず、借用もしないスマートポインタです。 それは &T とも似ていますが、ライフタイムによる制約がありません。 Weak<T> は永遠に有効であり続けることができます。 しかし、これは所有する Rc のライフタイムを超えて有効である可能性があるため、中身のデータへのアクセスが失敗し、 None を返すという可能性があります。 これは循環するデータ構造やその他のものについて便利です。

コスト

メモリに関する限り、 Rc<T> の割当ては1回です。ただし、普通の Box<T> と比べると (「強い」参照カウントと「弱い」参照カウントのために)、2ワード余分(つまり、2つの usize の値)に割り当てます。

Rc<T> では、それをクローンしたりそれがスコープから外れたりする度に参照カウントを増減するための計算コストが掛かります。 クローンはディープコピーではなく、それが単に内部の参照カウントを1増加させ、 Rc<T> のコピーを返すだけだということに注意しましょう。

セル型

Cell は内的ミュータビリティを提供します。 言い換えると、型がミュータブルな形式を持てないものであったとしても(例えば、それが & ポインタや Rc<T> の参照先であるとき)、操作できるデータを持つということです。

cellモジュールのドキュメントには、それらについての非常によい説明があります

それらの型は 一般的には 構造体のフィールドで見られますが、他の場所でも見られるかもしれません。

Cell<T>

Cell<T> はゼロコストで内的ミュータビリティを提供するものですが、 Copy 型のためだけのものです。 コンパイラは含まれている値によって所有されている全てのデータがスタック上にあることを認識しています。そのため、単純にデータが置き換えられることによって参照先のデータがメモリリークを起こす(又はもっと悪いことも!)心配はありません。

このラッパを使うことで、維持されている不変性に違反してしまう可能性もあるので、それを使うときには注意しましょう。 もしフィールドが Cell でラップされているならば、そのデータの塊はミュータブルで、最初にそれを読み込んだときとそれを使おうと思ったときで同じままだとは限らないということのよい目印になります。

fn main() { use std::cell::Cell; let x = Cell::new(1); let y = &x; let z = &x; x.set(2); y.set(3); z.set(4); println!("{}", x.get()); }
use std::cell::Cell;

let x = Cell::new(1);
let y = &x;
let z = &x;
x.set(2);
y.set(3);
z.set(4);
println!("{}", x.get());

ここでは同じ値を様々なイミュータブルな参照から変更できるということに注意しましょう。

これには次のものと同じ実行時のコストが掛かります。

fn main() { let mut x = 1; let y = &mut x; let z = &mut x; x = 2; *y = 3; *z = 4; println!("{}", x); }
let mut x = 1;
let y = &mut x;
let z = &mut x;
x = 2;
*y = 3;
*z = 4;
println!("{}", x);

しかし、それには実際に正常にコンパイルできるという追加の利点があります。

保証

これは「ミュータブルなエイリアスはない」という制約を、それが不要な場所において緩和します。 しかし、これはその制約が提供する保証をも緩和してしまいます。もし不変条件が Cell に保存されているデータに依存しているのであれば、注意すべきです。

これは &&mut の静的なルールの下では簡単な方法がない場合に、プリミティブやその他の Copy 型を変更するのに便利です。

Cell によって安全な方法で自由に変更できるようなデータへの内部の参照を得られるわけではありません。

コスト

Cell<T> の使用に実行時のコストは掛かりません。ただし、もしそれを大きな( Copy の)構造体をラップするために使っているのであれば、代わりに個々のフィールドを Cell<T> でラップする方がよいかもしれません。そうしなければ、各書込みが構造体の完全コピーを発生させることになるからです。

RefCell<T>

RefCell<T> もまた内的ミュータビリティを提供するものですが、 Copy 型に限定されません。

その代わり、それには実行時のコストが掛かります。 RefCell<T> は読み書きロックパターンを実行時に(シングルスレッドのミューテックスのように)強制します。この点が、それをコンパイル時に行う &T&mut T とは異なります。 これは borrow() 関数と borrow_mut() 関数によって行われます。それらは内部の参照カウントを変更し、それぞれイミュータブル、ミュータブルに参照を外すことのできるスマートポインタを戻します。 参照カウントはスマートポインタがスコープから外れたときに元に戻されます。 このシステムによって、ミュータブルな借用が有効なときには決してその他の借用が有効にならないということを動的に保証することができます。 もしプログラマがそのような借用を作ろうとすれば、スレッドはパニックするでしょう。

fn main() { use std::cell::RefCell; let x = RefCell::new(vec![1,2,3,4]); { println!("{:?}", *x.borrow()) } { let mut my_ref = x.borrow_mut(); my_ref.push(1); } }
use std::cell::RefCell;

let x = RefCell::new(vec![1,2,3,4]);
{
    println!("{:?}", *x.borrow())
}

{
    let mut my_ref = x.borrow_mut();
    my_ref.push(1);
}

Cell と同様に、これは主に、借用チェッカを満足させることが困難、又は不可能な状況で便利です。 一般的に、そのような変更はネストした形式では発生しないと考えられますが、それをチェックすることはよいことです。

大きく複雑なプログラムにとって、物事を単純にするために何かを RefCell の中に入れることは便利です。 例えば、Rustコンパイラの内部の ctxt構造体 にあるたくさんのマップはこのラッパの中にあります。 それらは(初期化の直後ではなく生成の過程で)一度だけ変更されるか、又はきれいに分離された場所で数回変更されます。 しかし、この構造体はあらゆる場所で全般的に使われているので、ミュータブルなポインタとイミュータブルなポインタとをジャグリング的に扱うのは難しく(あるいは不可能で)、おそらく拡張の困難な & ポインタのスープになってしまいます。 一方、 RefCell はそれらにアクセスするための(ゼロコストではありませんが)低コストの方法です。 将来、もし誰かが既に借用されたセルを変更しようとするコードを追加すれば、それは(普通は確定的に)パニックを引き起こすでしょう。これは、その違反した借用まで遡り得ます。

同様に、ServoのDOMではたくさんの変更が行われるようになっていて、そのほとんどはDOM型にローカルです。しかし、複数のDOMに縦横無尽にまたがり、様々なものを変更するものもあります。 全ての変更をガードするために RefCellCell を使うことで、あらゆる場所でのミュータビリティについて心配する必要がなくなり、それは同時に、変更が 実際に 起こっている場所を強調してくれます。

もし & ポインタを使ってもっと単純に解決できるのであれば、 RefCell は避けるべきであるということに注意しましょう。

保証

RefCell はミュータブルなエイリアスを作らせないという 静的な 制約を緩和し、それを 動的な 制約に置き換えます。 そのため、その保証は変わりません。

コスト

RefCell は割当てを行いませんが、データとともに(サイズ1ワードの)追加の「借用状態」の表示を持っています。

実行時には、各借用が参照カウントの変更又はチェックを発生させます。

同期型

前に挙げた型の多くはスレッドセーフな方法で使うことができません。 特に Rc<T>RefCell<T> は両方とも非アトミックな参照カウント( アトミックな 参照カウントとは、データ競合を発生させることなく複数のスレッドから増加させることができるもののことです)を使っていて、スレッドセーフな方法で使うことができません。 これによってそれらを低コストで使うことができるのですが、それらのスレッドセーフなバージョンも必要です。 それらは Arc<T>Mutex<T>RwLock<T> という形式で存在します。

非スレッドセーフな型はスレッド間で送ることが できません 。 これはコンパイル時にチェックされます。

sync モジュールには並行プログラミングのための便利なラッパがたくさんありますが、以下では有名なものだけをカバーします。

Arc<T>

Arc<T> はアトミックな参照カウントを使う Rc<T> の別バージョンです(そのため、「Arc」なのです)。 これはスレッド間で自由に送ることができます。

C++の shared_ptrArc と似ていますが、C++の場合、中身のデータは常にミュータブルです。 C++と同じセマンティクスで使うためには、 Arc<Mutex<T>>Arc<RwLock<T>>Arc<UnsafeCell<T>> を使うべきです 1UnsafeCell<T> はどんなデータでも持つことができ、実行時のコストも掛かりませんが、それにアクセスするためには unsafe ブロックが必要というセル型です)。 最後のものは、その使用がメモリをアンセーフにしないことを確信している場合にだけ使うべきです。 次のことを覚えましょう。構造体に書き込むのはアトミックな作業ではなく、vec.push()のような多くの関数は内部でメモリの再割当てを行い、アンセーフな挙動を引き起こす可能性があります。そのため単純な操作であるということだけでは UnsafeCell を正当化するには十分ではありません。

保証

Rc のように、これは最後の Arc がスコープから外れたときに(循環がなければ)中身のデータのためのデストラクタが実行されることを(スレッドセーフに)保証します。

コスト

これには参照カウントの変更(これは、それがクローンされたりスコープから外れたりする度に発生します)にアトミック性を使うための追加のコストが掛かります。 シングルスレッドにおいて、データを Arc から共有するのであれば、可能な場合は & ポインタを共有する方が適切です。

Mutex<T>RwLock<T>

Mutex<T>RwLock<T> はRAIIガード(ガードとは、ロックのようにそれらのデストラクタが呼び出されるまである状態を保持するオブジェクトのことです)による相互排他を提供します。 それらの両方とも、その lock() を呼び出すまでミューテックスは不透明です。その時点で、スレッドはロックが得られ、ガードが戻されるまでブロックします。 このガードを使うことで、中身のデータに(ミュータブルに)アクセスできるようになり、ロックはガードがスコープから外れたときに解放されます。

fn main() { { let guard = mutex.lock(); // guard dereferences mutably to the inner type // ガードがミュータブルに内部の型への参照を外す *guard += 1; // } // lock released when destructor runs } // デストラクタが実行されるときにロックは解除される }
{
    let guard = mutex.lock();
    // ガードがミュータブルに内部の型への参照を外す
    *guard += 1;
} // デストラクタが実行されるときにロックは解除される

RwLockには複数の読込みを効率化するという追加の利点があります。 それはライタのない限り常に、共有されたデータに対する複数のリーダを安全に持つことができます。そして、RwLockによってリーダは「読込みロック」を取得できます。 このようなロックは並行に取得することができ、参照カウントによって追跡することができます。 ライタは「書込みロック」を取得する必要があります。「書込みロック」はすべてのリーダがスコープから外れたときにだけ取得できます。

保証

それらのどちらもスレッド間での安全で共有されたミュータビリティを提供しますが、それらはデッドロックしがちです。 型システムによって、ある程度の追加のプロトコルの安全性を得ることができます。

コスト

それらはロックを保持するために内部でアトミック的な型を使います。それにはかなりコストが掛かります(それらは仕事が終わるまで、プロセッサ中のメモリ読込み全てをブロックする可能性があります)。 たくさんの並行なアクセスが起こる場合には、それらのロックを待つことが遅くなる可能性があります。

合成

Rustのコードを読むときに一般的な悩みは、 Rc<RefCell<Vec<T>>> のような型(又はそのような型のもっと複雑な合成)です。 その合成が何をしているのか、なぜ作者はこんなものを選んだのか(そして、自分のコード内でいつこんな合成を使うべきなのか)ということは、常に明らかなわけではありません。

普通、それは不要なコストを支払うことなく、必要とする保証を互いに組み合わせた場合です。

例えば、 Rc<RefCell<T>> はそのような合成の1つです。 Rc<T> そのものはミュータブルに参照を外すことができません。 Rc<T> は共有を提供し、共有されたミュータビリティはアンセーフな挙動に繋がる可能性があります。そのため、動的に証明された共有されたミュータビリティを得るために、 RefCell<T> を中に入れます。 これで共有されたミュータブルなデータを持つことになりますが、それは(リーダはなしで)ライタが1つだけ、又はリーダが複数という方法で共有することになります。

今度は、これをさらに次の段階に進めると、 Rc<RefCell<Vec<T>>>Rc<Vec<RefCell<T>>> を持つことができます。 それらは両方とも共有できるミュータブルなベクタですが、同じではありません。

1つ目について、 RefCell<T>Vec<T> をラップしているので、その Vec<T> 全体がミュータブルです。 同時に、それらは特定の時間において Vec 全体の唯一のミュータブルな借用になり得ます。 これは、コードがそのベクタの別の要素について、別の Rc ハンドルから同時には操作できないということを意味します。 しかし、 Vec<T> に対するプッシュやポップは好きなように行うことができます。 これは借用チェックが実行時に行われるという点で &mut Vec<T> と同様です。

2つ目について、借用は個々の要素に対して行われますが、ベクタ全体がイミュータブルになります。 そのため、異なる要素を別々に借用することができますが、ベクタに対するプッシュやポップを行うことはできません。 これは &mut [T]2 と同じですが、やはり借用チェックは実行時に行われます。

並行プログラムでは、 Arc<Mutex<T>> と似た状況に置かれます。それは共有されたミュータビリティと所有権を提供します。

それらを使ったコードを読むときには、1行1行進み、提供される保証とコストを見ましょう。

合成された型を選択するときには、その逆に考えなければなりません。必要とする保証が何であるか、必要とする合成がどの点にあるのかを理解しましょう。 例えば、もし Vec<RefCell<T>>RefCell<Vec<T>> のどちらかを選ぶのであれば、前の方で行ったようにトレードオフを理解し、選ばなければなりません。

&mut [T] ではその要素を変更できますが、その長さは変更することができません。


  1. Arc<UnsafeCell<T>>SendSync ではないため、実際にはコンパイルできません。しかし、 Arc<Wrapper<T>> を得るために、手動でそれを SendSync を実装した型でラップすることができます。ここでの Wrapperstruct Wrapper<T>(UnsafeCell<T>) です。 

  2. &[T]&mut [T]スライス です。それらはポインタと長さを持ち、ベクタや配列の一部を参照することができます。