Send、Sync、そしてコンパイルテスト
さて、実は考えるべきトレイトのペアがもう 1 つあります。ただし、それらは特別です。Rust の神聖ローマ帝国、すなわち Unsafe Opt-In Built-In Traits(OIBITs)である Send と Sync に対処しなければなりません。実際には opt-out で built-out なのですが(3 つのうち 1 つ合っていれば十分でしょう!)。
Copy と同じく、これらのトレイトには関連付けられたコードがまったくなく、あなたの型が特定の性質を持つことを示す単なるマーカーです。Send は、あなたの型を別のスレッドへ送っても安全であることを表します。Sync は、あなたの型をスレッド間で共有しても安全であることを表します(&Self: Send)。
LinkedList が共変であることに対するのと同じ議論がここにも当てはまります。一般に、凝った内部可変性のトリックを使わない普通のコレクションは、Send と Sync にしても安全です。
しかし私は、それらは opt out だと言いました。では実際のところ、私たちはもうそうなっているのでしょうか? どうやって知ればよいのでしょう?
コードに新しい魔法を追加しましょう。型が期待する性質を持っていない限りコンパイルされない、ランダムな private のガラクタです。
#[allow(dead_code)]
fn assert_properties() {
fn is_send<T: Send>() {}
fn is_sync<T: Sync>() {}
is_send::<LinkedList<i32>>();
is_sync::<LinkedList<i32>>();
is_send::<IntoIter<i32>>();
is_sync::<IntoIter<i32>>();
is_send::<Iter<i32>>();
is_sync::<Iter<i32>>();
is_send::<IterMut<i32>>();
is_sync::<IterMut<i32>>();
is_send::<Cursor<i32>>();
is_sync::<Cursor<i32>>();
fn linked_list_covariant<'a, T>(x: LinkedList<&'static T>) -> LinkedList<&'a T> { x }
fn iter_covariant<'i, 'a, T>(x: Iter<'i, &'static T>) -> Iter<'i, &'a T> { x }
fn into_iter_covariant<'a, T>(x: IntoIter<&'static T>) -> IntoIter<&'a T> { x }
}
cargo build
Compiling linked-list v0.0.3
error[E0277]: `NonNull<Node<i32>>` cannot be sent between threads safely
--> src\lib.rs:433:5
|
433 | is_send::<LinkedList<i32>>();
| ^^^^^^^^^^^^^^^^^^^^^^^^^^ `NonNull<Node<i32>>` cannot be sent between threads safely
|
= help: within `LinkedList<i32>`, the trait `Send` is not implemented for `NonNull<Node<i32>>`
= note: required because it appears within the type `Option<NonNull<Node<i32>>>`
note: required because it appears within the type `LinkedList<i32>`
--> src\lib.rs:8:12
|
8 | pub struct LinkedList<T> {
| ^^^^^^^^^^
note: required by a bound in `is_send`
--> src\lib.rs:430:19
|
430 | fn is_send<T: Send>() {}
| ^^^^ required by this bound in `is_send`
<a million more errors>
なんてことだ、どういうことだ! あの素晴らしい神聖ローマ帝国ジョークがあったのに!
実は、生ポインターには安全性ガードが 1 つしかないと言ったとき、私は嘘をついていました。これがもう 1 つです。安全側に倒すため、*const と *mut はどちらも明示的に Send と Sync から opt out しています。したがって、私たちは 実際に opt back in しなければなりません。
unsafe impl<T: Send> Send for LinkedList<T> {}
unsafe impl<T: Sync> Sync for LinkedList<T> {}
unsafe impl<'a, T: Send> Send for Iter<'a, T> {}
unsafe impl<'a, T: Sync> Sync for Iter<'a, T> {}
unsafe impl<'a, T: Send> Send for IterMut<'a, T> {}
unsafe impl<'a, T: Sync> Sync for IterMut<'a, T> {}
ここでは unsafe impl と書かなければならないことに注意してください。これらは unsafe trait です! unsafe コード(並行処理ライブラリなど)は、私たちがこれらのトレイトを正しく実装していることに依存できます! 実際のコードは存在しないので、私たちが行っている保証は単に「はい、私たちは確かにスレッド間で Send または Share しても安全です!」というものです。
軽々しくこれらを貼り付けてはいけませんが、ここで Certified Professional である私が言います。はい、これはまったく問題ありません。IntoIter に対して Send と Sync を実装する必要がないことに注目してください。これは LinkedList を含んでいるだけなので、Send と Sync が自動的に導出されます — 実際には opt out だと言いましたよね!(impl !Send for MyType {} という愉快な構文で opt out します。)
cargo build
Compiling linked-list v0.0.3
Finished dev [unoptimized + debuginfo] target(s) in 0.18s
よし、いいですね!
……待ってください。実際、これらである べきではない ものがそうでなかった場合、それは本当に危険です。特に IterMut は &mut T 「のような」ものなので、絶対に 共変であってはいけません。しかし、それをどう確認すればよいのでしょうか?
魔法です! いや、実際には rustdoc です! まあ、このために rustdoc を使う必要はありませんが、これが一番面白いやり方です。doccomment を書いてコードブロックを含めると、rustdoc はそれをコンパイルして実行しようとします。つまり、それを使ってメインのものに影響しない、新しい匿名の「プログラム」を作れるのです。
/// ```
/// use linked_list::IterMut;
///
/// fn iter_mut_covariant<'i, 'a, T>(x: IterMut<'i, &'static T>) -> IterMut<'i, &'a T> { x }
/// ```
fn iter_mut_invariant() {}
cargo test
...
Doc-tests linked-list
running 1 test
test src\lib.rs - assert_properties::iter_mut_invariant (line 458) ... FAILED
failures:
---- src\lib.rs - assert_properties::iter_mut_invariant (line 458) stdout ----
error[E0308]: mismatched types
--> src\lib.rs:461:86
|
6 | fn iter_mut_covariant<'i, 'a, T>(x: IterMut<'i, &'static T>) -> IterMut<'i, &'a T> { x }
| ^ lifetime mismatch
|
= note: expected struct `linked_list::IterMut<'_, &'a T>`
found struct `linked_list::IterMut<'_, &'static T>`
よし、これで不変であることを証明できました。しかし、ええと、これでテストが失敗するようになってしまいました。心配はいりません。rustdoc では、フェンスに compile_fail と注釈することで、それが期待される結果だと示せます!
(実際には「共変ではない」ことを証明しただけですが、正直なところ、型を「うっかり、かつ誤って反変」にできたなら、おめでとうございます、という感じです。)
/// ```compile_fail
/// use linked_list::IterMut;
///
/// fn iter_mut_covariant<'i, 'a, T>(x: IterMut<'i, &'static T>) -> IterMut<'i, &'a T> { x }
/// ```
fn iter_mut_invariant() {}
cargo test
Compiling linked-list v0.0.3
Finished test [unoptimized + debuginfo] target(s) in 0.49s
Running unittests src\lib.rs
...
Doc-tests linked-list
running 1 test
test src\lib.rs - assert_properties::iter_mut_invariant (line 458) - compile fail ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.12s
やった! compile_fail なしでテストを常に作ることをおすすめします。そうすれば、正しい理由で コンパイルに失敗することを確認できます。たとえば、そのテストは use を忘れた場合にも失敗します(したがって成功します)が、それは私たちの望むものではありません! コンパイラーからの特定のエラーを「要求」できるというのは概念的には魅力的ですが、これは事実上、コンパイラーがより良いエラーを出すこと を破壊的変更にしてしまう、完全な悪夢になるでしょう。私たちはコンパイラーに良くなってほしいので、いいえ、それは得られません。
(おっと、実際には compile_fail の横に欲しいエラーコードを指定することもできます。しかしこれは nightly でしか機能せず、上で述べた理由から依存するのは悪い考えです。非 nightly では黙って無視されます。)
/// ```compile_fail,E0308
/// use linked_list::IterMut;
///
/// fn iter_mut_covariant<'i, 'a, T>(x: IterMut<'i, &'static T>) -> IterMut<'i, &'a T> { x }
/// ```
fn iter_mut_invariant() {}
…ところで、実際に IterMut を不変にしていた箇所に気づきましたか? 私が Iter を「ただ」コピー&ペーストして最後に放り込んだだけなので、見落としやすい箇所でした。ここの最後の行です。
pub struct IterMut<'a, T> {
front: Link<T>,
back: Link<T>,
len: usize,
_boo: PhantomData<&'a mut T>,
}
その PhantomData を削除してみましょう。
cargo build
Compiling linked-list v0.0.3 (C:\Users\ninte\dev\contain\linked-list)
error[E0392]: parameter `'a` is never used
--> src\lib.rs:30:20
|
30 | pub struct IterMut<'a, T> {
| ^^ unused parameter
|
= help: consider removing `'a`, referring to it in a field, or using a marker such as `PhantomData`
はは! コンパイラは私たちの味方で、ライフタイムを単に使わないままにはさせてくれません。代わりに間違った例を使ってみましょう。
_boo: PhantomData<&'a T>,
cargo build
Compiling linked-list v0.0.3 (C:\Users\ninte\dev\contain\linked-list)
Finished dev [unoptimized + debuginfo] target(s) in 0.17s
ビルドできました! 今度はテストが問題を検出してくれるでしょうか?
cargo test
...
Doc-tests linked-list
running 1 test
test src\lib.rs - assert_properties::iter_mut_invariant (line 458) - compile fail ... FAILED
failures:
---- src\lib.rs - assert_properties::iter_mut_invariant (line 458) stdout ----
Test compiled successfully, but it's marked `compile_fail`.
failures:
src\lib.rs - assert_properties::iter_mut_invariant (line 458)
test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.15s
イェーイ!!! システムは動いています! ちゃんと仕事をしてくれるテストがあるのは最高です。迫りくるミスにそこまで怯えなくて済みますからね!