あまりにも多すぎる連結リストでRustを学ぶ
問題がある、または完成したコードを一度に全部確認したいですか? すべてGithubにあります!
注記: この本の現在の版はRust 2018を対象に書かれています。 Rust 2018はrustc 1.31(2018年12月8日)で初めてリリースされました。あなたのRustツールチェーンが 十分に新しい場合、
cargo newが作成するCargo.tomlファイルにはedition = "2018"という行が含まれているはずです(あるいは、これを遠い未来に読んでいるなら、 もっと大きな数字かもしれません!)。古いツールチェーンを使うことも可能ですが、 秘密のハードモードが解放され、本書の本文ではまったく触れられていない 追加のコンパイラーエラーに遭遇することになります。わあ、楽しそうですね!
Rustで連結リストを実装する方法について、かなり頻繁に質問されます。 正直なところ、答えは要件によって変わりますし、その場で質問に答えるのが それほど簡単ではないのは明らかです。そのため、この疑問に包括的に、そして きっぱりと答えるために、この本を書くことにしました。
このシリーズでは、6つの連結リストを実装してもらうことだけを通じて、 基本的および高度なRustプログラミングを教えます。その過程で、次のことを 学べるはずです。
- 次のポインター型:
&,&mut,Box,Rc,Arc,*const,*mut,NonNull(?) - 所有権、借用、継承された可変性、内部可変性、Copy
- すべてのキーワード: struct, enum, fn, pub, impl, use, …
- パターンマッチング、ジェネリクス、デストラクター
- テスト、新しいツールチェーンのインストール、
miriの使用 - Unsafe Rust: 生ポインター、エイリアシング、stacked borrows、UnsafeCell、variance
そうです。連結リストは本当にひどいので、それを現実のものにするだけで、 これらすべての概念に向き合うことになります。
すべてサイドバーにあります(モバイルでは折りたたまれているかもしれません)が、 すぐに参照できるように、これから作るものを以下に示します。
- ひどい単方向連結スタック
- まあまあな単方向連結スタック
- 永続的な単方向連結スタック
- ひどいが安全な双方向連結デック
- unsafeな単方向連結キュー
- TODO: まあまあなunsafe双方向連結デック
- ボーナス: たくさんのおかしなリスト
認識をそろえておくために、私がターミナルに入力するコマンドはすべて書き出します。 また、このプロジェクトの開発にはRustの標準パッケージマネージャーであるCargoも使います。 Rustプログラムを書くのにCargoが必須というわけではありませんが、rustcを直接使うより はるかに優れています。ちょっといじってみたいだけなら、 play.rust-lang.orgを使ってブラウザー上で簡単なプログラムを実行することもできます。
後のセクションでは、追加のRustツールをインストールするために「rustup」を使います。 すべてのRustツールチェーンをrustupでインストールすることを強くおすすめします。
さっそく始めて、プロジェクトを作りましょう。
> cargo new --lib lists
> cd lists
作業内容を失わないように、各リストは別々のファイルに配置します。
本物のRust学習体験には、コードを書き、コンパイラーに怒鳴られ、 それがいったい何を意味するのかを理解しようとすることが含まれる、という点には 触れておくべきでしょう。私は、これができるだけ頻繁に起こるよう慎重に配慮します。 Rustの一般に非常に優れたコンパイラーエラーとドキュメントを読み、理解することを学ぶのは、 生産的なRustプログラマーになるうえで信じられないほど重要です。
とはいえ、実はこれは嘘です。これを書いている間、ここで見せているよりも はるかに多くのコンパイラーエラーに遭遇しました。特に後半の章では、 どの言語でも遭遇するような、ランダムな「入力(コピペ)を間違えた」系のエラーは あまり見せません。これは、コンパイラーに怒鳴られることのガイド付きツアーです。
かなりゆっくり進めていきますし、正直なところ、ほぼずっとあまり真面目にはやりません。 プログラミングは楽しいものであるべきだと思うんです、まったく! もしあなたが、最大限に情報密度が高く、真面目で、形式的な内容を求めるタイプの人なら、 この本はあなた向けではありません。私がこれから作るものは何ひとつ、あなた向けではありません。 あなたは間違っています。
義務的な公共広告
完全に100%明確にしておくと、私は連結リストが嫌いです。 心の底から。連結リストはひどいデータ構造です。もちろん、連結リストには いくつか素晴らしいユースケースがあります。
- 大きなリストの分割やマージを大量に行いたい。大量に。
- すごいロックフリー並行処理の何かをやっている。
- カーネル/組み込み系のものを書いていて、侵入的リストを使いたい。
- 純粋関数型言語を使っていて、限定された意味論と変更の不在によって 連結リストのほうが扱いやすくなっている。
- … などなど!
しかし、Rustプログラムを書く人にとって、これらのケースはどれも非常にまれです。 99%の場合はVec(配列スタック)を使うべきで、残りの1%のうち99%の場合は VecDeque(配列デック)を使うべきです。これらは、アロケーション頻度の低さ、 メモリオーバーヘッドの低さ、真のランダムアクセス、キャッシュ局所性により、 ほとんどのワークロードに対して明らかに優れたデータ構造です。
連結リストは、トライと同じくらいニッチで曖昧なデータ構造です。トライは、 平均的なプログラマーが生産的なキャリア全体を通じて知らなくても幸せに過ごせる ニッチな構造だと私が主張しても、反発する人はほとんどいないでしょう。にもかかわらず、 連結リストには何やら奇妙な有名人のような地位があります。私たちはすべての学部生に 連結リストの書き方を教えています。これは、私がstd::collectionsから 排除できなかった唯一のニッチなコレクションです。 C++ではあのリストです!
私たちはコミュニティとして、「標準的な」データ構造としての連結リストにノーと言うべきです。 連結リストは、いくつかの素晴らしいユースケースを持つ立派なデータ構造ですが、 それらのユースケースは一般的ではなく、例外的なものです。
どうやら何人かの人は、この公共広告の最初の段落を読んだだけで、 そこで読むのをやめてしまうようです。文字どおり、私の素晴らしいユースケースのリストにある 項目の1つを挙げて、私の主張に反論しようとします。最初の段落のすぐ後にあるものです!
詳細な議論へ直接リンクできるように、私が目にした反論の試みをいくつか挙げ、 それらへの回答を示します。Rustを学びたいだけなら、 最初の章へ自由に進んでください!
パフォーマンスが常に重要とは限らない
そのとおりです!アプリケーションがI/Oバウンドかもしれませんし、問題のコードが まったく重要ではないコールドケースにあるのかもしれません。しかし、これは連結リストを 使うための議論ですらありません。これは何でも好きなものを使うための議論です。 なぜ連結リストで妥協するのでしょうか?連結ハッシュマップを使いましょう!
パフォーマンスが重要でないなら、配列という自然なデフォルトを適用しても 確実に問題ないはずです。
その場所へのポインターがあれば、O(1)で分割・追加・挿入・削除できる
そのとおりです!ただし、Bjarne Stroustrupが指摘しているように、 そのポインターを取得するのにかかる時間が、配列内のすべての要素を単にコピーするのに かかる時間(実際にはかなり速い)を完全に圧倒してしまうなら、これは実際には重要ではありません。
分割とマージのコストが非常に支配的なワークロードでない限り、キャッシュ効果とコードの複雑さにより それ以外すべての操作が受けるペナルティによって、理論上の利点は打ち消されます。
とはいえ、アプリケーションをプロファイリングした結果、分割とマージに多くの時間を費やしているなら、 連結リストで利益を得られる可能性はあります。
償却コストを受け入れられない
あなたはすでにかなりニッチな領域に入っています – ほとんどの用途では償却を許容できます。
それでも、配列のコストは最悪ケースで償却されます。配列を使っているからといって、
償却コストが発生するとは限りません。格納する要素数を予測できるなら
(あるいは上限だけでも分かっているなら)、必要な領域をすべて事前に予約できます。
私の経験では、必要になる要素数を予測できることは非常によくあります。特に Rust では、
まさにこの場合のために、すべてのイテレーターが size_hint を提供しています。
そうすれば、push と pop は真に O(1) の操作になります。そしてそれらは、
リンクリストでの push と pop よりもかなり高速になるでしょう。ポインターを
オフセットし、バイトを書き込み、整数をインクリメントするだけです。どんな種類の
アロケーターを呼び出す必要もありません。
低レイテンシとしてはどうでしょう?
しかし確かに、負荷を予測できない場合には、最悪ケースの レイテンシを削減できます!
リンクリストはスペースの無駄が少ない
さて、これは複雑です。「標準的な」配列のリサイズ戦略は、 配列の空きが最大でも半分になるように拡大または縮小することです。 これは確かに大量の無駄なスペースです。特に Rust では、コレクションを 自動的には縮小しません(どうせまた埋め戻すなら無駄だからです)。そのため、 無駄は無限大に近づき得ます!
しかし、これは最悪ケースのシナリオです。最良ケースでは、配列スタックには 配列全体でポインター3つ分のオーバーヘッドしかありません。基本的に オーバーヘッドはありません。
一方、リンクリストは要素ごとに無条件にスペースを浪費します。 単方向リンクリストはポインター1つ分を浪費し、双方向リンクリストは 2つ分を浪費します。配列とは異なり、相対的な無駄は要素のサイズに 比例します。巨大な要素なら、この無駄は 0 に近づきます。小さな 要素(たとえばバイト)なら、これは最大で 16 倍ものメモリオーバーヘッド (32 ビットでは 8 倍)になり得ます!
実際には、バイトにパディングが追加され、ノード全体のサイズがポインターに アラインされるため、23 倍(32 ビットでは 11 倍)に近くなります。
これはまた、アロケーターにとっての最良ケース、つまりノードの割り当てと 解放が密に行われており、断片化でメモリを失っていない場合を仮定しています。
しかし確かに、巨大な要素を扱い、負荷を予測できず、まともな アロケーターがあるなら、メモリを節約できます!
私は<関数型言語>でリンクリストを常用しています
素晴らしい!リンクリストは関数型言語では非常にエレガントに使えます。 なぜなら、一切の変更なしに操作でき、再帰的に記述でき、さらに遅延評価の 魔法によって無限リストも扱えるからです。
具体的には、リンクリストの良いところは、可変状態を必要とせずに反復を 表現できることです。次のステップは、単に次のサブリストを訪問するだけです。
Rust では、この種のことは主にイテレーターで行います。それらは無限にもでき、 関数型のリストと同じように map、filter、reverse、concatenate でき、 しかもすべてが同じように遅延的に行われます!
Rust では、部分配列を*スライス*として簡単に扱うこともできます。関数型言語でよく使う
head/tail の分割は、単に slice.split_at_mut(1)です。
長い間、Rust にはスライスに対するパターンマッチングの実験的な仕組みがあり、
それは非常にクールでしたが、その機能は安定化された際に簡略化されました。
それでも、基本的なスライスパターンは気が利いています!もちろん、
スライスはイテレーターに変換できます!
しかし確かに、イミュータブルなセマンティクスに制限されているなら、 リンクリストは非常に便利です。
なお、関数型プログラミングが必ずしも弱いとか悪いと言っているわけではありません。 しかし、それは根本的にセマンティクス上の制約があります。つまり、物事がどのように あるかについて語ることは主に許されますが、それらがどのように行われるべきかについては 語れません。これは実際には機能です。なぜなら、コンパイラーが大量の特殊な 変換を行えるようになり、あなたが心配しなくても、物事を行う最善の方法を 見つけられる可能性があるからです。しかしこれは、それについて心配することが できる余地を失う代償を伴います。たいていはエスケープハッチがありますが、 ある限界を超えると、結局また手続き型コードを書いているだけになります。
関数型言語であっても、実際にデータ構造が必要なときには、その仕事に適した データ構造を使うよう努めるべきです。確かに、単方向リンクリストは制御フローの 主要な道具ですが、大量のデータを実際に保存して問い合わせる方法としては、 本当に貧弱です。
リンクリストは並行データ構造を構築するのに優れています!
はい!とはいえ、並行データ構造を書くことは実際にはまったく別物であり、 軽く見るべきものではありません。間違いなく、多くの人が検討すらするような ものではありません。いったんそれが書かれてしまえば、あなたは実際には リンクリストを使うことを選んでいるわけでもありません。MPSC キューか 何かを使うことを選んでいるのです。この場合、実装戦略はかなり遠いところにあります!
しかし確かに、リンクリストはロックフリー並行性という暗黒の世界の デファクトの英雄です。
むにゃむにゃ、カーネル、組み込み、何とかかんとか、intrusive。
それはニッチです。自分の言語のランタイムすら使っていない状況について 話しているのです。それは、自分が何か奇妙なことをしているという危険信号では ないのでしょうか?
それはまた、非常に unsafe でもあります。
ですが、もちろん。スタック上に素晴らしいゼロアロケーションリストを構築してください。
イテレーターは無関係な挿入/削除によって無効化されない
それは繊細なダンスをしているようなものです。特に、ガベージコレクターが ない場合はそうです。詳細次第では、あなたの制御フローと所有権の パターンはおそらく少し絡まりすぎている、と私は主張するかもしれません。
しかし確かに、カーソルを使えば本当にクールでクレイジーなことができます。
リンクリストは単純で、教育に最適です!
まあ、そうですね。あなたはまさにその前提に捧げられた本を読んでいるのです。 単方向リンクリストはかなり単純です。双方向リンクリストは、 これから見るように、少し厄介になり得ます。
一息つきましょう
さて。これで片付きました。山ほどリンクリストを書きましょう。
悪い単方向リンクスタック
これは群を抜いて最長になります。基本的に Rust のすべてを紹介する必要があり、言語をより深く理解するために、いくつかのものを「あえて難しい方法で」構築していくからです。
最初のリストは src/first.rs に置きます。first.rs が私たちの lib で使うものだと Rust に伝える必要があります。そのために必要なのは、Cargo が作成してくれた src/lib.rs の先頭にこれを置くことだけです。
// lib.rs 内
pub mod first;
基本的なデータレイアウト
さて、連結リストとは何でしょうか?基本的には、ヒープ上のデータの断片(カーネルの人たちは黙っていてください!)が、順番に互いを指しているものです。連結リストは、手続き型プログラマーが絶対に手を出すべきではないものであり、関数型プログラマーがあらゆることに使うものです。であれば、連結リストの定義は関数型プログラマーに尋ねるのが公平でしょう。おそらく、次のような定義を示してくれるはずです。
List a = Empty | Elem a (List a)
これはおおよそ「List は Empty であるか、Element の後に List が続くものである」と読めます。これは 直和型 として表現された再帰的な定義です。直和型とは、「異なる値を持つことができ、その値が異なる型である場合もある型」を表す気取った名前です。Rust は直和型を enum と呼びます!C 風の言語から来た人にとっては、これはまさに皆さんが知っていて大好きな enum ですが、さらに強化されたものです。では、この関数型の定義を Rust に写してみましょう!
今のところは、話を簡単にするためにジェネリクスを避けます。符号付き 32 ビット整数の格納だけをサポートします。
// first.rs 内
// pub は、このモジュールの外部から List を使えるようにしたいことを示します
pub enum List {
Empty,
Elem(i32, List),
}
ふぅ、もう手一杯です。とりあえずこれをコンパイルしてみましょう。
> cargo build
error[E0072]: recursive type `first::List` has infinite size
--> src/first.rs:4:1
|
4 | pub enum List {
| ^^^^^^^^^^^^^ recursive type has infinite size
5 | Empty,
6 | Elem(i32, List),
| ---- recursive without indirection
|
= help: insert indirection (e.g., a `Box`, `Rc`, or `&`) at some point to make `first::List` representable
さて。皆さんはどうかわかりませんが、私は確かに関数型プログラミングコミュニティに裏切られた気分です。
実際にエラーメッセージを確認してみると(裏切られた件を乗り越えた後で)、rustc はこの問題を解決する方法を正確に教えてくれていることがわかります。
insert indirection (e.g., a
Box,Rc, or&) at some point to makefirst::Listrepresentable
なるほど、box。それは何でしょう?rust box でググってみましょう……
どれどれ……
pub struct Box<T>(_);ヒープ割り当てのためのポインター型。 詳細については、モジュールレベルのドキュメントを参照してください。
リンクをクリック
Box<T>は、気軽に「box」と呼ばれ、Rust における最も単純なヒープ割り当ての形を提供します。Box はこの割り当ての所有権を提供し、スコープを抜けるとその内容を drop します。例
box の作成:
let x = Box::new(5);再帰的なデータ構造の作成:
#![allow(unused)]
fn main() {
#[derive(Debug)]
enum List<T> {
Cons(T, Box<List<T>>),
Nil,
}
}
fn main() {
let list: List<i32> = List::Cons(1, Box::new(List::Cons(2, Box::new(List::Nil))));
println!("{:?}", list);
}
これは
Cons(1, Box(Cons(2, Box(Nil))))を出力します。再帰的な構造は boxed にする必要があります。なぜなら、Cons の定義が次のようになっていると、
Cons(T, List<T>),動作しないからです。これは、List のサイズがリスト内の要素数に依存するため、Cons にどれだけのメモリを割り当てればよいかわからないからです。定義されたサイズを持つ Box を導入することで、Cons がどれだけ大きくある必要があるかがわかります。
うわ、ええと。これは私が今まで見た中で、おそらく最も関連性が高く、役に立つドキュメントです。ドキュメントの最初にある内容が、文字どおり 私たちが書こうとしているもの、それが動作しなかった理由、そして修正方法そのもの です。
いやあ、ドキュメント最高ですね。
では、そのようにしてみましょう。
pub enum List {
Empty,
Elem(i32, Box<List>),
}
> cargo build
Finished dev [unoptimized + debuginfo] target(s) in 0.22s
やった、ビルドできました!
……しかし、これは実際には List の定義としてかなり愚かです。理由はいくつかあります。
2 つの要素を持つリストを考えてみましょう。
[] = Stack
() = Heap
[Elem A, ptr] -> (Elem B, ptr) -> (Empty, *junk*)
重要な問題が 2 つあります。
- 「私は実際には Node ではありません」と言っているだけのノードを割り当てている
- ノードの 1 つがまったくヒープ割り当てされていない
表面的には、この 2 つは互いに打ち消し合っているように見えます。余分なノードをヒープ割り当てしていますが、ノードの 1 つはまったくヒープ割り当てする必要がありません。しかし、リストの次のような潜在的なレイアウトを考えてみてください。
[ptr] -> (Elem A, ptr) -> (Elem B, *null*)
このレイアウトでは、ノードを常にヒープ割り当てするようになります。重要な違いは、最初のレイアウトにあった junk が存在しないことです。この junk とは何でしょうか?それを理解するには、enum がメモリ上でどのようにレイアウトされるかを見る必要があります。
一般に、次のような enum があるとします。
enum Foo {
D1(T1),
D2(T2),
...
Dn(Tn),
}
Foo は、それが enum のどの バリアント を表しているか(D1、D2、.. Dn)を示すために、何らかの整数を格納する必要があります。これが enum の タグ です。また、T1、T2、.. Tn のうち 最大 のものを格納するのに十分な領域も必要になります(さらにアラインメント要件を満たすための追加領域も必要です)。
ここでの大きなポイントは、Empty が 1 ビットの情報でしかないにもかかわらず、ポインターと要素のために十分な領域を必然的に消費するということです。なぜなら、いつでも Elem になれるようにしておく必要があるからです。したがって、最初のレイアウトは、junk でいっぱいの余分な要素をヒープ割り当てし、2 つ目のレイアウトより少し多くの領域を消費します。
ノードの 1 つがまったく割り当てられていないことも、おそらく意外なことに、常に割り当てるよりも 悪い です。これは、ノードのレイアウトが 不均一 になるためです。これはノードの push や pop にはあまり目立った影響を与えませんが、リストの分割と結合には影響します。
両方のレイアウトでリストを分割することを考えてみましょう。
layout 1:
[Elem A, ptr] -> (Elem B, ptr) -> (Elem C, ptr) -> (Empty *junk*)
split off C:
[Elem A, ptr] -> (Elem B, ptr) -> (Empty *junk*)
[Elem C, ptr] -> (Empty *junk*)
layout 2:
[ptr] -> (Elem A, ptr) -> (Elem B, ptr) -> (Elem C, *null*)
split off C:
[ptr] -> (Elem A, ptr) -> (Elem B, *null*)
[ptr] -> (Elem C, *null*)
レイアウト 2 の分割では、B のポインターをスタックにコピーし、古い値を null にするだけです。レイアウト 1 も最終的には同じことをしますが、それに加えて C をヒープからスタックへコピーする必要があります。結合はその逆の手順です。
連結リストの数少ない良い点の 1 つは、要素をノード自体の中で構築でき、その後それを実際に移動することなく、リスト間で自由に入れ替えられることです。ポインターを少しいじるだけで、ものが「移動」します。レイアウト 1 はこの性質を台無しにします。
さて、レイアウト 1 が悪いということにはそれなりに納得しました。では、List をどのように書き直せばよいでしょうか?たとえば、次のようにできるかもしれません。
pub enum List {
Empty,
ElemThenEmpty(i32),
ElemThenNotEmpty(i32, Box<List>),
}
これがさらに悪いアイデアに見えることを願います。特に注目すべきなのは、完全に無効な状態が存在するようになるため、ロジックが本当に複雑になることです。つまり、ElemThenNotEmpty(0, Box(Empty)) です。また、要素の割り当てが不均一であるという問題も 依然として 抱えています。
ただし、これには興味深い性質が1つあります。Empty のケースをアロケートすることを完全に避け、
ヒープアロケーションの総数を 1 つ減らします。残念ながら、
そうすることでさらに多くの空間を無駄にしてしまいます!これは、以前の
レイアウトがnull ポインター最適化を活用していたためです。
以前見たように、すべての enum は、そのビットが enum のどのバリアントを 表しているのかを指定するためにタグを格納する必要があります。しかし、 次のような特別な種類の enum がある場合は:
enum Foo {
A,
B(ContainsANonNullPtr),
}
null ポインター最適化が働き、タグに必要な空間が不要になります。
バリアントが A の場合、enum 全体がすべて 0 に設定されます。そうでなければ、
バリアントは B です。B は非ゼロポインターを含むため、決してすべて 0 には
なり得ないので、これが機能します。見事ですね!
この種の最適化が可能な他の enum や型を思いつきますか?
実際にはたくさんあります!これが、Rust が enum のレイアウトを完全に未指定のままにしている理由です。
Rust は、もう少し複雑な enum レイアウト最適化をいくつか行ってくれますが、
null ポインターのものは間違いなく最も重要です!
これは、&、&mut、Box、Rc、Arc、Vec、そして
Rust の他のいくつかの重要な型が Option に入れられてもオーバーヘッドを持たないことを意味します!
(これらの大半については、いずれ扱います。)
では、どうすれば余分なガラクタを避け、均一にアロケートし、さらにあの素晴らしい null ポインター最適化を得られるのでしょうか?要素を持つという考え方と、 別のリストをアロケートするという考え方を、もっと明確に分離する必要があります。 そのためには、もう少し C っぽく考える必要があります。つまり struct です!
enum が複数の値のうち1つを含められる型を宣言できるのに対して、 struct は複数の値を同時に含む型を宣言できます。List を 2 つの型に分けましょう。 List と Node です。
以前と同じように、List は Empty であるか、要素の後に別の List が続くものです。 「要素の後に別の List が続く」ケースをまったく別の型で表現することで、 Box をより最適な位置に持ち上げることができます:
struct Node {
elem: i32,
next: List,
}
pub enum List {
Empty,
More(Box<Node>),
}
優先事項を確認しましょう:
- リストの末尾が余分なガラクタをアロケートしない: チェック!
enumが素晴らしい null ポインター最適化形式になっている: チェック!- すべての要素が均一にアロケートされる: チェック!
よし!実際のところ、最初のレイアウト(公式 Rust ドキュメントで提案されているもの)が 問題を抱えていることを示すために使ったレイアウトを、まさにそのまま構築したことになります。
> cargo build
warning: private type `first::Node` in public interface (error E0446)
--> src/first.rs:8:10
|
8 | More(Box<Node>),
| ^^^^^^^^^
|
= note: #[warn(private_in_public)] on by default
= warning: this was previously accepted by the compiler but
is being phased out; it will become a hard error in a future release!
:(
Rust がまた怒っています。私たちは List を public にしました(人々に
使えるようにしたいからです)が、Node は public にしていません。問題は、
enum の内部は完全に public であり、private な型について public に語ることは
許されないという点です。Node 全体を完全に public にすることもできますが、
一般に Rust では、実装の詳細は private に保つことが好まれます。List を struct にして、
実装の詳細を隠せるようにしましょう:
pub struct List {
head: Link,
}
enum Link {
Empty,
More(Box<Node>),
}
struct Node {
elem: i32,
next: Link,
}
List はフィールドを 1 つだけ持つ struct なので、そのサイズはそのフィールドと同じです。
ゼロコスト抽象化、やった!
> cargo build
warning: field is never used: `head`
--> src/first.rs:2:5
|
2 | head: Link,
| ^^^^^^^^^^
|
= note: #[warn(dead_code)] on by default
warning: variant is never constructed: `Empty`
--> src/first.rs:6:5
|
6 | Empty,
| ^^^^^
warning: variant is never constructed: `More`
--> src/first.rs:7:5
|
7 | More(Box<Node>),
| ^^^^^^^^^^^^^^^
warning: field is never used: `elem`
--> src/first.rs:11:5
|
11 | elem: i32,
| ^^^^^^^^^
warning: field is never used: `next`
--> src/first.rs:12:5
|
12 | next: Link,
| ^^^^^^^^^^
よし、コンパイルできました!Rust はかなり怒っています。というのも、Rust から見ると、
私たちが書いたものはすべて完全に役立たずだからです。私たちは head を一度も使っておらず、
私たちのライブラリを使う人も、それが private なので使えません。推移的に、
これは Link と Node も役立たずであることを意味します。では、それを解決しましょう!
List のためのコードをいくつか実装してみましょう!
New
実際のコードを型に関連付けるには、impl ブロックを使います。
impl List {
// TODO、コードが動くようにする
}
次に必要なのは、実際にコードを書く方法を理解することです。Rust では 次のように関数を宣言します。
fn foo(arg1: Type1, arg2: Type2) -> ReturnType {
// 本体
}
最初に欲しいのは、リストを構築する方法です。実装の詳細は隠しているため、
それを関数として提供する必要があります。Rust でこれを行う通常の方法は、
静的メソッドを提供することです。これは impl の中にある
通常の関数にすぎません。
impl List {
pub fn new() -> Self {
List { head: Link::Empty }
}
}
これについていくつか注意点があります。
- Self は「
implの横の上部に書いたあの型」の別名です。同じことを 繰り返さずに済むので便利です! - 構造体のインスタンスは、構造体を宣言する場合とほぼ同じ方法で作成します。ただし、 フィールドの型を指定する代わりに、値で初期化します。
- enum のバリアントは、名前空間演算子である
::を使って参照します。 - 関数の最後の式は暗黙的に返されます。
これにより、単純な関数を少しすっきり書けます。他の C 系言語と同じように、
returnを使って早期に返すこともできます。
所有権 101
リストを構築できるようになったので、それを使って何かを行えるとよいでしょう。
それは「通常の」(非 static)メソッドで行います。Rust においてメソッドは関数の特殊な
ケースです。なぜなら、宣言された型を持たない self 引数があるからです。
fn foo(self, arg2: Type2) -> ReturnType {
// 本体
}
self が取り得る主な形式は 3 つあります: self、&mut self、&self です。
これら 3 つの形式は、Rust における所有権の主な 3 つの形式を表します。
self- 値&mut self- 可変参照&self- 共有参照
値は真の所有権を表します。値に対しては好きなことを何でもできます。
移動したり、破棄したり、変更したり、参照を通じて貸し出したりできます。何かを値渡しすると、
それは新しい場所へ移動されます。新しい場所がその値を所有するようになり、
古い場所からはもうそれにアクセスできなくなります。このため、ほとんどのメソッドは
self を望みません。リストを操作しようとしただけでそれが消えてしまうとしたら、
かなり残念でしょう!
可変参照は、自分が所有していない値への一時的な排他的アクセスを表します。
可変参照を持っている値に対しては、作業を終えたときにそれを有効な状態のままにしておく限り、
本当に好きなことを何でも行えます(そうしないと所有者に失礼でしょう!)。これは、実際に値を完全に
上書きできることを意味します。この非常に有用な特殊ケースが、ある値を別の値と入れ替えることです。
これはこれから頻繁に使います。&mut でできない唯一のことは、代わりの値を置かずに
値を取り出して移動することです。&mut self は、self を変更したいメソッドに最適です。
共有参照は、自分が所有していない値への一時的な共有アクセスを表します。
共有アクセスを持っているため、通常は何かを変更することは許されません。
& は、その値を博物館の展示に出すようなものだと考えてください。
& は、self を観察するだけのメソッドに最適です。
後ほど、変更に関するこのルールは特定の場合に回避できることを見ていきます。 これが、共有参照が不変参照と呼ばれない理由です。実際には、 可変参照は一意参照と呼ぶこともできますが、所有権を可変性と関連付けると、 99% の場合に正しい直感が得られることがわかっています。
Push
それでは、リストに値をプッシュする処理を書いてみましょう。push はリストを変更するため、
&mut self を受け取る必要があります。また、プッシュする i32 も受け取る必要があります。
impl List {
pub fn push(&mut self, elem: i32) {
// TODO
}
}
まず最初に、要素を格納するノードを作る必要があります。
pub fn push(&mut self, elem: i32) {
let new_node = Node {
elem: elem,
next: ?????
};
}
next には何が入るのでしょうか? そう、古いリスト全体です! これを……そのままやってしまえるでしょうか?
impl List {
pub fn push(&mut self, elem: i32) {
let new_node = Node {
elem: elem,
next: self.head,
};
}
}
> cargo build
error[E0507]: cannot move out of borrowed content
--> src/first.rs:19:19
|
19 | next: self.head,
| ^^^^^^^^^ cannot move out of borrowed content
だめでした。Rust が言っていることは正しいのですが、それが具体的に何を意味しているのか、 あるいはどう対処すべきなのかは、確かに明白ではありません。
借用された内容の外へ move することはできません
私たちは self.head フィールドを next に move しようとしていますが、Rust はそれを許してくれません。
そうすると、借用を終えて正当な所有者に「返す」ときに、self が部分的にしか初期化されていない状態になってしまいます。
前にも述べたように、それは &mut でできない唯一のことです。そんなことをするのは非常に失礼ですし、
Rust はとても礼儀正しいのです(信じられないほど危険でもありますが、もちろん Rust が気にしている理由が
それであるはずはありません)。
何かを戻し入れたらどうでしょう? 具体的には、今作っているノードです。
pub fn push(&mut self, elem: i32) {
let new_node = Box::new(Node {
elem: elem,
next: self.head,
});
self.head = Link::More(new_node);
}
> cargo build
error[E0507]: cannot move out of borrowed content
--> src/first.rs:19:19
|
19 | next: self.head,
| ^^^^^^^^^ cannot move out of borrowed content
うまくいきません。原理的には、これは Rust が実際に受け入れることもできそうなものですが、 受け入れてはくれません(さまざまな理由があります – 最も深刻なのは exception safety です)。 Rust にそれがなくなったと気づかれないように head を取り出す何らかの方法が必要です。助言を求めて、 悪名高い Rust ハッカー、インディアナ・ジョーンズに頼ってみましょう。

なるほど、インディーは mem::replace 作戦を提案しています。この非常に便利な関数を使うと、
ある値を別の値で置き換えることによって、借用の中から値を盗み出せます。
まず、ファイルの先頭で std::mem を取り込んで、mem がローカルスコープに入るようにしましょう。
use std::mem;
そして適切に使います。
pub fn push(&mut self, elem: i32) {
let new_node = Box::new(Node {
elem: elem,
next: mem::replace(&mut self.head, Link::Empty),
});
self.head = Link::More(new_node);
}
ここでは、self.head をリストの新しい head で置き換える前に、一時的に Link::Empty で replace しています。
正直に言うと、これはやらなければならないこととしてはかなり残念なものです。
悲しいことに、私たちは(今のところ)そうしなければなりません。
とはいえ、これで push はすべて完了です! たぶん。正直なところ、たぶんテストすべきでしょう。
今のところ、それを行う最も簡単な方法はおそらく pop を書いて、正しい結果を生成することを確認することです。
Pop
push と同じように、pop はリストを変更したがります。push と違って、実際には
何かを返したいです。しかし pop は厄介なコーナーケースにも対処しなければ
なりません。リストが空だったらどうするのでしょうか? このケースを表現するために、
頼れる Option 型を使います。
pub fn pop(&mut self) -> Option<i32> {
// TODO
}
Option<T> は、存在するかもしれない値を表す enum です。これは
Some(T) または None のどちらかになります。Link で行ったように、
このために独自の enum を作ることもできますが、ユーザーには私たちの戻り値の型が
一体何なのかを理解してもらいたいですし、Option はあまりにも広く使われているので
誰もが 知っています。実際、これは非常に基本的なものなので、すべてのファイルで
暗黙的にスコープにインポートされますし、そのバリアントである Some と None も
同様です(そのため Option::None と書く必要はありません)。
Option<T> の山括弧の部分は、Option が実際には T に対して ジェネリック である
ことを示しています。つまり、任意の 型について Option を作れるということです!
さて、この Link というものがありますが、これが Empty なのか More を持っているのかは
どうやって判断するのでしょうか? match によるパターンマッチングです!
pub fn pop(&mut self) -> Option<i32> {
match self.head {
Link::Empty => {
// TODO
}
Link::More(node) => {
// TODO
}
};
}
> cargo build
error[E0308]: mismatched types
--> src/first.rs:27:30
|
27 | pub fn pop(&mut self) -> Option<i32> {
| --- ^^^^^^^^^^^ expected enum `std::option::Option`, found ()
| |
| this function's body doesn't return
|
= note: expected type `std::option::Option<i32>`
found type `()`
おっと、pop は値を返さなければなりませんが、まだそれをしていません。None を
返すことも できます が、この場合は、まだこの関数の実装が終わっていないことを
示すために unimplemented!() を返すほうがおそらくよいでしょう。
unimplemented!() はマクロ(! はマクロであることを示します)で、そこに到達すると
プログラムをパニックさせます(制御された形でクラッシュさせるようなものです)。
pub fn pop(&mut self) -> Option<i32> {
match self.head {
Link::Empty => {
// TODO
}
Link::More(node) => {
// TODO
}
};
unimplemented!()
}
無条件のパニックは、発散関数 の一例です。発散関数は呼び出し元に
決して戻らないため、任意の型の値が期待される場所で使うことができます。ここでは、
unimplemented!() が Option<T> 型の値の代わりに使われています。
また、プログラム内で return を書く必要がないことにも注意してください。関数内の
最後の式(基本的には行)が暗黙的にその戻り値になります。これにより、本当に単純な
ことを少し簡潔に表現できます。他の C 系言語と同じように、return を使って明示的に
早期リターンすることも常にできます。
> cargo build
error[E0507]: cannot move out of borrowed content
--> src/first.rs:28:15
|
28 | match self.head {
| ^^^^^^^^^
| |
| cannot move out of borrowed content
| help: consider borrowing here: `&self.head`
...
32 | Link::More(node) => {
| ---- data moved here
|
note: move occurs because `node` has type `std::boxed::Box<first::Node>`, which does not implement the `Copy` trait
--> src/first.rs:32:24
|
32 | Link::More(node) => {
| ^^^^
勘弁してくれ Rust、もう放っておいて! いつものように、Rust は私たちにめちゃくちゃ
怒っています。ありがたいことに、今回は詳細も全部教えてくれています! デフォルトでは、
パターンマッチはその中身を新しい分岐へムーブしようとしますが、ここでは self を
値として所有していないので、それはできません。
help: consider borrowing here: `&self.head`
Rust は、これを修正するために match に参照を追加すべきだと言っています。🤷♀️ 試してみましょう。
pub fn pop(&mut self) -> Option<i32> {
match &self.head {
Link::Empty => {
// TODO
}
Link::More(node) => {
// TODO
}
};
unimplemented!()
}
> cargo build
warning: unused variable: `node`
--> src/first.rs:32:24
|
32 | Link::More(node) => {
| ^^^^ help: consider prefixing with an underscore: `_node`
|
= note: #[warn(unused_variables)] on by default
warning: field is never used: `elem`
--> src/first.rs:13:5
|
13 | elem: i32,
| ^^^^^^^^^
|
= note: #[warn(dead_code)] on by default
warning: field is never used: `next`
--> src/first.rs:14:5
|
14 | next: Link,
| ^^^^^^^^^^
やった、またコンパイルできました! では、そのロジックを考えてみましょう。Option を
作りたいので、そのための変数を作りましょう。Empty の場合は None を返す必要があります。
More の場合は Some(i32) を返し、リストの head を変更する必要があります。では、
基本的にそれをやってみましょうか?
pub fn pop(&mut self) -> Option<i32> {
let result;
match &self.head {
Link::Empty => {
result = None;
}
Link::More(node) => {
result = Some(node.elem);
self.head = node.next;
}
};
result
}
> cargo build
Compiling lists v0.1.0 (/Users/ADesires/dev/temp/lists)
error[E0507]: cannot move out of borrowed content
--> src/first.rs:35:29
|
35 | self.head = node.next;
| ^^^^^^^^^ cannot move out of borrowed content
head
desk
共有参照しか持っていないのに、node からムーブしようとしています。
一歩戻って、何をしようとしているのか考えたほうがよさそうです。やりたいことは 次のとおりです。
- リストが空かどうかを確認する。
- 空なら、単に None を返す
- 空で ない なら
- リストの head を取り除く
- その
elemを取り出す - リストの head をその
nextに置き換える Some(elem)を返す
重要な洞察は、私たちはものを 取り除きたい ということです。つまり、リストの head を
値として 取得したいということです。&self.head から得られる共有参照を通じて、
それを行うことは絶対にできません。また、私たちは self への可変参照「しか」
持っていないので、何かをムーブする唯一の方法は、それを 置き換える ことです。
また Empty ダンスをすることになりそうです!
試してみましょう。
pub fn pop(&mut self) -> Option<i32> {
let result;
match mem::replace(&mut self.head, Link::Empty) {
Link::Empty => {
result = None;
}
Link::More(node) => {
result = Some(node.elem);
self.head = node.next;
}
};
result
}
cargo build
Finished dev [unoptimized + debuginfo] target(s) in 0.22s
O M G
どんな 警告もなくコンパイルできました!!!!!
実は、ここで個人的な lint を適用します。返すためにこの result という値を
作りましたが、実際にはまったく必要ありませんでした! 関数が最後の式として
評価されるのと同じように、すべてのブロックもその最後の式として評価されます。
通常はセミコロンでこの振る舞いを抑制します。その場合、ブロックは代わりに空の
タプル () として評価されます。これは実際、push のように戻り値を宣言しない
関数が返す値です。
したがって、代わりに pop は次のように書けます。
pub fn pop(&mut self) -> Option<i32> {
match mem::replace(&mut self.head, Link::Empty) {
Link::Empty => None,
Link::More(node) => {
self.head = node.next;
Some(node.elem)
}
}
}
こちらのほうが少し簡潔で慣用的です。Link::Empty 分岐では波括弧が完全になくなっている ことに注意してください。評価する式が 1 つしかないからです。単純なケースのための、 ちょっと便利な省略記法です。
cargo build
Finished dev [unoptimized + debuginfo] target(s) in 0.22s
いいですね、まだ動きます!
テスト
さて、push と pop を書けたので、これで実際にスタックをテストできます! Rust と cargo はテストを第一級の機能としてサポートしているので、これはとても簡単です。必要なのは、関数を書いて、それに #[test] を付けるだけです。
一般に、Rust コミュニティでは、テストはそのテスト対象のコードの近くに置くようにしています。ただし通常は、「本物の」コードと衝突しないように、テスト用に新しい名前空間を作ります。first.rs を lib.rs に含めることを指定するために mod を使ったのと同じように、mod を使って、基本的には新しいファイル全体をインラインで作成できます。
// first.rs 内
mod test {
#[test]
fn basics() {
// TODO
}
}
そして、cargo test で実行します。
> cargo test
Compiling lists v0.1.0 (/Users/ADesires/dev/temp/lists)
Finished dev [unoptimized + debuginfo] target(s) in 1.00s
Running /Users/ADesires/dev/lists/target/debug/deps/lists-86544f1d97438f1f
running 1 test
test first::test::basics ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
; 0 filtered out
やった、何もしないテストが通りました! では、何もしないテストではなくしましょう。それには assert_eq! マクロを使います。これは特別なテスト用の魔法ではありません。やっていることは、渡した 2 つのものを比較し、一致しなければプログラムをパニックさせるだけです。そう、テストハーネスには、パニックを起こすことで失敗を示すのです!
mod test {
#[test]
fn basics() {
let mut list = List::new();
// 空のリストが正しく振る舞うことを確認
assert_eq!(list.pop(), None);
// リストに要素を入れる
list.push(1);
list.push(2);
list.push(3);
// 通常の削除を確認
assert_eq!(list.pop(), Some(3));
assert_eq!(list.pop(), Some(2));
// 何も壊れていないことを確認するため、さらにいくつか push する
list.push(4);
list.push(5);
// 通常の削除を確認
assert_eq!(list.pop(), Some(5));
assert_eq!(list.pop(), Some(4));
// 使い切った状態を確認
assert_eq!(list.pop(), Some(1));
assert_eq!(list.pop(), None);
}
}
> cargo test
error[E0433]: failed to resolve: use of undeclared type or module `List`
--> src/first.rs:43:24
|
43 | let mut list = List::new();
| ^^^^ use of undeclared type or module `List`
おっと! 新しいモジュールを作ったので、List を使うには明示的に取り込む必要があります。
mod test {
use super::List;
// それ以外はすべて同じ
}
> cargo test
warning: unused import: `super::List`
--> src/first.rs:45:9
|
45 | use super::List;
| ^^^^^^^^^^^
|
= note: #[warn(unused_imports)] on by default
Finished dev [unoptimized + debuginfo] target(s) in 0.43s
Running /Users/ADesires/dev/lists/target/debug/deps/lists-86544f1d97438f1f
running 1 test
test first::test::basics ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
; 0 filtered out
やった!
でも、あの警告はどういうことでしょう……? テストの中で明らかに List を使っています!
……ただしテスト時だけです! コンパイラを納得させるために(そして利用者に親切にするために)、test モジュール全体はテストを実行している場合にのみコンパイルされるべきだと示す必要があります。
#[cfg(test)]
mod test {
use super::List;
// それ以外はすべて同じ
}
テストについてはこれで全部です!
Drop
スタックを作り、そこに push し、そこから pop できるようになり、しかもそれがすべて正しく動くことまでテストできました!
リストのクリーンアップについて心配する必要はあるでしょうか?技術的には、まったくありません!C++ と同様に、Rust はリソースが使い終わったときに自動的にクリーンアップするためにデストラクタを使用します。ある型が Drop というトレイトを実装している場合、その型にはデストラクタがあります。トレイトとは、Rust におけるインターフェイスの気取った呼び方です。Drop トレイトは次のインターフェイスを持ちます。
pub trait Drop {
fn drop(&mut self);
}
基本的には、「スコープを抜けるときに、後始末をする時間を少しあげます」ということです。
Drop を実装している型を自分の中に含んでいて、やりたいことがそれらのデストラクタを呼ぶことだけであるなら、実際には Drop を実装する必要はありません。List の場合、やりたいことは head を drop することだけであり、それによって今度は Box<Node> を drop しようとするかもしれません。それらはすべて自動的に処理されます……ただし、ひとつ問題があります。
その自動処理はまずいことになります。
単純なリストを考えてみましょう。
list -> A -> B -> C
list が drop されると、A を drop しようとし、A は B を drop しようとし、B は C を drop しようとします。みなさんの中には、当然ながら不安になっている人もいるかもしれません。これは再帰コードであり、再帰コードはスタックを吹き飛ばす可能性があります!
みなさんの中には、「これは明らかに末尾再帰であり、まともな言語ならそのようなコードでスタックが吹き飛ばないことを保証するだろう」と考えている人もいるかもしれません。実は、これは間違いです!その理由を見るために、コンパイラが行わなければならないことを、コンパイラが行うように私たちの List に対して手動で Drop を実装することで書いてみましょう。
impl Drop for List {
fn drop(&mut self) {
// 注: 実際の Rust コードでは `drop` を明示的に呼び出すことはできません。
// ここではコンパイラのふりをしています!
self.head.drop(); // 末尾再帰 - 良い!
}
}
impl Drop for Link {
fn drop(&mut self) {
match *self {
Link::Empty => {} // 完了!
Link::More(ref mut boxed_node) => {
boxed_node.drop(); // 末尾再帰 - 良い!
}
}
}
}
impl Drop for Box<Node> {
fn drop(&mut self) {
self.ptr.drop(); // おっと、末尾再帰ではない!
deallocate(self.ptr);
}
}
impl Drop for Node {
fn drop(&mut self) {
self.next.drop();
}
}
Box の内容を解放した後で drop することはできないため、末尾再帰的な方法で drop する手段はありません!代わりに、List のために、ノードを Box から取り出す反復的な drop を手動で書く必要があります。
impl Drop for List {
fn drop(&mut self) {
let mut cur_link = mem::replace(&mut self.head, Link::Empty);
// `while let` == 「このパターンがマッチしなくなるまでこれを行う」
while let Link::More(mut boxed_node) = cur_link {
cur_link = mem::replace(&mut boxed_node.next, Link::Empty);
// boxed_node はここでスコープを抜け、drop されます。
// しかし、その Node の `next` フィールドは Link::Empty に設定されているため、
// 無制限の再帰は発生しません。
}
}
}
> cargo test
Running target/debug/lists-5c71138492ad4b4a
running 1 test
test first::test::basics ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured
すばらしい!

早すぎる最適化のためのボーナスセクション!
私たちの drop の実装は、実は while let Some(_) = self.pop() { } と非常に似ており、こちらのほうが確かに単純です。これはどのように違うのでしょうか。また、リストを整数以外のものを格納するように一般化し始めたとき、そこからどのようなパフォーマンス上の問題が生じる可能性があるでしょうか?
クリックして回答を展開
Pop は Option<i32> を返す一方で、私たちの実装は Link(Box<Node>)だけを操作します。つまり、私たちの実装はノードへのポインタだけを移動するのに対し、pop ベースの実装はノードに格納した値を移動します。リストを一般化し、誰かが VeryBigThingWithADropImpl(VBTWADI)のインスタンスを格納するために使う場合、これは非常に高コストになる可能性があります。Box はその内容の drop 実装をその場で実行できるため、この問題の影響を受けません。VBTWADI は、配列よりも連結リストを使うことが実際に望ましくなるまさにそのような種類のものなので、このケースで動作が悪いと少し残念です。
両方の実装の良いところを得たい場合は、新しいメソッド fn pop_node(&mut self) -> Link を追加できます。これにより、pop と drop の両方をきれいに導出できます。
最終的なコード
さて、6000語を費やした後で、実際に書くことができたコードはこれがすべてです:
#![allow(unused)]
fn main() {
use std::mem;
pub struct List {
head: Link,
}
enum Link {
Empty,
More(Box<Node>),
}
struct Node {
elem: i32,
next: Link,
}
impl List {
pub fn new() -> Self {
List { head: Link::Empty }
}
pub fn push(&mut self, elem: i32) {
let new_node = Box::new(Node {
elem: elem,
next: mem::replace(&mut self.head, Link::Empty),
});
self.head = Link::More(new_node);
}
pub fn pop(&mut self) -> Option<i32> {
match mem::replace(&mut self.head, Link::Empty) {
Link::Empty => None,
Link::More(node) => {
self.head = node.next;
Some(node.elem)
}
}
}
}
impl Drop for List {
fn drop(&mut self) {
let mut cur_link = mem::replace(&mut self.head, Link::Empty);
while let Link::More(mut boxed_node) = cur_link {
cur_link = mem::replace(&mut boxed_node.next, Link::Empty);
}
}
}
#[cfg(test)]
mod test {
use super::List;
#[test]
fn basics() {
let mut list = List::new();
// 空のリストが正しく振る舞うことを確認
assert_eq!(list.pop(), None);
// リストに要素を追加
list.push(1);
list.push(2);
list.push(3);
// 通常の削除を確認
assert_eq!(list.pop(), Some(3));
assert_eq!(list.pop(), Some(2));
// 何も壊れていないことを確認するために、さらにいくつか追加
list.push(4);
list.push(5);
// 通常の削除を確認
assert_eq!(list.pop(), Some(5));
assert_eq!(list.pop(), Some(4));
// 使い切った状態を確認
assert_eq!(list.pop(), Some(1));
assert_eq!(list.pop(), None);
}
}
}
まったく。80行もあって、その半分はテストでした!まあ、最初のこれは 時間がかかると言いましたからね!
そこそこの単方向リンクスタック
前の章では、必要最小限の単方向リンクスタックを書きました。 しかし、いくつかの設計上の判断のせいで、これは少しいまいちです。 もっといまいちでないものにしましょう。その過程で、次のことを行います。
- 車輪の再発明を取り消す
- リストが任意の要素型を扱えるようにする
- peek を追加する
- リストをイテレート可能にする
そして、その過程で次のことを学びます。
Optionの高度な使い方- ジェネリクス
- ライフタイム
- イテレータ
second.rs という新しいファイルを追加しましょう。
// lib.rs 内
pub mod first;
pub mod second;
そして、first.rs からすべてをコピーします。
Option を使う
特に観察眼の鋭い読者は、私たちが実際には Option のかなりひどいバージョンを再発明していたことに気づいたかもしれません。
enum Link {
Empty,
More(Box<Node>),
}
Link は単なる Option<Box<Node>> です。確かに、あちこちに
Option<Box<Node>> と書かずに済むのはよいことですし、pop とは違って、これを
外部に公開しているわけでもないので、問題ないかもしれません。しかし Option には、
私たちが手作業で実装してきたものの代わりになる、本当に素晴らしいメソッドがいくつかあります。それを自分たちで実装するのはやめて、
すべてを Options に置き換えましょう。まずは単純に、すべての名前を変更して Some と None を使うようにします。
use std::mem;
pub struct List {
head: Link,
}
// やった、型エイリアスだ!
type Link = Option<Box<Node>>;
struct Node {
elem: i32,
next: Link,
}
impl List {
pub fn new() -> Self {
List { head: None }
}
pub fn push(&mut self, elem: i32) {
let new_node = Box::new(Node {
elem: elem,
next: mem::replace(&mut self.head, None),
});
self.head = Some(new_node);
}
pub fn pop(&mut self) -> Option<i32> {
match mem::replace(&mut self.head, None) {
None => None,
Some(node) => {
self.head = node.next;
Some(node.elem)
}
}
}
}
impl Drop for List {
fn drop(&mut self) {
let mut cur_link = mem::replace(&mut self.head, None);
while let Some(mut boxed_node) = cur_link {
cur_link = mem::replace(&mut boxed_node.next, None);
}
}
}
これはわずかに良くなりましたが、大きな改善は Option のメソッドから得られます。
まず、mem::replace(&mut option, None) は信じられないほど
よくあるイディオムなので、Option は実際にこれをメソッドとして用意しています。それが take です。
pub struct List {
head: Link,
}
type Link = Option<Box<Node>>;
struct Node {
elem: i32,
next: Link,
}
impl List {
pub fn new() -> Self {
List { head: None }
}
pub fn push(&mut self, elem: i32) {
let new_node = Box::new(Node {
elem: elem,
next: self.head.take(),
});
self.head = Some(new_node);
}
pub fn pop(&mut self) -> Option<i32> {
match self.head.take() {
None => None,
Some(node) => {
self.head = node.next;
Some(node.elem)
}
}
}
}
impl Drop for List {
fn drop(&mut self) {
let mut cur_link = self.head.take();
while let Some(mut boxed_node) = cur_link {
cur_link = boxed_node.next.take();
}
}
}
次に、match option { None => None, Some(x) => Some(y) } は信じられないほど
よくあるイディオムなので、map と呼ばれるようになりました。map は関数を受け取り、
Some(x) の中の x に対してそれを実行し、Some(y) の中の y を生成します。正式な fn を書いて
それを map に渡すこともできますが、どうするかをインラインで書くほうがずっと好ましいです。
これを行う方法がクロージャです。クロージャは無名関数であり、
追加のすごい能力を持っています。それは、クロージャの外側にあるローカル変数を参照できることです!
これにより、あらゆる種類の条件付きロジックを書くのに非常に便利になります。
match を使っている唯一の場所は pop なので、そこだけを書き換えましょう。
pub fn pop(&mut self) -> Option<i32> {
self.head.take().map(|node| {
self.head = node.next;
node.elem
})
}
ああ、ずっと良くなりました。何も壊していないことを確認しましょう。
> cargo test
Running target/debug/lists-5c71138492ad4b4a
running 2 tests
test first::test::basics ... ok
test second::test::basics ... ok
test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured
素晴らしい!それでは、コードの振る舞いを実際に改善する作業に進みましょう。
すべてをジェネリックにする
これまでに Option と Box を使って、ジェネリクスについて少し触れてきました。しかしこれまでは、任意の要素に対して実際にジェネリックである新しい型を宣言することは避けてきました。
実は、これは本当に簡単です。今すぐすべての型をジェネリックにしましょう。
pub struct List<T> {
head: Link<T>,
}
type Link<T> = Option<Box<Node<T>>>;
struct Node<T> {
elem: T,
next: Link<T>,
}
すべてを少し山かっこ付きにするだけで、突然コードがジェネリックになります。もちろん、これだけを行うわけにはいきません。そうしないとコンパイラがものすごく怒ります。
> cargo test
error[E0107]: wrong number of type arguments: expected 1, found 0
--> src/second.rs:14:6
|
14 | impl List {
| ^^^^ expected 1 type argument
error[E0107]: wrong number of type arguments: expected 1, found 0
--> src/second.rs:36:15
|
36 | impl Drop for List {
| ^^^^ expected 1 type argument
問題はかなり明らかです。この List というものについて話していますが、それはもはや実在しません。Option や Box と同じように、これからは常に List<Something> について話さなければなりません。
では、これらすべての impl で使う Something は何でしょうか?List と同じように、実装もすべての T で動作してほしいのです。なので List と同じように、impl も山かっこ付きにしましょう。
impl<T> List<T> {
pub fn new() -> Self {
List { head: None }
}
pub fn push(&mut self, elem: T) {
let new_node = Box::new(Node {
elem: elem,
next: self.head.take(),
});
self.head = Some(new_node);
}
pub fn pop(&mut self) -> Option<T> {
self.head.take().map(|node| {
self.head = node.next;
node.elem
})
}
}
impl<T> Drop for List<T> {
fn drop(&mut self) {
let mut cur_link = self.head.take();
while let Some(mut boxed_node) = cur_link {
cur_link = boxed_node.next.take();
}
}
}
……これで終わりです!
> cargo test
Running target/debug/lists-5c71138492ad4b4a
running 2 tests
test first::test::basics ... ok
test second::test::basics ... ok
test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured
これで私たちのコードはすべて、T の任意の値に対して完全にジェネリックになりました。まったく、Rust は簡単ですね。特に称賛したいのは new です。これはまったく変更されていません。
pub fn new() -> Self {
List { head: None }
}
リファクタリングとコピペコーディングの守護者である Self の栄光に浸りましょう。また興味深い点として、list のインスタンスを構築するときに List<T> と書いていません。この部分は、List<T> を期待する関数からそれを返しているという事実に基づいて推論されます。
さて、まったく新しい振る舞いに進みましょう!
Peek
前回は、peek の実装にすら手を付けませんでした。では、やってしまいましょう。やるべきことは、リストの先頭にある要素への参照を、存在する場合に返すだけです。簡単そうですね。試してみましょう。
pub fn peek(&self) -> Option<&T> {
self.head.map(|node| {
&node.elem
})
}
> cargo build
error[E0515]: cannot return reference to local data `node.elem`
--> src/second.rs:37:13
|
37 | &node.elem
| ^^^^^^^^^^ returns a reference to data owned by the current function
error[E0507]: cannot move out of borrowed content
--> src/second.rs:36:9
|
36 | self.head.map(|node| {
| ^^^^^^^^^ cannot move out of borrowed content
はぁ。今度は何なんですか、Rust?
Map は self を値で受け取るため、その中にあるものから Option をムーブしてしまいます。以前は、ちょうどそれを take した後だったので問題ありませんでしたが、今回は実際には元の場所に残しておきたいのです。これを処理する正しい方法は、Option の as_ref メソッドを使うことです。このメソッドは次のように定義されています。
impl<T> Option<T> {
pub fn as_ref(&self) -> Option<&T>;
}
これは Option<T> を、その内部への参照の Option に降格します。明示的な match を使えば自分たちでもこれを行えますが、うんざりですね。余分な間接参照をたどるために追加の参照外しが必要になる、ということではありますが、ありがたいことに . 演算子がそれを処理してくれます。
pub fn peek(&self) -> Option<&T> {
self.head.as_ref().map(|node| {
&node.elem
})
}
cargo build
Finished dev [unoptimized + debuginfo] target(s) in 0.32s
決まりました。
このメソッドの可変版も、as_mut を使って作れます。
pub fn peek_mut(&mut self) -> Option<&mut T> {
self.head.as_mut().map(|node| {
&mut node.elem
})
}
> cargo build
楽勝です
テストするのを忘れないようにしましょう。
#[test]
fn peek() {
let mut list = List::new();
assert_eq!(list.peek(), None);
assert_eq!(list.peek_mut(), None);
list.push(1); list.push(2); list.push(3);
assert_eq!(list.peek(), Some(&3));
assert_eq!(list.peek_mut(), Some(&mut 3));
}
cargo test
Running target/debug/lists-5c71138492ad4b4a
running 3 tests
test first::test::basics ... ok
test second::test::basics ... ok
test second::test::peek ... ok
test result: ok. 3 passed; 0 failed; 0 ignored; 0 measured
いい感じです。しかし、その peek_mut の戻り値を実際にミューテートできるかどうかは、まだ本当にテストしていませんよね? 参照が可変であっても誰もそれをミューテートしないなら、その可変性を本当にテストしたと言えるのでしょうか? この Option<&mut T> に対して map を使い、意味深な値を入れてみましょう。
#[test]
fn peek() {
let mut list = List::new();
assert_eq!(list.peek(), None);
assert_eq!(list.peek_mut(), None);
list.push(1); list.push(2); list.push(3);
assert_eq!(list.peek(), Some(&3));
assert_eq!(list.peek_mut(), Some(&mut 3));
list.peek_mut().map(|&mut value| {
value = 42
});
assert_eq!(list.peek(), Some(&42));
assert_eq!(list.pop(), Some(42));
}
> cargo test
error[E0384]: cannot assign twice to immutable variable `value`
--> src/second.rs:100:13
|
99 | list.peek_mut().map(|&mut value| {
| -----
| |
| first assignment to `value`
| help: make this binding mutable: `mut value`
100 | value = 42
| ^^^^^^^^^^ cannot assign twice to immutable variable ^~~~~
コンパイラは value が不変だと文句を言っていますが、こちらはかなりはっきり &mut value と書いています。いったいどういうことでしょうか? 実は、クロージャの引数をこのように書いても、value が可変参照であることを指定したことにはなりません。代わりに、クロージャの引数に対してマッチされるパターンを作成します。|&mut value| は「引数は可変参照ですが、それが指している値を value にコピーしてください」という意味です。単に |value| を使えば、value の型は &mut i32 になり、実際に先頭をミューテートできます。
#[test]
fn peek() {
let mut list = List::new();
assert_eq!(list.peek(), None);
assert_eq!(list.peek_mut(), None);
list.push(1); list.push(2); list.push(3);
assert_eq!(list.peek(), Some(&3));
assert_eq!(list.peek_mut(), Some(&mut 3));
list.peek_mut().map(|value| {
*value = 42
});
assert_eq!(list.peek(), Some(&42));
assert_eq!(list.pop(), Some(42));
}
cargo test
Running target/debug/lists-5c71138492ad4b4a
running 3 tests
test first::test::basics ... ok
test second::test::basics ... ok
test second::test::peek ... ok
test result: ok. 3 passed; 0 failed; 0 ignored; 0 measured
ずっと良くなりました!
IntoIter
Rust では、コレクションは Iterator トレイトを使ってイテレートされます。これは Drop よりも少し複雑です。
pub trait Iterator {
type Item;
fn next(&mut self) -> Option<Self::Item>;
}
ここで新しく登場したのは type Item です。これは、Iterator のすべての実装が Item という関連型を持つことを宣言しています。この場合、これは next を呼び出したときに吐き出せる型です。
Iterator が Option<Self::Item> を返す理由は、このインターフェイスが has_next と get_next の概念をまとめているからです。次の値がある場合は
Some(value) を返し、ない場合は None を返します。これにより、API は一般に使いやすく安全に使用・実装できるようになり、同時に has_next と get_next の間の冗長なチェックやロジックを避けられます。いいですね!
残念ながら、Rust には yield 文のようなものは(まだ)ないので、そのロジックは自分で実装する必要があります。また、実際には各コレクションが実装するよう努めるべきイテレータが 3 種類あります。
- IntoIter -
T - IterMut -
&mut T - Iter -
&T
実のところ、List のインターフェイスを使って IntoIter を実装するための道具はすでにすべて揃っています。単に pop を何度も呼び出せばよいのです。そのため、IntoIter は List を包む newtype ラッパーとして実装するだけにします。
// タプル構造体は構造体の別形式で、
// 他の型の簡単なラッパーに便利です。
pub struct IntoIter<T>(List<T>);
impl<T> List<T> {
pub fn into_iter(self) -> IntoIter<T> {
IntoIter(self)
}
}
impl<T> Iterator for IntoIter<T> {
type Item = T;
fn next(&mut self) -> Option<Self::Item> {
// タプル構造体のフィールドには番号でアクセスする
self.0.pop()
}
}
そして、テストを書いてみましょう。
#[test]
fn into_iter() {
let mut list = List::new();
list.push(1); list.push(2); list.push(3);
let mut iter = list.into_iter();
assert_eq!(iter.next(), Some(3));
assert_eq!(iter.next(), Some(2));
assert_eq!(iter.next(), Some(1));
assert_eq!(iter.next(), None);
}
> cargo test
Running target/debug/lists-5c71138492ad4b4a
running 4 tests
test first::test::basics ... ok
test second::test::basics ... ok
test second::test::into_iter ... ok
test second::test::peek ... ok
test result: ok. 4 passed; 0 failed; 0 ignored; 0 measured
いいですね!
Iter
さて、Iter の実装を試してみましょう。今回は、List が欲しい機能をすべて提供してくれることには頼れません。自分たちで作る必要があります。必要な基本的なロジックは、次に yield したい現在のノードへのポインタを保持することです。そのノードは存在しない可能性があるため(リストが空である、またはすでに反復が完了している場合)、その参照は Option にしたいです。要素を yield したら、現在のノードの next ノードへ進みたいです。
では、これを試してみましょう。
pub struct Iter<T> {
next: Option<&Node<T>>,
}
impl<T> List<T> {
pub fn iter(&self) -> Iter<T> {
Iter { next: self.head.map(|node| &node) }
}
}
impl<T> Iterator for Iter<T> {
type Item = &T;
fn next(&mut self) -> Option<Self::Item> {
self.next.map(|node| {
self.next = node.next.map(|node| &node);
&node.elem
})
}
}
> cargo build
error[E0106]: missing lifetime specifier
--> src/second.rs:72:18
|
72 | next: Option<&Node<T>>,
| ^ expected lifetime parameter
error[E0106]: missing lifetime specifier
--> src/second.rs:82:17
|
82 | type Item = &T;
| ^ expected lifetime parameter
なんてことだ。ライフタイムだ。聞いたことはあります。悪夢だと聞いています。
新しいことを試してみましょう。あの error[E0106] というものが見えますか?これはコンパイラのエラーコードです。rustc に、ええと、--explain を使って説明してもらえます。
> rustc --explain E0106
このエラーは、型からライフタイムが欠落していることを示します。関数シグネチャ内でのエラーである場合、ライフタイム省略規則(以下を参照)に従っていないことが原因かもしれません。
このエラーに遭遇する場所の簡単な例をいくつか示します。
struct Foo { x: &bool } // エラー
struct Foo<'a> { x: &'a bool } // 正しい
enum Bar { A(u8), B(&bool), } // エラー
enum Bar<'a> { A(u8), B(&'a bool), } // 正しい
type MyStr = &str; // エラー
type MyStr<'a> = &'a str; //正しい
...
これは、ええと……あまり明確にはなりませんでした(これらのドキュメントは、私たちが現時点よりも Rust をよく理解している前提になっています)。しかし、構造体にこれらの 'a というものを追加すべきように見えますね。試してみましょう。
pub struct Iter<'a, T> {
next: Option<&'a Node<T>>,
}
> cargo build
error[E0106]: missing lifetime specifier
--> src/second.rs:83:22
|
83 | impl<T> Iterator for Iter<T> {
| ^^^^^^^ expected lifetime parameter
error[E0106]: missing lifetime specifier
--> src/second.rs:84:17
|
84 | type Item = &T;
| ^ expected lifetime parameter
error: aborting due to 2 previous errors
よし、だんだんパターンが見えてきました……可能な限りすべてに、この小さなやつらを追加してみましょう。
pub struct Iter<'a, T> {
next: Option<&'a Node<T>>,
}
impl<'a, T> List<T> {
pub fn iter(&'a self) -> Iter<'a, T> {
Iter { next: self.head.map(|node| &'a node) }
}
}
impl<'a, T> Iterator for Iter<'a, T> {
type Item = &'a T;
fn next(&'a mut self) -> Option<Self::Item> {
self.next.map(|node| {
self.next = node.next.map(|node| &'a node);
&'a node.elem
})
}
}
> cargo build
error: expected `:`, found `node`
--> src/second.rs:77:47
|
77 | Iter { next: self.head.map(|node| &'a node) }
| ---- while parsing this struct ^^^^ expected `:`
error: expected `:`, found `node`
--> src/second.rs:85:50
|
85 | self.next = node.next.map(|node| &'a node);
| ^^^^ expected `:`
error[E0063]: missing field `next` in initializer of `second::Iter<'_, _>`
--> src/second.rs:77:9
|
77 | Iter { next: self.head.map(|node| &'a node) }
| ^^^^ missing `next`
なんてことだ。Rust を壊してしまいました。
この 'a ライフタイムというものが一体何を意味しているのか、実際に理解すべきかもしれません。
ライフタイムは多くの人を怖がらせることがあります。なぜなら、プログラミングの黎明期から私たちが知り、愛してきた何かに対する変更だからです。実際には、ライフタイムはこの間ずっとプログラム全体に絡みついていたにもかかわらず、これまで私たちはなんとかライフタイムを避けてきました。
ガベージコレクションされる言語では、ライフタイムは不要です。なぜなら、ガベージコレクタが、すべてのものが必要なだけ魔法のように生存し続けることを保証してくれるからです。Rust のほとんどのデータは手動で管理されるため、そのデータには別の解決策が必要です。C と C++ は、スタック上のランダムなデータへのポインタを人々に自由に取らせると何が起きるのかを明確に示しています。すなわち、広範囲にわたる管理不能な安全性の欠如です。これは大まかに、次の 2 種類のエラーに分けられます。
- スコープを外れたものへのポインタを保持する
- 変更されて消えてしまったものへのポインタを保持する
ライフタイムはこれら両方の問題を解決します。そして 99% の場合、それは完全に透過的な方法で行われます。
では、ライフタイムとは何でしょうか?
非常に簡単に言えば、ライフタイムとは、プログラム内のどこかにあるコードの領域(~ブロック/スコープ)の名前です。それだけです。参照にライフタイムがタグ付けされている場合、その参照はその領域全体にわたって有効でなければならない、ということを意味しています。参照がどれだけ長く有効でなければならないか、また有効であり得るかについては、さまざまなものが要件を課します。ライフタイムシステム全体は、各参照の領域を最小化しようとする制約解決システムにすぎません。すべての制約を満たすライフタイムの組み合わせをうまく見つけられれば、プログラムはコンパイルされます!そうでなければ、何かが十分に長く生存しなかったというエラーが返ってきます。
関数本体の中では、一般的にライフタイムについて語ることはできませんし、いずれにせよそうしたいとも思わないでしょう。コンパイラは完全な情報を持っており、すべての制約を推論して最小のライフタイムを見つけることができます。しかし、型や API レベルでは、コンパイラはすべての情報を持っていません。コンパイラがあなたのやろうとしていることを理解できるように、異なるライフタイム間の関係を伝える必要があります。
原理的には、それらのライフタイムも省略できる可能性はありますが、そうするとすべての借用をチェックするには巨大なプログラム全体解析が必要になり、気が遠くなるほど非局所的なエラーを生み出すことになります。Rust のシステムでは、すべての借用チェックを各関数本体の中で独立して行うことができ、すべてのエラーはかなり局所的であるはずです(あるいは型のシグネチャが間違っています)。
しかし、これまで関数シグネチャに参照を書いたことがあり、それで問題ありませんでした!これは、非常によくある特定のケースでは、Rust が自動的にライフタイムを選んでくれるからです。これがライフタイム省略です。
具体的には次のようになります。
// 入力の参照は 1 つだけなので、出力はその入力から派生している必要がある
fn foo(&A) -> &B; // 以下の糖衣構文:
fn foo<'a>(&'a A) -> &'a B;
// 入力が多数ある場合、それらはすべて独立していると仮定する
fn foo(&A, &B, &C); // 以下の糖衣構文:
fn foo<'a, 'b, 'c>(&'a A, &'b B, &'c C);
// メソッドでは、すべての出力ライフタイムは `self` から派生していると仮定する
fn foo(&self, &B, &C) -> &D; // 以下の糖衣構文:
fn foo<'a, 'b, 'c>(&'a self, &'b B, &'c C) -> &'a D;
では、fn foo<'a>(&'a A) -> &'a B は何を 意味する のでしょうか?実用上の意味としては、入力は少なくとも出力と同じ長さだけ生存していなければならない、ということだけです。つまり、出力を長い間保持し続けるなら、入力が有効でなければならないリージョンも広がります。出力の使用をやめると、コンパイラは入力も無効になってよいことを認識します。
この仕組みが整っていれば、Rust は解放後に何も使用されないこと、そして未解放の参照が存在している間は何もミューテートされないことを保証できます。単にすべての制約がうまく成り立つようにしているだけです!
よし。では。Iter です。
ライフタイムがない状態まで巻き戻しましょう。
pub struct Iter<T> {
next: Option<&Node<T>>,
}
impl<T> List<T> {
pub fn iter(&self) -> Iter<T> {
Iter { next: self.head.map(|node| &node) }
}
}
impl<T> Iterator for Iter<T> {
type Item = &T;
fn next(&mut self) -> Option<Self::Item> {
self.next.map(|node| {
self.next = node.next.map(|node| &node);
&node.elem
})
}
}
関数シグネチャと型シグネチャにだけライフタイムを追加する必要があります。
// Iter は *何らかの* ライフタイムに対してジェネリックであり、それ自体は気にしない
pub struct Iter<'a, T> {
next: Option<&'a Node<T>>,
}
// ここにはライフタイムはない。List には関連付けられたライフタイムがない
impl<T> List<T> {
// ここでは、iter を作成する *まさにその* 借用のために
// 新しいライフタイムを宣言する。これで &self は Iter が存在する間、
// 有効である必要がある。
pub fn iter<'a>(&'a self) -> Iter<'a, T> {
Iter { next: self.head.map(|node| &node) }
}
}
// ここにはライフタイムが *ある*。Iter には定義する必要のあるライフタイムがあるため
impl<'a, T> Iterator for Iter<'a, T> {
// ここにも必要。これは型宣言である
type Item = &'a T;
// ここは何も変更する必要がない。上記によって処理される。
// Self は引き続き信じられないほど最高で素晴らしい
fn next(&mut self) -> Option<Self::Item> {
self.next.map(|node| {
self.next = node.next.map(|node| &node);
&node.elem
})
}
}
よし、今度こそできたと思います、みんな。
cargo build
error[E0308]: mismatched types
--> src/second.rs:77:22
|
77 | Iter { next: self.head.map(|node| &node) }
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^ expected struct `second::Node`, found struct `std::boxed::Box`
|
= note: expected type `std::option::Option<&second::Node<T>>`
found type `std::option::Option<&std::boxed::Box<second::Node<T>>>`
error[E0308]: mismatched types
--> src/second.rs:85:25
|
85 | self.next = node.next.map(|node| &node);
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^ expected struct `second::Node`, found struct `std::boxed::Box`
|
= note: expected type `std::option::Option<&'a second::Node<T>>`
found type `std::option::Option<&std::boxed::Box<second::Node<T>>>`
(╯°□°)╯︵ ┻━┻
OK。では。ライフタイムエラーは修正しましたが、今度はいくつか新しい型エラーが出ています。
格納したいのは &Node ですが、得られているのは &Box<Node> です。OK、これは十分簡単です。参照を取る前に Box をデリファレンスすればよいだけです。
impl<T> List<T> {
pub fn iter<'a>(&'a self) -> Iter<'a, T> {
Iter { next: self.head.map(|node| &*node) }
}
}
impl<'a, T> Iterator for Iter<'a, T> {
type Item = &'a T;
fn next(&mut self) -> Option<Self::Item> {
self.next.map(|node| {
self.next = node.next.map(|node| &*node);
&node.elem
})
}
}
cargo build
Compiling lists v0.1.0 (/Users/ADesires/dev/temp/lists)
error[E0515]: cannot return reference to local data `*node`
--> src/second.rs:77:43
|
77 | Iter { next: self.head.map(|node| &*node) }
| ^^^^^^ returns a reference to data owned by the current function
error[E0507]: cannot move out of borrowed content
--> src/second.rs:77:22
|
77 | Iter { next: self.head.map(|node| &*node) }
| ^^^^^^^^^ cannot move out of borrowed content
error[E0515]: cannot return reference to local data `*node`
--> src/second.rs:85:46
|
85 | self.next = node.next.map(|node| &*node);
| ^^^^^^ returns a reference to data owned by the current function
error[E0507]: cannot move out of borrowed content
--> src/second.rs:85:25
|
85 | self.next = node.next.map(|node| &*node);
| ^^^^^^^^^ cannot move out of borrowed content
(ノಥ益ಥ)ノ ┻━┻
as_ref を忘れていたので、box を map の中へムーブしてしまっています。つまり、それはドロップされることになり、つまり、私たちの参照はダングリングしてしまうということです。
pub struct Iter<'a, T> {
next: Option<&'a Node<T>>,
}
impl<T> List<T> {
pub fn iter<'a>(&'a self) -> Iter<'a, T> {
Iter { next: self.head.as_ref().map(|node| &*node) }
}
}
impl<'a, T> Iterator for Iter<'a, T> {
type Item = &'a T;
fn next(&mut self) -> Option<Self::Item> {
self.next.map(|node| {
self.next = node.next.as_ref().map(|node| &*node);
&node.elem
})
}
}
cargo build
Compiling lists v0.1.0 (/Users/ADesires/dev/temp/lists)
error[E0308]: mismatched types
--> src/second.rs:77:22
|
77 | Iter { next: self.head.as_ref().map(|node| &*node) }
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ expected struct `second::Node`, found struct `std::boxed::Box`
|
= note: expected type `std::option::Option<&second::Node<T>>`
found type `std::option::Option<&std::boxed::Box<second::Node<T>>>`
error[E0308]: mismatched types
--> src/second.rs:85:25
|
85 | self.next = node.next.as_ref().map(|node| &*node);
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ expected struct `second::Node`, found struct `std::boxed::Box`
|
= note: expected type `std::option::Option<&'a second::Node<T>>`
found type `std::option::Option<&std::boxed::Box<second::Node<T>>>`
😭
as_ref によって、取り除く必要のある間接参照の層がもう 1 つ追加されました。
pub struct Iter<'a, T> {
next: Option<&'a Node<T>>,
}
impl<T> List<T> {
pub fn iter<'a>(&'a self) -> Iter<'a, T> {
Iter { next: self.head.as_deref() }
}
}
impl<'a, T> Iterator for Iter<'a, T> {
type Item = &'a T;
fn next(&mut self) -> Option<Self::Item> {
self.next.map(|node| {
self.next = node.next.as_deref();
&node.elem
})
}
}
cargo build
🎉 🎉 🎉
as_deref と as_deref_mut 関数は、Rust 1.40 時点で安定化されています。それ以前は、
map(|node| &**node) と map(|node| &mut**node) を行う必要がありました。
「うわ、その &** ってやつ、本当にぎこちないな」と思っているかもしれませんが、それは間違っていません。
しかし、上質なワインのように Rust は時とともに良くなっていくので、もうそのようなことをする必要はありません。
通常、Rust はこの種の変換を暗黙的に行うのが非常に得意です。
これは deref coercion と呼ばれるプロセスを通じて行われ、基本的には型チェックが通るように
コード全体に * を挿入できます。これができるのは、ポインターを決しておかしくしないことを保証する
borrow checker があるからです!
しかし今回のケースでは、クロージャーと、&T ではなく Option<&T> を持っているという事実の組み合わせが
少し複雑すぎてうまく処理できないため、明示的に書いて手助けする必要があります。幸い、私の経験ではこれはかなりまれです。
完全を期すために言うと、turbofish を使って 別の ヒントを与えることもできます。
self.next = node.next.as_ref().map::<&Node<T>, _>(|node| &node);
見てのとおり、map はジェネリック関数です。
pub fn map<U, F>(self, f: F) -> Option<U>
ターボフィッシュ、::<> によって、これらのジェネリクスの型が何であるべきだと考えているかを
コンパイラに伝えられます。この場合、::<&Node<T>, _> は「&Node<T> を返すべきであり、
もう一方の型については知らない/気にしない」という意味です。
これにより、コンパイラは &node に deref coercion を適用すべきだと分かるため、
こちらで手動ですべての * を適用する必要がなくなります!
しかし今回のケースでは、これが本当に改善だとは思いません。これは単に、deref coercion と、 時には便利な turbofish を見せびらかすための、うっすらとした口実にすぎませんでした。😅
何もしていない状態になっていないことなどを確認するために、テストを書きましょう。
#[test]
fn iter() {
let mut list = List::new();
list.push(1); list.push(2); list.push(3);
let mut iter = list.iter();
assert_eq!(iter.next(), Some(&3));
assert_eq!(iter.next(), Some(&2));
assert_eq!(iter.next(), Some(&1));
}
> cargo test
Running target/debug/lists-5c71138492ad4b4a
running 5 tests
test first::test::basics ... ok
test second::test::basics ... ok
test second::test::into_iter ... ok
test second::test::iter ... ok
test second::test::peek ... ok
test result: ok. 4 passed; 0 failed; 0 ignored; 0 measured
やったぜ。
最後に、ここでは実際にライフタイム省略を適用できることに注意してください。
impl<T> List<T> {
pub fn iter<'a>(&'a self) -> Iter<'a, T> {
Iter { next: self.head.as_deref() }
}
}
これは次と同等です。
impl<T> List<T> {
pub fn iter(&self) -> Iter<T> {
Iter { next: self.head.as_deref() }
}
}
やった、ライフタイムが減りました!
あるいは、構造体がライフタイムを含んでいることを「隠す」のが気持ち悪い場合は、
Rust 2018 の「明示的に省略されたライフタイム」構文である '_ を使えます。
impl<T> List<T> {
pub fn iter(&self) -> Iter<'_, T> {
Iter { next: self.head.as_deref() }
}
}
IterMut
正直に言うと、IterMut はヤバいです。それ自体がかなり突飛な 言い方に思えるかもしれません。だって、Iter とまったく同じはずでしょう!
意味論的にはその通りです。しかし、共有参照と可変参照の性質により、 Iter は「自明」なのに対し、IterMut は本物の魔法です。
重要な洞察は、Iter に対する Iterator の実装から得られます。
impl<'a, T> Iterator for Iter<'a, T> {
type Item = &'a T;
fn next(&mut self) -> Option<Self::Item> { /* 処理 */ }
}
これは次のように脱糖できます。
impl<'a, T> Iterator for Iter<'a, T> {
type Item = &'a T;
fn next<'b>(&'b mut self) -> Option<&'a T> { /* 処理 */ }
}
next のシグネチャは、入力と出力のライフタイムの間に
何の 制約も設けていません!なぜこれが重要なのでしょうか?つまり、
無条件に next を何度でも呼び出せるということです!
let mut list = List::new();
list.push(1); list.push(2); list.push(3);
let mut iter = list.iter();
let x = iter.next().unwrap();
let y = iter.next().unwrap();
let z = iter.next().unwrap();
いいですね!
共有参照については、これは 間違いなく問題ありません。そもそも共有参照の要点は、 同時に大量に持てることだからです。しかし、可変参照は共存 できません。 可変参照の要点は、それが排他的であることです。
その結果、安全なコードを使って IterMut を書くのはかなり難しくなります (それがそもそも何を意味するのかについては、まだ触れていませんが……)。驚くことに、 IterMut は実際、多くの構造に対して完全に安全に実装できます!
まずは Iter のコードをそのまま取り、すべてを可変に変えるところから始めます。
pub struct IterMut<'a, T> {
next: Option<&'a mut Node<T>>,
}
impl<T> List<T> {
pub fn iter_mut(&self) -> IterMut<'_, T> {
IterMut { next: self.head.as_deref_mut() }
}
}
impl<'a, T> Iterator for IterMut<'a, T> {
type Item = &'a mut T;
fn next(&mut self) -> Option<Self::Item> {
self.next.map(|node| {
self.next = node.next.as_deref_mut();
&mut node.elem
})
}
}
> cargo build
error[E0596]: cannot borrow `self.head` as mutable, as it is behind a `&` reference
--> src/second.rs:95:25
|
94 | pub fn iter_mut(&self) -> IterMut<'_, T> {
| ----- help: consider changing this to be a mutable reference: `&mut self`
95 | IterMut { next: self.head.as_deref_mut() }
| ^^^^^^^^^ `self` is a `&` reference, so the data it refers to cannot be borrowed as mutable
error[E0507]: cannot move out of borrowed content
--> src/second.rs:103:9
|
103 | self.next.map(|node| {
| ^^^^^^^^^ cannot move out of borrowed content
さて、ここでは2つの異なるエラーが出ているようです。ただし最初のものはとても明確で、
修正方法まで教えてくれています!共有参照を可変参照へ格上げすることはできないので、
iter_mut は &mut self を受け取る必要があります。ただの愚かなコピペミスです。
pub fn iter_mut(&mut self) -> IterMut<'_, T> {
IterMut { next: self.head.as_deref_mut() }
}
もう一方はどうでしょうか?
おっと!実は前の節で iter の実装を書いたときに、うっかりミスをしていました。
それが動いていたのは、単に運が良かっただけです!
ここで初めて Copy の魔法に出会いました。所有権を導入したとき、 何かをムーブすると、それはもう使えなくなると言いました。いくつかの型では、 これは完全に理にかなっています。私たちの良き友人である Box は、ヒープ上のアロケーションを 管理してくれます。そして、そのメモリを解放する必要があると2つのコード片に 思わせたくは絶対にありません。
しかし、他の型ではこれは くだらない ものです。整数には 所有権の意味論はありません。ただの意味のない数値です!これが、整数が Copy としてマークされている理由です。Copy 型は、ビット単位のコピーで完全にコピー可能だと 分かっている型です。そのため、それらには超能力があります。ムーブされても、古い値が まだ 使用可能なのです。その結果、Copy 型なら、参照の中からでも 置換なしにムーブできます!
Rust のすべての数値プリミティブ(i32、u64、bool、f32、char など)は Copy です。 また、そのすべての構成要素が Copy である限り、任意のユーザー定義型も Copy として宣言できます。
このコードが動いていた理由として重要なのは、共有参照も Copy であることです!
& は Copy なので、Option<&> も 同様に Copy です。そのため、self.next.map を
行ったときも、Option がコピーされるだけだったので問題ありませんでした。今はそれができません。
なぜなら &mut は Copy ではないからです(もし &mut をコピーできるなら、同じメモリ位置への
&mut が2つできてしまい、これは禁止されています)。代わりに、Option を得るには適切に
take すべきです。
fn next(&mut self) -> Option<Self::Item> {
self.next.take().map(|node| {
self.next = node.next.as_deref_mut();
&mut node.elem
})
}
> cargo build
えっ……すごい。マジかよ!IterMut がそのまま動いた!
これをテストしてみましょう。
#[test]
fn iter_mut() {
let mut list = List::new();
list.push(1); list.push(2); list.push(3);
let mut iter = list.iter_mut();
assert_eq!(iter.next(), Some(&mut 3));
assert_eq!(iter.next(), Some(&mut 2));
assert_eq!(iter.next(), Some(&mut 1));
}
> cargo test
Running target/debug/lists-5c71138492ad4b4a
running 6 tests
test first::test::basics ... ok
test second::test::basics ... ok
test second::test::iter_mut ... ok
test second::test::into_iter ... ok
test second::test::iter ... ok
test second::test::peek ... ok
test result: ok. 7 passed; 0 failed; 0 ignored; 0 measured
はい。動いています。
マジかよ。
どういうこと。
いや、実際これは動くことが 想定されている のですが、たいていは何か くだらないものが邪魔をするものです!ここではっきりさせましょう。
私たちは今、単方向連結リストを受け取り、そのリスト内のすべての要素への 可変参照を高々1回ずつ返すコード片を実装しました。 しかも、それを行うことが静的に検証されています。そして完全に安全です。さらに、 何か突飛なことをする必要もありませんでした。
私に言わせれば、これはかなり大きなことです。これが動く理由はいくつかあります。
Option<&mut>をtakeするので、その可変参照への排他的アクセスを得られます。 誰かがそれを再び見ることを心配する必要はありません。- Rust は、可変参照を指し先の構造体のサブフィールドへ分割してもよいことを理解しています。 なぜなら、「上へ戻る」方法がなく、それらが確実に互いに素だからです。
実は、この基本的なロジックを適用すると、配列や木に対しても安全な IterMut を 得ることができます!さらに、そのイテレータを DoubleEnded にして、イテレータを 前から かつ 後ろから同時に消費することさえできます!うわお!
最終的なコード
さて、2つ目のリストについてはこれで終わりです。最終的なコードはこちらです!
#![allow(unused)]
fn main() {
pub struct List<T> {
head: Link<T>,
}
type Link<T> = Option<Box<Node<T>>>;
struct Node<T> {
elem: T,
next: Link<T>,
}
impl<T> List<T> {
pub fn new() -> Self {
List { head: None }
}
pub fn push(&mut self, elem: T) {
let new_node = Box::new(Node {
elem: elem,
next: self.head.take(),
});
self.head = Some(new_node);
}
pub fn pop(&mut self) -> Option<T> {
self.head.take().map(|node| {
self.head = node.next;
node.elem
})
}
pub fn peek(&self) -> Option<&T> {
self.head.as_ref().map(|node| {
&node.elem
})
}
pub fn peek_mut(&mut self) -> Option<&mut T> {
self.head.as_mut().map(|node| {
&mut node.elem
})
}
pub fn into_iter(self) -> IntoIter<T> {
IntoIter(self)
}
pub fn iter(&self) -> Iter<'_, T> {
Iter { next: self.head.as_deref() }
}
pub fn iter_mut(&mut self) -> IterMut<'_, T> {
IterMut { next: self.head.as_deref_mut() }
}
}
impl<T> Drop for List<T> {
fn drop(&mut self) {
let mut cur_link = self.head.take();
while let Some(mut boxed_node) = cur_link {
cur_link = boxed_node.next.take();
}
}
}
pub struct IntoIter<T>(List<T>);
impl<T> Iterator for IntoIter<T> {
type Item = T;
fn next(&mut self) -> Option<Self::Item> {
// タプル構造体のフィールドには数値でアクセスする
self.0.pop()
}
}
pub struct Iter<'a, T> {
next: Option<&'a Node<T>>,
}
impl<'a, T> Iterator for Iter<'a, T> {
type Item = &'a T;
fn next(&mut self) -> Option<Self::Item> {
self.next.map(|node| {
self.next = node.next.as_deref();
&node.elem
})
}
}
pub struct IterMut<'a, T> {
next: Option<&'a mut Node<T>>,
}
impl<'a, T> Iterator for IterMut<'a, T> {
type Item = &'a mut T;
fn next(&mut self) -> Option<Self::Item> {
self.next.take().map(|node| {
self.next = node.next.as_deref_mut();
&mut node.elem
})
}
}
#[cfg(test)]
mod test {
use super::List;
#[test]
fn basics() {
let mut list = List::new();
// 空のリストが正しく動作することを確認
assert_eq!(list.pop(), None);
// リストに要素を追加
list.push(1);
list.push(2);
list.push(3);
// 通常の削除を確認
assert_eq!(list.pop(), Some(3));
assert_eq!(list.pop(), Some(2));
// 何も壊れていないことを確認するために、さらにいくつか追加
list.push(4);
list.push(5);
// 通常の削除を確認
assert_eq!(list.pop(), Some(5));
assert_eq!(list.pop(), Some(4));
// 使い切った状態を確認
assert_eq!(list.pop(), Some(1));
assert_eq!(list.pop(), None);
}
#[test]
fn peek() {
let mut list = List::new();
assert_eq!(list.peek(), None);
assert_eq!(list.peek_mut(), None);
list.push(1); list.push(2); list.push(3);
assert_eq!(list.peek(), Some(&3));
assert_eq!(list.peek_mut(), Some(&mut 3));
list.peek_mut().map(|value| {
*value = 42
});
assert_eq!(list.peek(), Some(&42));
assert_eq!(list.pop(), Some(42));
}
#[test]
fn into_iter() {
let mut list = List::new();
list.push(1); list.push(2); list.push(3);
let mut iter = list.into_iter();
assert_eq!(iter.next(), Some(3));
assert_eq!(iter.next(), Some(2));
assert_eq!(iter.next(), Some(1));
assert_eq!(iter.next(), None);
}
#[test]
fn iter() {
let mut list = List::new();
list.push(1); list.push(2); list.push(3);
let mut iter = list.iter();
assert_eq!(iter.next(), Some(&3));
assert_eq!(iter.next(), Some(&2));
assert_eq!(iter.next(), Some(&1));
}
#[test]
fn iter_mut() {
let mut list = List::new();
list.push(1); list.push(2); list.push(3);
let mut iter = list.iter_mut();
assert_eq!(iter.next(), Some(&mut 3));
assert_eq!(iter.next(), Some(&mut 2));
assert_eq!(iter.next(), Some(&mut 1));
}
}
}
だいぶ充実してきました!
永続的な単方向連結スタック
さて、可変な単方向連結スタックの技術は習得しました。
単一所有から共有所有へ移行し、永続的で不変な単方向連結リストを書いてみましょう。これはまさに、関数型プログラマーがよく知り、愛してやまないリストです。head または tail を取得して、誰かの head を別の誰かの tail に載せることができます… そして… 基本的にはそれだけです。不変性は実に強力です。
この過程では、主に Rc と Arc に慣れていくことになりますが、これは次のリスト、つまり流れを変えるリストへの準備になります。
third.rs という新しいファイルを追加しましょう。
// lib.rs 内
pub mod first;
pub mod second;
pub mod third;
今回はコピペなしです。これはクリーンルームでの作業です。
レイアウト
さて、レイアウトについて振り出しに戻りましょう。
永続リストについて最も重要なのは、 リストの末尾を基本的に無料で操作できるということです。
たとえば、永続リストでは次のようなワークロードは珍しくありません。
list1 = A -> B -> C -> D
list2 = tail(list1) = B -> C -> D
list3 = push(list2, X) = X -> B -> C -> D
しかし、最終的にはメモリが次のようになっていてほしいわけです。
list1 -> A ---+
|
v
list2 ------> B -> C -> D
^
|
list3 -> X ---+
これは Box ではうまくいきません。なぜなら、B の所有権は共有されているからです。誰が
それを解放すべきでしょうか? list2 を drop したら、B は解放されるのでしょうか? Box なら当然
そうなると期待するはずです!
関数型言語 — そして実際にはほとんどすべての他の言語 — は、 ガベージコレクションを使うことでこれを回避しています。ガベージコレクションの魔法があれば、 B は誰もそれを見なくなった後で初めて解放されます。やったね!
Rust には、これらの言語が持つようなガベージコレクタはありません。 それらはトレーシング GC を持っており、実行時にそこら中にあるすべてのメモリを掘り進めて、 何がガベージなのかを自動的に判断します。一方、現在の Rust にあるのは 参照カウントだけです。参照カウントは非常に単純な GC と考えることができます。 多くのワークロードでは、トレーシングコレクタよりもスループットが大幅に低く、 循環を作れてしまうと完全に破綻します。でもまあ、これしかないんです!ありがたいことに、 今回のユースケースでは循環に遭遇することは決してありません (自分で証明してみても構いません — 私は絶対にやりません)。
では、参照カウントによるガベージコレクションはどう行うのでしょうか? Rc です! Rc は
Box とよく似ていますが、複製でき、そのメモリは、それから派生したすべての Rc が
drop されたときにのみ解放されます。残念ながら、この柔軟性には深刻な代償があります。
内部に対して共有参照しか取得できないのです。これはつまり、
リストの 1 つから実際にデータを取り出すことも、それらを変更することもできないということです。
では、レイアウトはどのようになるのでしょうか? 以前はこうでした。
pub struct List<T> {
head: Link<T>,
}
type Link<T> = Option<Box<Node<T>>>;
struct Node<T> {
elem: T,
next: Link<T>,
}
Box を Rc に変えるだけでよいのでしょうか?
// third.rs 内
pub struct List<T> {
head: Link<T>,
}
type Link<T> = Option<Rc<Node<T>>>;
struct Node<T> {
elem: T,
next: Link<T>,
}
cargo build
error[E0412]: cannot find type `Rc` in this scope
--> src/third.rs:5:23
|
5 | type Link<T> = Option<Rc<Node<T>>>;
| ^^ not found in this scope
help: possible candidate is found in another module, you can import it into scope
|
1 | use std::rc::Rc;
|
おっと、痛烈な一撃です。可変リストで使っていたものとは違い、Rc はあまりにも 冴えないので、すべての Rust プログラムに暗黙的にインポートされることすらありません。 なんて負け犬なんだ。
use std::rc::Rc;
cargo build
warning: field is never used: `head`
--> src/third.rs:4:5
|
4 | head: Link<T>,
| ^^^^^^^^^^^^^
|
= note: #[warn(dead_code)] on by default
warning: field is never used: `elem`
--> src/third.rs:10:5
|
10 | elem: T,
| ^^^^^^^
warning: field is never used: `next`
--> src/third.rs:11:5
|
11 | next: Link<T>,
| ^^^^^^^^^^^^^
問題なさそうです。Rust で書くのは相変わらず完全に取るに足りない作業です。 Box を Rc に検索置換するだけで、それで終わりにできるに違いありません!
…
いいえ。いいえ、できません。
基本
ここまでで、Rust の基本についてはすでに多くを理解しているので、単純なことの多くは改めて実装できます。
コンストラクターについては、また単にコピー&ペーストできます。
impl<T> List<T> {
pub fn new() -> Self {
List { head: None }
}
}
push と pop は、もはやあまり意味をなしません。代わりに、ほぼ同じことを提供する prepend と tail を用意できます。
まずは先頭に追加するところから始めましょう。これはリストと要素を受け取り、List を返します。ミュータブルなリストの場合と同じく、新しいノードを作成し、その next 値として古いリストを持たせたいところです。唯一新しい点は、その next 値をどうやって取得するかです。なぜなら、何もミューテートすることは許されていないからです。
私たちの祈りへの答えは Clone トレイトです。Clone はほぼすべての型に実装されており、共有参照だけが与えられた状態で、「これと同じような別のもの」を論理的に独立した形で取得する汎用的な方法を提供します。C++ のコピーコンストラクターのようなものですが、暗黙的に呼び出されることはありません。
特に Rc は、参照カウントをインクリメントする方法として Clone を使います。そのため、Box を移動して部分リストの中に入れるのではなく、古いリストの head を clone するだけです。head に対して match する必要すらありません。Option が、まさに私たちの望むことを行う Clone 実装を提供しているからです。
よし、試してみましょう。
pub fn prepend(&self, elem: T) -> List<T> {
List { head: Some(Rc::new(Node {
elem: elem,
next: self.head.clone(),
}))}
}
> cargo build
warning: field is never used: `elem`
--> src/third.rs:10:5
|
10 | elem: T,
| ^^^^^^^
|
= note: #[warn(dead_code)] on by default
warning: field is never used: `next`
--> src/third.rs:11:5
|
11 | next: Link<T>,
| ^^^^^^^^^^^^^
わあ、Rust は実際にフィールドを使っているかどうかについて本当に厳格です。これらのフィールドの使用を実際に観測できるコンシューマーが存在しないことまで判断できます!とはいえ、ここまではよさそうです。
tail は、この操作の論理的な逆です。これはリストを受け取り、最初の要素を取り除いたリスト全体を返します。要するに、リスト内の2番目の要素を clone するだけです(存在する場合)。これを試してみましょう。
pub fn tail(&self) -> List<T> {
List { head: self.head.as_ref().map(|node| node.next.clone()) }
}
cargo build
error[E0308]: mismatched types
--> src/third.rs:27:22
|
27 | List { head: self.head.as_ref().map(|node| node.next.clone()) }
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ expected struct `std::rc::Rc`, found enum `std::option::Option`
|
= note: expected type `std::option::Option<std::rc::Rc<_>>`
found type `std::option::Option<std::option::Option<std::rc::Rc<_>>>`
うーん、やらかしました。map は Y を返すことを期待していますが、ここでは Option<Y> を返しています。ありがたいことに、これもよくある Option のパターンなので、and_then を使えば Option を返せます。
pub fn tail(&self) -> List<T> {
List { head: self.head.as_ref().and_then(|node| node.next.clone()) }
}
> cargo build
素晴らしいです。
tail ができたので、おそらく head も提供すべきでしょう。これは最初の要素への参照を返します。これはミュータブルなリストの peek そのものです。
pub fn head(&self) -> Option<&T> {
self.head.as_ref().map(|node| &node.elem)
}
> cargo build
いいですね。
これでテストできるだけの機能は揃いました。
#[cfg(test)]
mod test {
use super::List;
#[test]
fn basics() {
let list = List::new();
assert_eq!(list.head(), None);
let list = list.prepend(1).prepend(2).prepend(3);
assert_eq!(list.head(), Some(&3));
let list = list.tail();
assert_eq!(list.head(), Some(&2));
let list = list.tail();
assert_eq!(list.head(), Some(&1));
let list = list.tail();
assert_eq!(list.head(), None);
// 空のtailが機能することを確認する
let list = list.tail();
assert_eq!(list.head(), None);
}
}
> cargo test
Running target/debug/lists-5c71138492ad4b4a
running 5 tests
test first::test::basics ... ok
test second::test::into_iter ... ok
test second::test::basics ... ok
test second::test::iter ... ok
test third::test::basics ... ok
test result: ok. 5 passed; 0 failed; 0 ignored; 0 measured
完璧です!
Iter も、ミュータブルなリストのときと同じです。
pub struct Iter<'a, T> {
next: Option<&'a Node<T>>,
}
impl<T> List<T> {
pub fn iter(&self) -> Iter<'_, T> {
Iter { next: self.head.as_deref() }
}
}
impl<'a, T> Iterator for Iter<'a, T> {
type Item = &'a T;
fn next(&mut self) -> Option<Self::Item> {
self.next.map(|node| {
self.next = node.next.as_deref();
&node.elem
})
}
}
#[test]
fn iter() {
let list = List::new().prepend(1).prepend(2).prepend(3);
let mut iter = list.iter();
assert_eq!(iter.next(), Some(&3));
assert_eq!(iter.next(), Some(&2));
assert_eq!(iter.next(), Some(&1));
}
cargo test
Running target/debug/lists-5c71138492ad4b4a
running 7 tests
test first::test::basics ... ok
test second::test::basics ... ok
test second::test::iter ... ok
test second::test::into_iter ... ok
test second::test::peek ... ok
test third::test::basics ... ok
test third::test::iter ... ok
test result: ok. 6 passed; 0 failed; 0 ignored; 0 measured
一体誰が動的型付けのほうが簡単だなんて言ったのでしょうか?
(間抜けな連中です)
この型には IntoIter や IterMut を実装できないことに注意してください。要素には共有アクセスしかありません。
Drop
ミュータブルリストと同様に、再帰的デストラクタ問題があります。 確かに、イミュータブルリストではこの問題はそこまで深刻ではありません。もし どこかの別のリストの head である別のノードに到達したとしても、それを 再帰的にドロップすることはありません。しかし、それでも気にかけるべき問題ではあり、 どう対処するかはそれほど明確ではありません。以前は次のように解決しました。
impl<T> Drop for List<T> {
fn drop(&mut self) {
let mut cur_link = self.head.take();
while let Some(mut boxed_node) = cur_link {
cur_link = boxed_node.next.take();
}
}
}
問題はループの本体です。
cur_link = boxed_node.next.take();
これは Box の中にある Node を変更していますが、Rc ではそれはできません。Rc は 共有アクセスしか提供しないからです。任意の数の他の Rc がそれを指している可能性があります。
しかし、このノードを知っている最後のリストが自分たちだと分かっているなら、 実際には Rc から Node をムーブしても問題ありません。そうすれば、 いつ止めるべきかも分かります。Node を取り出せなくなったときです。
そして見てください。Rc にはまさにこれを行うメソッドがあります。try_unwrap です。
impl<T> Drop for List<T> {
fn drop(&mut self) {
let mut head = self.head.take();
while let Some(node) = head {
if let Ok(mut node) = Rc::try_unwrap(node) {
head = node.next.take();
} else {
break;
}
}
}
}
cargo test
Compiling lists v0.1.0 (/Users/ADesires/dev/too-many-lists/lists)
Finished dev [unoptimized + debuginfo] target(s) in 1.10s
Running /Users/ADesires/dev/too-many-lists/lists/target/debug/deps/lists-86544f1d97438f1f
running 8 tests
test first::test::basics ... ok
test second::test::basics ... ok
test second::test::into_iter ... ok
test second::test::iter ... ok
test second::test::iter_mut ... ok
test second::test::peek ... ok
test third::test::basics ... ok
test third::test::iter ... ok
test result: ok. 8 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
素晴らしい! いいですね。
Arc
不変な連結リストを使う理由の 1 つは、スレッド間でデータを共有することです。 結局のところ、共有された可変状態は諸悪の根源であり、それを解決する 1 つの方法は、 可変 の部分を永久に消し去ることです。
ただし、私たちのリストはまったくスレッドセーフではありません。スレッドセーフにするには、 参照カウントをアトミックに操作する必要があります。そうしないと、2 つのスレッドが 参照カウントをインクリメントしようとして、片方しか反映されない可能性があります。 すると、リストが早すぎるタイミングで解放されてしまうかもしれません!
スレッドセーフ性を得るには、Arc を使う必要があります。Arc は、参照カウントが
アトミックに変更されるという点を除けば、Rc と完全に同一です。
これは必要ない場合には多少のオーバーヘッドになるため、Rust は両方を公開しています。
私たちのリストを作るために必要なのは、Rc への参照をすべて
std::sync::Arc に置き換えることだけです。それだけです。スレッドセーフになりました。完了です!
しかし、ここで興味深い疑問が生じます。ある型がスレッドセーフかどうかを、 どうやって知るのでしょうか?うっかり壊してしまうことはあるのでしょうか?
いいえ!Rust ではスレッドセーフ性をうっかり壊すことはできません!
これが成り立つ理由は、Rust がスレッドセーフ性を Send と Sync という
2 つのトレイトで第一級のものとしてモデル化しているからです。
ある型が Send であるとは、それを別のスレッドへ移動しても安全であることを意味します。ある型が Sync であるとは、
複数のスレッド間で共有しても安全であることを意味します。つまり、T が Sync なら、&T は
Send です。ここでいう安全とは、データ競合を引き起こすことが不可能であるという意味です(より一般的な問題である競合状態と混同しないでください)。
これらはマーカートレイトです。これは凝った言い方をすると、インターフェイスをまったく提供しないトレイトということです。あなたは Send であるか、そうでないかのどちらかです。それは単に、 他の API が要求できる性質にすぎません。適切に Send でなければ、 別のスレッドへ送ることは静的に不可能です!すばらしい!
Send と Sync はまた、完全に Send 型と Sync 型で構成されているかどうかに基づいて、 自動的に導出されるトレイトでもあります。これは、自分が Copy 型だけでできている場合にのみ Copy を実装できることに似ていますが、条件を満たしているなら、その実装を自動的に行ってくれるという点が異なります。
ほとんどすべての型は Send かつ Sync です。多くの型が Send であるのは、自分のデータを完全に 所有しているからです。多くの型が Sync であるのは、スレッド間でデータを共有する唯一の方法が、 共有参照の背後に置くことであり、それによってデータが不変になるからです!
しかし、これらの性質に反する特別な型があります。それは内部可変性を持つ型です。 これまで私たちは、実際にはほとんど継承された可変性(別名、外部可変性)だけを扱ってきました。 つまり、値の可変性は、そのコンテナの可変性から継承されるというものです。 言い換えると、非可変な値のあるフィールドを、気分で勝手に変更することはできません。
内部可変性を持つ型はこれに反します。共有参照を通じて値を変更できるようにするからです。 内部可変性には大きく 2 つの分類があります。単一スレッドのコンテキストでのみ機能するセルと、 マルチスレッドのコンテキストで機能するロックです。明らかな理由から、使える場合には セルのほうが安価です。さらに、ロックのように振る舞うプリミティブであるアトミックもあります。
では、これらすべては Rc や Arc とどのような関係があるのでしょうか?実のところ、どちらも 参照カウントのために内部可変性を使っています。さらに悪いことに、この参照カウントは すべてのインスタンス間で共有されます!Rc は単にセルを使っているため、スレッドセーフではありません。 Arc はアトミックを使っているため、スレッドセーフです。もちろん、 型を Arc に入れたからといって、その型を魔法のようにスレッドセーフにできるわけではありません。Arc も他の型と同じように、 スレッドセーフ性を導出できるだけです。
アトミックメモリモデルや、導出されない Send 実装の細かい詳細には、本当に本当に本当に踏み込みたくありません。 言うまでもなく、Rust のスレッドセーフ性の話を深く掘り下げていくと、物事はより複雑になります。 高水準の利用者としては、すべてがただ動くので、実際にはあまり考える必要はありません。
最終コード
イミュータブルスタックについて本当に言いたいことは、これで全部です。今ではリストの実装が かなり得意になってきました!
#![allow(unused)]
fn main() {
use std::rc::Rc;
pub struct List<T> {
head: Link<T>,
}
type Link<T> = Option<Rc<Node<T>>>;
struct Node<T> {
elem: T,
next: Link<T>,
}
impl<T> List<T> {
pub fn new() -> Self {
List { head: None }
}
pub fn prepend(&self, elem: T) -> List<T> {
List { head: Some(Rc::new(Node {
elem: elem,
next: self.head.clone(),
}))}
}
pub fn tail(&self) -> List<T> {
List { head: self.head.as_ref().and_then(|node| node.next.clone()) }
}
pub fn head(&self) -> Option<&T> {
self.head.as_ref().map(|node| &node.elem)
}
pub fn iter(&self) -> Iter<'_, T> {
Iter { next: self.head.as_deref() }
}
}
impl<T> Drop for List<T> {
fn drop(&mut self) {
let mut head = self.head.take();
while let Some(node) = head {
if let Ok(mut node) = Rc::try_unwrap(node) {
head = node.next.take();
} else {
break;
}
}
}
}
pub struct Iter<'a, T> {
next: Option<&'a Node<T>>,
}
impl<'a, T> Iterator for Iter<'a, T> {
type Item = &'a T;
fn next(&mut self) -> Option<Self::Item> {
self.next.map(|node| {
self.next = node.next.as_deref();
&node.elem
})
}
}
#[cfg(test)]
mod test {
use super::List;
#[test]
fn basics() {
let list = List::new();
assert_eq!(list.head(), None);
let list = list.prepend(1).prepend(2).prepend(3);
assert_eq!(list.head(), Some(&3));
let list = list.tail();
assert_eq!(list.head(), Some(&2));
let list = list.tail();
assert_eq!(list.head(), Some(&1));
let list = list.tail();
assert_eq!(list.head(), None);
// 空のtailが動作することを確認する
let list = list.tail();
assert_eq!(list.head(), None);
}
#[test]
fn iter() {
let list = List::new().prepend(1).prepend(2).prepend(3);
let mut iter = list.iter();
assert_eq!(iter.next(), Some(&3));
assert_eq!(iter.next(), Some(&2));
assert_eq!(iter.next(), Some(&1));
}
}
}
悪いが安全な双方向連結デック
Rc と内部可変性について見てきた今、興味深い考えが浮かびます……もしかすると、Rc 越しにミューテートすることが できる のかもしれません。そして それが 可能なら、双方向連結 リストを完全に安全に実装できるかもしれません!
その過程で 内部可変性 に慣れ、おそらく安全であることが 正しい ことを意味しないと、苦労して学ぶことになるでしょう。双方向連結リストは難しく、私はいつもどこかで間違えます。
fourth.rs という新しいファイルを追加しましょう。
// lib.rs 内
pub mod first;
pub mod second;
pub mod third;
pub mod fourth;
これはまた別のクリーンルーム作業になりますが、いつものように、おそらく再びそのまま適用できるロジックが見つかるでしょう。
免責事項: この章は基本的に、これが非常に悪いアイデアであることを示すデモンストレーションです。
レイアウト
この設計の鍵となるのは RefCell 型です。RefCell の中核は、
次の 2 つのメソッドです。
fn borrow(&self) -> Ref<'_, T>;
fn borrow_mut(&self) -> RefMut<'_, T>;
borrow と borrow_mut のルールは、& と &mut のルールとまったく同じです。
borrow は何度でも呼び出せますが、borrow_mut には
排他性が必要です。
これを静的に強制するのではなく、RefCell は実行時に強制します。
ルールを破ると、RefCell は単に panic してプログラムをクラッシュさせます。
では、なぜこれらの Ref や RefMut というものを返すのでしょうか? それらは基本的に、
借用版の Rc のように振る舞います。また、スコープから外れるまで RefCell を
借用されたままにします。これについては後で扱います。
これで Rc と RefCell を使って、私たちは……循環参照を回収できない、 信じられないほど冗長で、至るところで可変なガベージコレクション付き言語に なれるわけです! や、やったぁぁぁ……
さて、私たちは 双方向リンク にしたいと考えています。これは、各ノードが 前のノードと次のノードへのポインタを持つことを意味します。また、リスト自体も 最初のノードと最後のノードへのポインタを持ちます。これにより、リストの 両端 で高速な挿入と削除が可能になります。
つまり、おそらく次のようなものが欲しいはずです。
use std::rc::Rc;
use std::cell::RefCell;
pub struct List<T> {
head: Link<T>,
tail: Link<T>,
}
type Link<T> = Option<Rc<RefCell<Node<T>>>>;
struct Node<T> {
elem: T,
next: Link<T>,
prev: Link<T>,
}
> cargo build
warning: field is never used: `head`
--> src/fourth.rs:5:5
|
5 | head: Link<T>,
| ^^^^^^^^^^^^^
|
= note: #[warn(dead_code)] on by default
warning: field is never used: `tail`
--> src/fourth.rs:6:5
|
6 | tail: Link<T>,
| ^^^^^^^^^^^^^
warning: field is never used: `elem`
--> src/fourth.rs:12:5
|
12 | elem: T,
| ^^^^^^^
warning: field is never used: `next`
--> src/fourth.rs:13:5
|
13 | next: Link<T>,
| ^^^^^^^^^^^^^
warning: field is never used: `prev`
--> src/fourth.rs:14:5
|
14 | prev: Link<T>,
| ^^^^^^^^^^^^^
おお、ビルドできました! dead code の警告がたくさん出ていますが、ビルドできました! 使ってみましょう。
構築していく
よし、リストの構築から始めましょう。この新しい仕組みなら、これはかなり単純です。new は相変わらず些細で、すべてのフィールドを None にするだけです。
また少し扱いづらくなってきたので、Node のコンストラクタも切り出しましょう。
impl<T> Node<T> {
fn new(elem: T) -> Rc<RefCell<Self>> {
Rc::new(RefCell::new(Node {
elem: elem,
prev: None,
next: None,
}))
}
}
impl<T> List<T> {
pub fn new() -> Self {
List { head: None, tail: None }
}
}
> cargo build
**大量の dead code 警告が出たがビルドは通った**
やった!
では、リストの先頭に push する処理を書いてみましょう。 双方向リンクリストはかなり複雑なので、もう少し多くの作業が必要になります。 単方向リンクリストの操作は簡単なワンライナーに還元できましたが、双方向リンクリストの操作はかなり複雑です。
特に、空リスト周辺の境界ケースを特別に扱う必要があります。
ほとんどの操作は head または tail ポインタだけに触れます。
しかし空リストへ遷移するとき、または空リストから遷移するときには、両方を同時に編集する必要があります。
メソッドが妥当かどうかを検証する簡単な方法は、次の不変条件を維持することです。各ノードには、それを指すポインタがちょうど 2 つあるべきです。 リストの中央にある各ノードは、その前のノードと次のノードから指され、端にあるノードはリスト自身から指されます。
試しにやってみましょう。
pub fn push_front(&mut self, elem: T) {
// 新しいノードには +2 リンクが必要で、それ以外はすべて +0 のはず
let new_head = Node::new(elem);
match self.head.take() {
Some(old_head) => {
// 空でないリストなので、old_head を接続する必要がある
old_head.prev = Some(new_head.clone()); // +1 new_head
new_head.next = Some(old_head); // +1 old_head
self.head = Some(new_head); // +1 new_head, -1 old_head
// 合計: +2 new_head, +0 old_head -- OK!
}
None => {
// 空リストなので、tail を設定する必要がある
self.tail = Some(new_head.clone()); // +1 new_head
self.head = Some(new_head); // +1 new_head
// 合計: +2 new_head -- OK!
}
}
}
cargo build
error[E0609]: no field `prev` on type `std::rc::Rc<std::cell::RefCell<fourth::Node<T>>>`
--> src/fourth.rs:39:26
|
39 | old_head.prev = Some(new_head.clone()); // +1 new_head
| ^^^^ unknown field
error[E0609]: no field `next` on type `std::rc::Rc<std::cell::RefCell<fourth::Node<T>>>`
--> src/fourth.rs:40:26
|
40 | new_head.next = Some(old_head); // +1 old_head
| ^^^^ unknown field
よし。コンパイラエラーです。幸先のよいスタートです。よいスタートです。
なぜノードの prev フィールドや next フィールドにアクセスできないのでしょうか? 単に Rc<Node> を使っていたときには動いていました。
どうやら RefCell が邪魔をしているようです。
ドキュメントを確認したほうがよさそうです。
Google で「rust refcell」を検索
動的にチェックされる借用ルールを持つミュータブルなメモリ位置
詳細については、モジュールレベルのドキュメントを参照してください。
リンクをクリック
共有可能なミュータブルコンテナ。
Cell<T>型とRefCell<T>型の値は共有参照(つまり一般的な&T型)を通じて変更できますが、ほとんどの Rust 型は一意な(&mut T)参照を通じてしか変更できません。Cell<T>とRefCell<T>は、典型的な Rust 型が示す「継承された可変性」と対照的に、「内部可変性」を提供すると言います。Cell 型には 2 つの種類があります。
Cell<T>とRefCell<T>です。Cell<T>は、1 回のメソッド呼び出しで内部の値を変更するgetメソッドとsetメソッドを提供します。ただしCell<T>は、Copyを実装する型としか互換性がありません。他の型では、変更前に書き込みロックを取得してRefCell<T>型を使う必要があります。
RefCell<T>は Rust のライフタイムを使って「動的借用」を実装します。これは、内部の値に対する一時的で排他的なミュータブルアクセスを主張できるプロセスです。RefCell<T>の借用は「実行時」に追跡されます。これは、Rust のネイティブな参照型がコンパイル時に完全に静的に追跡されるのとは異なります。RefCell<T>の借用は動的であるため、すでにミュータブルに借用されている値を借用しようとすることが可能です。この場合、スレッドの panic が発生します。内部可変性を選ぶべき場合
値を変更するには一意なアクセスが必要になる、より一般的な継承された可変性は、Rust がポインタのエイリアシングについて強力に推論し、クラッシュバグを静的に防ぐことを可能にする主要な言語要素の 1 つです。そのため、継承された可変性が望ましく、内部可変性はいわば最後の手段です。ただし、cell 型は本来許可されない場所での変更を可能にするため、内部可変性が適切な場合、あるいは使用が必須となる場合があります。たとえば次のような場合です。
- 共有型に継承された可変性のルートを導入する。
- 論理的にイミュータブルなメソッドの実装詳細。
Cloneのミュータブルな実装。共有型に継承された可変性のルートを導入する
Rc<T>やArc<T>を含む共有スマートポインタ型は、複数の関係者の間で clone して共有できるコンテナを提供します。含まれる値は複数のエイリアスを持ち得るため、ミュータブル参照としてではなく共有参照としてしか借用できません。 cell がなければ、共有ボックスの内部にあるデータを変更することはまったく不可能になります!そのため、共有ポインタ型の内部に
RefCell<T>を入れて可変性を再導入するのは非常によく行われます。use std::collections::HashMap; use std::cell::RefCell; use std::rc::Rc; fn main() { let shared_map: Rc<RefCell<_>> = Rc::new(RefCell::new(HashMap::new())); shared_map.borrow_mut().insert("africa", 92388); shared_map.borrow_mut().insert("kyoto", 11837); shared_map.borrow_mut().insert("piccadilly", 11826); shared_map.borrow_mut().insert("marbles", 38); }この例では
Arc<T>ではなくRc<T>を使っていることに注意してください。RefCell<T>はシングルスレッドのシナリオ向けです。マルチスレッドの状況で共有可変性が必要な場合は、Mutex<T>の使用を検討してください。
いやあ、Rust のドキュメントは相変わらず驚くほど素晴らしいですね。
私たちが気にする重要な部分は、この行です。
shared_map.borrow_mut().insert("africa", 92388);
特に borrow_mut というものです。どうやら RefCell を明示的に借用する必要があるようです。
. 演算子が代わりにやってくれるわけではありません。変ですね。試してみましょう。
pub fn push_front(&mut self, elem: T) {
let new_head = Node::new(elem);
match self.head.take() {
Some(old_head) => {
old_head.borrow_mut().prev = Some(new_head.clone());
new_head.borrow_mut().next = Some(old_head);
self.head = Some(new_head);
}
None => {
self.tail = Some(new_head.clone());
self.head = Some(new_head);
}
}
}
> cargo build
warning: field is never used: `elem`
--> src/fourth.rs:12:5
|
12 | elem: T,
| ^^^^^^^
|
= note: #[warn(dead_code)] on by default
おお、ビルドできました! またもドキュメントの勝利です。
分解する
pop_front は基本的に push_front と同じロジックを、逆向きにしたものであるはずです。試してみましょう。
pub fn pop_front(&mut self) -> Option<T> {
// 古いheadをtakeして、-2になるようにする必要がある
self.head.take().map(|old_head| { // -1 old
match old_head.borrow_mut().next.take() {
Some(new_head) => { // -1 new
// リストは空にならない
new_head.borrow_mut().prev.take(); // -1 old
self.head = Some(new_head); // +1 new
// 合計: -2 old, +0 new
}
None => {
// リストが空になる
self.tail.take(); // -1 old
// 合計: -2 old, (newはなし)
}
}
old_head.elem
})
}
> cargo build
error[E0609]: no field `elem` on type `std::rc::Rc<std::cell::RefCell<fourth::Node<T>>>`
--> src/fourth.rs:64:22
|
64 | old_head.elem
| ^^^^ unknown field
しまった。RefCells。たぶんまた borrow_mut しないといけないですね……。
pub fn pop_front(&mut self) -> Option<T> {
self.head.take().map(|old_head| {
match old_head.borrow_mut().next.take() {
Some(new_head) => {
new_head.borrow_mut().prev.take();
self.head = Some(new_head);
}
None => {
self.tail.take();
}
}
old_head.borrow_mut().elem
})
}
cargo build
error[E0507]: cannot move out of borrowed content
--> src/fourth.rs:64:13
|
64 | old_head.borrow_mut().elem
| ^^^^^^^^^^^^^^^^^^^^^^^^^^ cannot move out of borrowed content
はぁ
cannot move out of borrowed content
ふむ…… Box には本当に甘やかされていたようです。borrow_mut で得られるのは &mut Node<T> だけで、そこからムーブすることはできません!
必要なのは、RefCell<T> を受け取って T を返してくれる何かです。ドキュメントでそういうものがないか確認してみましょう。
fn into_inner(self) -> TRefCell を消費し、ラップされている値を返します。
これは有望そうです!
old_head.into_inner().elem
> cargo build
error[E0507]: cannot move out of an `Rc`
--> src/fourth.rs:64:13
|
64 | old_head.into_inner().elem
| ^^^^^^^^ cannot move out of an `Rc`
ああ、だめでした。into_inner は RefCell からムーブしたがりますが、それはできません。なぜなら RefCell は Rc の中にあるからです。前の章で見たように、Rc<T> はその内部への共有参照を得ることしか許してくれません。それは当然です。なぜなら、それこそが参照カウントポインターの存在意義そのものだからです。共有されるのです!
参照カウントされたリストに Drop を実装しようとしたとき、これは問題になりました。そして解決策も同じです。Rc::try_unwrap です。これは refcount が 1 の場合に Rc の中身を取り出します。
Rc::try_unwrap(old_head).unwrap().into_inner().elem
Rc::try_unwrap は Result<T, Rc<T>> を返します。Result は基本的に一般化された Option で、None の場合にデータが紐づいているものです。この場合は、unwrap しようとした Rc です。失敗する場合については気にしないので(プログラムを正しく書けていれば、これは成功しなければなりません)、単にそれに対して unwrap を呼びます。
さて、次はどんなコンパイラーエラーが出るか見てみましょう(正直、出るに決まっています)。
> cargo build
error[E0599]: no method named `unwrap` found for type `std::result::Result<std::cell::RefCell<fourth::Node<T>>, std::rc::Rc<std::cell::RefCell<fourth::Node<T>>>>` in the current scope
--> src/fourth.rs:64:38
|
64 | Rc::try_unwrap(old_head).unwrap().into_inner().elem
| ^^^^^^
|
= note: the method `unwrap` exists but the following trait bounds were not satisfied:
`std::rc::Rc<std::cell::RefCell<fourth::Node<T>>> : std::fmt::Debug`
うわあ。Result に対する unwrap は、エラーの場合をデバッグ出力できることを要求します。RefCell<T> は、T が Debug を実装している場合にのみ Debug を実装します。Node は Debug を実装していません。
それを実装するのではなく、ok で Result を Option に変換して回避しましょう。
Rc::try_unwrap(old_head).ok().unwrap().into_inner().elem
頼む。
cargo build
よし。
ふぅ
やりました。
push と pop を実装しました。
古い stack の基本テストを拝借してテストしましょう(ここまでに実装したのはそれだけなので)。
#[cfg(test)]
mod test {
use super::List;
#[test]
fn basics() {
let mut list = List::new();
// 空リストが正しく振る舞うことを確認
assert_eq!(list.pop_front(), None);
// リストに要素を追加
list.push_front(1);
list.push_front(2);
list.push_front(3);
// 通常の削除を確認
assert_eq!(list.pop_front(), Some(3));
assert_eq!(list.pop_front(), Some(2));
// 何も壊れていないことを確認するため、さらにいくつかpush
list.push_front(4);
list.push_front(5);
// 通常の削除を確認
assert_eq!(list.pop_front(), Some(5));
assert_eq!(list.pop_front(), Some(4));
// 使い切ったことを確認
assert_eq!(list.pop_front(), Some(1));
assert_eq!(list.pop_front(), None);
}
}
cargo test
Running target/debug/lists-5c71138492ad4b4a
running 9 tests
test first::test::basics ... ok
test fourth::test::basics ... ok
test second::test::iter_mut ... ok
test second::test::basics ... ok
test fifth::test::iter_mut ... ok
test third::test::basics ... ok
test second::test::iter ... ok
test third::test::iter ... ok
test second::test::into_iter ... ok
test result: ok. 9 passed; 0 failed; 0 ignored; 0 measured
決まりました。
リストから要素を適切に取り除けるようになったので、Drop を実装できます。今回の Drop は概念的に少し興味深いものです。以前は、無制限の再帰を避けるためにわざわざスタックへ Drop を実装しましたが、今回はそもそも何かを起こすために Drop を実装する必要があります。
Rc はサイクルを扱えません。サイクルがあると、すべてが他のすべてを生かし続けます。実のところ、双方向リストは小さなサイクルが連なった大きな鎖にすぎません! そのため、リストを drop すると、両端のノードの refcount は 1 までデクリメントされます……そしてその後は何も起きません。まあ、リストにノードがちょうど 1 つだけ含まれているなら問題ありません。しかし理想的には、リストは複数の要素を含んでいても正しく動作するべきです。そう思うのは私だけかもしれませんが。
見てきたように、要素の削除は少し面倒でした。なので、私たちにとって一番簡単なのは、None が返るまで単に pop し続けることです。
impl<T> Drop for List<T> {
fn drop(&mut self) {
while self.pop_front().is_some() {}
}
}
cargo build
(実際には、可変スタックでもこれを行うことはできましたが、近道は物事を理解している人のためのものです!)
push と pop の _back 版の実装を見てもよいのですが、それらは単なるコピペ作業なので、この章の後のほうに回します。今はもっと面白いものを見ていきましょう!
ピーク
さて、push と pop は乗り切った。正直に言うと、ちょっと感情的になってしまった。コンパイル時の正しさというのは、とんでもなく中毒性がある。
簡単なことをしてクールダウンしよう。peek_front を実装するだけにしよう。前はいつも本当に簡単だった。今回も簡単なはずだよね?
だよね?
実際、これはコピペするだけでいける気がする!
pub fn peek_front(&self) -> Option<&T> {
self.head.as_ref().map(|node| {
&node.elem
})
}
待て。今回は違う。
pub fn peek_front(&self) -> Option<&T> {
self.head.as_ref().map(|node| {
// 借用!!!!
&node.borrow().elem
})
}
ハハッ。
cargo build
error[E0515]: cannot return value referencing temporary value
--> src/fourth.rs:66:13
|
66 | &node.borrow().elem
| ^ ----------^^^^^
| | |
| | temporary value created here
| |
| returns a value referencing data owned by the current function
よし、もうコンピューターを燃やす。
これは単方向連結スタックとまったく同じロジックだ。なぜ挙動が違うんだ。なぜだ。
答えは、実のところこの章全体の教訓そのものだ。RefCell はすべてを悲しみに変える。これまでは、RefCell はただの厄介者だった。これからは悪夢になる。
では何が起きているのか?それを理解するために、borrow の定義に戻る必要がある。
fn borrow<'a>(&'a self) -> Ref<'a, T>
fn borrow_mut<'a>(&'a self) -> RefMut<'a, T>
レイアウトの節でこう言った。
これを静的に強制するのではなく、RefCell は実行時に強制する。 ルールを破ると、RefCell は単に panic してプログラムをクラッシュさせる。 なぜこの Ref や RefMut というものを返すのか?ええと、これらは基本的に借用に対する
Rcのように振る舞う。さらに、これらがスコープを抜けるまで RefCell を借用状態に保つ。 これについては後で扱う。
その「後」だ。
Ref と RefMut は、それぞれ Deref と DerefMut を実装している。そのため、ほとんどの意図や目的において、これらは &T や &mut T とまったく同じように振る舞う。しかし、これらのトレイトの仕組み上、返される参照は実際の RefCell ではなく、Ref のライフタイムに結び付いている。つまり、参照を保持している限り、Ref もその場に存在していなければならない。
これは実際、正しさのために必要だ。Ref が drop されると、RefCell にもう借用されていないことを伝える。だから、もし Ref が存在している期間より長く参照を保持することに成功してしまったなら、参照がまだ残っている間に RefMut を取得できてしまい、Rust の型システムを完全にぶち壊せることになる。
では、これは私たちをどこに置き去りにするのか?私たちは参照を返したいだけだが、この Ref というものを保持しておく必要がある。しかし、peek から参照を返した瞬間に関数は終わり、Ref はスコープを抜ける。
😖
私の知る限り、ここでは実際のところ完全に詰んでいる。RefCell の使用をあのように完全にカプセル化することはできない。
でも……実装の詳細を完全に隠すことを諦めたらどうだろう?Ref を返すようにしたらどうだろう?
pub fn peek_front(&self) -> Option<Ref<T>> {
self.head.as_ref().map(|node| {
node.borrow()
})
}
> cargo build
error[E0412]: cannot find type `Ref` in this scope
--> src/fourth.rs:63:40
|
63 | pub fn peek_front(&self) -> Option<Ref<T>> {
| ^^^ not found in this scope
help: possible candidates are found in other modules, you can import them into scope
|
1 | use core::cell::Ref;
|
1 | use std::cell::Ref;
|
ぶるぷ。いくつか import しないと。
use std::cell::{Ref, RefCell};
> cargo build
error[E0308]: mismatched types
--> src/fourth.rs:64:9
|
64 | / self.head.as_ref().map(|node| {
65 | | node.borrow()
66 | | })
| |__________^ expected type parameter, found struct `fourth::Node`
|
= note: expected type `std::option::Option<std::cell::Ref<'_, T>>`
found type `std::option::Option<std::cell::Ref<'_, fourth::Node<T>>>`
うーん……その通りだ。手元にあるのは Ref<Node<T>> だけど、欲しいのは Ref<T> だ。カプセル化の望みをすべて捨てて、それをそのまま返すこともできる。あるいは、さらに複雑にして、Ref<Node<T>> を新しい型でラップし、&T へのアクセスだけを公開することもできる。
そのどちらの選択肢も、ちょっと微妙だ。
代わりに、もっと深く潜っていくことにしよう。少し楽しもう。その楽しみの源はこの怪物だ。
map<U, F>(orig: Ref<'b, T>, f: F) -> Ref<'b, U>
where F: FnOnce(&T) -> &U,
U: ?Sized
借用されたデータのコンポーネントに対する新しい Ref を作る。
そう、Option に対して map できるのと同じように、Ref に対しても map できる。
きっとどこかの誰かは、モナドだとか何とかでめちゃくちゃ興奮しているのだろうけど、私はそういうのはどうでもいい。それに None のようなケースがないので、これは適切なモナドではない気もするが、話がそれた。
これはクールで、私にとって重要なのはそれだけだ。これが必要だ。
pub fn peek_front(&self) -> Option<Ref<T>> {
self.head.as_ref().map(|node| {
Ref::map(node.borrow(), |node| &node.elem)
})
}
> cargo build
よっしゃあああ
スタックのテストをいじって、これが動いていることを確認しよう。Ref が比較を実装していないという事実に対処するために、少しいじる必要がある。
#[test]
fn peek() {
let mut list = List::new();
assert!(list.peek_front().is_none());
list.push_front(1); list.push_front(2); list.push_front(3);
assert_eq!(&*list.peek_front().unwrap(), &3);
}
> cargo test
Running target/debug/lists-5c71138492ad4b4a
running 10 tests
test first::test::basics ... ok
test fourth::test::basics ... ok
test second::test::basics ... ok
test fourth::test::peek ... ok
test second::test::iter_mut ... ok
test second::test::into_iter ... ok
test third::test::basics ... ok
test second::test::peek ... ok
test second::test::iter ... ok
test third::test::iter ... ok
test result: ok. 10 passed; 0 failed; 0 ignored; 0 measured
素晴らしい!
対称的なガラクタ
さて、組み合わせ論的な対称性を全部片付けてしまいましょう。
やるべきことは、基本的なテキスト置換だけです:
tail <-> head
next <-> prev
front -> back
ああ、それから peek 用に _mut バリアントを追加する必要もあります。
use std::cell::{Ref, RefCell, RefMut};
//..
pub fn push_back(&mut self, elem: T) {
let new_tail = Node::new(elem);
match self.tail.take() {
Some(old_tail) => {
old_tail.borrow_mut().next = Some(new_tail.clone());
new_tail.borrow_mut().prev = Some(old_tail);
self.tail = Some(new_tail);
}
None => {
self.head = Some(new_tail.clone());
self.tail = Some(new_tail);
}
}
}
pub fn pop_back(&mut self) -> Option<T> {
self.tail.take().map(|old_tail| {
match old_tail.borrow_mut().prev.take() {
Some(new_tail) => {
new_tail.borrow_mut().next.take();
self.tail = Some(new_tail);
}
None => {
self.head.take();
}
}
Rc::try_unwrap(old_tail).ok().unwrap().into_inner().elem
})
}
pub fn peek_back(&self) -> Option<Ref<T>> {
self.tail.as_ref().map(|node| {
Ref::map(node.borrow(), |node| &node.elem)
})
}
pub fn peek_back_mut(&mut self) -> Option<RefMut<T>> {
self.tail.as_ref().map(|node| {
RefMut::map(node.borrow_mut(), |node| &mut node.elem)
})
}
pub fn peek_front_mut(&mut self) -> Option<RefMut<T>> {
self.head.as_ref().map(|node| {
RefMut::map(node.borrow_mut(), |node| &mut node.elem)
})
}
そしてテストを大幅に肉付けします:
#[test]
fn basics() {
let mut list = List::new();
// 空のリストが正しく振る舞うことを確認
assert_eq!(list.pop_front(), None);
// リストを埋める
list.push_front(1);
list.push_front(2);
list.push_front(3);
// 通常の削除を確認
assert_eq!(list.pop_front(), Some(3));
assert_eq!(list.pop_front(), Some(2));
// 何も壊れていないことを確認するため、さらにいくつか push する
list.push_front(4);
list.push_front(5);
// 通常の削除を確認
assert_eq!(list.pop_front(), Some(5));
assert_eq!(list.pop_front(), Some(4));
// 枯渇を確認
assert_eq!(list.pop_front(), Some(1));
assert_eq!(list.pop_front(), None);
// ---- back -----
// 空のリストが正しく振る舞うことを確認
assert_eq!(list.pop_back(), None);
// リストを埋める
list.push_back(1);
list.push_back(2);
list.push_back(3);
// 通常の削除を確認
assert_eq!(list.pop_back(), Some(3));
assert_eq!(list.pop_back(), Some(2));
// 何も壊れていないことを確認するため、さらにいくつか push する
list.push_back(4);
list.push_back(5);
// 通常の削除を確認
assert_eq!(list.pop_back(), Some(5));
assert_eq!(list.pop_back(), Some(4));
// 枯渇を確認
assert_eq!(list.pop_back(), Some(1));
assert_eq!(list.pop_back(), None);
}
#[test]
fn peek() {
let mut list = List::new();
assert!(list.peek_front().is_none());
assert!(list.peek_back().is_none());
assert!(list.peek_front_mut().is_none());
assert!(list.peek_back_mut().is_none());
list.push_front(1); list.push_front(2); list.push_front(3);
assert_eq!(&*list.peek_front().unwrap(), &3);
assert_eq!(&mut *list.peek_front_mut().unwrap(), &mut 3);
assert_eq!(&*list.peek_back().unwrap(), &1);
assert_eq!(&mut *list.peek_back_mut().unwrap(), &mut 1);
}
テストしていないケースはあるでしょうか? おそらくあります。ここでは組み合わせ論的な空間が本当に膨れ上がっています。少なくとも私たちのコードは明らかに間違っているわけではありません。
> cargo test
Running target/debug/lists-5c71138492ad4b4a
running 10 tests
test first::test::basics ... ok
test fourth::test::basics ... ok
test second::test::basics ... ok
test fourth::test::peek ... ok
test second::test::iter ... ok
test third::test::iter ... ok
test second::test::into_iter ... ok
test second::test::iter_mut ... ok
test second::test::peek ... ok
test third::test::basics ... ok
test result: ok. 10 passed; 0 failed; 0 ignored; 0 measured
いいですね。コピペこそ最高のプログラミングです。
イテレーション
この厄介者をイテレートすることに取り組んでみましょう。
IntoIter
IntoIter はいつものように最も簡単です。スタックをラップして
pop を呼び出すだけです。
pub struct IntoIter<T>(List<T>);
impl<T> List<T> {
pub fn into_iter(self) -> IntoIter<T> {
IntoIter(self)
}
}
impl<T> Iterator for IntoIter<T> {
type Item = T;
fn next(&mut self) -> Option<Self::Item> {
self.0.pop_front()
}
}
しかし、興味深い新展開があります。以前はリストに対して「自然な」 イテレーション順序は常に 1 つしかありませんでしたが、Deque は本質的に 双方向です。前から後ろへ進むことの何がそんなに特別なのでしょうか?逆方向に イテレートしたい人がいたらどうでしょう?
Rust には実はこれに対する答えがあります。DoubleEndedIterator です。DoubleEndedIterator は
Iterator から継承しており(つまり、すべての DoubleEndedIterator は Iterator です)、
新しいメソッドを 1 つ要求します。それが next_back です。これは
next とまったく同じシグネチャを持ちますが、反対側の端から要素を生成することが期待されます。
DoubleEndedIterator のセマンティクスは私たちにとって非常に便利です。イテレータが
deque になるのです。両端が収束するまで前後から要素を消費でき、その時点で
イテレータは空になります。
Iterator と next の場合とよく似て、next_back は実のところ
DoubleEndedIterator の利用者が本当に気にするものではないことがわかります。むしろ、
このインターフェイスの最も良い部分は、要素を逆順に生成する新しいイテレータを作るために
イテレータをラップする rev メソッドを公開していることです。
このセマンティクスはかなり単純です。反転されたイテレータに対する next の呼び出しは、
単に next_back の呼び出しです。
ともあれ、私たちはすでに deque なので、この API を提供するのはかなり簡単です。
impl<T> DoubleEndedIterator for IntoIter<T> {
fn next_back(&mut self) -> Option<T> {
self.0.pop_back()
}
}
そして試してみましょう。
#[test]
fn into_iter() {
let mut list = List::new();
list.push_front(1); list.push_front(2); list.push_front(3);
let mut iter = list.into_iter();
assert_eq!(iter.next(), Some(3));
assert_eq!(iter.next_back(), Some(1));
assert_eq!(iter.next(), Some(2));
assert_eq!(iter.next_back(), None);
assert_eq!(iter.next(), None);
}
cargo test
Running target/debug/lists-5c71138492ad4b4a
running 11 tests
test fourth::test::basics ... ok
test fourth::test::peek ... ok
test fourth::test::into_iter ... ok
test first::test::basics ... ok
test second::test::basics ... ok
test second::test::iter ... ok
test second::test::iter_mut ... ok
test third::test::iter ... ok
test third::test::basics ... ok
test second::test::into_iter ... ok
test second::test::peek ... ok
test result: ok. 11 passed; 0 failed; 0 ignored; 0 measured
いいですね。
Iter
Iter は少し融通が利きません。またあのひどい Ref たちを相手にしなければなりません!
Ref があるため、以前のように &Node を格納することはできません。
代わりに、Ref<Node> を格納してみましょう。
pub struct Iter<'a, T>(Option<Ref<'a, Node<T>>>);
impl<T> List<T> {
pub fn iter(&self) -> Iter<T> {
Iter(self.head.as_ref().map(|head| head.borrow()))
}
}
> cargo build
ここまでは順調です。next の実装は少し厄介になりそうですが、古いスタックの IterMut と
同じ基本ロジックで、そこに追加の RefCell の狂気が加わったものだと思います。
impl<'a, T> Iterator for Iter<'a, T> {
type Item = Ref<'a, T>;
fn next(&mut self) -> Option<Self::Item> {
self.0.take().map(|node_ref| {
self.0 = node_ref.next.as_ref().map(|head| head.borrow());
Ref::map(node_ref, |node| &node.elem)
})
}
}
cargo build
error[E0521]: borrowed data escapes outside of closure
--> src/fourth.rs:155:13
|
153 | fn next(&mut self) -> Option<Self::Item> {
| --------- `self` is declared here, outside of the closure body
154 | self.0.take().map(|node_ref| {
155 | self.0 = node_ref.next.as_ref().map(|head| head.borrow());
| ^^^^^^ -------- borrow is only valid in the closure body
| |
| reference to `node_ref` escapes the closure body here
error[E0505]: cannot move out of `node_ref` because it is borrowed
--> src/fourth.rs:156:22
|
153 | fn next(&mut self) -> Option<Self::Item> {
| --------- lifetime `'1` appears in the type of `self`
154 | self.0.take().map(|node_ref| {
155 | self.0 = node_ref.next.as_ref().map(|head| head.borrow());
| ------ -------- borrow of `node_ref` occurs here
| |
| assignment requires that `node_ref` is borrowed for `'1`
156 | Ref::map(node_ref, |node| &node.elem)
| ^^^^^^^^ move out of `node_ref` occurs here
しまった。
node_ref の寿命が十分ではありません。通常の参照とは異なり、Rust は
このように Ref を単純に分割させてはくれません。head.borrow() から得られる Ref は
node_ref と同じ期間だけしか生存できませんが、私たちは最終的に
Ref::map 呼び出しでそれを破棄してしまいます。
私たちが求めている関数は存在し、[map_split][] と呼ばれています。
pub fn map_split<U, V, F>(orig: Ref<'b, T>, f: F) -> (Ref<'b, U>, Ref<'b, V>) where
F: FnOnce(&T) -> (&U, &V),
U: ?Sized,
V: ?Sized,
うへえ。試してみましょう……
fn next(&mut self) -> Option<Self::Item> {
self.0.take().map(|node_ref| {
let (next, elem) = Ref::map_split(node_ref, |node| {
(&node.next, &node.elem)
});
self.0 = next.as_ref().map(|head| head.borrow());
elem
})
}
cargo build
Compiling lists v0.1.0 (/Users/ADesires/dev/temp/lists)
error[E0521]: borrowed data escapes outside of closure
--> src/fourth.rs:159:13
|
153 | fn next(&mut self) -> Option<Self::Item> {
| --------- `self` is declared here, outside of the closure body
...
159 | self.0 = next.as_ref().map(|head| head.borrow());
| ^^^^^^ ---- borrow is only valid in the closure body
| |
| reference to `next` escapes the closure body here
うーん。ライフタイムを正しくするには、再び Ref::Map する必要があります。しかし Ref::Map は
Ref を返し、私たちが必要としているのは Option<Ref> です。けれど、Option に対して map するには
Ref を経由する必要があります……
長いあいだ遠くを見つめる
??????
fn next(&mut self) -> Option<Self::Item> {
self.0.take().map(|node_ref| {
let (next, elem) = Ref::map_split(node_ref, |node| {
(&node.next, &node.elem)
});
self.0 = if next.is_some() {
Some(Ref::map(next, |next| &**next.as_ref().unwrap()))
} else {
None
};
elem
})
}
error[E0308]: mismatched types
--> src/fourth.rs:162:22
|
162 | Some(Ref::map(next, |next| &**next.as_ref().unwrap()))
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ expected struct `fourth::Node`, found struct `std::cell::RefCell`
|
= note: expected type `std::cell::Ref<'_, fourth::Node<_>>`
found type `std::cell::Ref<'_, std::cell::RefCell<fourth::Node<_>>>`
ああ。そうか。RefCell が複数あるのです。リストの奥へ進めば進むほど、 各 RefCell の下でより深くネストされていきます。保持している未解放の 借用をすべて表すために、いわば Ref のスタックを維持する必要があります。 というのも、ある要素を見るのをやめるなら、その前にあるすべての RefCell の 借用カウントをデクリメントしなければならないからです……………..
ここでできることは何もないと思います。行き止まりです。RefCell から 抜け出すことを試してみましょう。
では Rc はどうでしょう。そもそも参照を保存する必要があると誰が言ったのでしょうか?
Rc 全体を Clone して、リストの途中への扱いやすい所有ハンドルを得ればよいのでは
ないでしょうか?
pub struct Iter<T>(Option<Rc<Node<T>>>);
impl<T> List<T> {
pub fn iter(&self) -> Iter<T> {
Iter(self.head.as_ref().map(|head| head.clone()))
}
}
impl<T> Iterator for Iter<T> {
type Item =
うーん……待って、今度は何を返せばいいのでしょう? &T? Ref<T>?
いいえ、そのどれもうまくいきません……Iter にはもうライフタイムがないのです! &T も
Ref<T> も、next に入る前に何らかのライフタイムをあらかじめ宣言する必要があります。
しかし Rc からなんとか取り出したものは何であれ、Iterator を借用することになるはずで……
頭が……痛い……ああああああああ
もしかすると……Rc を……map して……Rc<T> を得られるのでしょうか? そんなものはあるのでしょうか?
Rc のドキュメントにはそのようなものは見当たりません。実際には、それを可能にする
クレートを作った人がいます。
しかし待ってください。たとえそれをしたとしても、さらに大きな問題があります。
恐るべきイテレータの無効化という亡霊です。以前は、Iter がリストを借用して
完全に不変にしていたため、イテレータの無効化とは完全に無縁でした。しかし Iter が
Rc を返すなら、それらはリストをまったく借用しません! つまり、リスト内部への
ポインタを保持している間に、利用者がそのリストに対して push や pop を呼び始められる
ということです!
なんてことだ、それはどうなるのでしょうか?!
まあ、push は実際には問題ありません。リストのある部分範囲へのビューを持っていて、 リストは単に私たちの視界の外側へ伸びていくだけです。大した問題ではありません。
しかし pop は別の話です。私たちの範囲外の要素を pop しているなら、それでも
問題ないはずです。それらのノードは見えないので、何も起きません。しかし、私たちが
指しているノードを pop しようとすると……すべてが吹き飛びます! 具体的には、
try_unwrap の結果を unwrap しようとしたとき、実際には失敗し、プログラム全体が
panic します。
これは実際かなりすごいことです。リスト内部への所有ポインタを大量に取得し、 それと同時にリストを変更できます。そして、私たちが指しているノードを削除しようとするまでは ただ動くのです。さらにその場合でも、ダングリングポインタなどが発生するわけではなく、 プログラムは決定的に panic します!
しかし、Rc の map に加えてイテレータの無効化にも対処しなければならないのは、
どうにも……よくありません。Rc<RefCell> は本当に、本当に、ついに私たちを裏切りました。
興味深いことに、永続スタックの場合とは逆転した状況を経験しました。永続スタックは
データの所有権を取り戻すのに苦労した一方で参照はいくらでも取得できましたが、
私たちのリストは所有権を得ることには何の問題もない一方で、参照を貸し出すことに
非常に苦労しました。
もっとも公平を期すなら、苦労の大部分は、実装の詳細を隠し、まともな API を 持ちたいということを中心にしていました。あちこちで Node を渡し回すつもりなら、 すべて問題なく行うことはできました。
実際、同じ要素に対して可変アクセスしていないことをランタイムでチェックされる、 複数の並行した IterMut を作ることだってできました!
本当に、この設計は API の利用者に決して公開されない内部データ構造により適しています。 内部可変性は、安全なアプリケーションを書くには素晴らしいものです。安全なライブラリには、 それほど向いていません。
ともあれ、これが Iter と IterMut を諦めるということです。実装することはできるでしょうが、 うへえ。
最終コード
さて、これで Rust における 100% 安全な双方向リンクリストを実装したことになります。実装は悪夢のようで、実装の詳細が漏れ出しており、いくつかの基本的な操作もサポートしていません。
それでも、存在はしています。
ああ、それに Rc と RefCell の間の正当性のために、大量の「不要な」ランタイムチェックだらけでもあります。不要、を引用符付きにしたのは、それらが実際には全体が 実際に安全である ことを保証するために必要だからです。実際にそれらのチェックが必要だった箇所にいくつか遭遇しました。双方向リンクリストのエイリアシングと所有権の話は、ひどくもつれています!
それでも、これは私たちにできることです。特に、内部データ構造を利用者に公開することを気にしないのであれば。
ここから先は、このコインのもう一方の面に焦点を当てていきます。つまり、実装を unsafe にすることで、すべての制御を取り戻すことです。
#![allow(unused)]
fn main() {
use std::rc::Rc;
use std::cell::{Ref, RefMut, RefCell};
pub struct List<T> {
head: Link<T>,
tail: Link<T>,
}
type Link<T> = Option<Rc<RefCell<Node<T>>>>;
struct Node<T> {
elem: T,
next: Link<T>,
prev: Link<T>,
}
impl<T> Node<T> {
fn new(elem: T) -> Rc<RefCell<Self>> {
Rc::new(RefCell::new(Node {
elem: elem,
prev: None,
next: None,
}))
}
}
impl<T> List<T> {
pub fn new() -> Self {
List { head: None, tail: None }
}
pub fn push_front(&mut self, elem: T) {
let new_head = Node::new(elem);
match self.head.take() {
Some(old_head) => {
old_head.borrow_mut().prev = Some(new_head.clone());
new_head.borrow_mut().next = Some(old_head);
self.head = Some(new_head);
}
None => {
self.tail = Some(new_head.clone());
self.head = Some(new_head);
}
}
}
pub fn push_back(&mut self, elem: T) {
let new_tail = Node::new(elem);
match self.tail.take() {
Some(old_tail) => {
old_tail.borrow_mut().next = Some(new_tail.clone());
new_tail.borrow_mut().prev = Some(old_tail);
self.tail = Some(new_tail);
}
None => {
self.head = Some(new_tail.clone());
self.tail = Some(new_tail);
}
}
}
pub fn pop_back(&mut self) -> Option<T> {
self.tail.take().map(|old_tail| {
match old_tail.borrow_mut().prev.take() {
Some(new_tail) => {
new_tail.borrow_mut().next.take();
self.tail = Some(new_tail);
}
None => {
self.head.take();
}
}
Rc::try_unwrap(old_tail).ok().unwrap().into_inner().elem
})
}
pub fn pop_front(&mut self) -> Option<T> {
self.head.take().map(|old_head| {
match old_head.borrow_mut().next.take() {
Some(new_head) => {
new_head.borrow_mut().prev.take();
self.head = Some(new_head);
}
None => {
self.tail.take();
}
}
Rc::try_unwrap(old_head).ok().unwrap().into_inner().elem
})
}
pub fn peek_front(&self) -> Option<Ref<T>> {
self.head.as_ref().map(|node| {
Ref::map(node.borrow(), |node| &node.elem)
})
}
pub fn peek_back(&self) -> Option<Ref<T>> {
self.tail.as_ref().map(|node| {
Ref::map(node.borrow(), |node| &node.elem)
})
}
pub fn peek_back_mut(&mut self) -> Option<RefMut<T>> {
self.tail.as_ref().map(|node| {
RefMut::map(node.borrow_mut(), |node| &mut node.elem)
})
}
pub fn peek_front_mut(&mut self) -> Option<RefMut<T>> {
self.head.as_ref().map(|node| {
RefMut::map(node.borrow_mut(), |node| &mut node.elem)
})
}
pub fn into_iter(self) -> IntoIter<T> {
IntoIter(self)
}
}
impl<T> Drop for List<T> {
fn drop(&mut self) {
while self.pop_front().is_some() {}
}
}
pub struct IntoIter<T>(List<T>);
impl<T> Iterator for IntoIter<T> {
type Item = T;
fn next(&mut self) -> Option<T> {
self.0.pop_front()
}
}
impl<T> DoubleEndedIterator for IntoIter<T> {
fn next_back(&mut self) -> Option<T> {
self.0.pop_back()
}
}
#[cfg(test)]
mod test {
use super::List;
#[test]
fn basics() {
let mut list = List::new();
// 空のリストが正しく振る舞うことを確認
assert_eq!(list.pop_front(), None);
// リストを埋める
list.push_front(1);
list.push_front(2);
list.push_front(3);
// 通常の削除を確認
assert_eq!(list.pop_front(), Some(3));
assert_eq!(list.pop_front(), Some(2));
// 何も壊れていないことを確認するために、さらにいくつか push する
list.push_front(4);
list.push_front(5);
// 通常の削除を確認
assert_eq!(list.pop_front(), Some(5));
assert_eq!(list.pop_front(), Some(4));
// 使い切りを確認
assert_eq!(list.pop_front(), Some(1));
assert_eq!(list.pop_front(), None);
// ---- 後ろ -----
// 空のリストが正しく振る舞うことを確認
assert_eq!(list.pop_back(), None);
// リストを埋める
list.push_back(1);
list.push_back(2);
list.push_back(3);
// 通常の削除を確認
assert_eq!(list.pop_back(), Some(3));
assert_eq!(list.pop_back(), Some(2));
// 何も壊れていないことを確認するために、さらにいくつか push する
list.push_back(4);
list.push_back(5);
// 通常の削除を確認
assert_eq!(list.pop_back(), Some(5));
assert_eq!(list.pop_back(), Some(4));
// 使い切りを確認
assert_eq!(list.pop_back(), Some(1));
assert_eq!(list.pop_back(), None);
}
#[test]
fn peek() {
let mut list = List::new();
assert!(list.peek_front().is_none());
assert!(list.peek_back().is_none());
assert!(list.peek_front_mut().is_none());
assert!(list.peek_back_mut().is_none());
list.push_front(1); list.push_front(2); list.push_front(3);
assert_eq!(&*list.peek_front().unwrap(), &3);
assert_eq!(&mut *list.peek_front_mut().unwrap(), &mut 3);
assert_eq!(&*list.peek_back().unwrap(), &1);
assert_eq!(&mut *list.peek_back_mut().unwrap(), &mut 1);
}
#[test]
fn into_iter() {
let mut list = List::new();
list.push_front(1); list.push_front(2); list.push_front(3);
let mut iter = list.into_iter();
assert_eq!(iter.next(), Some(3));
assert_eq!(iter.next_back(), Some(1));
assert_eq!(iter.next(), Some(2));
assert_eq!(iter.next_back(), None);
assert_eq!(iter.next(), None);
}
}
}
まあまあな unsafe 単方向連結キュー
さて、あの参照カウント付きの内部可変性の話は少し手に負えなくなってきました。 Rust は本当に、一般的にああいうことをするよう求めているわけではないですよね? まあ、はいともいいえとも言えます。Rc と Refcell は単純なケースを扱うには非常に便利ですが、 扱いにくくなることもあります。特に、それが起きていることを隠したい場合はなおさらです。 もっと良い方法があるはずです!
この章では、単方向連結リストに戻り、生ポインタと Unsafe Rust に少し触れるために、単方向連結キューを実装します。
ナレーター: そして私は間違いを指摘します。
そして私たちは一切間違いを犯しません。
fifth.rs という新しいファイルを追加しましょう。
// lib.rs 内
pub mod first;
pub mod second;
pub mod third;
pub mod fourth;
pub mod fifth;
私たちのコードは主に second.rs から派生したものになります。というのも、連結リストの世界では、 キューはほとんどスタックを拡張したものだからです。それでも、レイアウトなどに関して対処したい 根本的な問題がいくつかあるため、最初から進めていきます。
レイアウト
では、単方向リンクキューとはどのようなものでしょうか? 単方向リンクスタックでは、 リストの一方の端に push し、同じ端から pop していました。スタックとキューの唯一の 違いは、キューはもう一方の端から pop するという点です。したがって、スタックの実装からは 次のようになります。
input list:
[Some(ptr)] -> (A, Some(ptr)) -> (B, None)
stack push X:
[Some(ptr)] -> (X, Some(ptr)) -> (A, Some(ptr)) -> (B, None)
stack pop:
[Some(ptr)] -> (A, Some(ptr)) -> (B, None)
キューを作るには、どちらの操作をリストの末尾へ移動するかを決めるだけです。 push でしょうか、それとも pop でしょうか? リストは単方向リンクなので、 実はどちらの操作でも同じ労力で末尾へ移動できます。
push を末尾へ移動するには、None までずっとたどっていき、それを新しい要素を持つ
Some に設定するだけです。
input list:
[Some(ptr)] -> (A, Some(ptr)) -> (B, None)
flipped push X:
[Some(ptr)] -> (A, Some(ptr)) -> (B, Some(ptr)) -> (X, None)
pop を末尾へ移動するには、None の直前のノードまでずっとたどっていき、
それを take するだけです。
input list:
[Some(ptr)] -> (A, Some(ptr)) -> (B, Some(ptr)) -> (X, None)
flipped pop:
[Some(ptr)] -> (A, Some(ptr)) -> (B, None)
今日のところはこれをやって終わりにすることもできますが、それではひどいものになります! これらの操作はどちらもリスト全体を走査します。このようなキュー実装は正しいインターフェイスを 公開しているので確かにキューだ、と主張する人もいるでしょう。しかし私は、性能保証も インターフェイスの一部だと考えています。正確な漸近的な境界にはこだわりません。 単に「速い」か「遅い」かです。キューは push と pop が速いことを保証しており、 リスト全体を走査するのは間違いなく速くありません。
重要な観察点の 1 つは、同じことを何度も繰り返して大量の作業を無駄にしていることです。 その作業をすべて「キャッシュ」して再利用できないでしょうか? もちろん、できます! リストの末尾へのポインタを保存して、そこへ直接ジャンプすればよいのです!
これでうまくいくのは、push と pop の反転のうち一方だけであることがわかります。
pop を反転するには「tail」ポインタを後ろへ戻す必要がありますが、
リストは単方向リンクなので、それを効率的に行うことはできません。
代わりに push を反転するなら、「head」ポインタを前へ進めるだけでよく、
これは簡単です。
試してみましょう。
use std::mem;
pub struct List<T> {
head: Link<T>,
tail: Link<T>, // 新規!
}
type Link<T> = Option<Box<Node<T>>>;
struct Node<T> {
elem: T,
next: Link<T>,
}
impl<T> List<T> {
pub fn new() -> Self {
List { head: None, tail: None }
}
pub fn push(&mut self, elem: T) {
let new_tail = Box::new(Node {
elem: elem,
// tail に push するとき、next は常に None です
next: None,
});
// 古い tail を、新しい tail を指すように入れ替える
let old_tail = mem::replace(&mut self.tail, Some(new_tail));
match old_tail {
Some(mut old_tail) => {
// 古い tail が存在した場合、それが新しい tail を指すように更新する
old_tail.next = Some(new_tail);
}
None => {
// そうでなければ、head がそれを指すように更新する
self.head = Some(new_tail);
}
}
}
}
この種のことにはかなり慣れているはずなので、ここでは impl の詳細を少し速いペースで
進めています。もっとも、このコードを必ずしも最初の試行で書けると期待すべき、
というわけではありません。以前対処してきた試行錯誤の一部を飛ばしているだけです。
実際には、このコードを書くときにここでは見せていない大量のミスをしましたが、
mut や ; を付け忘れるところを何度も見せても、学びにならなくなってしまいます。
心配しないでください。他のエラーメッセージはたっぷり見ることになります!
> cargo build
error[E0382]: use of moved value: `new_tail`
--> src/fifth.rs:38:38
|
26 | let new_tail = Box::new(Node {
| -------- move occurs because `new_tail` has type `std::boxed::Box<fifth::Node<T>>`, which does not implement the `Copy` trait
...
33 | let old_tail = mem::replace(&mut self.tail, Some(new_tail));
| -------- value moved here
...
38 | old_tail.next = Some(new_tail);
| ^^^^^^^^ value used here after move
しまった!
moved された値
new_tailの使用
Box は Copy を実装していないので、単に 2 つの場所に代入することはできません。
さらに重要なのは、Box はそれが指しているものを所有しており、drop されるときに
それを解放しようとするということです。もし push の実装がコンパイルできてしまったら、
リストの tail を二重解放することになります! 実際、書かれているとおりでは、
このコードは push のたびに old_tail を解放してしまいます。ひえっ! 🙀
さて、非所有ポインタの作り方はわかっています。それは単なる参照です!
pub struct List<T> {
head: Link<T>,
tail: Option<&mut Node<T>>, // 新規!
}
type Link<T> = Option<Box<Node<T>>>;
struct Node<T> {
elem: T,
next: Link<T>,
}
impl<T> List<T> {
pub fn new() -> Self {
List { head: None, tail: None }
}
pub fn push(&mut self, elem: T) {
let new_tail = Box::new(Node {
elem: elem,
// tail に push するとき、next は常に None です
next: None,
});
// box を正しい場所に置き、それからその Node への参照を取得する
let new_tail = match self.tail.take() {
Some(old_tail) => {
// 古い tail が存在した場合、それが新しい tail を指すように更新する
old_tail.next = Some(new_tail);
old_tail.next.as_deref_mut()
}
None => {
// そうでなければ、head がそれを指すように更新する
self.head = Some(new_tail);
self.head.as_deref_mut()
}
};
self.tail = new_tail;
}
}
ここにはそれほど難しいことはありません。前のコードと同じ基本的な考え方ですが、 実際の Box を詰め込んだ場所から tail の参照を取り出すために、 暗黙の return の便利さを使っている点が違います。
> cargo build
error[E0106]: missing lifetime specifier
--> src/fifth.rs:3:18
|
3 | tail: Option<&mut Node<T>>, // NEW!
| ^ expected lifetime parameter
ああ、そうでした。型の中の参照にはライフタイムを与える必要があります。うーん……
この参照のライフタイムは何でしょう? これは IterMut のように見えますよね?
IterMut で行ったことを試して、単にジェネリックな 'a を追加してみましょう。
pub struct List<'a, T> {
head: Link<T>,
tail: Option<&'a mut Node<T>>, // 新規!
}
type Link<T> = Option<Box<Node<T>>>;
struct Node<T> {
elem: T,
next: Link<T>,
}
impl<'a, T> List<'a, T> {
pub fn new() -> Self {
List { head: None, tail: None }
}
pub fn push(&mut self, elem: T) {
let new_tail = Box::new(Node {
elem: elem,
// 末尾に push すると、next は常に None になる
next: None,
});
// Box を適切な場所に置き、それからその Node への参照を取得する
let new_tail = match self.tail.take() {
Some(old_tail) => {
// 古い tail が存在する場合は、新しい tail を指すように更新する
old_tail.next = Some(new_tail);
old_tail.next.as_deref_mut()
}
None => {
// そうでない場合は、それを指すように head を更新する
self.head = Some(new_tail);
self.head.as_deref_mut()
}
};
self.tail = new_tail;
}
}
cargo build
error[E0495]: cannot infer an appropriate lifetime for autoref due to conflicting requirements
--> src/fifth.rs:35:27
|
35 | self.head.as_deref_mut()
| ^^^^^^^^^^^^
|
note: first, the lifetime cannot outlive the anonymous lifetime #1 defined on the method body at 18:5...
--> src/fifth.rs:18:5
|
18 | / pub fn push(&mut self, elem: T) {
19 | | let new_tail = Box::new(Node {
20 | | elem: elem,
21 | | // When you push onto the tail, your next is always None
... |
39 | | self.tail = new_tail;
40 | | }
| |_____^
note: ...so that reference does not outlive borrowed content
--> src/fifth.rs:35:17
|
35 | self.head.as_deref_mut()
| ^^^^^^^^^
note: but, the lifetime must be valid for the lifetime 'a as defined on the impl at 13:6...
--> src/fifth.rs:13:6
|
13 | impl<'a, T> List<'a, T> {
| ^^
= note: ...so that the expression is assignable:
expected std::option::Option<&'a mut fifth::Node<T>>
found std::option::Option<&mut fifth::Node<T>>
うわ、これは本当に詳細なエラーメッセージです。少し気がかりです。なぜなら、私たちが何か本当にめちゃくちゃなことをしていると示唆しているからです。興味深い部分はここです。
lifetime は、impl で定義された lifetime
'aに対して有効でなければならない
私たちは self から借用していますが、コンパイラはそれが 'a と同じだけ長く続くことを求めています。では、self は実際にその長さだけ続く、と伝えたらどうなるでしょうか..?
pub fn push(&'a mut self, elem: T) {
cargo build
warning: field is never used: `elem`
--> src/fifth.rs:9:5
|
9 | elem: T,
| ^^^^^^^
|
= note: #[warn(dead_code)] on by default
おや、うまくいきました!素晴らしい!
pop もやってしまいましょう。
pub fn pop(&'a mut self) -> Option<T> {
// リストの現在の head を取得する
self.head.take().map(|head| {
let head = *head;
self.head = head.next;
// `head` がなくなった場合は、必ず tail を `None` に設定する。
if self.head.is_none() {
self.tail = None;
}
head.elem
})
}
そして、そのための簡単なテストを書きます。
#[cfg(test)]
mod test {
use super::List;
#[test]
fn basics() {
let mut list = List::new();
// 空のリストが正しく振る舞うことを確認する
assert_eq!(list.pop(), None);
// リストに要素を追加する
list.push(1);
list.push(2);
list.push(3);
// 通常の削除を確認する
assert_eq!(list.pop(), Some(1));
assert_eq!(list.pop(), Some(2));
// 何も壊れていないことを確認するために、さらにいくつか push する
list.push(4);
list.push(5);
// 通常の削除を確認する
assert_eq!(list.pop(), Some(3));
assert_eq!(list.pop(), Some(4));
// 空になるまで取り出すことを確認する
assert_eq!(list.pop(), Some(5));
assert_eq!(list.pop(), None);
}
}
cargo test
error[E0499]: cannot borrow `list` as mutable more than once at a time
--> src/fifth.rs:68:9
|
65 | assert_eq!(list.pop(), None);
| ---- first mutable borrow occurs here
...
68 | list.push(1);
| ^^^^
| |
| second mutable borrow occurs here
| first borrow later used here
error[E0499]: cannot borrow `list` as mutable more than once at a time
--> src/fifth.rs:69:9
|
65 | assert_eq!(list.pop(), None);
| ---- first mutable borrow occurs here
...
69 | list.push(2);
| ^^^^
| |
| second mutable borrow occurs here
| first borrow later used here
error[E0499]: cannot borrow `list` as mutable more than once at a time
--> src/fifth.rs:70:9
|
65 | assert_eq!(list.pop(), None);
| ---- first mutable borrow occurs here
...
70 | list.push(3);
| ^^^^
| |
| second mutable borrow occurs here
| first borrow later used here
....
** WAY MORE LINES OF ERRORS **
....
error: aborting due to 11 previous errors
🙀🙀🙀🙀🙀🙀🙀🙀🙀🙀🙀🙀🙀🙀🙀🙀🙀🙀🙀🙀🙀🙀🙀
なんということでしょう。
コンパイラが私たちに向かって盛大に吐き散らすのも無理はありません。私たちは Rust における重大な罪を犯しました。自分自身への参照を、自分自身の中に格納したのです。どういうわけか、push と pop の実装の中では、これがまったく筋が通っていると Rust を納得させることに成功してしまいました(正直、できたことに本当に驚きました)。
これがある程度うまくいく理由は、Rust にはそもそも自分自身の中へのポインタという概念が実質的に存在しないからです。コードの各部分は、単独で見れば技術的には正しいのです(push と pop を一度だけ呼ぶことはできます)。しかし、その後、私たちが作り出したものの不条理さが効いてきて、すべてが固まってしまいます。
私たちが書いたものにも何らかの用途はあるのかもしれませんが、少なくとも私に言わせれば、これは構文的には有効な意味不明な代物にすぎません。私たちは lifetime 'a を持つ何かを自分が含んでおり、push と pop はその lifetime の間ずっと self を借用する、と言っているのです。それは奇妙ですが、Rust は私たちのコードの各部分を個別に見て、破られているルールがないと判断できます。
しかし、実際にリストを使おうとした瞬間、コンパイラはすぐに「はい、あなたは self を 'a の間ミュータブルに借用しました。したがって 'a の終わりまで self はもう使えません」と言い、同時に「あなたは 'a を含んでいるので、それはリストが存在する全期間に対して有効でなければなりません」と言います。
これはほとんど矛盾ですが、解は1つあります。push または pop した瞬間、リストは自分自身をその場に「ピン留め」し、それ以上アクセスできなくなります。ことわざに言う自分の尻尾を飲み込み、夢の世界へと昇っていったのです。
ナレーター: この本が最初に書かれたときには存在していませんでしたが、Rust は 実際に pin という概念を有用なものとして正式化しました! これはおそらく、借用チェッカー 以来、言語に加えられた最も複雑な追加でした。 とはいえ、私たちのリストはピン留めされたくはありません!
Pin は async-await/futures/coroutines にとっては必要で有用です。なぜなら コンパイラは、関数のすべてのローカル変数を何らかの構造体にまとめ、 future/coroutine が再開可能になるまでどこかに保存できる必要があるからです。 ローカル変数は他のローカル変数を参照でき、私たちはそれが動作することを望むため、 これらの構造体は自分自身への参照を含むことになり得ます!
そのため、Rust が
awaitやyieldを行うには、ピン留めされた値を適切に記述し、 操作できる方法が必要です。ありがたいことに、これらのものはすべて大部分 自動的なコンパイラの仕組みの中に隠されており、通常の状況では実際にPin(あるいは Futures でさえ)について考える必要はありません。主な例外は、 tokio のような async ランタイム を構築・設計している人たちにとっては、 この仕組みが非常に重要だということです。この本では async ランタイムを実装しません。私の友人たちが
Pinでできる さまざまな「クールな」(めちゃくちゃな)トリックを知っていることは分かっていますが、 私の見る限り、それらを知らないままでいたほうが幸せそうです。 私はこれからも、ピン留めされた型など実在せず、私を傷つけることはできないと 自分に言い聞かせ続けます。
私たちの pop 実装は、自分自身への参照を自分自身の内部に保存することが
なぜ本当に危険になり得るのかを示唆しています。
// ...
if self.head.is_none() {
self.tail = None;
}
これを忘れたらどうなるでしょう? その場合、tail はリストから削除された 何らかのノードを指すことになります。そのようなノードは即座に解放されるため、 Rust が私たちを守ってくれるはずだったダングリングポインタを持つことになってしまいます!
そして実際、Rust はその種の危険から私たちを守ってくれています。ただし、とても…… 回りくどい方法で。
では、どうすればよいのでしょうか? Rc<RefCell>> 地獄に戻る?
勘弁してください。
いいえ、代わりに私たちは脱線して生ポインタを使います。 私たちのレイアウトは次のようになります。
pub struct List<T> {
head: Link<T>,
tail: *mut Node<T>, // 危険 危険
}
type Link<T> = Option<Box<Node<T>>>;
struct Node<T> {
elem: T,
next: Link<T>,
}
これで終わりです。参照カウント付き動的借用チェックなんていう軟弱な たわごとは一切ありません! 本物の。ハードな。未チェックの。ポインタです。
ナレーター: 実際には、この実装は依然として危険なほど間違っていましたが、その教訓を学ぶ時はまだ来ていませんでした。次のセクションでは、いつものように痛い目を見てそれを学ぶことになります。
みんなで C になりましょう。一日中 C でいましょう。
ただいま。準備はできています。
こんにちは、unsafe。
ナレーター: わあ、ここで著者の信じられないほどの思い上がりです。
Unsafe Rust
これは深刻で、大きく、複雑で、危険なトピックです。 あまりに深刻なので、これについてはまるごと別の本を書きました。
要するに、他の言語を呼び出すことを許可した途端、すべての言語は実際には unsafe になります。なぜなら、C に任意にひどいことをさせられるからです。そうです。Java、Python、Ruby、Haskell……Foreign Function Interface (FFI) の前では、誰もが途方もなく unsafe なのです。
Rust は、自身を 2 つの言語、Safe Rust と Unsafe Rust に分けることでこの真実を受け入れています。ここまで私たちは Safe Rust だけを扱ってきました。これは完全に 100% safe です……ただし、FFI で Unsafe Rust を呼び出せる点を除けば。
Unsafe Rust は Safe Rust のスーパーセットです。すべてのセマンティクスとルールにおいて Safe Rust と完全に同じですが、C に付きまとう恐ろしい未定義動作を引き起こしうる、途方もなく unsafe なことをいくつか追加で行うことが許されているだけです。
繰り返しますが、これは本当に巨大なトピックで、興味深いコーナーケースがたくさんあります。 私はこれについて本当に深く踏み込みたくありません(まあ、本当は踏み込みたいのですが。実際に踏み込みました。その本を読んでください)。大丈夫です。なぜなら連結リストでは、実のところそのほとんどすべてを無視できるからです。
ナレーター: これは嘘だったが、2015 年には真実に見えた。
私たちが使う主な Unsafe の道具は生ポインターです。生ポインターは基本的に C のポインターです。固有のエイリアシング規則はありません。ライフタイムもありません。null になれます。アラインメントがずれていることがあります。ダングリングになれます。初期化されていないメモリを指すことができます。整数との相互変換ができます。別の型を指すようにキャストできます。ミュータビリティ? キャストすればよいのです。ほとんど何でもありであり、それはつまり、ほとんど何でも悪い方向に進みうるということです。
ナレーター: 固有のエイリアシング規則がない、だって? ああ、若さゆえの無邪気さよ。
これはかなり厄介な代物であり、正直なところ、これらに一切触れずに済むなら、そのほうが幸せな人生を送れるでしょう。残念ながら、私たちは連結リストを書きたいのであり、連結リストはひどいものです。つまり unsafe ポインターを使わなければならないということです。
生ポインターには *const T と *mut T の 2 種類があります。これらは C の const T* と T* を意図したものですが、C がそれらをどういう意味だと考えているかについて、私たちは実際のところそこまで気にしません。*const T は &T としてしかデリファレンスできませんが、変数のミュータビリティとよく似ていて、これは誤った使用に対する lint にすぎません。せいぜい、最初に *const を *mut にキャストする必要がある、という意味でしかありません。もっとも、そのポインターの参照先をミューテートする権限が実際にはないなら、ひどい目に遭うことになります。
ともあれ、コードを書いていくうちに、これについてよりよい感触がつかめるでしょう。今のところは、
*mut T == &unchecked mut T です!
基本
語り手: このセクションには根本的な誤りが潜んでいます。というのも、それこそがこの本全体の要点だからです。ただし、いったん
unsafeを使い始めると、誤ったことをしていてもすべてがコンパイルされ、一見動作しているように見えることがあります。その根本的な間違いは次のセクションで特定します。このセクションの内容を実際のプロダクションコードで使わないでください!
さて、基本に戻りましょう。リストをどのように構築すればよいでしょうか?
以前は単にこうしていました。
impl<T> List<T> {
pub fn new() -> Self {
List { head: None, tail: None }
}
}
しかし、もう tail には Option を使っていません。
> cargo build
error[E0308]: mismatched types
--> src/fifth.rs:15:34
|
15 | List { head: None, tail: None }
| ^^^^ expected *-ptr, found
| enum `std::option::Option`
|
= note: expected type `*mut fifth::Node<T>`
found type `std::option::Option<_>`
Option を使うこともできるのですが、Box とは異なり、*mut は nullable です。つまり、null ポインタ最適化の恩恵を受けられません。代わりに、None を表すために null を使います。
では、どうやって null ポインタを得るのでしょうか?いくつか方法はありますが、私は std::ptr::null_mut() を使うのが好みです。望むなら 0 as *mut _ を使うこともできますが、それはちょっとごちゃごちゃしているように見えます。
use std::ptr;
// 定義...
impl<T> List<T> {
pub fn new() -> Self {
List { head: None, tail: ptr::null_mut() }
}
}
cargo build
warning: field is never used: `head`
--> src/fifth.rs:4:5
|
4 | head: Link<T>,
| ^^^^^^^^^^^^^
|
= note: #[warn(dead_code)] on by default
warning: field is never used: `tail`
--> src/fifth.rs:5:5
|
5 | tail: *mut Node<T>,
| ^^^^^^^^^^^^^^^^^^
warning: field is never used: `elem`
--> src/fifth.rs:11:5
|
11 | elem: T,
| ^^^^^^^
warning: field is never used: `head`
--> src/fifth.rs:12:5
|
12 | head: Link<T>,
| ^^^^^^^^^^^^^
コンパイラ、しーっ。すぐに使うから。
さて、もう一度 push を書く作業に進みましょう。今回は、挿入後に Option<&mut Node<T>> を取得するのではなく、Box の中身への *mut Node<T> をすぐに取得します。これを健全に行えることはわかっています。なぜなら、Box の中身は、Box 自体を移動しても安定したアドレスを持つからです。もちろん、これは安全ではありません。なぜなら、単に Box を drop してしまえば、解放済みメモリへのポインタを持つことになるからです。
通常のポインタから生ポインタを作るにはどうすればよいでしょうか?型強制です!変数が生ポインタとして宣言されていれば、通常の参照はそこへ型強制されます。
let raw_tail: *mut _ = &mut *new_tail;
必要な情報はすべて揃いました。コードを、おおよそ以前の参照版へと変換できます。
pub fn push(&mut self, elem: T) {
let mut new_tail = Box::new(Node {
elem: elem,
next: None,
});
let raw_tail: *mut _ = &mut *new_tail;
// .is_null は null をチェックし、None をチェックすることと同等です
if !self.tail.is_null() {
// 古い tail が存在していた場合は、新しい tail を指すように更新します
self.tail.next = Some(new_tail);
} else {
// そうでなければ、head がそれを指すように更新します
self.head = Some(new_tail);
}
self.tail = raw_tail;
}
> cargo build
error[E0609]: no field `next` on type `*mut fifth::Node<T>`
--> src/fifth.rs:31:23
|
31 | self.tail.next = Some(new_tail);
| ----------^^^^
| |
| help: `self.tail` is a raw pointer;
| try dereferencing it: `(*self.tail).next`
え?Node へのポインタを持っているのに、なぜ next フィールドを取得できないのでしょうか?
生ポインタを使うとき、Rust はちょっと意地悪です。生ポインタの中身へアクセスするには、手動でそれを deref することを要求します。なぜなら、それは非常に unsafe な操作だからです。では、そうしましょう。
*self.tail.next = Some(new_tail);
> cargo build
error[E0609]: no field `next` on type `*mut fifth::Node<T>`
--> src/fifth.rs:31:23
|
31 | *self.tail.next = Some(new_tail);
| -----------^^^^
| |
| help: `self.tail` is a raw pointer;
| try dereferencing it: `(*self.tail).next`
ううう、演算子の優先順位。
(*self.tail).next = Some(new_tail);
> cargo build
error[E0133]: dereference of raw pointer is unsafe and requires
unsafe function or block
--> src/fifth.rs:31:13
|
31 | (*self.tail).next = Some(new_tail);
| ^^^^^^^^^^^^^^^^^ dereference of raw pointer
|
= note: raw pointers may be NULL, dangling or unaligned;
they can violate aliasing rules and cause data races:
all of these are undefined behavior
こんなに。大変で。あるべきじゃ。ない。
Unsafe Rust は Safe Rust にとっての FFI 言語のようなものだ、と私が言ったのを覚えていますか?さて、コンパイラは、この FFI 的なことを行っている場所を明示的に区切ることを求めています。選択肢は 2 つあります。第一に、関数全体を unsafe としてマークできます。その場合、その関数は Unsafe Rust の関数となり、unsafe コンテキストでのみ呼び出せます。これはあまりよくありません。なぜなら、私たちはリストを安全に使えるようにしたいからです。第二に、関数の中に unsafe ブロックを書いて、FFI 境界を区切ることができます。これにより、関数全体としては安全であると宣言されます。こちらをやりましょう。
pub fn push(&mut self, elem: T) {
let mut new_tail = Box::new(Node {
elem: elem,
next: None,
});
let raw_tail: *mut _ = &mut *new_tail;
if !self.tail.is_null() {
// こんにちはコンパイラ、私は危険なことをしているとわかっています。
// そして、決して間違いを犯さない良いプログラマであると約束します。
unsafe {
(*self.tail).next = Some(new_tail);
}
} else {
self.head = Some(new_tail);
}
self.tail = raw_tail;
}
> cargo build
warning: field is never used: `elem`
--> src/fifth.rs:11:5
|
11 | elem: T,
| ^^^^^^^
|
= note: #[warn(dead_code)] on by default
やった!
今のところ unsafe ブロックを書かなければならなかった場所がそこだけというのは、なかなか興味深いです。あちこちで生ポインタ関連のことをしているのに、どういうことでしょうか?
実は、unsafe に関して Rust はものすごく規則にうるさい杓子定規な存在です。私たちは当然ながら Safe Rust プログラムの集合を最大化したいと考えています。なぜなら、それらは私たちがはるかに強い確信を持てるプログラムだからです。これを実現するために、Rust は unsafety の表面積を最小限に慎重に切り出しています。生ポインタを扱ってきた他のすべての場所では、それらを代入しているか、それらが null かどうかを観察しているだけだったことに注意してください。
生ポインタを実際に dereference しない限り、それらは完全に安全に行えることです。単に整数を読み書きしているだけです!生ポインタで実際に問題に巻き込まれる可能性があるのは、それを実際に dereference した場合だけです。したがって Rust は、その操作だけが unsafe であり、それ以外は完全に安全だと言っているのです。
超。杓子定規。でも技術的には正しい。
語り手: 世界の反対側のどこかで、あるハードウェアエンジニアが 背筋に寒気を覚える — きっとまた誰かがポインタは単なる整数だと 主張しているに違いない。彼女は新しいハードウェアポインタ認証方式の 提案書に目を落とし、一筋の涙を流す。隣の部屋のコンパイラエンジニアは 何も感じない — 彼らはずっと前に、常に厚手のセーターを着ることを 学んでいた。
ポインタ操作の一部だけが実際に unsafe であるという状況は、興味深い問題を引き起こします。unsafe ブロックで unsafety のスコープを区切ることになっているにもかかわらず、実際にはそのブロックの外側で確立された状態に依存しているからです。関数の外側にさえ依存しています!
これを私は unsafe 汚染 と呼んでいます。モジュール内で unsafe を使った瞬間に、そのモジュール全体が unsafety で汚染されます。unsafe コードのすべての不変条件が守られるようにするには、あらゆるものが正しく書かれていなければなりません。
この汚染はプライバシーによって管理可能になります。モジュールの外側から見ると、私たちのすべての構造体フィールドは完全に private なので、他の誰も私たちの状態を勝手な方法でいじることはできません。公開している API のどの組み合わせによっても悪いことが起きない限り、外部の観察者から見れば、私たちのコードはすべて safe なのです!そして実際、これは FFI の場合と何も違いません。ある Python の数学ライブラリが内部で C を呼び出していたとしても、安全なインターフェイスを公開している限り、誰も気にする必要はありません。
ともあれ、pop に進みましょう。これはリファレンス版とほとんどそのまま同じです。
pub fn pop(&mut self) -> Option<T> {
self.head.take().map(|head| {
let head = *head;
self.head = head.next;
if self.head.is_none() {
self.tail = ptr::null_mut();
}
head.elem
})
}
ここでも、safety が状態を持つ別のケースが見られます。この関数で tail ポインタを null にし忘れても、その場ではまったく問題は起きません。しかし、その後の push 呼び出しが、ダングリングした tail に書き込み始めてしまいます!
試してみましょう。
#[cfg(test)]
mod test {
use super::List;
#[test]
fn basics() {
let mut list = List::new();
// 空のリストが正しく振る舞うことを確認
assert_eq!(list.pop(), None);
// リストに要素を追加
list.push(1);
list.push(2);
list.push(3);
// 通常の削除を確認
assert_eq!(list.pop(), Some(1));
assert_eq!(list.pop(), Some(2));
// 何も壊れていないことを確認するために、さらにいくつか push する
list.push(4);
list.push(5);
// 通常の削除を確認
assert_eq!(list.pop(), Some(3));
assert_eq!(list.pop(), Some(4));
// 要素を取り尽くす場合を確認
assert_eq!(list.pop(), Some(5));
assert_eq!(list.pop(), None);
// 取り尽くした場合にポインタが正しく修正されたことを確認
list.push(6);
list.push(7);
// 通常の削除を確認
assert_eq!(list.pop(), Some(6));
assert_eq!(list.pop(), Some(7));
assert_eq!(list.pop(), None);
}
}
これは単にスタックのテストで、期待される pop の結果を逆向きにしたものです。また、pop における tail ポインタ破損のケースが発生しないことを確認するため、最後に追加の手順も入れています。
cargo test
running 12 tests
test fifth::test::basics ... ok
test first::test::basics ... ok
test fourth::test::basics ... ok
test fourth::test::peek ... ok
test second::test::basics ... ok
test fourth::test::into_iter ... ok
test second::test::into_iter ... ok
test second::test::iter ... ok
test second::test::iter_mut ... ok
test second::test::peek ... ok
test third::test::basics ... ok
test third::test::iter ... ok
test result: ok. 12 passed; 0 failed; 0 ignored; 0 measured
金の星です!
語り手: 来るぞ…
Miri
神経質に笑う このunsafeなやつ、めちゃくちゃ簡単じゃないか。どうしてみんなが違うことを言うのかわからない。私たちのプログラムは完璧に動いている。
ナレーター: 🙂
…そうだよね?
ナレーター: 🙂
さて、私たちは今やunsafeコードを書いているので、コンパイラは以前ほどミスを見つける手助けをしてくれない。テストはたまたま動いただけで、実際には非決定的なことをしていたのかもしれない。未定義動作っぽい何かを。
でも、何ができるというのだろう? 私たちは窓をこじ開けて、rustcの教室から抜け出してしまった。もう誰も助けてはくれない。
…待って、路地にいるあの怪しげな人は誰?
「おい坊や、Rustコードをインタプリトしてみないか?」
えっ、いや? どうして、
「ワイルドだぜ。プログラムの実際の動的実行がRustのメモリモデルのセマンティクスに従っているかを検証できるんだ。ぶっ飛ぶぞ…」
何?
「未定義動作をしているかどうかをチェックするんだ。」
インタプリタを一度だけ試してみてもいいかもしれない。
「rustupはインストールしてあるよな?」
もちろん。最新のRustツールチェーンを使うためのあのツールだからね!
> rustup +nightly-2022-01-21 component add miri
info: syncing channel updates for 'nightly-2022-01-21-x86_64-pc-windows-msvc'
info: latest update on 2022-01-21, rust version 1.60.0-nightly (777bb86bc 2022-01-20)
info: downloading component 'cargo'
info: downloading component 'clippy'
info: downloading component 'rust-docs'
info: downloading component 'rust-std'
info: downloading component 'rustc'
info: downloading component 'rustfmt'
info: installing component 'cargo'
info: installing component 'clippy'
info: installing component 'rust-docs'
info: installing component 'rust-std'
info: installing component 'rustc'
info: installing component 'rustfmt'
info: downloading component 'miri'
info: installing component 'miri'
今、私のコンピュータに何をインストールしたの!?
「良いブツだ」
ナレーター: ツールチェーンのバージョンについて、少し妙なことが起きています:
今インストールしているツールである
miriは、rustcの内部と密接に連携するため、nightlyツールチェーンでしか利用できません。
+nightly-2022-01-21は、その日付のRust nightlyツールチェーンでmiriをインストールしたい、ということをrustupに伝えます。特定の日付を指定しているのは、miriが遅れて数日分のnightlyではビルドできないことがあるためです。指定したツールチェーンがまだインストールされていなければ、rustupは+で指定したツールチェーンを自動的にダウンロードします。2022-01-21は、miriのサポートがあると私が知っているnightlyというだけです。それはこのステータスページで確認できます。 運が良いと思うなら、単に
+nightlyを使っても構いません。
cargo miri経由でmiriを呼び出すときも、miriをインストールしたツールチェーンを指定するために、この+構文を使います。毎回指定したくない場合は、rustup override setを使えます。
> cargo +nightly-2022-01-21 miri test
I will run `"cargo.exe" "install" "xargo"` to install
a recent enough xargo. Proceed? [Y/n]
えっ、一体全体XARGOって何?
「大丈夫、気にするな。」
> y
Updating crates.io index
Installing xargo v0.3.24
...
Finished release [optimized] target(s) in 10.65s
Installing C:\Users\ninte\.cargo\bin\xargo-check.exe
Installing C:\Users\ninte\.cargo\bin\xargo.exe
Installed package `xargo v0.3.24` (executables `xargo-check.exe`, `xargo.exe`)
I will run `"rustup" "component" "add" "rust-src"` to install
the `rust-src` component for the selected toolchain. Proceed? [Y/n]
えっ???
「Rustのソースコードのコピーを持つのが嫌いな人なんている?」
> y
info: downloading component 'rust-src'
info: installing component 'rust-src'
「おお、準備できたぞ。ここからがいいところだ。」
Compiling lists v0.1.0 (C:\Users\ninte\dev\tmp\lists)
Finished test [unoptimized + debuginfo] target(s) in 0.25s
Running unittests (lists-5cc11d9ee5c3e924.exe)
error: Undefined Behavior: trying to reborrow for Unique at alloc84055,
but parent tag <209678> does not have an appropriate item in
the borrow stack
--> \lib\rustlib\src\rust\library\core\src\option.rs:846:18
|
846 | Some(x) => Some(f(x)),
| ^ trying to reborrow for Unique at alloc84055,
| but parent tag <209678> does not have an
| appropriate item in the borrow stack
|
= help: this indicates a potential bug in the program:
it performed an invalid operation, but the rules it
violated are still experimental
= help: see https://github.com/rust-lang/unsafe-code-guidelines/blob/master/wip/stacked-borrows.md
for further information
= note: inside `std::option::Option::<std::boxed::Box<fifth::Node<i32>>>::map::<i32, [closure@src\fifth.rs:31:30: 40:10]>` at \lib\rustlib\src\rust\library\core\src\option.rs:846:18
note: inside `fifth::List::<i32>::pop` at src\fifth.rs:31:9
--> src\fifth.rs:31:9
|
31 | / self.head.take().map(|head| {
32 | | let head = *head;
33 | | self.head = head.next;
34 | |
... |
39 | | head.elem
40 | | })
| |__________^
note: inside `fifth::test::basics` at src\fifth.rs:74:20
--> src\fifth.rs:74:20
|
74 | assert_eq!(list.pop(), Some(1));
| ^^^^^^^^^^
note: inside closure at src\fifth.rs:62:5
--> src\fifth.rs:62:5
|
61 | #[test]
| ------- in this procedural macro expansion
62 | / fn basics() {
63 | | let mut list = List::new();
64 | |
65 | | // Check empty list behaves right
... |
96 | | assert_eq!(list.pop(), None);
97 | | }
| |_____^
...
error: aborting due to previous error
うわ。とんでもないエラーだ。
「だろう、このヤバいやつを見ろよ。こういうのは見ていて最高だ。」
ありがとう?
「ほら、エストラジオールの瓶も持っていけ。後で必要になる。」
待って、なんで?
「これからメモリモデルについて考えることになるんだ。信じろ。」
ナレーター: その謎の人物はその後、狐に変身し、壁の穴を抜けて走り去っていきました。著者はそれから数分間、今起きたすべての出来事を理解しようとして、中空を見つめていました。
路地裏の謎の狐は、私のジェンダー以外についても正しかった。miriは本当に最高のブツだ。
では、miriとは何なのだろう?
Rust の中間レベル中間表現(MIR)用の実験的なインタープリターです。cargo プロジェクトのバイナリやテストスイートを実行し、たとえば次のような特定の種類の未定義動作を検出できます。
- 境界外メモリアクセスと use-after-free
- 初期化されていないデータの不正な使用
- intrinsic の事前条件違反(unreachable_unchecked に到達する、copy_nonoverlapping を重複する範囲で呼び出す、など)
- 十分にアラインされていないメモリアクセスと参照
- いくつかの基本的な型不変条件の違反(たとえば、0 または 1 ではない bool、あるいは不正な enum 判別子)
- 実験的: 参照型のエイリアシングを管理する Stacked Borrows ルールの違反
- 実験的: データ競合(ただし弱いメモリ効果は対象外)
さらに、Miri はメモリリークについても知らせてくれます。実行終了時にまだ割り当てられたままのメモリがあり、そのメモリがグローバル static から到達可能でない場合、Miri はエラーを発生させます。
…
ただし、Miri はプログラム内の未定義動作をすべて検出できるわけではなく、すべてのプログラムを実行できるわけでもないことに注意してください
TL;DR: これはあなたのプログラムを解釈し、実行時にルールを破って未定義動作をやらかしたかどうかを検知します。未定義動作は一般に実行時に発生するものなので、これは必要です。もしその問題がコンパイル時に見つけられるなら、コンパイラが単にそれをエラーにするはずです!
ubsan や tsan のようなツールに馴染みがあるなら、基本的にはそれらを全部まとめて、さらに過激にしたものです。
Miri は今、教室の窓の外にナイフを持ってぶら下がっています。学習用ナイフです。
Miri に私たちの作業をチェックしてもらいたくなったら、次のようにテストスイートを解釈するよう頼めます。
> cargo +nightly-2022-01-21 miri test
では、彼らが私たちの机に彫り込んだ内容をもう少し詳しく見てみましょう。
error: Undefined Behavior: trying to reborrow for Unique at alloc84055, but parent tag <209678> does not have an appropriate item in the borrow stack
--> \lib\rustlib\src\rust\library\core\src\option.rs:846:18
|
846 | Some(x) => Some(f(x)),
| ^ trying to reborrow for Unique at alloc84055,
| but parent tag <209678> does not have an
| appropriate item in the borrow stack
|
= help: this indicates a potential bug in the program: it
performed an invalid operation, but the rules it
violated are still experimental
= help: see
https://github.com/rust-lang/unsafe-code-guidelines/blob/master/wip/stacked-borrows.md
for further information
まあ、私たちがエラーを起こしたことはわかりますが、これはわかりにくいエラーメッセージです。「borrow stack」とは何でしょうか?
次のセクションでそれを解明してみます。
Stacked Borrows を理解しようとする
前のセクションでは、unsafe な単方向連結キューを miri で実行してみました。すると、stacked borrows のルールを破っていると言われ、いくつかのドキュメントへのリンクが示されました。
通常ならドキュメントを案内しながら説明するところですが、私たちはそのドキュメントの主な対象読者ではありません。あれは Rust のセマンティクスに取り組んでいるコンパイラ開発者や研究者向けに設計されています。
なのでここでは、「stacked borrows」の高レベルな考え方だけを説明し、そのルールに従うためのシンプルな戦略を示します。
ナレーター: Stacked Borrows は Rust のセマンティックモデルとしてはまだ「実験的」なものなので、これらのルールを破っているからといって、あなたのプログラムが実際に「間違っている」とは限りません。しかし、あなたが文字どおりコンパイラに取り組んでいるのでない限り、miri が文句を言ったらプログラムを修正すべきです。未定義動作に関しては、用心するに越したことはありません。
動機: ポインターエイリアシング
破ったルールが何かに入る前に、そもそもなぜそのルールが存在するのかを理解しておくと役立ちます。動機となる問題はいくつかありますが、最も重要なのはポインターエイリアシングだと思います。
2つのポインターが指すメモリ領域が重なっているとき、それらのポインターはエイリアスすると言います。「別名を名乗る」人を2つの異なる名前で参照できるのと同じように、その重なったメモリ領域は2つの異なるポインターで参照できます。これは問題を引き起こす可能性があります。
コンパイラは、ポインターエイリアシングに関する情報を使ってメモリへのアクセスを最適化します。そのため、その情報が間違っていると、プログラムは誤ってコンパイルされ、でたらめな動作をします。
ナレーター: 実際のところ、エイリアシングでより問題になるのはポインターそのものよりもメモリアクセスであり、本当に重要になるのはアクセスの一方が変更を伴う場合だけです。ポインターが強調されるのは、ルールを結びつける対象として都合がよいからです。
ポインターエイリアシング情報がなぜ重要なのかを理解するために、小さな怒れる男のたとえ話を考えてみましょう。
ある日、Michiel が本棚を眺めていると、見覚えのない本が目に入りました。彼らはそれを本棚から取り出して、表紙を見ました。
「ああそうだ、昔の War and Peace だ。もちろん読んだことがある本だ。Peace がいっぱい出てくるところが大好きだったな。」
突然、ドアをノックする音がしました。Michiel は本を棚に戻してドアを開けました。そこにいたのは、彼らの宿敵 Hamslaw でした。Hamslaw が Michiel の明らかに劣ったコードゴルフ能力について壊滅的な一言を放とうとしたそのとき、彼らは好機を感じ取りました。
「ねえ Hamslaw、War and Peace って読んだことある?」
「ふん、War and Peace なんて実際には誰も読んでないでしょ。」
「まあ私は読んだけどね。ほら、本棚のあそこにあるだろ。つまり当然読んだってことだよ。」
Hamslaw は信じられませんでした。彼女の顔は、いつもの得意げな表情から、怒りと決意に満ちた鉄仮面のような顔つきへと変わりました。Hamslaw は Michiel を押しのけて本棚まで早足で進み、千人のヴァルキリーの怒りを込めて、その大冊を安置場所から引き抜きました。彼女はその古い本を手の中でひっくり返し、表紙を見た瞬間、震え始めました。
Michiel は自分の明らかに比類なき才気を自慢しようと身構えましたが、Hamslaw の突然の笑い声に遮られました。
「これ War and Peace じゃないわ。War and Feet よ!」
Hamslaw の顔には涙が流れていました。これは明らかに彼女の人生最高の瞬間でした。
「そ、そんな! さっき見たばかりなのに!」
彼らは Hamslaw から本を奪い取り、表紙を確認しました。確かに、「Peace」という単語は引っかいて消され、「Feet」に置き換えられていました。Michiel は愕然としました。これは明らかに彼らの人生最悪の瞬間でした。
彼らは膝から崩れ落ち、本棚をぼんやりと見つめました。どうしてこんなことが起きたのでしょうか? 表紙を確認したのはほんの一瞬前だったのに!
すると、本棚の中で何かが少し動くのが見えました。小さな男でした。Michiel がこれまで見た中で最も怒りに満ちたしかめっ面をした、小さな男でした。その小さな男は Michiel に向かって中指を立て、「誰もお前を信じない」と口だけで言い、本の間へと消えていきました。
Michiel の計画は完璧だったはずですが、シャーピーを持ち、破壊を望む小さな怒れる男の可能性を考慮できていませんでした。彼らは本の表紙に何と書いてあるかを知っていると思っていましたし、誰もそれを変えられるはずがないと思っていました。しかし悲しいかな、彼らは間違っていたのです。
Hamslaw はすでに、自分の信じがたい勝利を記念する zine の制作に取りかかっていました — 地元のインターネットカフェでの Michiel の評判が回復することは、もう二度とないでしょう。
Michiel のようになりたい人はいませんが、小さな怒れる男を常に恐れて生きたい人もいません。私たちは、小さな怒れる男がいつこちらにいたずらを仕掛けうるのかを知りたいのです。彼がいるときは、すべてを使う前に確認することについて、とても慎重で疑い深くなります。しかし小さな怒れる男がいなくなったら、物事を覚えておけるようになりたいのです。
これがポインターエイリアシングの(かなり単純化した)核心です。つまり、コンパイラは値を何度も読み込み直す代わりに「覚えておく」(キャッシュする)ことがいつ安全だと仮定できるのか、ということです。それを知るには、あなたの背後でメモリを変更する小さな怒れる男たちが存在しうるときを、コンパイラが知る必要があります。
ナレーター: コンパイラはこの情報をストアのキャッシュにも使います。これは、誰にも気づかれないと思うなら、物事をメモリにコミットするのを避けられるという意味です。この場合も問題は小さな怒れる男たちですが、問題を起こすには彼らはメモリを読むだけで十分です。
安全な Stacked Borrows
では、コンパイラに良いポインターエイリアシング情報を持たせたいとして、それは可能でしょうか? どうやら Rust はそのために設計されているように見えます。ミュータブル参照は定義上エイリアスされず、共有参照は互いにエイリアスしうるものの、変更はできません。完璧です! 出荷しましょう!
ただし、実際にはもっと複雑です。次のようにミュータブルポインターを「再借用」できます。
#![allow(unused)]
fn main() {
let mut data = 10;
let ref1 = &mut data;
let ref2 = &mut *ref1;
*ref2 += 2;
*ref1 += 1;
println!("{}", data);
}
これはコンパイルも実行も問題なくできます。どういうことでしょうか?
何が起きているのかは、2つの使用箇所を入れ替えると分かります。
let mut data = 10;
let ref1 = &mut data;
let ref2 = &mut *ref1;
// 順序を入れ替えた!
*ref1 += 1;
*ref2 += 2;
println!("{}", data);
error[E0503]: cannot use `*ref1` because it was mutably borrowed
--> src/main.rs:6:5
|
4 | let ref2 = &mut *ref1;
| ---------- borrow of `*ref1` occurs here
5 |
6 | *ref1 += 1;
| ^^^^^^^^^^ use of borrowed `*ref1`
7 | *ref2 += 2;
| ---------- borrow later used here
For more information about this error, try `rustc --explain E0503`.
error: could not compile `playground` due to previous error
突然コンパイラエラーになりました!
ミュータブルポインターを再借用すると、元のポインターは、その借用者が使い終わる(それ以上使われなくなる)まで、もう使用できません。
動作するコードでは、使用箇所がきれいに入れ子になっています。ポインターを再借用し、新しいポインターをしばらく使い、それから古いポインターを再び使う前に新しいポインターの使用をやめています。動作しないコードでは、そうなっていません。使用箇所を任意に入り混じらせているだけです。 これが、再借用がありながらエイリアス情報も保持できる仕組みです。私たちの再借用はすべて明確にネストしているため、任意の時点でそのうちの1つだけを「生きている」と見なせます。
ところで、きれいにネストしたものを表現するのに最適な方法って何でしょう? スタックです。借用のスタックです。
ほら、これがStacked Borrowsです!
借用スタックの一番上にあるものが「生きている」ものであり、自分が実質的にエイリアスされていないことを知っています。ポインターを再借用すると、新しいポインターがスタックにプッシュされ、その生きているポインターになります。古いポインターを使うと、その上にある借用スタック上のすべてをポップすることで、そのポインターが再び生き返ります。この時点で、そのポインターは自分が再借用されていたこと、そしてメモリが変更されているかもしれないことを「知っています」が、同時に再び排他的アクセスを持っていることも知っています――小さな怒れる男たちを心配する必要はありません。
つまり、実際には再借用されたポインターにアクセスすることは常に問題ありません。なぜなら、その上にあるものをいつでもすべてポップできるからです。本当に問題なのは、すでに借用スタックからポップされてしまったポインターにアクセスすることです――その場合はやらかしています。
ありがたいことに、上の例で見たように、借用チェッカーの設計により安全なRustプログラムはこれらのルールに従います。ただし、コンパイラーは一般に、この問題をStacked Borrowsの観点とは「逆向き」に捉えます。ref1を使うとref2が無効化される、と言う代わりに、ref2はそのすべての使用箇所で必ず有効でなければならず、順番を破って問題を起こしているのはref1のほうだ、と主張します。
したがって、「*ref1は可変借用されているため使用できません」となります。結果は同じですが(特に非字句ライフタイムでは)、おそらくより直感的な形で表現されています。
しかし、unsafeポインターを使い始めると、借用チェッカーは助けてくれません!
UnsafeなStacked Borrows
そこで、コンパイラーがunsafeポインターを適切に追跡できないとしても、なんとかしてそれらをこのStacked Borrowsシステムに参加させる方法が欲しいわけです。また、失敗してUBを引き起こすことがあまりにも簡単にならないように、システムはかなり寛容であってほしいとも思っています。
これは難しい問題で、私は解決方法を知りません。しかしStacked Borrowsに取り組んだ人たちは、もっともらしいものを考案し、miriはそれを実装しようとしています。
非常に大まかな考え方としては、参照(またはその他の安全なポインター)を生ポインターに変換するとき、それは基本的に再借用を行うのと同じだ、というものです。したがって、その生ポインターはそのメモリに対して好きなことをしてよくなり、その再借用が失効すると、通常の再借用でそれが起きる場合と同じようになります。
しかし問題は、その再借用がいつ失効するのかです。おそらく、元の参照を再び使い始めたときに失効させるのがよいでしょう。そうでなければ、きれいにネストしたスタックにはなりません。
でも待ってください。生ポインターを参照へ変換できます! そして生ポインターはコピーできます! もし&mut -> *mut -> &mut -> *mutと進んでから、最初の*mutにアクセスしたらどうなるのでしょう? その場合、Stacked Borrowsはいったいどう動作するのでしょうか?
正直なところ、私にはわかりません! だから複雑なのです。実際、それらはさらに複雑です。なぜなら、Stacked Borrowsはより寛容になろうとしていて、より多くのunsafeコードが期待どおりに動作することを許そうとしているからです。だから私は、間違いを見つける助けとして、miri上で実行するのです。
実際、この厄介さがあるため、miriには追加で実験的な、さらに厳格なモードがあります: -Zmiri-tag-raw-pointers。
これを有効にするには、次のようにMIRIFLAGS環境変数経由で渡す必要があります。
MIRIFLAGS="-Zmiri-tag-raw-pointers" cargo +nightly-2022-01-21 miri test
またはWindowsでは、変数をグローバルに設定する必要があるため、次のようにします。
$env:MIRIFLAGS="-Zmiri-tag-raw-pointers"
cargo +nightly-2022-01-21 miri test
私たちは通常、自分たちの作業にさらに自信を持つために、この追加の厳格モードに従うようにします。また、これはある意味で「より単純」でもあるため、Stacked Borrowsをいじって直感を得るには実際により適しています。
Stacked Borrowsを管理する
したがって、生ポインターを使うときは、単純で荒っぽく、できれば大きな誤差の余地を持つヒューリスティックに従うようにします。
生ポインターを使い始めたら、生ポインターだけを使うようにする。
これにより、生ポインターがそのメモリにアクセスする「許可」を偶然失ってしまう可能性を、できるだけ低くできます。
ナレーター: これは2つの点で単純化しすぎています。
安全なポインターは、エイリアスだけでなく、しばしばより多くの性質を主張します。メモリが割り当てられていること、アラインされていること、指し先の型を格納するのに十分な大きさがあること、指し先が適切に初期化されていること、などです。そのため、物事が疑わしい状態にあるときに、それらを無造作に振り回すのはさらに危険です。
生ポインターの世界に留まっていたとしても、任意のメモリを好き勝手にエイリアスできるわけではありません。ポインターは概念的には特定の「割り当て」に結び付いており(これはスタック上のローカル変数ほど細かい粒度になり得ます)、ある割り当てからポインターを取り出し、オフセットして、別の割り当て内のメモリにアクセスするべきではありません。もしこれが許されるなら、あらゆる場所で小さな怒れる男たちの脅威が常に存在することになります。これが、「ポインターは単なる整数である」という見方が問題のある見方である理由の一部です。
さて、それでも私たちはインターフェイスには安全な参照が欲しいです。なぜなら、リストのユーザーがそれを知ったり心配したりしなくて済むような、優れた安全な抽象化を構築したいからです。
そこで、次のようにします。
- メソッドの開始時に、入力参照を使って生ポインターを取得する
- その時点以降は、unsafeポインターだけを使うよう最善を尽くす
- 必要に応じて、最後に生ポインターを安全なポインターへ変換し直す
ただし、私たちの型のフィールドはprivateなので、それらは完全に生ポインターのままにします。
実際、私たちが犯した大きな間違いの一部は、Boxを使い続けたことでした! Boxには、コンパイラーに「これは&mutによく似ている。なぜならそのポインターを一意に所有しているからだ」と伝える特別なアノテーションがあります。これは真実です!
しかし、私たちがリストの末尾に保持していた生ポインターはBoxの内部を指していたため、Boxに通常どおりアクセスするたびに、その生ポインターの「再借用」をおそらく無効化していたのです! ☠
次のセクションでは本来の姿に戻り、大量の例に頭をぶつけていきます。
Stacked Borrows のテスト
前セクションの Rust 向け(簡略化された)メモリモデルの TL;DR:
- Rust は概念的には「借用スタック」を維持することで再借用を扱います
- スタックの一番上にあるものだけが「live」(排他的アクセスを持つ状態)です
- より下にあるものにアクセスすると、それが「live」になり、その上にあるものは pop されます
- 借用スタックから pop されたポインターを使うことは許されません
- 借用チェッカーは、安全なコードがこれに従うことを保証します
- Miri は理論上、実行時に生ポインターがこれに従うことをチェックします
ここまで理論や考え方がたくさん出てきましたが、この本の真の核心に進みましょう。つまり、悪いコードを書いて、ツールに怒鳴らせることです。これから 大量 の例を見て、私たちのメンタルモデルが理にかなっているかを確認し、Stacked Borrows の直感をつかんでいきます。
ナレーター: 実践において未定義動作を捕まえるのは厄介な仕事です。結局のところ、コンパイラーが文字どおり「起こらない」と 仮定している 状況を扱っているからです。
運がよければ、今日のところは「動いているように見える」でしょう。しかし、それはより賢いコンパイラーやコードへのわずかな変更に対する時限爆弾になります。本当に 運がよければ、確実にクラッシュしてくれるので、単に間違いを見つけて修正できます。しかし運が悪ければ、奇妙で不可解な形で壊れます。
Miri は、rustc が持つプログラムの最も素朴で最適化されていない見方を取得し、解釈しながら追加の状態を追跡することで、これに対処しようとします。「サニタイザー」としては、これはかなり決定的で堅牢なアプローチですが、決して 完璧 にはなりません。テストプログラムが実際にその UB を伴う実行を持っている必要がありますし、十分に大きいプログラムでは、あらゆる種類の非決定性を導入するのは非常に簡単です(HashMap はデフォルトで RNG を使います!)。
Miri がプログラムの実行を承認したからといって、UB が存在しないという絶対確実な主張として受け取ることは決してできません。また、本当はそうではないのに、Miri が何かを UB だと 考える 可能性もあります。しかし、物事がどのように機能するかについてのメンタルモデルがあり、Miri もそれに同意しているように見えるなら、それは私たちが正しい道筋にいる良い兆候です。
基本的な借用
前のセクションでは、借用チェッカーがこのコードを好まないことを見ました。
let mut data = 10;
let ref1 = &mut data;
let ref2 = &mut *ref1;
// 順序を入れ替えています!
*ref1 += 1;
*ref2 += 2;
println!("{}", data);
ref2 を *mut に置き換えると何が起こるか見てみましょう。
unsafe {
let mut data = 10;
let ref1 = &mut data;
let ptr2 = ref1 as *mut _;
// 順序を入れ替えています!
*ref1 += 1;
*ptr2 += 2;
println!("{}", data);
}
cargo run
Compiling miri-sandbox v0.1.0
Finished dev [unoptimized + debuginfo] target(s) in 0.71s
Running `target\debug\miri-sandbox.exe`
13
Rustc はこれに完全に満足しているようです。警告はなく、プログラムは期待どおりの結果を生成しました! では、miri(厳格モード)がこれをどう考えるか見てみましょう。
MIRIFLAGS="-Zmiri-tag-raw-pointers" cargo +nightly-2022-01-21 miri run
Finished dev [unoptimized + debuginfo] target(s) in 0.00s
Running cargo-miri.exe target\miri
error: Undefined Behavior: no item granting read access
to tag <untagged> at alloc748 found in borrow stack.
--> src\main.rs:9:9
|
9 | *ptr2 += 2;
| ^^^^^^^^^^ no item granting read access to tag <untagged>
| at alloc748 found in borrow stack.
|
= help: this indicates a potential bug in the program:
it performed an invalid operation, but the rules it
violated are still experimental
良いですね! 物事がどのように機能するかについての直感的なモデルは持ちこたえました。コンパイラーは私たちの代わりに問題を捕まえられませんでしたが、miri は捕まえてくれました。
もう少し複雑なもの、以前に触れた &mut -> *mut -> &mut -> *mut のケースを試してみましょう。
unsafe {
let mut data = 10;
let ref1 = &mut data;
let ptr2 = ref1 as *mut _;
let ref3 = &mut *ptr2;
let ptr4 = ref3 as *mut _;
// 最初の生ポインターに先にアクセスする
*ptr2 += 2;
// 次に「借用スタック」の順序でアクセスする
*ptr4 += 4;
*ref3 += 3;
*ptr2 += 2;
*ref1 += 1;
println!("{}", data);
}
cargo run
22
MIRIFLAGS="-Zmiri-tag-raw-pointers" cargo +nightly-2022-01-21 miri run
error: Undefined Behavior: no item granting read access
to tag <1621> at alloc748 found in borrow stack.
--> src\main.rs:13:5
|
13 | *ptr4 += 4;
| ^^^^^^^^^^ no item granting read access to tag <1621>
| at alloc748 found in borrow stack.
|
おお、確かに! 厳格モードでは、miri は 2 つの生ポインターを「見分ける」ことができ、2 番目のポインターを使うと 1 番目のポインターが無効化されます。すべてを台無しにしている最初の使用を取り除くと、すべてがうまくいくか見てみましょう。
unsafe {
let mut data = 10;
let ref1 = &mut data;
let ptr2 = ref1 as *mut _;
let ref3 = &mut *ptr2;
let ptr4 = ref3 as *mut _;
// 「借用スタック」の順序でアクセスする
*ptr4 += 4;
*ref3 += 3;
*ptr2 += 2;
*ref1 += 1;
println!("{}", data);
}
cargo run
20
MIRIFLAGS="-Zmiri-tag-raw-pointers" cargo +nightly-2022-01-21 miri run
20
いいですね。
ええ、この時点でもう全員、プログラミング言語のメモリモデル設計と実装で博士号を取れると私はかなり確信しています。コンパイラーなんていったい誰が 必要 とするのでしょう。こんなの 簡単 です。
ナレーター: 簡単ではありませんでした。それでも、あなたのことを誇りに思います。
配列のテスト
配列とポインターオフセット(add と sub)をいじってみましょう。これは動くはずですよね?
unsafe {
let mut data = [0; 10];
let ref1_at_0 = &mut data[0]; // 0 番目の要素への参照
let ptr2_at_0 = ref1_at_0 as *mut i32; // 0 番目の要素へのポインター
let ptr3_at_1 = ptr2_at_0.add(1); // 1 番目の要素へのポインター
*ptr3_at_1 += 3;
*ptr2_at_0 += 2;
*ref1_at_0 += 1;
// [3, 3, 0, ...] になるはず
println!("{:?}", &data[..]);
}
cargo run
[3, 3, 0, 0, 0, 0, 0, 0, 0, 0]
MIRIFLAGS="-Zmiri-tag-raw-pointers" cargo +nightly-2022-01-21 miri run
error: Undefined Behavior: no item granting read access
to tag <1619> at alloc748+0x4 found in borrow stack.
--> src\main.rs:8:5
|
8 | *ptr3_at_1 += 3;
| ^^^^^^^^^^^^^^^ no item granting read access to tag <1619>
| at alloc748+0x4 found in borrow stack.
大学院への出願書類を破り捨てる
何が起こったのでしょうか? 私たちは借用スタックをまったく問題なく使っています! ptr -> ptr と進むと何か奇妙なことが起こるのでしょうか? ポインターをコピーするだけにして、すべて同じ場所を指すようにしたらどうなるでしょう。
#![allow(unused)]
fn main() {
unsafe {
let mut data = [0; 10];
let ref1_at_0 = &mut data[0]; // 0 番目の要素への参照
let ptr2_at_0 = ref1_at_0 as *mut i32; // 0 番目の要素へのポインター
let ptr3_at_0 = ptr2_at_0; // 0 番目の要素へのポインター
*ptr3_at_0 += 3;
*ptr2_at_0 += 2;
*ref1_at_0 += 1;
// [6, 0, 0, ...] になるはず
println!("{:?}", &data[..]);
}
}
cargo run
[6, 0, 0, 0, 0, 0, 0, 0, 0, 0]
MIRIFLAGS="-Zmiri-tag-raw-pointers" cargo +nightly-2022-01-21 miri run
[6, 0, 0, 0, 0, 0, 0, 0, 0, 0]
いいえ、これは問題なく動きます。たまたま運がよかったのかもしれないので、ポインターを本当にひどくごちゃごちゃにしてみましょう。
#![allow(unused)]
fn main() {
unsafe {
let mut data = [0; 10];
let ref1_at_0 = &mut data[0]; // 0番目の要素への参照
let ptr2_at_0 = ref1_at_0 as *mut i32; // 0番目の要素へのポインター
let ptr3_at_0 = ptr2_at_0; // 0番目の要素へのポインター
let ptr4_at_0 = ptr2_at_0.add(0); // 0番目の要素へのポインター
let ptr5_at_0 = ptr3_at_0.add(1).sub(1); // 0番目の要素へのポインター
// ポインターの使用をめちゃくちゃに入り混ぜたもの
*ptr3_at_0 += 3;
*ptr2_at_0 += 2;
*ptr4_at_0 += 4;
*ptr5_at_0 += 5;
*ptr3_at_0 += 3;
*ptr2_at_0 += 2;
*ref1_at_0 += 1;
// [20, 0, 0, ...] になるはず
println!("{:?}", &data[..]);
}
}
cargo run
[20, 0, 0, 0, 0, 0, 0, 0, 0, 0]
MIRIFLAGS="-Zmiri-tag-raw-pointers" cargo +nightly-2022-01-21 miri run
[20, 0, 0, 0, 0, 0, 0, 0, 0, 0]
違います!Miri は、他の生ポインターから派生した生ポインターに関しては、実際にははるかに寛容です。それらはすべて同じ「借用」(あるいは miri が呼ぶところの tag)を共有します。
生ポインターを使い始めると、それらは自由に自分自身の小さな怒れる男たちへと分裂して、自分たち同士で好き勝手できます。これが問題ないのは、コンパイラーがそれを理解しており、参照に対して行うのと同じようには読み書きを最適化しないためです。
ナレーター: コードが十分に単純であれば、コンパイラーは派生ポインターをすべて追跡し、可能なところでは最適化を続けられますが、参照に対して使える推論よりもずっと壊れやすくなります。
では、本当の問題は何でしょうか?
data は 1 つの「アロケーション」(ローカル変数)ですが、ref1_at_0 が借用しているのは最初の要素だけです。Rust では、借用を分割して、アロケーションの特定の部分にだけ適用できるようになっています!試してみましょう。
unsafe {
let mut data = [0; 10];
let ref1_at_0 = &mut data[0]; // 0番目の要素への参照
let ref2_at_1 = &mut data[1]; // 1番目の要素への参照
let ptr3_at_0 = ref1_at_0 as *mut i32; // 0番目の要素へのポインター
let ptr4_at_1 = ref2_at_1 as *mut i32; // 1番目の要素へのポインター
*ptr4_at_1 += 4;
*ptr3_at_0 += 3;
*ref2_at_1 += 2;
*ref1_at_0 += 1;
// [3, 3, 0, ...] になるはず
println!("{:?}", &data[..]);
}
error[E0499]: cannot borrow `data[_]` as mutable more than once at a time
--> src\main.rs:5:21
|
4 | let ref1_at_0 = &mut data[0]; // Reference to 0th element
| ------------ first mutable borrow occurs here
5 | let ref2_at_1 = &mut data[1]; // Reference to 1th element
| ^^^^^^^^^^^^ second mutable borrow occurs here
6 | let ptr3_at_0 = ref1_at_0 as *mut i32; // Ptr to 0th element
| --------- first borrow later used here
|
= help: consider using `.split_at_mut(position)` or similar method
to obtain two mutable non-overlapping sub-slices
しまった!Rust は、これらの借用が互いに素であることを証明するために配列のインデックスを追跡しませんが、安全に動作すると仮定できる方法でスライスを複数の部分に分割するための split_at_mut を提供しています。
#![allow(unused)]
fn main() {
unsafe {
let mut data = [0; 10];
let slice1 = &mut data[..];
let (slice2_at_0, slice3_at_1) = slice1.split_at_mut(1);
let ref4_at_0 = &mut slice2_at_0[0]; // 0番目の要素への参照
let ref5_at_1 = &mut slice3_at_1[0]; // 1番目の要素への参照
let ptr6_at_0 = ref4_at_0 as *mut i32; // 0番目の要素へのポインター
let ptr7_at_1 = ref5_at_1 as *mut i32; // 1番目の要素へのポインター
*ptr7_at_1 += 7;
*ptr6_at_0 += 6;
*ref5_at_1 += 5;
*ref4_at_0 += 4;
// [10, 12, 0, ...] になるはず
println!("{:?}", &data[..]);
}
}
cargo run
[10, 12, 0, 0, 0, 0, 0, 0, 0, 0]
MIRIFLAGS="-Zmiri-tag-raw-pointers" cargo +nightly-2022-01-21 miri run
[10, 12, 0, 0, 0, 0, 0, 0, 0, 0]
おお、動きます!スライスは、コンパイラーと miri に「この範囲内のすべてのメモリーについて大きな借用を取るよ」と適切に伝えるので、すべての要素を変更できることがわかるのです。
また、split_at_mut のような操作が許可されているということは、借用はスタックというよりツリーに近い場合がある、ということも示しています。というのも、1 つの大きな借用を、互いに素な小さな借用たちに分割しても、すべてが問題なく動くからです。
(実際の Stacked Borrows モデルでは、スタックが概念的にはプログラムの各バイトに対する権限を追跡しているので、すべては依然としてスタックなのだと思いますが……?)
スライスを直接ポインターに変換したらどうなるでしょうか?そのポインターはスライス全体にアクセスできるのでしょうか?
#![allow(unused)]
fn main() {
unsafe {
let mut data = [0; 10];
let slice1_all = &mut data[..]; // 配列全体に対するスライス
let ptr2_all = slice1_all.as_mut_ptr(); // 配列全体に対するポインター
let ptr3_at_0 = ptr2_all; // 0番目の要素へのポインター(同じもの)
let ptr4_at_1 = ptr2_all.add(1); // 1番目の要素へのポインター
let ref5_at_0 = &mut *ptr3_at_0; // 0番目の要素への参照
let ref6_at_1 = &mut *ptr4_at_1; // 1番目の要素への参照
*ref6_at_1 += 6;
*ref5_at_0 += 5;
*ptr4_at_1 += 4;
*ptr3_at_0 += 3;
// お楽しみとして、ループですべての要素を変更する
// (これにはどの生ポインターでも使える。それらは借用を共有している!)
for idx in 0..10 {
*ptr2_all.add(idx) += idx;
}
// お楽しみとして、これと同じコードの安全なバージョン
for (idx, elem_ref) in slice1_all.iter_mut().enumerate() {
*elem_ref += idx;
}
// [8, 12, 4, 6, 8, 10, 12, 14, 16, 18] になるはず
println!("{:?}", &data[..]);
}
}
cargo run
[8, 12, 4, 6, 8, 10, 12, 14, 16, 18]
MIRIFLAGS="-Zmiri-tag-raw-pointers" cargo +nightly-2022-01-21 miri run
[8, 12, 4, 6, 8, 10, 12, 14, 16, 18]
いいですね!ポインターは単なる整数ではありません。ポインターには関連付けられたメモリー範囲があり、Rust ではその範囲を狭めることが許されています!
共有参照のテスト
これまでの例では、できるだけ単純にするために、可変参照だけを使い、読み取り・変更・書き込みの操作(+=)だけを行うよう、かなり慎重にしてきました。
しかし Rust には、読み取り専用で自由にコピーできる共有参照があります。それらはどのように動作するべきでしょうか?さて、生ポインターは自由にコピーでき、それらが 1 つの借用を「共有する」と言うことで扱えることを見てきました。共有参照も同じように考えられるのではないでしょうか?
値を読み取る関数で試してみましょう(println! は auto-ref/deref まわりで少し魔法のように振る舞うことがあるので、確実に意図したものをテストできるように関数でラップしています)。
fn opaque_read(val: &i32) {
println!("{}", val);
}
unsafe {
let mut data = 10;
let mref1 = &mut data;
let sref2 = &mref1;
let sref3 = sref2;
let sref4 = &*sref2;
// 共有参照の読み取りをランダムに入り混ぜたもの
opaque_read(sref3);
opaque_read(sref2);
opaque_read(sref4);
opaque_read(sref2);
opaque_read(sref3);
*mref1 += 1;
opaque_read(&data);
}
cargo run
warning: unnecessary `unsafe` block
--> src\main.rs:6:1
|
6 | unsafe {
| ^^^^^^ unnecessary `unsafe` block
|
= note: `#[warn(unused_unsafe)]` on by default
warning: `miri-sandbox` (bin "miri-sandbox") generated 1 warning
10
10
10
10
10
11
そういえば、raw pointer で何もしていませんでしたが、少なくともすべての共有参照が相互に入れ替えて使っても問題ないことは確認できます。では、raw pointer も混ぜてみましょう。
fn opaque_read(val: &i32) {
println!("{}", val);
}
unsafe {
let mut data = 10;
let mref1 = &mut data;
let ptr2 = mref1 as *mut i32;
let sref3 = &mref1;
let ptr4 = sref3 as *mut i32;
*ptr4 += 4;
opaque_read(sref3);
*ptr2 += 2;
*mref1 += 1;
opaque_read(&data);
}
cargo run
error[E0606]: casting `&&mut i32` as `*mut i32` is invalid
--> src\main.rs:11:16
|
11 | let ptr4 = sref3 as *mut i32;
| ^^^^^^^^^^^^^^^^^
おっと、実際には & ではなく & &mut をいじっていました! Rust は、それが問題にならない場面では、それをうまく覆い隠してくれます。let sref3 = &*mref1 で正しく再借用しましょう。
cargo run
error[E0606]: casting `&i32` as `*mut i32` is invalid
--> src\main.rs:11:16
|
11 | let ptr4 = sref3 as *mut i32;
| ^^^^^^^^^^^^^^^^^
だめです。Rust はまだそれを許してくれません! 共有参照は *const にしかキャストできず、それは読み取りしかできません。ですが、もし単に……こう……したら……?
let ptr4 = sref3 as *const i32 as *mut i32;
cargo run
14
17
えっ。OK、そうですか、わかりました? 素晴らしいキャストシステムですね Rust。まるで *const が、C API を記述するためと、正しい使い方をぼんやり示唆するためにしか実際には存在しない、かなり役に立たない型であるかのようです(実際そうですし、そうしています)。miri はどう考えるでしょう?
MIRIFLAGS="-Zmiri-tag-raw-pointers" cargo +nightly-2022-01-21 miri run
error: Undefined Behavior: no item granting write access to
tag <1621> at alloc742 found in borrow stack.
--> src\main.rs:13:5
|
13 | *ptr4 += 4;
| ^^^^^^^^^^ no item granting write access to tag <1621>
| at alloc742 found in borrow stack.
残念ながら、ダブルキャストでコンパイラの文句を回避することはできますが、それでこの操作が実際に許可されるわけではありません。共有参照を取得するとき、私たちはその値を変更しないと約束しています。
これが重要なのは、共有借用が借用スタックからポップされたとき、その下にある mutable pointer は、メモリが変更されていないと仮定できることを意味するからです。メモリを読んでいる小さな怒れる男たちがいたかもしれません(そのため書き込みはコミットされる必要がありました)が、彼らはそれを変更できなかったので、mutable pointer は自分たちが最後に書き込んだ値がまだそこにあると仮定できます!
共有参照がひとたび借用スタック上に置かれると、その上にプッシュされるものはすべて読み取り権限しか持ちません。
ただし、これは可能です。
#![allow(unused)]
fn main() {
fn opaque_read(val: &i32) {
println!("{}", val);
}
unsafe {
let mut data = 10;
let mref1 = &mut data;
let ptr2 = mref1 as *mut i32;
let sref3 = &*mref1;
let ptr4 = sref3 as *const i32 as *mut i32;
opaque_read(&*ptr4);
opaque_read(sref3);
*ptr2 += 2;
*mref1 += 1;
opaque_read(&data);
}
}
実際に読み取りしか行わない限り、mutable raw pointer を作成しても依然として「問題ない」ことに注目してください!
cargo run
10
10
13
MIRIFLAGS="-Zmiri-tag-raw-pointers" cargo +nightly-2022-01-21 miri run
10
10
13
念のため、共有参照が通常どおりポップされることも確認してみましょう。
fn opaque_read(val: &i32) {
println!("{}", val);
}
unsafe {
let mut data = 10;
let mref1 = &mut data;
let ptr2 = mref1 as *mut i32;
let sref3 = &*mref1;
*ptr2 += 2;
opaque_read(sref3); // 間違った順序で読み取り?
*mref1 += 1;
opaque_read(&data);
}
cargo run
12
13
MIRIFLAGS="-Zmiri-tag-raw-pointers" cargo +nightly-2022-01-21 miri run
error: Undefined Behavior: trying to reborrow for SharedReadOnly
at alloc742, but parent tag <1620> does not have an appropriate
item in the borrow stack
--> src\main.rs:13:17
|
13 | opaque_read(sref3); // Read in the wrong order?
| ^^^^^ trying to reborrow for SharedReadOnly
| at alloc742, but parent tag <1620>
| does not have an appropriate item
| in the borrow stack
|
おや、特定のタグではなく SharedReadOnly についての、少し違うエラーメッセージまで得られました。これは納得できます。いったん何らかの共有参照があると、基本的にそれ以外のすべては大きな SharedReadOnly のスープのようなものなので、それらを区別する必要はないのです!
内部可変性のテスト
本の本当にひどい章を覚えていますか。RefCell と Rc で linked list を作ろうとして、この忌々しい linked list を書こうとすると、あらゆることがいつも以上にひどくなったあの章です。
私たちは共有参照はミューテーションに使えないと主張し続けてきましたが、その章は、内部可変性によって共有参照越しに実際にはミューテーションできる、という話がすべてでした。素敵でシンプルな std::cell::Cell 型を試してみましょう。
#![allow(unused)]
fn main() {
use std::cell::Cell;
unsafe {
let mut data = Cell::new(10);
let mref1 = &mut data;
let ptr2 = mref1 as *mut Cell<i32>;
let sref3 = &*mref1;
sref3.set(sref3.get() + 3);
(*ptr2).set((*ptr2).get() + 2);
mref1.set(mref1.get() + 1);
println!("{}", data.get());
}
}
ああ、なんて美しい混乱でしょう。miri がこれに唾を吐くのを見るのは楽しみです。
cargo run
16
MIRIFLAGS="-Zmiri-tag-raw-pointers" cargo +nightly-2022-01-21 miri run
16
待って、本当に? それは問題ないのですか? なぜ? どうして? そもそも Cell とは何なのでしょう?
stdlib の南京錠を叩き壊す
pub struct Cell<T: ?Sized> {
value: UnsafeCell<T>,
}
UnsafeCell とは一体何なのでしょう?
本気だと stdlib に示すために、さらに別の南京錠を叩き壊す
#[lang = "unsafe_cell"]
#[repr(transparent)]
#[repr(no_niche)]
pub struct UnsafeCell<T: ?Sized> {
value: T,
}
ああ、これは魔法使いの魔法です。なるほど。たぶん。#[lang = "unsafe_cell"] は、文字どおり UnsafeCell は UnsafeCell だと言っているだけです。鍵を壊すのはやめて、std::cell::UnsafeCell の実際のドキュメントを確認しましょう。
Rust における内部可変性の中核となるプリミティブ。
&T参照を持っている場合、通常 Rust では、コンパイラは&Tがイミュータブルなデータを指しているという知識に基づいて最適化を行います。たとえばエイリアスを通じて、または&Tを&mut Tに transmute することでそのデータをミューテーションすることは、未定義動作と見なされます。UnsafeCell<T>は&Tに対するイミュータビリティ保証をオプトアウトします。共有参照&UnsafeCell<T>は、ミューテーション中のデータを指していてもよいです。これは「内部可変性」と呼ばれます。
ああ、これは本当にただの魔法使いの魔法です。 UnsafeCell は基本的に、コンパイラに「おい聞いてくれ、このメモリではちょっとふざけたことをやるから、いつものエイリアシングの仮定は一切するな」と伝えるものです。大きな「注意: 小さな怒れる男たち横断中」の標識を立てるようなものです。
UnsafeCell を追加すると miri がどう満足するのか見てみましょう:
use std::cell::UnsafeCell;
fn opaque_read(val: &i32) {
println!("{}", val);
}
unsafe {
let mut data = UnsafeCell::new(10);
let mref1 = data.get_mut(); // 内容への可変参照を取得
let ptr2 = mref1 as *mut i32;
let sref3 = &*ptr2;
*ptr2 += 2;
opaque_read(sref3);
*mref1 += 1;
println!("{}", *data.get());
}
cargo run
12
13
MIRIFLAGS="-Zmiri-tag-raw-pointers" cargo +nightly-2022-01-21 miri run
error: Undefined Behavior: trying to reborrow for SharedReadOnly
at alloc748, but parent tag <1629> does not have an appropriate
item in the borrow stack
--> src\main.rs:15:17
|
15 | opaque_read(sref3);
| ^^^^^ trying to reborrow for SharedReadOnly
| at alloc748, but parent tag <1629> does
| not have an appropriate item in the
| borrow stack
|
待って、何だって? 魔法の言葉は唱えたじゃないですか! 連邦政府承認済みの儀式強化用ヤギの血をこんなに用意したのに、どうすればいいんですか?
まあ、確かに唱えました。しかしその後で、get_mut を使ってその呪文を完全に捨ててしまいました。これは UnsafeCell の中を覗き込み、結局それに対する正真正銘の &mut i32 を作ってしまうものです!
考えてみてください: もしコンパイラが &mut i32 が UnsafeCell の中を見ている可能性があると仮定しなければならないなら、エイリアシングについて何の仮定もできなくなってしまいます! あらゆるものが小さな怒れる男たちでいっぱいかもしれないのです。
つまり必要なのは、ポインタ型の中に UnsafeCell を保持して、コンパイラがこちらの意図を理解できるようにすることです。
#![allow(unused)]
fn main() {
use std::cell::UnsafeCell;
fn opaque_read(val: &i32) {
println!("{}", val);
}
unsafe {
let mut data = UnsafeCell::new(10);
let mref1 = &mut data; // *外側*への可変参照
let ptr2 = mref1.get(); // 内側への生ポインタを取得
let sref3 = &*mref1; // *外側*への共有参照を取得
*ptr2 += 2; // 生ポインタで変更
opaque_read(&*sref3.get()); // 共有参照から読み取り
*sref3.get() += 3; // 共有参照経由で書き込み
*mref1.get() += 1; // 可変参照で変更
println!("{}", *data.get());
}
}
cargo run
12
16
MIRIFLAGS="-Zmiri-tag-raw-pointers" cargo +nightly-2022-01-21 miri run
12
16
動きました! これで結局、この血を全部捨てずに済みそうです。
いや、ちょっと待ってください。ここではまだ順序が少しおかしなことになっています。最初に ptr2 を作り、その後で可変ポインタから sref3 を作りました。そして共有ポインタより先に生ポインタを使っています。これは全部……間違っているように見えます。
いや待ってください、Cell の例でも同じことをしていましたね。うーん。
次の 2 つのどちらかを結論せざるを得ません:
- Miri は不完全で、これは実際にはまだ UB である。
- 私たちの単純化したモデルは、実は単純化しすぎである。
私は後者に賭けますが、念のため、stacked borrows の単純化モデルにおいて間違いなく完全に堅牢なバージョンを作ってみましょう:
#![allow(unused)]
fn main() {
use std::cell::UnsafeCell;
fn opaque_read(val: &i32) {
println!("{}", val);
}
unsafe {
let mut data = UnsafeCell::new(10);
let mref1 = &mut data;
// 借用が*間違いなく*完全に積み重なるよう、この 2 つを入れ替える
let sref2 = &*mref1;
// 最大限安全にするため、共有参照から ptr を派生させる!
let ptr3 = sref2.get();
*ptr3 += 3;
opaque_read(&*sref2.get());
*sref2.get() += 2;
*mref1.get() += 1;
println!("{}", *data.get());
}
}
cargo run
13
16
MIRIFLAGS="-Zmiri-tag-raw-pointers" cargo +nightly-2022-01-21 miri run
13
16
さて、最初の実装が実際には正しいかもしれない理由の 1 つは、本気で考えてみると、エイリアシングに関する限り &UnsafeCell<T> は実際には *mut T と何も違わないからです。無限にコピーできるし、それを通じて変更できます!
つまり、ある意味では単に 2 つの生ポインタを作り、それらを普通に交換可能なものとして使っただけです。どちらも可変参照から派生しているのは少し怪しいので、2 つ目を作った時点で 1 つ目は borrow stack からポップされるべきなのかもしれません。しかし、実際には可変参照の内容にアクセスしているわけではなく、そのアドレスをコピーしているだけなので、それは本当に必要というわけではありません。
let sref2 = &*mref1 のような行は、ひっかけめいたものです。構文的にはデリファレンスしているように見えますが、デリファレンス単体は実際には何かの動作なのでしょうか? &my_tuple.0 を考えてみてください。実際には my_tuple や .0 に対して何かをしているわけではありません。それらを使ってメモリ上の位置を参照し、その前に & を置くことで「これをロードするな、アドレスだけ書き留めろ」と言っているだけです。
&* も同じです: * は単に「このポインタが指している場所について話そう」と言っていて、& は単に「ではそのアドレスを書き留めろ」と言っているだけです。もちろんそれは元のポインタが持っていた値と同じです。ただしポインタの型は変わっています。なぜなら、ええと、型です!
とはいえ、&** とすると、最初の * で実際に値をロードしています! * は奇妙です!
ナレーター: 君が「lvalue」という単語を知っていることなんて、誰も気にしていないよ、Jonathan。Rust ではそれを places と呼ぶんだ。これはまったく別物で、しかもずっとクールなんだよ?
Box をテストする
ところで、このものすごく長い脱線を始めた理由を覚えていますか? 覚えていない? 変ですね。
それは、Box と生ポインタを混ぜたからでした。Box は、指しているメモリの一意な所有権を主張するので、&mut にある程度似ています。その主張をテストしてみましょう:
unsafe {
let mut data = Box::new(10);
let ptr1 = (&mut *data) as *mut i32;
*data += 10;
*ptr1 += 1;
// 21 になるはず
println!("{}", data);
}
cargo run
21
MIRIFLAGS="-Zmiri-tag-raw-pointers" cargo +nightly-2022-01-21 miri run
error: Undefined Behavior: no item granting read access
to tag <1707> at alloc763 found in borrow stack.
--> src\main.rs:7:5
|
7 | *ptr1 += 1;
| ^^^^^^^^^^ no item granting read access to tag <1707>
| at alloc763 found in borrow stack.
|
はい、miri はこれを嫌がります。正しい順序で行えば大丈夫か確認しましょう:
#![allow(unused)]
fn main() {
unsafe {
let mut data = Box::new(10);
let ptr1 = (&mut *data) as *mut i32;
*ptr1 += 1;
*data += 10;
// 21 になるはず
println!("{}", data);
}
}
cargo run
21
MIRIFLAGS="-Zmiri-tag-raw-pointers" cargo +nightly-2022-01-21 miri run
21
はい!
やれやれ皆さん、これで stacked borrows について話したり考えたりするのはついに終わりです!
……待って、Box でこの問題をどう解決するんですか? もちろん、このようなおもちゃのプログラムを書くことはできますが、Box をどこかに保存して、生ポインタを潜在的に長い間保持しておく必要があります。きっと何かが混ざって無効化されてしまうのでは?
素晴らしい質問です! それに答えるために、ついに私たちの真の使命へ戻ることになります: くそったれな連結リストを書くことです。 待って、また連結リストを書かなきゃいけないの? 皆さん、早まらないでください。冷静になりましょう。ちょっと待ってください、私が議論すべき他の興味深い問題がきっとあるはずで—
レイアウトと基本 2: 生でいこう
前の3つのセクションの要約:
&、&mut、Boxのような安全なポインターと、*mutや*constのような unsafe なポインターをでたらめに混ぜるのは未定義動作のもとです。なぜなら、安全なポインターは追加の制約を導入し、それを生ポインターで守れていないからです。
ああ神よ、また連結リストを書かなきゃいけないのか。わかった。わかったよ。大丈夫。僕らは大丈夫。
このセクションの大部分はかなり手早く片付けます。設計については最初の試みのあたりですでに議論しましたし、やったことは、安全なポインターと unsafe なポインターを混ぜ合わせた方法を除けば、基本的に正しかったからです。
レイアウト
なので新しいレイアウトでは生ポインターだけを使うことにします。そうすればすべてが完璧になり、二度と間違いを犯すことはありません。
これが古い壊れたレイアウトです。
#![allow(unused)]
fn main() {
pub struct List<T> {
head: Link<T>,
tail: *mut Node<T>, // 無実で親切
}
type Link<T> = Option<Box<Node<T>>>; // 本当の悪
struct Node<T> {
elem: T,
next: Link<T>,
}
}
そしてこれが新しいレイアウトです。
#![allow(unused)]
fn main() {
pub struct List<T> {
head: Link<T>,
tail: *mut Node<T>,
}
type Link<T> = *mut Node<T>; // ずっと良い
struct Node<T> {
elem: T,
next: Link<T>,
}
}
思い出してください。生ポインターを使っているとき、Option はそれほど素敵でも便利でもありません。なので、もう使いません。後のセクションでは NonNull 型を見ていきますが、今は気にしなくて大丈夫です。
基本
List::new は基本的に同じです。
use ptr;
impl<T> List<T> {
pub fn new() -> Self {
List { head: ptr::null_mut(), tail: ptr::null_mut() }
}
}
Push も基本的におな-
pub fn push(&mut self, elem: T) {
let mut new_tail = Box::new(
待って、もう Box は使っていないんでした。Box なしでどうやってメモリを確保するのでしょうか?
まあ、std::alloc::alloc を使えば できる でしょうが、それは台所に刀を持ち込むようなものです。仕事はしてくれますが、ちょっと過剰で扱いづらいです。
Box を 持ちたい、でも、持ちたくない。完全にぶっ飛んでいるけど もしかすると 実行可能な選択肢の1つは、こんなことをすることでしょう。
struct Node<T> {
elem: T,
real_next: Option<Box<Node<T>>>,
next: *mut Node<T>,
}
考え方としては、Box を作ってそれをノードに格納しますが、その中への生ポインターを取り、Node を使い終えて破棄したくなるまでその生ポインターだけを使う、というものです。その後 real_next から Box を take して drop できます。これは、僕らのかなり単純化した stacked borrows モデルに適合するんじゃないかと 思います。
もしこれを作ってみたいなら、「楽しんで」ください。でも、ひどく見えますよね? これは Rc と RefCell の章ではありません。僕らはもうこの ゲーム をするつもりはありません。単純できれいなものを作るだけです。
なので代わりに、とても素敵な Box::into_raw 関数を使います。
pub fn into_raw(b: Box<T>) -> *mut TBox を消費し、ラップされた生ポインターを返します。
ポインターは適切にアラインされ、null ではありません。
この関数を呼び出した後、Box が以前管理していたメモリについては呼び出し元が責任を負います。特に、呼び出し元は Box が使用するメモリレイアウトを考慮して、T を適切に破棄し、メモリを解放する必要があります。これを行う最も簡単な方法は、
Box::from_raw関数で生ポインターを Box に戻し、Box のデストラクターにクリーンアップを実行させることです。注: これは関連関数です。つまり、
b.into_raw()ではなくBox::into_raw(b)として呼び出す必要があります。これは、内部の型のメソッドと競合しないようにするためです。例
自動的なクリーンアップのために、Box::from_raw で生ポインターを Box に戻す:
let x = Box::new(String::from("Hello")); let ptr = Box::into_raw(x); let x = unsafe { Box::from_raw(ptr) };
いいですね。これは僕らのユースケース向けに 文字どおり 設計されたものに見えます。僕らが従おうとしているルールにも合っています。安全なものから始め、生ポインターに変換し、最後にだけ(Drop したいときに)安全なものへ戻す、というルールです。
これは基本的に、奇妙な real_next を使う方法とまったく同じですが、Box が結局は生ポインターとまったく同じポインターであるにもかかわらず、それを格納してあれこれ面倒を見る必要がありません。
また、今ではあらゆる場所で生ポインターを使っているので、unsafe ブロックを狭く保つことを気にするのはやめましょう。今やすべてが unsafe です。(ずっとそうだったのですが、ときには自分に嘘をつくのもいいものです。)
pub fn push(&mut self, elem: T) {
unsafe {
// Boxをただちに生ポインターに変換する
let new_tail = Box::into_raw(Box::new(Node {
elem: elem,
next: ptr::null_mut(),
}));
if !self.tail.is_null() {
(*self.tail).next = new_tail;
} else {
self.head = new_tail;
}
self.tail = new_tail;
}
}
生ポインターに徹するようになったら、そのコードは実際かなりきれいに見えるようになりましたね!
次は pop です。これも前回残したものとかなり似ていますが、割り当てをクリーンアップするために Box::from_raw を使うことを忘れないようにしなければなりません。
pub fn pop(&mut self) -> Option<T> {
unsafe {
if self.head.is_null() {
None
} else {
// 墓場から蘇れ
let head = Box::from_raw(self.head);
self.head = head.next;
if self.head.is_null() {
self.tail = ptr::null_mut();
}
Some(head.elem)
}
}
}
僕らのかわいい take や map は死にました。これからは手動で null をチェックして設定する必要があります。
ついでに、デストラクターも入れておきましょう。今回は、かわいくて単純なので、ただ繰り返し pop する形で実装します。
impl<T> Drop for List<T> {
fn drop(&mut self) {
while let Some(_) = self.pop() { }
}
}
さて、いよいよ正念場です。
#[cfg(test)]
mod test {
use super::List;
#[test]
fn basics() {
let mut list = List::new();
// 空のリストが正しく振る舞うことを確認
assert_eq!(list.pop(), None);
// リストに要素を追加
list.push(1);
list.push(2);
list.push(3);
// 通常の削除を確認
assert_eq!(list.pop(), Some(1));
assert_eq!(list.pop(), Some(2));
// 何も壊れていないことを確認するため、さらにいくつか追加
list.push(4);
list.push(5);
// 通常の削除を確認
assert_eq!(list.pop(), Some(3));
assert_eq!(list.pop(), Some(4));
// 使い切りを確認
assert_eq!(list.pop(), Some(5));
assert_eq!(list.pop(), None);
// 使い切った場合にポインターが正しく修正されたことを確認
list.push(6);
list.push(7);
// 通常の削除を確認
assert_eq!(list.pop(), Some(6));
assert_eq!(list.pop(), Some(7));
assert_eq!(list.pop(), None);
}
}
cargo test
running 12 tests
test fifth::test::basics ... ok
test first::test::basics ... ok
test fourth::test::basics ... ok
test fourth::test::peek ... ok
test second::test::basics ... ok
test fourth::test::into_iter ... ok
test second::test::into_iter ... ok
test second::test::iter ... ok
test second::test::iter_mut ... ok
test second::test::peek ... ok
test third::test::basics ... ok
test third::test::iter ... ok
test result: ok. 12 passed; 0 failed; 0 ignored; 0 measured
よし、でも miri は同意してくれるかな?
MIRIFLAGS="-Zmiri-tag-raw-pointers" cargo +nightly-2022-01-21 miri test
running 12 tests
test fifth::test::basics ... ok
test first::test::basics ... ok
test fourth::test::basics ... ok
test fourth::test::peek ... ok
test second::test::basics ... ok
test fourth::test::into_iter ... ok
test second::test::into_iter ... ok
test second::test::iter ... ok
test second::test::iter_mut ... ok
test second::test::peek ... ok
test third::test::basics ... ok
test third::test::iter ... ok
test result: ok. 12 passed; 0 failed; 0 ignored; 0 measured
イエーーーイ!!!!!
マジで動いた!
たぶん!
未定義動作を見つけられなかったからといって、それが問題を引き起こそうと待ち構えていないことの証明にはならないが、連結リストについてのジョーク本のために俺が進んで払う厳密さには限度があるので、これは 100% 機械検証済みの証明と呼ぶことにするし、そうじゃないと言う奴は俺の COQ でもしゃぶってろ!
∴ QED □
余計なガラクタ
push と pop が書けたので、それ以外は奇妙なことに、スタックの場合と実際まったく同じです。リストの長さを変更する操作だけが、tail ポインタに触れる必要があります。
しかしもちろん、すべてが unsafe ポインタになったので、それらを使うようにコードを書き直す必要があります! そしてどうせすべてのコードに触れるなら、 何か見落としていないか確認する機会にしてしまいましょう。
とはいえ、まずはスタック実装からコードをコピペするところから始めましょう。
// ...
pub struct IntoIter<T>(List<T>);
pub struct Iter<'a, T> {
next: Option<&'a Node<T>>,
}
pub struct IterMut<'a, T> {
next: Option<&'a mut Node<T>>,
}
IntoIter は問題なさそうですが、Iter と IterMut は、もう自分たちの型では安全なポインタを決して使わない、という単純なルールを破っています。安全を期して、これらを生ポインタを使うように変更しましょう。
pub struct IntoIter<T>(List<T>);
pub struct Iter<'a, T> {
next: *mut Node<T>,
}
pub struct IterMut<'a, T> {
next: *mut Node<T>,
}
impl<T> List<T> {
pub fn into_iter(self) -> IntoIter<T> {
IntoIter(self)
}
pub fn iter(&self) -> Iter<'_, T> {
Iter { next: self.head }
}
pub fn iter_mut(&mut self) -> IterMut<'_, T> {
IterMut { next: self.head }
}
}
よさそうです!
error[E0392]: parameter `'a` is never used
--> src\fifth.rs:17:17
|
17 | pub struct Iter<'a, T> {
| ^^ unused parameter
|
= help: consider removing `'a`, referring to it in a field,
or using a marker such as `PhantomData`
error[E0392]: parameter `'a` is never used
--> src\fifth.rs:21:20
|
21 | pub struct IterMut<'a, T> {
| ^^ unused parameter
|
= help: consider removing `'a`, referring to it in a field,
or using a marker such as `PhantomData`
よくなさそうです! 彼らが言っているこの PhantomData とは何でしょう?
Tを所有している「かのように振る舞う」ものを示すために使われるゼロサイズ型。型に
PhantomData<T>フィールドを追加すると、その型が実際にはそうしていなくても、型Tの値を格納しているかのように振る舞うことをコンパイラに伝えます。この情報は、特定の安全性プロパティを計算するときに使われます。
PhantomData<T>の使い方について、より詳しい説明は Nomicon を参照してください。
おいおい、早まらないでください。私たちは私が書いた本を読んでいるんです。どこかの大きなオタクがたぶん書いた、あの別の本ではありません! どうせそこにデータ構造を書くなら、配列スタックみたいなつまらないもので、連結リストではないに決まっています。
未使用のライフタイムパラメータ
おそらく PhantomData の最も一般的なユースケースは、未使用のライフタイムパラメータを持つ構造体であり、典型的には何らかの unsafe コードの一部として使われます。
なるほど、つまり私たちは型の中でライフタイムに名前を付けているけれど、実際には使っていないわけです。PhantomData の道に進むこともできますが、それは次章の二重連結リストのために取っておきたいです。そこでは本当にそれが必要になります。
私たちは、実際には PhantomData が必要ないという興味深い状況にいます。たぶん。私はそう主張することにして、それが正しいと信じます。そして最後に miri が私たちに怒鳴ってきたら、その点を認めて PhantomData をやることにします。
実際にやることは、これらの Iterator 型に参照を戻して、一部の場所ではまだ参照を使えることを喜ぶ、というものです。イテレータを使うときには、まだある種の適切なネストがあるので、これは健全だと思います。つまり、イテレータを作成し、しばらく安全な参照を使い、その後イテレータを破棄します。
イテレータがなくなって初めて、リストにアクセスし、tail ポインタや Box をいじる必要がある push や pop のようなものを呼び出せます。さて、イテレーション中には多くの生ポインタを逆参照することになるので、そこにはある種の混在がありますが、それらの参照は unsafe ポインタの再借用として考えられるはずです。
私自身も 100% 納得しているわけではありませんが、とにかく試してみたいんです!
pub struct IntoIter<T>(List<T>);
pub struct Iter<'a, T> {
next: Option<&'a Node<T>>,
}
pub struct IterMut<'a, T> {
next: Option<&'a mut Node<T>>,
}
impl<T> List<T> {
pub fn into_iter(self) -> IntoIter<T> {
IntoIter(self)
}
pub fn iter(&self) -> Iter<'_, T> {
unsafe {
Iter { next: self.head.as_ref() }
}
}
pub fn iter_mut(&mut self) -> IterMut<'_, T> {
unsafe {
IterMut { next: self.head.as_mut() }
}
}
}
参照を格納するつもりなら、生ポインタを参照の Option に昇格させる必要があります。ポインタが null かどうかを確認することもできますが、これは、厄介な ptr::as_ref と ptr::as_mut メソッドを使ってもよいと思う、信じられないほど狭いケースのひとつです。
私はふだんはこれらのメソッドを疫病のように避けることを勧めています。なぜなら、これらは驚くような厄介なことをいくつか行いますし、私の「簡単なルール」はそうすることを避けることなのに、本質的に参照を再導入してしまうからです!
これらのメソッドには多くの警告が付いていますが、最も興味深いのはこれです。
返されるライフタイム
'aは任意に選ばれ、データの実際のライフタイムを必ずしも反映しないため、Rust のエイリアシング規則を自分で強制しなければなりません。特に、このライフタイムの間、ポインタが指すメモリには、他のいかなるポインタ経由でもアクセス(読み取りまたは書き込み)してはなりません。
ほら、25 ページにわたって話していたやつです! ここで参照を使っても私たちは絶対に大丈夫だと、私はすでに断言しました。なのでエイリアシングは解決です! もうひとつの邪悪な部分はシグネチャです。
pub unsafe fn as_mut<'a>(self) -> Option<&'a mut T>
そのライフタイムが入力にまったく結び付いていないことがわかりますか? なぜなら self が値渡しだからです。そうです、これを「非境界ライフタイム」と呼びます。そして厄介なものです。こちらが求めるだけ大きいふりをしてくれます。'static でさえも! それに対処する方法は、それを境界付けられている場所に置くことです。たいていの場合、それは単に「関数シグネチャによって制限されるように、できるだけ早くこの値を関数から返す」という意味です。
いやあ、不安ですが、このまま押し進めましょう! スタックからいくつかのイテレータ impl を盗んできます。
impl<T> Iterator for IntoIter<T> {
type Item = T;
fn next(&mut self) -> Option<Self::Item> {
self.0.pop()
}
}
impl<'a, T> Iterator for Iter<'a, T> {
type Item = &'a T;
fn next(&mut self) -> Option<Self::Item> {
unsafe {
self.next.map(|node| {
self.next = node.next.as_ref();
&node.elem
})
}
}
}
impl<'a, T> Iterator for IterMut<'a, T> {
type Item = &'a mut T;
fn next(&mut self) -> Option<Self::Item> {
unsafe {
self.next.take().map(|node| {
self.next = node.next.as_mut();
&mut node.elem
})
}
}
}
真実の瞬間です…
```text
cargo test
running 15 tests
test fifth::test::basics ... ok
test fifth::test::into_iter ... ok
test fifth::test::iter ... ok
test fifth::test::iter_mut ... ok
test first::test::basics ... ok
test fourth::test::basics ... ok
test fourth::test::into_iter ... ok
test fourth::test::peek ... ok
test second::test::basics ... ok
test second::test::into_iter ... ok
test second::test::iter ... ok
test second::test::iter_mut ... ok
test second::test::peek ... ok
test third::test::iter ... ok
test third::test::basics ... ok
test result: ok. 15 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out;
MIRIFLAGS="-Zmiri-tag-raw-pointers" cargo +nightly-2022-01-21 miri test
running 15 tests
test fifth::test::basics ... ok
test fifth::test::into_iter ... ok
test fifth::test::iter ... ok
test fifth::test::iter_mut ... ok
test first::test::basics ... ok
test fourth::test::basics ... ok
test fourth::test::into_iter ... ok
test fourth::test::peek ... ok
test second::test::basics ... ok
test second::test::into_iter ... ok
test second::test::iter ... ok
test second::test::iter_mut ... ok
test second::test::peek ... ok
test third::test::basics ... ok
test third::test::iter ... ok
test result: ok. 15 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
やった!!! 思い知ったか、語り手! ときには私だって間違えないんだ!
語り手: でも、そもそもの狙いは、その間違いが読者に教えるためにある、ということではなかったのですか。
ああそうだよ、でもときには「私が正しくて、unsafe コードについて私が何か言うときは全員が私の言うことを聞くべきだ、なぜなら私はイテレータ実装の健全性について考えるのにあまりにも長い時間を費やしてきたからだ」というのが教訓になることもあるんだよ?! いいか?! いいな。
ともあれ、これが peek と peek_mut です。
pub fn peek(&self) -> Option<&T> {
unsafe {
self.head.as_ref()
}
}
pub fn peek_mut(&mut self) -> Option<&mut T> {
unsafe {
self.head.as_mut()
}
}
もう間違えることなんてないので、テストすらしません。
語り手:
cargo build
error[E0308]: mismatched types
--> src\fifth.rs:66:13
|
25 | impl<T> List<T> {
| - this type parameter
...
64 | pub fn peek(&self) -> Option<&T> {
| ---------- expected `Option<&T>`
| because of return type
65 | unsafe {
66 | self.head.as_ref()
| ^^^^^^^^^^^^^^^^^^ expected type parameter `T`,
| found struct `fifth::Node`
|
= note: expected enum `Option<&T>`
found enum `Option<&fifth::Node<T>>`
わかったよ。
pub fn peek(&self) -> Option<&T> {
unsafe {
self.head.as_ref().map(|node| &node.elem)
}
}
pub fn peek_mut(&mut self) -> Option<&mut T> {
unsafe {
self.head.as_mut().map(|node| &mut node.elem)
}
}
どうやら私はこれからも間違え続けるらしいので、これからは一段と慎重になって、「miri food」と呼ぶ新しいテストを追加します。つまり、miri が私たちの間違いを見つけやすいように、いろいろいじくり回して私たちの API をごちゃ混ぜに使うものです。
#[test]
fn miri_food() {
let mut list = List::new();
list.push(1);
list.push(2);
list.push(3);
assert!(list.pop() == Some(1));
list.push(4);
assert!(list.pop() == Some(2));
list.push(5);
assert!(list.peek() == Some(&3));
list.push(6);
list.peek_mut().map(|x| *x *= 10);
assert!(list.peek() == Some(&30));
assert!(list.pop() == Some(30));
for elem in list.iter_mut() {
*elem *= 100;
}
let mut iter = list.iter();
assert_eq!(iter.next(), Some(&400));
assert_eq!(iter.next(), Some(&500));
assert_eq!(iter.next(), Some(&600));
assert_eq!(iter.next(), None);
assert_eq!(iter.next(), None);
assert!(list.pop() == Some(400));
list.peek_mut().map(|x| *x *= 10);
assert!(list.peek() == Some(&5000));
list.push(7);
// そこらに放り出して、dtor に自分で仕事をさせる
}
cargo test
running 16 tests
test fifth::test::basics ... ok
test fifth::test::into_iter ... ok
test fifth::test::iter ... ok
test fifth::test::iter_mut ... ok
test fifth::test::miri_food ... ok
test first::test::basics ... ok
test fourth::test::basics ... ok
test fourth::test::into_iter ... ok
test fourth::test::peek ... ok
test second::test::into_iter ... ok
test second::test::basics ... ok
test second::test::iter_mut ... ok
test second::test::peek ... ok
test third::test::iter ... ok
test second::test::iter ... ok
test third::test::basics ... ok
test result: ok. 16 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
MIRIFLAGS="-Zmiri-tag-raw-pointers" cargo +nightly-2022-01-21 miri test
running 16 tests
test fifth::test::basics ... ok
test fifth::test::into_iter ... ok
test fifth::test::iter ... ok
test fifth::test::iter_mut ... ok
test fifth::test::miri_food ... ok
test first::test::basics ... ok
test fourth::test::basics ... ok
test fourth::test::into_iter ... ok
test fourth::test::peek ... ok
test second::test::into_iter ... ok
test second::test::basics ... ok
test second::test::iter_mut ... ok
test second::test::peek ... ok
test third::test::iter ... ok
test second::test::iter ... ok
test third::test::basics ... ok
test result: ok. 16 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
完璧です。
最終コード
さて、ごくごくわずかな unsafe を加えることで、素朴な safe キューより線形時間の改善を達成し、 さらに safe スタックのロジックをほぼすべて再利用できました!
まあ、miri に完全に叩きのめされて、Rust のメモリモデルについて短い修士論文を書く羽目になった部分を除けば、ですが。よくあることです。
とはいえ、明るい面としては、変な Rc や RefCell のあれこれを書く必要は ありませんでした。
#![allow(unused)]
fn main() {
use std::ptr;
pub struct List<T> {
head: Link<T>,
tail: *mut Node<T>,
}
type Link<T> = *mut Node<T>;
struct Node<T> {
elem: T,
next: Link<T>,
}
pub struct IntoIter<T>(List<T>);
pub struct Iter<'a, T> {
next: Option<&'a Node<T>>,
}
pub struct IterMut<'a, T> {
next: Option<&'a mut Node<T>>,
}
impl<T> List<T> {
pub fn new() -> Self {
List { head: ptr::null_mut(), tail: ptr::null_mut() }
}
pub fn push(&mut self, elem: T) {
unsafe {
let new_tail = Box::into_raw(Box::new(Node {
elem: elem,
next: ptr::null_mut(),
}));
if !self.tail.is_null() {
(*self.tail).next = new_tail;
} else {
self.head = new_tail;
}
self.tail = new_tail;
}
}
pub fn pop(&mut self) -> Option<T> {
unsafe {
if self.head.is_null() {
None
} else {
let head = Box::from_raw(self.head);
self.head = head.next;
if self.head.is_null() {
self.tail = ptr::null_mut();
}
Some(head.elem)
}
}
}
pub fn peek(&self) -> Option<&T> {
unsafe {
self.head.as_ref().map(|node| &node.elem)
}
}
pub fn peek_mut(&mut self) -> Option<&mut T> {
unsafe {
self.head.as_mut().map(|node| &mut node.elem)
}
}
pub fn into_iter(self) -> IntoIter<T> {
IntoIter(self)
}
pub fn iter(&self) -> Iter<'_, T> {
unsafe {
Iter { next: self.head.as_ref() }
}
}
pub fn iter_mut(&mut self) -> IterMut<'_, T> {
unsafe {
IterMut { next: self.head.as_mut() }
}
}
}
impl<T> Drop for List<T> {
fn drop(&mut self) {
while let Some(_) = self.pop() { }
}
}
impl<T> Iterator for IntoIter<T> {
type Item = T;
fn next(&mut self) -> Option<Self::Item> {
self.0.pop()
}
}
impl<'a, T> Iterator for Iter<'a, T> {
type Item = &'a T;
fn next(&mut self) -> Option<Self::Item> {
unsafe {
self.next.map(|node| {
self.next = node.next.as_ref();
&node.elem
})
}
}
}
impl<'a, T> Iterator for IterMut<'a, T> {
type Item = &'a mut T;
fn next(&mut self) -> Option<Self::Item> {
unsafe {
self.next.take().map(|node| {
self.next = node.next.as_mut();
&mut node.elem
})
}
}
}
#[cfg(test)]
mod test {
use super::List;
#[test]
fn basics() {
let mut list = List::new();
// 空リストが正しく振る舞うことを確認
assert_eq!(list.pop(), None);
// リストに要素を追加
list.push(1);
list.push(2);
list.push(3);
// 通常の削除を確認
assert_eq!(list.pop(), Some(1));
assert_eq!(list.pop(), Some(2));
// 何も壊れていないことを確認するために、さらにいくつか追加
list.push(4);
list.push(5);
// 通常の削除を確認
assert_eq!(list.pop(), Some(3));
assert_eq!(list.pop(), Some(4));
// 枯渇を確認
assert_eq!(list.pop(), Some(5));
assert_eq!(list.pop(), None);
// 枯渇の場合にポインタが正しく修正されたことを確認
list.push(6);
list.push(7);
// 通常の削除を確認
assert_eq!(list.pop(), Some(6));
assert_eq!(list.pop(), Some(7));
assert_eq!(list.pop(), None);
}
#[test]
fn into_iter() {
let mut list = List::new();
list.push(1); list.push(2); list.push(3);
let mut iter = list.into_iter();
assert_eq!(iter.next(), Some(1));
assert_eq!(iter.next(), Some(2));
assert_eq!(iter.next(), Some(3));
assert_eq!(iter.next(), None);
}
#[test]
fn iter() {
let mut list = List::new();
list.push(1); list.push(2); list.push(3);
let mut iter = list.iter();
assert_eq!(iter.next(), Some(&1));
assert_eq!(iter.next(), Some(&2));
assert_eq!(iter.next(), Some(&3));
assert_eq!(iter.next(), None);
}
#[test]
fn iter_mut() {
let mut list = List::new();
list.push(1); list.push(2); list.push(3);
let mut iter = list.iter_mut();
assert_eq!(iter.next(), Some(&mut 1));
assert_eq!(iter.next(), Some(&mut 2));
assert_eq!(iter.next(), Some(&mut 3));
assert_eq!(iter.next(), None);
}
#[test]
fn miri_food() {
let mut list = List::new();
list.push(1);
list.push(2);
list.push(3);
assert!(list.pop() == Some(1));
list.push(4);
assert!(list.pop() == Some(2));
list.push(5);
assert!(list.peek() == Some(&3));
list.push(6);
list.peek_mut().map(|x| *x *= 10);
assert!(list.peek() == Some(&30));
assert!(list.pop() == Some(30));
for elem in list.iter_mut() {
*elem *= 100;
}
let mut iter = list.iter();
assert_eq!(iter.next(), Some(&400));
assert_eq!(iter.next(), Some(&500));
assert_eq!(iter.next(), Some(&600));
assert_eq!(iter.next(), None);
assert_eq!(iter.next(), None);
assert!(list.pop() == Some(400));
list.peek_mut().map(|x| *x *= 10);
assert!(list.peek() == Some(&5000));
list.push(7);
// そのまま破棄して、デストラクタ自身を実行させる
}
}
}
本番品質の unsafe な双方向連結デック
ついにここまで来た。私の最大の宿敵、std::collections::LinkedList、双方向連結デックだ。
私が破壊しようとして、失敗した相手だ。
私たちの物語は、2014 年が終わりに近づき、Rust 初の安定版リリースである Rust 1.0 のリリースが目前に迫っていた頃に始まる。私は std::collections、当時私たちが親しみを込めて libcollections と呼んでいたものの面倒を見る役割を担っていた。
libcollections は何年もの間、みんなの「かわいいアイデア」や「なんとなく役に立つもの」の投げ込み場だった。Rust が駆け出しの実験的な言語だった頃はそれでまったく問題なかったが、私の子どもたちが巣立ち、安定化されるのであれば、自分たちの価値を証明しなければならなかった。
それまでは私は彼ら全員を励まし育ててきたが、今や彼らが自らの欠点について裁きを受ける時が来たのだ。
私は岩盤に爪を食い込ませ、最も愚かな子どもたちのために墓石を刻んだ。誰の目にも入るよう町の広場に置いた、陰惨な記念碑だ。
TreeMap、TreeSet、TrieMap、TrieSet、LruCache、EnumSet を殺す
彼らの運命は決まった。私の言葉は絶対だったからだ。他のコレクションたちは私の残虐さに恐れおののいたが、彼らもまだ母の怒りから安全だったわけではない。私はすぐに、さらに 2 つの墓石を携えて戻ってきた。
Bit の双子は、倒れた仲間たちよりも狡猾だったが、私から逃れるだけの力はなかった。ほとんどの者は私の仕事が終わったと思っていたが、私はすぐにもう 1 つを奪った。
VecMap は隠密によって生き残ろうとしていた — とても小さく、害のなさそうな存在だったのだ! しかし、それは私が未来に思い描く libcollections には不十分だった。
私は大地を見渡し、残されたものを見た。
- Vec と VecDeque - 丈夫で単純、コンピューティングの心臓。
- HashMap と HashSet - 強力で賢明、コンピューティングの脳。
- BTreeMap と BTreeSet - ぎこちないが必要、コンピューティングの肝臓。
- BinaryHeap - 狡猾で器用、コンピューティングの足首。
私は満足してうなずいた。単純で効果的だ。私の仕事は終わっ—
いや、DList、そんなはずはない! あの悲劇的なガベージコレクション事件で死んだと思っていたのに! それは間違いなく事故であり、断じて意図的なものではなかったあの事件で!
彼らは自らの死を偽装し、新しい名前を名乗っていた。しかし、それでもやはり彼らだった。LinkedList、コンピューティングにおける影のある信用ならない策士だ。
私は耳を傾けてくれる者すべてに彼らの悪事を広めたが、人々の心は動かなかった。LinkedList は舌先の巧みな悪魔で、私の周囲の全員に、自分がコンピューティングの何か根本的で自然なデータ構造であるかのように信じ込ませていた。C++ にさえ、それが the list だと信じ込ませていたのだ!
「LinkedList のない標準ライブラリなんてあり得るのか?」
簡単に! 些細なことだ!
「自明ではない unsafe コードだから、標準ライブラリに入っているのは理にかなっている!」
GPU ドライバーや動画コーデックもそうだ。libcollections はミニマリストなのだ!
しかし悲しいかな、私がその同族に気を取られている間に、LinkedList はあまりにも多くの味方を集め、あまりにも強くなっていた。
私は研究室へ逃げ込み、それに対抗して破壊できるような邪悪なクローンや強化サイボーグ・レプリカントを考案しようとしたが、私の研究が「殺意に満ちすぎた悪」だとか何とかいうくだらない理由で、助成金が尽きてしまった。
LinkedList が勝った。私は敗北し、追放を余儀なくされた。
しかし今、あなたはここにいる。ここまで来たのだ。きっと今なら、LinkedList の放蕩の深さを理解できるはずだ! 来たまえ。これを完全に破壊するために知るべきことを、私がすべて見せてあげよう — unsafe な本番品質の双方向連結デックを実装するために知るべきことのすべてを。
どれほど本番品質か? そうだな、私の古代の Rust 1.0 linked-list クレートを完全に書き直すことにしよう。std にあるものより客観的に優れている、あのクレートだ。2015 年から安定版 Rust で Cursor を備えていたものだ! 2022 年の標準ライブラリにすらまだないものだ!
レイアウト
まずは敵の構造を研究するところから始めましょう。Doubly-Linked List は概念的には単純ですが、まさにそこがあなたを欺き、操るところです。これは、これまで何度も見てきたのと同じ種類の連結リストですが、リンクが両方向に伸びています。リンクが 2 倍なら、邪悪さも 2 倍です。
つまり、これではなく(見やすくするために Some/None 周りは外します):
... -> (A, ptr) -> (B, ptr) -> ...
こうなります:
... <-> (ptr, A, ptr) <-> (ptr, B, ptr) <-> ...
これにより、リストをどちらの方向からでも走査したり、カーソルで前後に移動したりできます。
この柔軟性と引き換えに、各ノードは 2 倍の数のポインタを格納しなければならず、各操作ははるかに多くのポインタを修正しなければなりません。これはかなり大きな複雑化であり、ミスを起こしやすくなるため、かなり多くのテストを行うことになります。
また、私が意図的にリストの端を描いていないことにも気づいたかもしれません。これは、ここが実装に関して本当に擁護可能な選択肢が存在する場所の 1 つだからです。私たちの実装には 2 つのポインタが絶対に必要です。1 つはリストの先頭へのポインタ、もう 1 つはリストの末尾へのポインタです。
私の考えでは、これを行う主な方法は 2 つあります。「従来型」と「ダミーノード」です。
従来型のアプローチは、Stack で行った方法の単純な拡張です — head ポインタと tail ポインタをスタック上に格納するだけです:
[ptr, ptr] <-> (ptr, A, ptr) <-> (ptr, B, ptr)
^ ^
+----------------------------------------+
これは問題ありませんが、1 つ欠点があります。それはコーナーケースです。リストには端が 2 つあるため、コーナーケースも 2 倍になります。片方を忘れて深刻なバグを抱え込むのは簡単です。
ダミーノードのアプローチは、データを含まない余分なノードをリストに追加し、両端をリング状につなげることで、これらのコーナーケースをならそうとします:
[ptr] -> (ptr, ?DUMMY?, ptr) <-> (ptr, A, ptr) <-> (ptr, B, ptr)
^ ^
+-------------------------------------------------+
こうすることで、すべてのノードはリスト内の前後のノードへの実際のポインタを常に持つことになります。リストから最後の要素を削除した場合でさえ、最終的にはダミーノードが自分自身を指すようにつなぎ直すだけです:
[ptr] -> (ptr, ?DUMMY?, ptr)
^ ^
+-------------+
私の中には、これを非常に満足感がありエレガントだと感じる部分があります。残念ながら、これには実用上の問題がいくつかあります:
問題 1: 特に空のリストにおいて、余分な間接参照とアロケーションが発生します。空のリストにもダミーノードを含めなければならないためです。考えられる解決策には次のものがあります:
-
何かが挿入されるまでダミーノードをアロケートしない: 単純で効果的ですが、ダミーポインタを使うことで避けようとしていたコーナーケースの一部が戻ってきてしまいます!
-
静的な copy-on-write の空シングルトンなダミーノードを使い、Copy-On-Write のチェックを通常のチェックに便乗させる本当に巧妙な仕組みを用意する: 正直かなり惹かれますし、私は本当にそういうクソが大好きなのですが、この本ではその暗い道を進むわけにはいきません。そういう倒錯したものを見たいなら、ThinVec のソースコードを読んでください。
-
ダミーノードをスタック上に格納する - C++ 風のムーブコンストラクタがない言語では実用的ではありません。pinning を使えば、ここで何か奇妙なことができるのは確かですが、やりません。
問題 2: ダミーノードにはどんな値を格納するのでしょうか? 整数ならもちろん問題ありませんが、Box だらけのリストを格納している場合はどうでしょう? その値を初期化することが不可能かもしれません! 考えられる解決策には次のものがあります:
-
すべてのノードに
Option<T>を格納させる: 単純で効果的ですが、同時に肥大化して面倒です。 -
すべてのノードに
MaybeUninit<T>を格納させる。恐ろしく面倒です。 -
ダミーノードがデータフィールドを含まないようにするための、本当に慎重で巧妙な継承風の型偽装。これも魅力的ですが、非常に危険で面倒です。そういう倒錯したものを見たいなら、BTreeMap のソースを読んでください。
Rust のような言語では、問題の方が利便性を大きく上回るため、従来型のレイアウトを使うことにします。前の章の unsafe queue で使ったのと同じ基本設計を使います:
#![allow(unused)]
fn main() {
pub struct LinkedList<T> {
front: Link<T>,
back: Link<T>,
len: usize,
}
type Link<T> = *mut Node<T>;
struct Node<T> {
front: Link<T>,
back: Link<T>,
elem: T,
}
}
(doubly-linked-deque に到達した今、私たちはついに自分たちを LinkedList と呼ぶ権利を得ました。なぜなら、これこそが真の Linked List だからです。)
これはまだ真の本番品質のレイアウトとは言えません。悪くはありませんが、自分たちが何をしているのかを Rust にもう少しうまく伝えるために使える手品があります。そのためには、もっと……深く潜る必要があります。
変性と PhantomData
これを今先送りにして後で直すのは面倒なことになるので、ハードコアなレイアウト関連のことを今やってしまいます。
unsafe な Rust コレクションを作るうえでの、ひどい5騎士がいます。
ありがたいことに、最後の2つは私たちにとって問題にはなりません。
3つ目は、問題にしようと思えばできなくはないですが、割に合いません – LinkedList を選んだ時点で、メモリ効率の戦いはすでに100倍くらい放棄しているのです。
2つ目は、以前の私は本当に重要で std もいろいろ手を入れているものだと強く主張していたものですが、デフォルトは安全で、それをいじる方法は不安定で、デフォルトの制限に気づくには本当にとんでもなく頑張る必要があるので、心配しなくてかまいません。
となると残るのは変性だけです。正直なところ、これもたぶん先送りにできますが、私は今でもコレクションの人間としての誇りを持っているので、変性のやつをやります。
さて、驚きですが、Rust にはサブタイピングがあります。特に、&'big T は &'small T のサブタイプです。なぜでしょうか? それは、あるコードがプログラム内の特定の領域だけ生きる参照を必要としている場合、通常はそれより長く生きる参照を渡してもまったく問題ないからです。直感的にも、それはそのとおりですよね?
なぜこれが重要なのでしょうか? 同じ型の2つの値を受け取るコードを考えてみましょう。
fn take_two<T>(_val1: T, _val2: T) { }
これはひどく退屈なコードなので、T=&u32 でも問題なく動くと期待してよいはずですよね?
#![allow(unused)]
fn main() {
fn two_refs<'big: 'small, 'small>(
big: &'big u32,
small: &'small u32,
) {
take_two(big, small);
}
fn take_two<T>(_val1: T, _val2: T) { }
}
はい、これは問題なくコンパイルできます!
では少し遊んで、そうですね、std::cell::Cell で包んでみましょう。
#![allow(unused)]
fn main() {
use std::cell::Cell;
fn two_refs<'big: 'small, 'small>(
// 注: この2行が変わりました
big: Cell<&'big u32>,
small: Cell<&'small u32>,
) {
take_two(big, small);
}
fn take_two<T>(_val1: T, _val2: T) { }
}
error[E0623]: lifetime mismatch
--> src/main.rs:7:19
|
4 | big: Cell<&'big u32>,
| ---------
5 | small: Cell<&'small u32>,
| ----------- these two types are declared with different lifetimes...
6 | ) {
7 | take_two(big, small);
| ^^^^^ ...but data from `small` flows into `big` here
えっ??? ライフタイムには触っていないのに、なぜコンパイラは怒っているのでしょうか!?
ああまあ、ライフタイムの「サブタイピング」関連は本当に単純なので、参照を何かで包むと壊れるに違いありません。ほら、Vec でも壊れます。
#![allow(unused)]
fn main() {
fn two_refs<'big: 'small, 'small>(
big: Vec<&'big u32>,
small: Vec<&'small u32>,
) {
take_two(big, small);
}
fn take_two<T>(_val1: T, _val2: T) { }
}
Finished dev [unoptimized + debuginfo] target(s) in 1.07s
Running `target/debug/playground`
ほら、これもコンパイルできな――待って、何??? Vec は魔法なの??????
ええ、そうです。でも、違います。魔法はずっと私たちの中にあり、その魔法こそが ✨変性✨ です。
血みどろの詳細を全部知りたいなら、Nomicon のサブタイピングの章を読んでください。基本的に、サブタイピングは常に安全とは限りません。特に、可変参照が関わる場合は安全ではありません。mem::swap のようなものを使えるせいで、突然、あっ、ダングリングポインターだ! となるからです。
「可変参照のような」ものは不変です。つまり、それらのジェネリックパラメーターではサブタイピングが起こるのをブロックします。したがって安全性のために、&mut T は T に関して不変であり、Cell<T> も T に関して不変です。なぜなら &Cell<T> は基本的にただの &mut T だからです(内部可変性のため)。
不変でないもののほとんどは共変であり、それは単にサブタイピングがそれを「通り抜けて」通常どおり機能し続けるという意味です(サブタイピングを逆向きにする反変な型もありますが、本当にまれで誰も好きではないので、もう触れません)。
コレクションは一般にデータへの可変ポインターを含むので、それらも不変だと思うかもしれませんが、実際にはそうである必要はありません! Rust の所有権システムのおかげで、Vec<T> は意味論的には T と等価であり、つまり共変であっても安全なのです!
残念ながら、この定義は不変です。
#![allow(unused)]
fn main() {
pub struct LinkedList<T> {
front: Link<T>,
back: Link<T>,
len: usize,
}
type Link<T> = *mut Node<T>;
struct Node<T> {
front: Link<T>,
back: Link<T>,
elem: T,
}
}
では Rust は実際にどのようにしてものごとの変性を決めているのでしょうか? 1.0 より前の古き良き時代には、望む変性を人々に指定させてみたりもしましたが……それは完全な大惨事でした! サブタイピングと変性を理解するのは本当に難しく、コア開発者たちでさえ基本的な用語について本気で意見が割れていました! そこで私たちは「例による変性」というアプローチに移りました。コンパイラは単にフィールドを見て、その変性をコピーします。何らかの食い違いがあれば、不変が常に勝ちます。なぜならそれが安全だからです。
では、私たちの型定義の何に Rust は怒っているのでしょうか? *mut です!
Rust の生ポインターは、基本的には何でもできるようにしようとしていますが、安全性機能がちょうど1つだけあります。ほとんどの人は、変性とサブタイピングが Rust に存在することをまったく知らず、誤って共変になっているとひどく危険なので、*mut T は不変です。なぜなら、それが &mut T 「として」使われている可能性が高いからです。
これは、Rust でコレクションを書くことに長い時間を費やしてきたまさに私という人間にとって、非常に腹立たしいことです。だから私が std::ptr::NonNull を作ったとき、この小さな魔法を入れました。
*mut Tとは異なり、NonNull<T>はTに関して共変になるように選ばれました。これにより、共変な型を構築するときにNonNull<T>を使えるようになりますが、実際には共変であるべきでない型で使うと、健全性を損なうリスクが生じます。
でも、ちょっと待ってください。そのインターフェイスは *mut T を中心に作られています。どういうことでしょう! ただの魔法なのでしょうか?! 見てみましょう。
#![allow(unused)]
fn main() {
pub struct NonNull<T> {
pointer: *const T,
}
impl<T> NonNull<T> {
pub unsafe fn new_unchecked(ptr: *mut T) -> Self {
// 安全性: 呼び出し元は `ptr` が非 null であることを保証しなければなりません。
unsafe { NonNull { pointer: ptr as *const T } }
}
}
}
いいえ。ここに魔法はありません! NonNull は単に *const T が共変であるという事実を悪用し、それを代わりに格納しているだけです。API 境界で *mut T との間でキャストを行い、*mut T を格納している「ように見せて」いるのです。トリックはそれだけです! これが Rust のコレクションが共変である仕組みです! そして、これはみじめです! だから私は、良いポインター型があなたの代わりにそれをやってくれるようにしました! どういたしまして! サブタイピングのフットガンをお楽しみください!
あなたのあらゆる問題への解決策は NonNull を使うことです。そして再び null 許容ポインタを持ちたくなったら、Option<NonNull<T>> を使います。本当にわざわざそんなことをするんですか..?
はい!最悪ですが、私たちはプロダクショングレードのリンクリストを作っているので、嫌なこともきちんと受け入れて、難しいやり方で物事を進めます(単に裸の *const T を使ってあちこちでキャストしてもよいのですが、エルゴノミクス科学のために、これがどれほど苦痛なのかを本気で見てみたいのです…)。
というわけで、最終的な型定義は次のようになります。
#![allow(unused)]
fn main() {
use std::ptr::NonNull;
// !!!ここが変更されました!!!
pub struct LinkedList<T> {
front: Link<T>,
back: Link<T>,
len: usize,
}
type Link<T> = Option<NonNull<Node<T>>>;
struct Node<T> {
front: Link<T>,
back: Link<T>,
elem: T,
}
}
…いや待ってください、最後にもう 1 つだけ。生ポインタ関連のことをするときはいつでも、ポインタを保護するために Ghost を追加するべきです。
use std::marker::PhantomData;
pub struct LinkedList<T> {
front: Link<T>,
back: Link<T>,
len: usize,
/// 意味論上、T の値を値として格納します。
_boo: PhantomData<T>,
}
このケースでは、PhantomData が実際に必要だとは思いませんが、NonNull(または一般に生ポインタ)を使うときはいつでも、安全のため、そして自分が何をしているつもりなのかをコンパイラや他の人に明確に示すために、常に追加するべきです。
PhantomData は、あなたの型の中に概念上は存在するけれど、さまざまな理由(間接参照、型消去、…)で実際には存在しない追加の「例」フィールドをコンパイラに与える方法です。このケースでは、自分たちの型が T の値を格納している「かのように」振る舞うと主張しているため NonNull を使っているので、それを明示するために PhantomData を追加します。
stdlib がこれを行う理由は実際には他にもあります。というのも、呪われた Drop Check overrides にアクセスできるからです。しかしその機能は何度も作り直されてきたので、PhantomData の件が今でもそれにとって意味のあるものなのか、実際のところ私はわかっていません。それでも私は永遠にそれをカーゴカルトし続けるつもりです。Drop Check Magic が脳に焼き付いているので!
(Node は文字どおり T を格納しているので、これをする必要はありません。やったね!)
…さて、本当にこれでレイアウトは完了です!実際の基本機能に進みましょう!
基本
さて、ここは本書の中でもひどい部分で、この章を書くのに 7 年もかかった理由です! すでに 5 回はやった本当に退屈なことを大量に一気に片付ける時間です。ただし、すべてを 2 回ずつ、しかも Option<NonNull<Node<T>>> でやらなければならないので、やたら冗長で長くなります!
impl<T> LinkedList<T> {
pub fn new() -> Self {
Self {
front: None,
back: None,
len: 0,
_boo: PhantomData,
}
}
}
PhantomData はフィールドを持たない奇妙な型なので、型名を言うだけで作れます。肩すくめ
pub fn push_front(&mut self, elem: T) {
// 安全性: リンクリストなんだから、何を求めてる?
unsafe {
let new = NonNull::new_unchecked(Box::into_raw(Box::new(Node {
front: None,
back: None,
elem,
})));
if let Some(old) = self.front {
// 新しい先頭を古いものの前に置く
(*old).front = Some(new);
(*new).back = Some(old);
} else {
// 先頭がないなら、これは空リストなので back も
// 設定する必要がある。ついでに、やらかした場合に備えて
// テスト用の整合性チェックも入れておく。
debug_assert!(self.back.is_none());
debug_assert!(self.front.is_none());
debug_assert!(self.len == 0);
self.back = Some(new);
}
self.front = Some(new);
self.len += 1;
}
}
error[E0614]: type `NonNull<Node<T>>` cannot be dereferenced
--> src\lib.rs:39:17
|
39 | (*old).front = Some(new);
| ^^^^^^
ああそうだ、私は本当にこのポインターっぽい子供たちが大嫌いです。as_ptr で NonNull から生ポインターを明示的に取り出す必要があります。なぜなら DerefMut は &mut を使って定義されていて、unsafe コードの中に安全な参照を無造作に持ち込みたくないからです!
(*old.as_ptr()).front = Some(new);
(*new.as_ptr()).back = Some(old);
Compiling linked-list v0.0.3
warning: field is never read: `elem`
--> src\lib.rs:16:5
|
16 | elem: T,
| ^^^^^^^
|
= note: `#[warn(dead_code)]` on by default
warning: `linked-list` (lib) generated 1 warning (1 duplicate)
warning: `linked-list` (lib test) generated 1 warning
Finished test [unoptimized + debuginfo] target(s) in 0.33s
いいですね。次は pop(と len)です。
pub fn pop_front(&mut self) -> Option<T> {
unsafe {
// pop する先頭ノードがある場合にだけ、何かをする必要がある。
// もはや `take` をいじくり回す必要がないことに注意。
// なぜなら、すべてが Copy であり、失敗しても実行される
// デストラクタはないから……そうだよね? :) そーうだよね? :)))
self.front.map(|node| {
// Box を生き返らせて、その値を move し、
// Drop できるようにする(Box は引き続きこれを魔法のように理解してくれる)。
let boxed_node = Box::from_raw(node.as_ptr());
let result = boxed_node.elem;
// 次のノードを新しい先頭にする。
self.front = boxed_node.back;
if let Some(new) = self.front {
// 削除されたノードへの参照を片付ける
(*new.as_ptr()).front = None;
} else {
// 先頭が null になったなら、このリストは空になった!
debug_assert!(self.len == 1);
self.back = None;
}
self.len -= 1;
result
// ここで Box は暗黙的に解放され、T がないことを認識している。
})
}
}
pub fn len(&self) -> usize {
self.len
}
Compiling linked-list v0.0.3
Finished dev [unoptimized + debuginfo] target(s) in 0.37s
私には妥当に見えます。テストを書く時間です!
#[cfg(test)]
mod test {
use super::LinkedList;
#[test]
fn test_basic_front() {
let mut list = LinkedList::new();
// 空リストを壊そうとしてみる
assert_eq!(list.len(), 0);
assert_eq!(list.pop_front(), None);
assert_eq!(list.len(), 0);
// 要素が 1 つのリストを壊そうとしてみる
list.push_front(10);
assert_eq!(list.len(), 1);
assert_eq!(list.pop_front(), Some(10));
assert_eq!(list.len(), 0);
assert_eq!(list.pop_front(), None);
assert_eq!(list.len(), 0);
// いじくり回す
list.push_front(10);
assert_eq!(list.len(), 1);
list.push_front(20);
assert_eq!(list.len(), 2);
list.push_front(30);
assert_eq!(list.len(), 3);
assert_eq!(list.pop_front(), Some(30));
assert_eq!(list.len(), 2);
list.push_front(40);
assert_eq!(list.len(), 3);
assert_eq!(list.pop_front(), Some(40));
assert_eq!(list.len(), 2);
assert_eq!(list.pop_front(), Some(20));
assert_eq!(list.len(), 1);
assert_eq!(list.pop_front(), Some(10));
assert_eq!(list.len(), 0);
assert_eq!(list.pop_front(), None);
assert_eq!(list.len(), 0);
assert_eq!(list.pop_front(), None);
assert_eq!(list.len(), 0);
}
}
Compiling linked-list v0.0.3
Finished test [unoptimized + debuginfo] target(s) in 0.40s
Running unittests src\lib.rs
running 1 test
test test::test_basic_front ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
やったー、私たちは完璧です!
……そうですよね?
Drop とパニック安全性
さて、このコメントに気づきましたか。
#![allow(unused)]
fn main() {
// もう `take` をあれこれ扱う必要はないことに注意
// すべてが Copy で、失敗しても実行される dtor は
// 存在しないから……ですよね? :) ですよねぇ? :)))
}
これは正しいでしょうか?
すみません、あなたはいま読んでいる本を忘れたんですか? もちろん間違っています!(ある意味では。)
pop_front の内部本体をもう一度見てみましょう。
// Box を復活させて、その値を move し、
// Drop できるようにする(Box はこのことを引き続き魔法のように理解してくれる)。
let boxed_node = Box::from_raw(node.as_ptr());
let result = boxed_node.elem;
// 次のノードを新しい front にする。
self.front = boxed_node.back;
if let Some(new) = self.front {
// 削除されたノードへの参照をクリーンアップする
(*new.as_ptr()).front = None;
} else {
// front が null になったなら、このリストは空になった!
debug_assert!(self.len == 1);
self.back = None;
}
self.len -= 1;
result
// Box はここで暗黙的に解放され、T がないことを認識している。
バグが見えますか? 恐ろしいことに、実はこの行です。
debug_assert!(self.len == 1);
本当に? テストのためのこの忌々しい整合性チェックがバグなんですか?? そうです!!! まあ、コレクションを正しく実装していれば、これはバグであるべきではありませんが、「ああ、len を最新に保つのが下手だね」くらいの無害なものを、悪用可能なメモリ安全性バグに変えてしまう可能性があります! なぜでしょう? パニックする可能性があるからです! たいていの場合、パニックについて考えたり心配したりする必要はありませんが、本当に unsafe なコードを書き始め、「不変条件」を軽々しく扱い始めたら、パニックに対して極度に警戒する必要があります!
例外安全性(別名パニック安全性、別名アンワインド安全性、……)について話さなければなりません。
要するに、デフォルトでは、パニックはアンワインドします。アンワインドとは、「すべての関数を即座に return させる」ことを気取って言っただけのものです。「まあ、全員が return するならプログラムは死ぬ寸前なんだから、なぜ気にする必要があるの?」と思うかもしれませんが、それは間違いです!
気にしなければならない理由は 2 つあります。関数が return するとデストラクタが実行されること、そしてアンワインドは捕捉できることです。どちらの場合も、パニック後にコードが実行され続ける可能性があるため、パニックが起こり得るあらゆる時点で、unsafe なコレクションが常に何らかの一貫した状態にあることを慎重に確認する必要があります。なぜなら、各パニックは暗黙の早期 return だからです!
その行に到達したとき、私たちのコレクションがどのような状態にあるか考えてみましょう。
スタック上には boxed_node があり、そこから要素を取り出しています。この時点で return した場合、Box は drop され、そのノードは解放されます。もうわかりましたか……? self.back はまだその解放済みノードを指しています! コレクションの残りを実装して、self.back をいろいろな用途に使い始めると、これは use-after-free につながる可能性があります! うわっ!
興味深いことに、この行にも似た問題がありますが、ずっと安全です。
self.len -= 1;
デフォルトでは、Rust はデバッグビルドでアンダーフローとオーバーフローをチェックし、それらが発生するとパニックします。そうです、すべての算術演算はパニック安全性上の危険なのです! こちらのほうがましなのは、すべての不変条件を修復した後で発生するため、メモリ安全性の問題を引き起こさないからです……ただし、len が正しいと信じない限りは、ですが。とはいえ、アンダーフローするならそれは間違いなく誤っているので、どのみち詰んでいました! debug assert は、ある意味ではより悪いです。些細な問題を致命的な問題へと悪化させ得るからです!
「不変条件」という用語を何度か持ち出してきましたが、それはパニック安全性にとって非常に有用な概念だからです! 基本的には、コレクションの外部の観察者に対して、私たちが常に保っている特定の性質があります。LinkedList であれば、そのひとつは、リスト内で到達可能なノードはすべて、まだ割り当てられて初期化されている、というものです。
実装の内部では、誰かに気づかれる前に必ず修復する限り、不変条件を一時的に破る柔軟性が少しあります。これは実際、Rust の所有権と借用システムがコレクションにもたらす「キラーアプリ」のひとつです。操作が &mut Self を必要とするなら、私たちはそのコレクションへの排他的アクセスを持っていることが保証され、誰もこっそり干渉できないとわかっているので、一時的に不変条件を破っても構わないのです。
おそらくこれを最もよく表しているのが Vec::drain です。これは実際に、Vec の中核的な不変条件を完全にぶち壊し、Vec の先頭、あるいは中央から値を move し始めることさえ許します。これが健全である理由は、返す Drain イテレータが Vec への &mut を保持しているため、すべてのアクセスがそれを通して制御されるからです! Drain イテレータがなくなるまで誰も Vec を観察できず、その後、そのデストラクタが誰かに気づかれる前に Vec を「修復」できるので、完璧で――
完璧ではありません。残念ながら、自分が制御していないコード内のデストラクタが実行されることには依存できません。そのため、Drain であっても、型の不変条件が常に保たれるように少し追加の作業が必要です。ただし、ちょっと間の抜けたやり方です。最初に Vec の len を 0 に設定するだけです。そうすれば、誰かが Drain をリークしても、安全な Vec は残ります……ただし、大量のデータも失うことになります。お前が俺をリークするなら、俺もお前をリークする! 目には目を! 真の正義です!
パニック安全性のために実際にデストラクタを使える状況については、BinaryHeap::sift_up のケーススタディを見てください。
ともあれ、私たちの LinkedList ではこうした凝った仕組みはすべて必要ありません。不変条件をどこで破るのか、何が正しいと信頼・要求するのかにもう少し注意し、厄介な作業の最中に不要なアンワインドを導入しないようにすればよいだけです。
この場合、コードをもう少し堅牢にする選択肢は 2 つあります。
-
Option::take のような操作をもっと積極的に使う。なぜなら、これらはより「トランザクショナル」で、不変条件を保つ傾向があるからです。
-
debug_assert を削除し、ユーザーコードでは決して実行されない専用の「整合性チェック」関数で、よりよいテストを書ける自分たちを信じる。
原則としては最初の選択肢が好きですが、二重リンクリストでは実際にはあまりうまく機能しません。なぜなら、すべてが二重に冗長にエンコードされているからです。ここでは Option::take は問題を解決しませんが、debug_assert を 1 行下に移動すれば解決します。とはいえ本当に、なぜ自分たちで難しくするのでしょう? その debug_assert は削除し、パニックし得るものはすべてメソッドの先頭か末尾に置くようにしましょう。そこでは不変条件が成り立っていることがわかっているはずです。
(このように考えると、それらを事前条件と事後条件と捉えるほうがより正確かもしれませんが、本当はできる限りそれらを不変条件として扱うよう努めるべきです!)
これが現在の完全な実装です。
#![allow(unused)]
fn main() {
use std::ptr::NonNull;
use std::marker::PhantomData;
pub struct LinkedList<T> {
front: Link<T>,
back: Link<T>,
len: usize,
_boo: PhantomData<T>,
}
type Link<T> = Option<NonNull<Node<T>>>;
struct Node<T> {
front: Link<T>,
back: Link<T>,
elem: T,
}
impl<T> LinkedList<T> {
pub fn new() -> Self {
Self {
front: None,
back: None,
len: 0,
_boo: PhantomData,
}
}
pub fn push_front(&mut self, elem: T) {
// SAFETY: これは連結リストです。他に何を求めているんですか?
unsafe {
let new = NonNull::new_unchecked(Box::into_raw(Box::new(Node {
front: None,
back: None,
elem,
})));
if let Some(old) = self.front {
// 新しい front を古いものの前に置く
(*old.as_ptr()).front = Some(new);
(*new.as_ptr()).back = Some(old);
} else {
// front がなければ、これは空のリストなので
// back も設定する必要がある。
self.back = Some(new);
}
// これらは常に行われる!
self.front = Some(new);
self.len += 1;
}
}
pub fn pop_front(&mut self) -> Option<T> {
unsafe {
// pop する front ノードがある場合だけ処理を行う必要がある。
self.front.map(|node| {
// Box を蘇らせて、その値を move out し、
// Drop できるようにする(Box は引き続きこれを魔法のように理解してくれる)。
let boxed_node = Box::from_raw(node.as_ptr());
let result = boxed_node.elem;
// 次のノードを新しい front にする。
self.front = boxed_node.back;
if let Some(new) = self.front {
// 削除されたノードへの参照をクリーンアップする
(*new.as_ptr()).front = None;
} else {
// front が null になったなら、このリストは空になった!
self.back = None;
}
self.len -= 1;
result
// Box はここで暗黙的に解放され、T がないことを知っている。
})
}
}
pub fn len(&self) -> usize {
self.len
}
}
}
ここでは何が panic し得るのでしょうか? 正直なところ、それを知るには Rust のかなりの専門家である必要がありますが、ありがたいことに、私はそうです!
このコードで 可能性として panic し得る箇所として私に見えるのは(誰かが debug_asserts を有効にして標準ライブラリを再コンパイルするような、とんでもない悪ふざけは除きますが、これは絶対にやるべきことではありません)、Box::new(メモリ不足条件の場合)と len の算術演算だけです。それらはすべて、メソッドの本当に末尾か本当に先頭にあるので、ええ、私たちはちゃんと安全にやれています!
……Box::new が panic し得ることに驚きましたか? panic とはそういうものです! 心配しなくて済むように、それらの不変条件を保つようにしましょう!
退屈な組み合わせ論
さて、通常進行の linked list に戻りましょう!
まずは、pop があれば些細な Drop を片付けましょう。
impl<T> Drop for LinkedList<T> {
fn drop(&mut self) {
// 停止するまで pop する
while let Some(_) = self.pop_front() { }
}
}
front、front_mut、back、back_mut、iter、iter_mut、into_iter、… といった、本当に退屈な組み合わせ的な実装をたくさん埋める必要があります。
マクロか何かでやってもいいですが、正直なところ、それはコピー&ペーストより悪い運命です。ここでは大量のコピー&ペーストをするだけにします。これまでの push/pop 実装は、front と back を文字通り入れ替えるだけでコードが正しく動き、正しいことを表すように、私は非常に注意深く作ってきました! 苦い経験に万歳! (ノードについて “prev and next” と言いたくなる誘惑は強いですが、ミスを避けるためには、できるだけ一貫して “front” と “back” と言い続けることに本当に価値があると私は思います。)
ではまず、front です。
pub fn front(&self) -> Option<&T> {
unsafe {
self.front.map(|node| &(*node.as_ptr()).elem)
}
}
ところで実は、この本はかなり古く、その後 ? 演算子のような素敵な新機能がいくつか追加されました。これは Option::None で早期 return するものですが、これでコードはより良くなるでしょうか?
pub fn front(&self) -> Option<&T> {
unsafe {
Some(&(*self.front?.as_ptr()).elem)
}
}
たぶん? これくらい単純なものだとどちらとも言えませんし、前のセクションでは早期 return が私たちにとってちょっと不気味だという話を散々したので、ここではもう少し明示的な方を好むべきかもしれません(私は map の実装のままでいきます)。次は front_mut です。
pub fn front_mut(&mut self) -> Option<&mut T> {
unsafe {
self.front.map(|node| &mut (*node.as_ptr()).elem)
}
}
back 版は最後にまとめて載せます。
次は iterator です。これまでのすべての list と違って、私たちはついに DoubleEndedIterator を使う能力を解禁しました。さらに本番品質を目指すなら ExactSizeIterator も実装します。
つまり、next と size_hint に加えて、next_back と len もサポートします。
注意深い読者は、IterMut が両端 iterator だとかなり怪しく見えることに気づくかもしれませんが、実際にはそれでも健全です!
… これは大量のボイラープレートになりそうです。やっぱりマクロを書くべきかも… いや、いや、それでもそっちの方が悪い運命です。
pub struct Iter<'a, T> {
front: Link<T>,
back: Link<T>,
len: usize,
_boo: PhantomData<&'a T>,
}
impl<T> LinkedList<T> {
pub fn iter(&self) -> Iter<T> {
Iter {
front: self.front,
back: self.back,
len: self.len,
_boo: PhantomData,
}
}
}
impl<'a, T> IntoIterator for &'a LinkedList<T> {
type IntoIter = Iter<'a, T>;
type Item = &'a T;
fn into_iter(self) -> Self::IntoIter {
self.iter()
}
}
impl<'a, T> Iterator for Iter<'a, T> {
type Item = &'a T;
fn next(&mut self) -> Option<Self::Item> {
// ここで self.front == self.back を条件としてチェックしたくなるが、
// 最後の要素を yield するときに正しく動かない! その手の
// ことが配列でだけ機能するのは、"one-past-the-end" ポインタがあるから。
if self.len > 0 {
// front を unwrap することもできるが、こちらの方が安全で簡単
self.front.map(|node| unsafe {
self.len -= 1;
self.front = (*node.as_ptr()).back;
&(*node.as_ptr()).elem
})
} else {
None
}
}
fn size_hint(&self) -> (usize, Option<usize>) {
(self.len, Some(self.len))
}
}
impl<'a, T> DoubleEndedIterator for Iter<'a, T> {
fn next_back(&mut self) -> Option<Self::Item> {
if self.len > 0 {
self.back.map(|node| unsafe {
self.len -= 1;
self.back = (*node.as_ptr()).front;
&(*node.as_ptr()).elem
})
} else {
None
}
}
}
impl<'a, T> ExactSizeIterator for Iter<'a, T> {
fn len(&self) -> usize {
self.len
}
}
…これはただの .iter() ですね…
IterMut は最後に貼り付けることにします。あれは文字通り同じコードで、あちこちに mut が入っているだけです。まずは into_iter を片付けましょう。ありがたいことに、コレクションをラップして next で pop を使うという、実績ある解決策にまだ頼れます。
pub struct IntoIter<T> {
list: LinkedList<T>,
}
impl<T> LinkedList<T> {
pub fn into_iter(self) -> IntoIter<T> {
IntoIter {
list: self
}
}
}
impl<T> IntoIterator for LinkedList<T> {
type IntoIter = IntoIter<T>;
type Item = T;
fn into_iter(self) -> Self::IntoIter {
self.into_iter()
}
}
impl<T> Iterator for IntoIter<T> {
type Item = T;
fn next(&mut self) -> Option<Self::Item> {
self.list.pop_front()
}
fn size_hint(&self) -> (usize, Option<usize>) {
(self.list.len, Some(self.list.len))
}
}
impl<T> DoubleEndedIterator for IntoIter<T> {
fn next_back(&mut self) -> Option<Self::Item> {
self.list.pop_back()
}
}
impl<T> ExactSizeIterator for IntoIter<T> {
fn len(&self) -> usize {
self.list.len
}
}
まだ大量のボイラープレートですが、少なくとも満足感のあるボイラープレートです。
さて、組み合わせ的な部分をすべて埋めたコード全体がこちらです。
```rust
use std::ptr::NonNull;
use std::marker::PhantomData;
pub struct LinkedList<T> {
front: Link<T>,
back: Link<T>,
len: usize,
_boo: PhantomData<T>,
}
type Link<T> = Option<NonNull<Node<T>>>;
struct Node<T> {
front: Link<T>,
back: Link<T>,
elem: T,
}
pub struct Iter<'a, T> {
front: Link<T>,
back: Link<T>,
len: usize,
_boo: PhantomData<&'a T>,
}
pub struct IterMut<'a, T> {
front: Link<T>,
back: Link<T>,
len: usize,
_boo: PhantomData<&'a mut T>,
}
pub struct IntoIter<T> {
list: LinkedList<T>,
}
impl<T> LinkedList<T> {
pub fn new() -> Self {
Self {
front: None,
back: None,
len: 0,
_boo: PhantomData,
}
}
pub fn push_front(&mut self, elem: T) {
// SAFETY: これは連結リストです、他にどうしろと?
unsafe {
let new = NonNull::new_unchecked(Box::into_raw(Box::new(Node {
front: None,
back: None,
elem,
})));
if let Some(old) = self.front {
// 新しい front を古いものの前に置く
(*old.as_ptr()).front = Some(new);
(*new.as_ptr()).back = Some(old);
} else {
// front がない場合、このリストは空なので back も
// 設定する必要がある。
self.back = Some(new);
}
// これらは常に行われる!
self.front = Some(new);
self.len += 1;
}
}
pub fn push_back(&mut self, elem: T) {
// SAFETY: これは連結リストです、他にどうしろと?
unsafe {
let new = NonNull::new_unchecked(Box::into_raw(Box::new(Node {
back: None,
front: None,
elem,
})));
if let Some(old) = self.back {
// 新しい back を古いものの前に置く
(*old.as_ptr()).back = Some(new);
(*new.as_ptr()).front = Some(old);
} else {
// back がない場合、このリストは空なので front も
// 設定する必要がある。
self.front = Some(new);
}
// これらは常に行われる!
self.back = Some(new);
self.len += 1;
}
}
pub fn pop_front(&mut self) -> Option<T> {
unsafe {
// pop する front ノードがある場合にだけ処理すればよい。
self.front.map(|node| {
// Box を蘇らせて、その値を取り出し、
// それを Drop できるようにする(Box は引き続き魔法のようにこれを理解してくれる)。
let boxed_node = Box::from_raw(node.as_ptr());
let result = boxed_node.elem;
// 次のノードを新しい front にする。
self.front = boxed_node.back;
if let Some(new) = self.front {
// 削除されたノードへの参照を掃除する
(*new.as_ptr()).front = None;
} else {
// front が null になったなら、このリストは空になった!
self.back = None;
}
self.len -= 1;
result
// Box はここで暗黙に解放され、T が存在しないことを理解している。
})
}
}
pub fn pop_back(&mut self) -> Option<T> {
unsafe {
// pop する back ノードがある場合にだけ処理すればよい。
self.back.map(|node| {
// Box の front を蘇らせて、その値を取り出し、
// それを Drop できるようにする(Box は引き続き魔法のようにこれを理解してくれる)。
let boxed_node = Box::from_raw(node.as_ptr());
let result = boxed_node.elem;
// 次のノードを新しい back にする。
self.back = boxed_node.front;
if let Some(new) = self.back {
// 削除されたノードへの参照を掃除する
(*new.as_ptr()).back = None;
} else {
// back が null になったなら、このリストは空になった!
self.front = None;
}
self.len -= 1;
result
// Box はここで暗黙に解放され、T が存在しないことを理解している。
})
}
}
pub fn front(&self) -> Option<&T> {
unsafe {
self.front.map(|node| &(*node.as_ptr()).elem)
}
}
pub fn front_mut(&mut self) -> Option<&mut T> {
unsafe {
self.front.map(|node| &mut (*node.as_ptr()).elem)
}
}
pub fn back(&self) -> Option<&T> {
unsafe {
self.back.map(|node| &(*node.as_ptr()).elem)
}
}
pub fn back_mut(&mut self) -> Option<&mut T> {
unsafe {
self.back.map(|node| &mut (*node.as_ptr()).elem)
}
}
pub fn len(&self) -> usize {
self.len
}
pub fn iter(&self) -> Iter<T> {
Iter {
front: self.front,
back: self.back,
len: self.len,
_boo: PhantomData,
}
}
pub fn iter_mut(&mut self) -> IterMut<T> {
IterMut {
front: self.front,
back: self.back,
len: self.len,
_boo: PhantomData,
}
}
pub fn into_iter(self) -> IntoIter<T> {
IntoIter {
list: self
}
}
}
impl<T> Drop for LinkedList<T> {
fn drop(&mut self) {
// 停止する必要が出るまで pop する
while let Some(_) = self.pop_front() { }
}
}
impl<'a, T> IntoIterator for &'a LinkedList<T> {
type IntoIter = Iter<'a, T>;
type Item = &'a T;
fn into_iter(self) -> Self::IntoIter {
self.iter()
}
}
impl<'a, T> Iterator for Iter<'a, T> {
type Item = &'a T;
fn next(&mut self) -> Option<Self::Item> {
// self.front == self.back はここで確認したくなる条件だが、
// 最後の要素を yield するには正しく動作しない! その種のことは
// "one-past-the-end" ポインタがあるため配列でのみ機能する。
if self.len > 0 {
// front を unwrap することもできるが、こちらの方が安全で簡単
self.front.map(|node| unsafe {
self.len -= 1;
self.front = (*node.as_ptr()).back;
&(*node.as_ptr()).elem
})
} else {
None
}
}
fn size_hint(&self) -> (usize, Option<usize>) {
(self.len, Some(self.len))
}
}
impl<'a, T> DoubleEndedIterator for Iter<'a, T> {
fn next_back(&mut self) -> Option<Self::Item> {
if self.len > 0 {
self.back.map(|node| unsafe {
self.len -= 1;
self.back = (*node.as_ptr()).front;
&(*node.as_ptr()).elem
})
} else {
None
}
}
}
impl<'a, T> ExactSizeIterator for Iter<'a, T> {
fn len(&self) -> usize {
self.len
}
}
impl<'a, T> IntoIterator for &'a mut LinkedList<T> {
type IntoIter = IterMut<'a, T>;
type Item = &'a mut T;
fn into_iter(self) -> Self::IntoIter {
self.iter_mut()
}
}
impl<'a, T> Iterator for IterMut<'a, T> {
type Item = &'a mut T;
fn next(&mut self) -> Option<Self::Item> {
// self.front == self.back はここで確認したくなる条件だが、
// 最後の要素を yield するには正しく動作しない! その種のことは
// "one-past-the-end" ポインタがあるため配列でのみ機能する。
if self.len > 0 {
// front を unwrap することもできるが、こちらの方が安全で簡単
self.front.map(|node| unsafe {
self.len -= 1;
self.front = (*node.as_ptr()).back;
&mut (*node.as_ptr()).elem
})
} else {
None
}
}
fn size_hint(&self) -> (usize, Option<usize>) {
(self.len, Some(self.len))
}
}
impl<'a, T> DoubleEndedIterator for IterMut<'a, T> {
fn next_back(&mut self) -> Option<Self::Item> {
if self.len > 0 {
self.back.map(|node| unsafe {
self.len -= 1;
self.back = (*node.as_ptr()).front;
&mut (*node.as_ptr()).elem
})
} else {
None
}
}
}
impl<'a, T> ExactSizeIterator for IterMut<'a, T> {
fn len(&self) -> usize {
self.len
}
}
impl<T> IntoIterator for LinkedList<T> {
type IntoIter = IntoIter<T>;
type Item = T;
fn into_iter(self) -> Self::IntoIter {
self.into_iter()
}
}
impl<T> Iterator for IntoIter<T> {
type Item = T;
fn next(&mut self) -> Option<Self::Item> {
self.list.pop_front()
}
fn size_hint(&self) -> (usize, Option<usize>) {
(self.list.len, Some(self.list.len))
}
}
impl<T> DoubleEndedIterator for IntoIter<T> {
fn next_back(&mut self) -> Option<Self::Item> {
self.list.pop_back()
}
}
impl<T> ExactSizeIterator for IntoIter<T> {
fn len(&self) -> usize {
self.list.len
}
}
#[cfg(test)]
mod test {
use super::LinkedList;
#[test]
fn test_basic_front() {
let mut list = LinkedList::new();
// 空のリストを壊してみる
assert_eq!(list.len(), 0);
assert_eq!(list.pop_front(), None);
assert_eq!(list.len(), 0);
// 1 要素のリストを壊してみる
list.push_front(10);
assert_eq!(list.len(), 1);
assert_eq!(list.pop_front(), Some(10));
assert_eq!(list.len(), 0);
assert_eq!(list.pop_front(), None);
assert_eq!(list.len(), 0);
// いじくり回す
list.push_front(10);
assert_eq!(list.len(), 1);
list.push_front(20);
assert_eq!(list.len(), 2);
list.push_front(30);
assert_eq!(list.len(), 3);
assert_eq!(list.pop_front(), Some(30));
assert_eq!(list.len(), 2);
list.push_front(40);
assert_eq!(list.len(), 3);
assert_eq!(list.pop_front(), Some(40));
assert_eq!(list.len(), 2);
assert_eq!(list.pop_front(), Some(20));
assert_eq!(list.len(), 1);
assert_eq!(list.pop_front(), Some(10));
assert_eq!(list.len(), 0);
assert_eq!(list.pop_front(), None);
assert_eq!(list.len(), 0);
assert_eq!(list.pop_front(), None);
assert_eq!(list.len(), 0);
}
}
ランダムなビットを埋める
ねえ、君はプロダクション品質になりたいって言ってたよね?
「よい」コレクションになるように放り込む、雑多なものをもう少し用意しよう:
impl<T> LinkedList<T> {
pub fn is_empty(&self) -> bool {
self.len == 0
}
pub fn clear(&mut self) {
// おや、また drop だ
while let Some(_) = self.pop_front() { }
}
}
そして今や、誰もが期待する実装すべきトレイトがたくさんある:
impl<T> Default for LinkedList<T> {
fn default() -> Self {
Self::new()
}
}
impl<T: Clone> Clone for LinkedList<T> {
fn clone(&self) -> Self {
let mut new_list = Self::new();
for item in self {
new_list.push_back(item.clone());
}
new_list
}
}
impl<T> Extend<T> for LinkedList<T> {
fn extend<I: IntoIterator<Item = T>>(&mut self, iter: I) {
for item in iter {
self.push_back(item);
}
}
}
impl<T> FromIterator<T> for LinkedList<T> {
fn from_iter<I: IntoIterator<Item = T>>(iter: I) -> Self {
let mut list = Self::new();
list.extend(iter);
list
}
}
impl<T: Debug> Debug for LinkedList<T> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_list().entries(self).finish()
}
}
impl<T: PartialEq> PartialEq for LinkedList<T> {
fn eq(&self, other: &Self) -> bool {
self.len() == other.len() && self.iter().eq(other)
}
fn ne(&self, other: &Self) -> bool {
self.len() != other.len() || self.iter().ne(other)
}
}
impl<T: Eq> Eq for LinkedList<T> { }
impl<T: PartialOrd> PartialOrd for LinkedList<T> {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
self.iter().partial_cmp(other)
}
}
impl<T: Ord> Ord for LinkedList<T> {
fn cmp(&self, other: &Self) -> Ordering {
self.iter().cmp(other)
}
}
impl<T: Hash> Hash for LinkedList<T> {
fn hash<H: Hasher>(&self, state: &mut H) {
self.len().hash(state);
for item in self {
item.hash(state);
}
}
}
これらは間違いなく全部ゼロから書いたのであって、std の実装をコピーしただけではない。だってすごく面白いし、Hash を手動で実装する微妙な注意点も、間違いなく覚えているからね。うん、これは僕がいつもいつも考えていることだよ……
さて、ここには実際にいくつか注目すべき点がある。
まず、厄介な名前空間の衝突だ。理由はともかく、今の std には Hash や Debug という名前のマクロがあり、そのためトレイトをインポートしていないと、本来の「トレイトが見つからない」ではなく、マクロに関する非常に不可解なエラーが出る。
もうひとつ話しておきたい興味深い点は Hash そのものだ。len をハッシュしているのがわかるだろうか? これは実は本当に重要だ! コレクションが長さをハッシュしないと、意図せず自分自身をプレフィックス衝突に対して脆弱にしてしまう可能性がある。たとえば、["he", "llo"] と ["hello"] を区別するものは何だろう? 誰も長さや何らかの「区切り」をハッシュしていなければ、何もない! ハッシュ衝突が偶発的に、あるいは悪意を持って起こるのを簡単にしすぎると、深刻な悲しみを招くことがあるので、とにかくやっておこう!
さて、これが現在のコードだ:
```rust
use std::cmp::Ordering;
use std::fmt::{self, Debug};
use std::hash::{Hash, Hasher};
use std::iter::FromIterator;
use std::ptr::NonNull;
use std::marker::PhantomData;
pub struct LinkedList<T> {
front: Link<T>,
back: Link<T>,
len: usize,
_boo: PhantomData<T>,
}
type Link<T> = Option<NonNull<Node<T>>>;
struct Node<T> {
front: Link<T>,
back: Link<T>,
elem: T,
}
pub struct Iter<'a, T> {
front: Link<T>,
back: Link<T>,
len: usize,
_boo: PhantomData<&'a T>,
}
pub struct IterMut<'a, T> {
front: Link<T>,
back: Link<T>,
len: usize,
_boo: PhantomData<&'a mut T>,
}
pub struct IntoIter<T> {
list: LinkedList<T>,
}
impl<T> LinkedList<T> {
pub fn new() -> Self {
Self {
front: None,
back: None,
len: 0,
_boo: PhantomData,
}
}
pub fn push_front(&mut self, elem: T) {
// 安全性: これは連結リストだ、どうしろと?
unsafe {
let new = NonNull::new_unchecked(Box::into_raw(Box::new(Node {
front: None,
back: None,
elem,
})));
if let Some(old) = self.front {
// 新しい front を古いものの前に置く
(*old.as_ptr()).front = Some(new);
(*new.as_ptr()).back = Some(old);
} else {
// front がない場合、このリストは空なので
// back も設定する必要がある。
self.back = Some(new);
}
// これらは常に行う!
self.front = Some(new);
self.len += 1;
}
}
pub fn push_back(&mut self, elem: T) {
// 安全性: これは連結リストだ、どうしろと?
unsafe {
let new = NonNull::new_unchecked(Box::into_raw(Box::new(Node {
back: None,
front: None,
elem,
})));
if let Some(old) = self.back {
// 新しい back を古いものの前に置く
(*old.as_ptr()).back = Some(new);
(*new.as_ptr()).front = Some(old);
} else {
// back がない場合、このリストは空なので
// front も設定する必要がある。
self.front = Some(new);
}
// これらは常に行う!
self.back = Some(new);
self.len += 1;
}
}
pub fn pop_front(&mut self) -> Option<T> {
unsafe {
// pop する front ノードがある場合だけ処理すればよい。
self.front.map(|node| {
// Box を生き返らせて、その値を取り出し、
// Drop できるようにする(Box は引き続きこれを魔法のように理解してくれる)。
let boxed_node = Box::from_raw(node.as_ptr());
let result = boxed_node.elem;
// 次のノードを新しい front にする。
self.front = boxed_node.back;
if let Some(new) = self.front {
// 削除されたノードへの参照をクリーンアップする
(*new.as_ptr()).front = None;
} else {
// front が今 null なら、このリストは空になった!
self.back = None;
}
self.len -= 1;
result
// Box はここで暗黙的に解放され、T がないことを知っている。
})
}
}
pub fn pop_back(&mut self) -> Option<T> {
unsafe {
// pop する back ノードがある場合だけ処理すればよい。
self.back.map(|node| {
// Box を生き返らせて、その値を取り出し、
// Drop できるようにする(Box は引き続きこれを魔法のように理解してくれる)。
let boxed_node = Box::from_raw(node.as_ptr());
let result = boxed_node.elem;
// 次のノードを新しい back にする。
self.back = boxed_node.front;
if let Some(new) = self.back {
// 削除されたノードへの参照をクリーンアップする
(*new.as_ptr()).back = None;
} else {
// back が今 null なら、このリストは空になった!
self.front = None;
}
self.len -= 1;
result
// Box はここで暗黙的に解放され、T がないことを知っている。
})
}
}
pub fn front(&self) -> Option<&T> {
unsafe {
self.front.map(|node| &(*node.as_ptr()).elem)
}
}
pub fn front_mut(&mut self) -> Option<&mut T> {
unsafe {
self.front.map(|node| &mut (*node.as_ptr()).elem)
}
}
pub fn back(&self) -> Option<&T> {
unsafe {
self.back.map(|node| &(*node.as_ptr()).elem)
}
}
pub fn back_mut(&mut self) -> Option<&mut T> {
unsafe {
self.back.map(|node| &mut (*node.as_ptr()).elem)
}
}
pub fn len(&self) -> usize {
self.len
}
pub fn is_empty(&self) -> bool {
self.len == 0
}
pub fn clear(&mut self) {
// おや、また drop だ
while let Some(_) = self.pop_front() { }
}
pub fn iter(&self) -> Iter<T> {
Iter {
front: self.front,
back: self.back,
len: self.len,
_boo: PhantomData,
}
}
pub fn iter_mut(&mut self) -> IterMut<T> {
IterMut {
front: self.front,
back: self.back,
len: self.len,
_boo: PhantomData,
}
}
pub fn into_iter(self) -> IntoIter<T> {
IntoIter {
list: self
}
}
}
impl<T> Drop for LinkedList<T> {
fn drop(&mut self) {
// 止めざるを得なくなるまで pop する
while let Some(_) = self.pop_front() { }
}
}
impl<T> Default for LinkedList<T> {
fn default() -> Self {
Self::new()
}
}
impl<T: Clone> Clone for LinkedList<T> {
fn clone(&self) -> Self {
let mut new_list = Self::new();
for item in self {
new_list.push_back(item.clone());
}
new_list
}
}
impl<T> Extend<T> for LinkedList<T> {
fn extend<I: IntoIterator<Item = T>>(&mut self, iter: I) {
for item in iter {
self.push_back(item);
}
}
}
impl<T> FromIterator<T> for LinkedList<T> {
fn from_iter<I: IntoIterator<Item = T>>(iter: I) -> Self {
let mut list = Self::new();
list.extend(iter);
list
}
}
impl<T: Debug> Debug for LinkedList<T> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_list().entries(self).finish()
}
}
impl<T: PartialEq> PartialEq for LinkedList<T> {
fn eq(&self, other: &Self) -> bool {
self.len() == other.len() && self.iter().eq(other)
}
fn ne(&self, other: &Self) -> bool {
self.len() != other.len() || self.iter().ne(other)
}
}
impl<T: Eq> Eq for LinkedList<T> { }
impl<T: PartialOrd> PartialOrd for LinkedList<T> {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
self.iter().partial_cmp(other)
}
}
impl<T: Ord> Ord for LinkedList<T> {
fn cmp(&self, other: &Self) -> Ordering {
self.iter().cmp(other)
}
}
impl<T: Hash> Hash for LinkedList<T> {
fn hash<H: Hasher>(&self, state: &mut H) {
self.len().hash(state);
for item in self {
item.hash(state);
}
}
}
impl<'a, T> IntoIterator for &'a LinkedList<T> {
type IntoIter = Iter<'a, T>;
type Item = &'a T;
fn into_iter(self) -> Self::IntoIter {
self.iter()
}
}
impl<'a, T> Iterator for Iter<'a, T> {
type Item = &'a T;
fn next(&mut self) -> Option<Self::Item> {
// ここで self.front == self.back をチェックする条件は魅力的だが、
// 最後の要素を yield するには正しく動かない!その手の
// ものが配列でのみ機能するのは、「終端の 1 つ先」のポインタがあるからだ。
if self.len > 0 {
// front を unwrap することもできるが、こちらのほうが安全で簡単
self.front.map(|node| unsafe {
self.len -= 1;
self.front = (*node.as_ptr()).back;
&(*node.as_ptr()).elem
})
} else {
None
}
}
fn size_hint(&self) -> (usize, Option<usize>) {
(self.len, Some(self.len))
}
}
impl<'a, T> DoubleEndedIterator for Iter<'a, T> {
fn next_back(&mut self) -> Option<Self::Item> {
if self.len > 0 {
self.back.map(|node| unsafe {
self.len -= 1;
self.back = (*node.as_ptr()).front;
&(*node.as_ptr()).elem
})
} else {
None
}
}
}
impl<'a, T> ExactSizeIterator for Iter<'a, T> {
fn len(&self) -> usize {
self.len
}
}
impl<'a, T> IntoIterator for &'a mut LinkedList<T> {
type IntoIter = IterMut<'a, T>;
type Item = &'a mut T;
fn into_iter(self) -> Self::IntoIter {
self.iter_mut()
}
}
impl<'a, T> Iterator for IterMut<'a, T> {
type Item = &'a mut T;
fn next(&mut self) -> Option<Self::Item> {
// ここで self.front == self.back をチェックする条件は魅力的だが、
// 最後の要素を yield するには正しく動かない!その手の
// ものが配列でのみ機能するのは、「終端の 1 つ先」のポインタがあるからだ。
if self.len > 0 {
// front を unwrap することもできるが、こちらのほうが安全で簡単
self.front.map(|node| unsafe {
self.len -= 1;
self.front = (*node.as_ptr()).back;
&mut (*node.as_ptr()).elem
})
} else {
None
}
}
fn size_hint(&self) -> (usize, Option<usize>) {
(self.len, Some(self.len))
}
}
impl<'a, T> DoubleEndedIterator for IterMut<'a, T> {
fn next_back(&mut self) -> Option<Self::Item> {
if self.len > 0 {
self.back.map(|node| unsafe {
self.len -= 1;
self.back = (*node.as_ptr()).front;
&mut (*node.as_ptr()).elem
})
} else {
None
}
}
}
impl<'a, T> ExactSizeIterator for IterMut<'a, T> {
fn len(&self) -> usize {
self.len
}
}
impl<T> IntoIterator for LinkedList<T> {
type IntoIter = IntoIter<T>;
type Item = T;
fn into_iter(self) -> Self::IntoIter {
self.into_iter()
}
}
impl<T> Iterator for IntoIter<T> {
type Item = T;
fn next(&mut self) -> Option<Self::Item> {
self.list.pop_front()
}
fn size_hint(&self) -> (usize, Option<usize>) {
(self.list.len, Some(self.list.len))
}
}
impl<T> DoubleEndedIterator for IntoIter<T> {
fn next_back(&mut self) -> Option<Self::Item> {
self.list.pop_back()
}
}
impl<T> ExactSizeIterator for IntoIter<T> {
fn len(&self) -> usize {
self.list.len
}
}
#[cfg(test)]
mod test {
use super::LinkedList;
#[test]
fn test_basic_front() {
let mut list = LinkedList::new();
// 空のリストを壊そうとしてみる
assert_eq!(list.len(), 0);
assert_eq!(list.pop_front(), None);
assert_eq!(list.len(), 0);
// 1 要素のリストを壊そうとしてみる
list.push_front(10);
assert_eq!(list.len(), 1);
assert_eq!(list.pop_front(), Some(10));
assert_eq!(list.len(), 0);
assert_eq!(list.pop_front(), None);
assert_eq!(list.len(), 0);
// いろいろ試す
list.push_front(10);
assert_eq!(list.len(), 1);
list.push_front(20);
assert_eq!(list.len(), 2);
list.push_front(30);
assert_eq!(list.len(), 3);
assert_eq!(list.pop_front(), Some(30));
assert_eq!(list.len(), 2);
list.push_front(40);
assert_eq!(list.len(), 3);
assert_eq!(list.pop_front(), Some(40));
assert_eq!(list.len(), 2);
assert_eq!(list.pop_front(), Some(20));
assert_eq!(list.len(), 1);
assert_eq!(list.pop_front(), Some(10));
assert_eq!(list.len(), 0);
assert_eq!(list.pop_front(), None);
assert_eq!(list.len(), 0);
assert_eq!(list.pop_front(), None);
assert_eq!(list.len(), 0);
}
}
テスト
さて、テストをしばらく先延ばしにしていました。というのも、まあ、私たちはもう Rust の達人で、もうミスなんてしないことはお互い分かっていますからね!それに、これは古いクレートの書き直しなので、テストはすでに全部あります。テストですし、皆さんはもうテストを何度も見てきました。こちらです:
#[cfg(test)]
mod test {
use super::LinkedList;
fn generate_test() -> LinkedList<i32> {
list_from(&[0, 1, 2, 3, 4, 5, 6])
}
fn list_from<T: Clone>(v: &[T]) -> LinkedList<T> {
v.iter().map(|x| (*x).clone()).collect()
}
#[test]
fn test_basic_front() {
let mut list = LinkedList::new();
// 空のリストを壊そうとしてみる
assert_eq!(list.len(), 0);
assert_eq!(list.pop_front(), None);
assert_eq!(list.len(), 0);
// 要素が1つのリストを壊そうとしてみる
list.push_front(10);
assert_eq!(list.len(), 1);
assert_eq!(list.pop_front(), Some(10));
assert_eq!(list.len(), 0);
assert_eq!(list.pop_front(), None);
assert_eq!(list.len(), 0);
// いろいろ試す
list.push_front(10);
assert_eq!(list.len(), 1);
list.push_front(20);
assert_eq!(list.len(), 2);
list.push_front(30);
assert_eq!(list.len(), 3);
assert_eq!(list.pop_front(), Some(30));
assert_eq!(list.len(), 2);
list.push_front(40);
assert_eq!(list.len(), 3);
assert_eq!(list.pop_front(), Some(40));
assert_eq!(list.len(), 2);
assert_eq!(list.pop_front(), Some(20));
assert_eq!(list.len(), 1);
assert_eq!(list.pop_front(), Some(10));
assert_eq!(list.len(), 0);
assert_eq!(list.pop_front(), None);
assert_eq!(list.len(), 0);
assert_eq!(list.pop_front(), None);
assert_eq!(list.len(), 0);
}
#[test]
fn test_basic() {
let mut m = LinkedList::new();
assert_eq!(m.pop_front(), None);
assert_eq!(m.pop_back(), None);
assert_eq!(m.pop_front(), None);
m.push_front(1);
assert_eq!(m.pop_front(), Some(1));
m.push_back(2);
m.push_back(3);
assert_eq!(m.len(), 2);
assert_eq!(m.pop_front(), Some(2));
assert_eq!(m.pop_front(), Some(3));
assert_eq!(m.len(), 0);
assert_eq!(m.pop_front(), None);
m.push_back(1);
m.push_back(3);
m.push_back(5);
m.push_back(7);
assert_eq!(m.pop_front(), Some(1));
let mut n = LinkedList::new();
n.push_front(2);
n.push_front(3);
{
assert_eq!(n.front().unwrap(), &3);
let x = n.front_mut().unwrap();
assert_eq!(*x, 3);
*x = 0;
}
{
assert_eq!(n.back().unwrap(), &2);
let y = n.back_mut().unwrap();
assert_eq!(*y, 2);
*y = 1;
}
assert_eq!(n.pop_front(), Some(0));
assert_eq!(n.pop_front(), Some(1));
}
#[test]
fn test_iterator() {
let m = generate_test();
for (i, elt) in m.iter().enumerate() {
assert_eq!(i as i32, *elt);
}
let mut n = LinkedList::new();
assert_eq!(n.iter().next(), None);
n.push_front(4);
let mut it = n.iter();
assert_eq!(it.size_hint(), (1, Some(1)));
assert_eq!(it.next().unwrap(), &4);
assert_eq!(it.size_hint(), (0, Some(0)));
assert_eq!(it.next(), None);
}
#[test]
fn test_iterator_double_end() {
let mut n = LinkedList::new();
assert_eq!(n.iter().next(), None);
n.push_front(4);
n.push_front(5);
n.push_front(6);
let mut it = n.iter();
assert_eq!(it.size_hint(), (3, Some(3)));
assert_eq!(it.next().unwrap(), &6);
assert_eq!(it.size_hint(), (2, Some(2)));
assert_eq!(it.next_back().unwrap(), &4);
assert_eq!(it.size_hint(), (1, Some(1)));
assert_eq!(it.next_back().unwrap(), &5);
assert_eq!(it.next_back(), None);
assert_eq!(it.next(), None);
}
#[test]
fn test_rev_iter() {
let m = generate_test();
for (i, elt) in m.iter().rev().enumerate() {
assert_eq!(6 - i as i32, *elt);
}
let mut n = LinkedList::new();
assert_eq!(n.iter().rev().next(), None);
n.push_front(4);
let mut it = n.iter().rev();
assert_eq!(it.size_hint(), (1, Some(1)));
assert_eq!(it.next().unwrap(), &4);
assert_eq!(it.size_hint(), (0, Some(0)));
assert_eq!(it.next(), None);
}
#[test]
fn test_mut_iter() {
let mut m = generate_test();
let mut len = m.len();
for (i, elt) in m.iter_mut().enumerate() {
assert_eq!(i as i32, *elt);
len -= 1;
}
assert_eq!(len, 0);
let mut n = LinkedList::new();
assert!(n.iter_mut().next().is_none());
n.push_front(4);
n.push_back(5);
let mut it = n.iter_mut();
assert_eq!(it.size_hint(), (2, Some(2)));
assert!(it.next().is_some());
assert!(it.next().is_some());
assert_eq!(it.size_hint(), (0, Some(0)));
assert!(it.next().is_none());
}
#[test]
fn test_iterator_mut_double_end() {
let mut n = LinkedList::new();
assert!(n.iter_mut().next_back().is_none());
n.push_front(4);
n.push_front(5);
n.push_front(6);
let mut it = n.iter_mut();
assert_eq!(it.size_hint(), (3, Some(3)));
assert_eq!(*it.next().unwrap(), 6);
assert_eq!(it.size_hint(), (2, Some(2)));
assert_eq!(*it.next_back().unwrap(), 4);
assert_eq!(it.size_hint(), (1, Some(1)));
assert_eq!(*it.next_back().unwrap(), 5);
assert!(it.next_back().is_none());
assert!(it.next().is_none());
}
#[test]
fn test_eq() {
let mut n: LinkedList<u8> = list_from(&[]);
let mut m = list_from(&[]);
assert!(n == m);
n.push_front(1);
assert!(n != m);
m.push_back(1);
assert!(n == m);
let n = list_from(&[2, 3, 4]);
let m = list_from(&[1, 2, 3]);
assert!(n != m);
}
#[test]
fn test_ord() {
let n = list_from(&[]);
let m = list_from(&[1, 2, 3]);
assert!(n < m);
assert!(m > n);
assert!(n <= n);
assert!(n >= n);
}
#[test]
fn test_ord_nan() {
let nan = 0.0f64 / 0.0;
let n = list_from(&[nan]);
let m = list_from(&[nan]);
assert!(!(n < m));
assert!(!(n > m));
assert!(!(n <= m));
assert!(!(n >= m));
let n = list_from(&[nan]);
let one = list_from(&[1.0f64]);
assert!(!(n < one));
assert!(!(n > one));
assert!(!(n <= one));
assert!(!(n >= one));
let u = list_from(&[1.0f64, 2.0, nan]);
let v = list_from(&[1.0f64, 2.0, 3.0]);
assert!(!(u < v));
assert!(!(u > v));
assert!(!(u <= v));
assert!(!(u >= v));
let s = list_from(&[1.0f64, 2.0, 4.0, 2.0]);
let t = list_from(&[1.0f64, 2.0, 3.0, 2.0]);
assert!(!(s < t));
assert!(s > one);
assert!(!(s <= one));
assert!(s >= one);
}
#[test]
fn test_debug() {
let list: LinkedList<i32> = (0..10).collect();
assert_eq!(format!("{:?}", list), "[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]");
let list: LinkedList<&str> = vec!["just", "one", "test", "more"]
.iter().copied()
.collect();
assert_eq!(format!("{:?}", list), r#"["just", "one", "test", "more"]"#);
}
#[test]
fn test_hashmap() {
// これをキーとして HashMap が動作することを確認する
let list1: LinkedList<i32> = (0..10).collect();
let list2: LinkedList<i32> = (1..11).collect();
let mut map = std::collections::HashMap::new();
assert_eq!(map.insert(list1.clone(), "list1"), None);
assert_eq!(map.insert(list2.clone(), "list2"), None);
assert_eq!(map.len(), 2);
assert_eq!(map.get(&list1), Some(&"list1"));
assert_eq!(map.get(&list2), Some(&"list2"));
assert_eq!(map.remove(&list1), Some("list1"));
assert_eq!(map.remove(&list2), Some("list2"));
assert!(map.is_empty());
}
}
さて、いよいよ真実の瞬間です:
cargo test
Finished test [unoptimized + debuginfo] target(s) in 0.00s
Running unittests src\lib.rs
running 12 tests
test test::test_basic ... ok
test test::test_basic_front ... ok
test test::test_eq ... ok
test test::test_iterator ... ok
test test::test_iterator_mut_double_end ... ok
test test::test_ord_nan ... ok
test test::test_iterator_double_end ... ok
test test::test_mut_iter ... ok
test test::test_rev_iter ... ok
test test::test_hashmap ... ok
test test::test_ord ... ok
test test::test_debug ... ok
test result: ok. 12 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
$env:MIRIFLAGS="-Zmiri-tag-raw-pointers"
cargo miri test
Compiling linked-list v0.0.3
Finished test [unoptimized + debuginfo] target(s) in 0.35s
Running unittests src\lib.rs
running 12 tests
test test::test_basic ... ok
test test::test_basic_front ... ok
test test::test_debug ... ok
test test::test_eq ... ok
test test::test_hashmap ... ok
test test::test_iterator ... ok
test test::test_iterator_double_end ... ok
test test::test_iterator_mut_double_end ... ok
test test::test_mut_iter ... ok
test test::test_ord ... ok
test test::test_ord_nan ... ok
test test::test_rev_iter ... ok
test result: ok. 12 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
😭
やりました。本当にやらかしていなかったんです。これは引っかけではありません! 私たちの練習と訓練のすべてがついに報われました。とうとう良いコードを書けたんです!!!
さて、厄介ごともすべて片付いたので、面白いことに戻れます!
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
イェーイ!!! システムは動いています! ちゃんと仕事をしてくれるテストがあるのは最高です。迫りくるミスにそこまで怯えなくて済みますからね!
カーソル入門
OK!!! これで std の 1.0 実装に肩を並べる LinkedList ができました! もちろんそれは、私たちの LinkedList がいまだに完全に役立たずだということでもあります。Deque を連結リストとして実装するという途方もない性能ペナルティを受け入れたうえで、それを実際に有用にする API は何も持っていません。
連結リストの「キラーアプリ」に対して、私たちがどうなのかを見てみましょう。
- 🚫 奇妙な intrusive なことができる
- 🚫 奇妙な lockfree なことができる
- 🚫 Dynamically Sized Typesを格納できる
- 🌟 償却なしの O(1) push/pop(malloc が O(1) だと信じる気があるなら)
- 🚫 O(1) のリスト分割
- 🚫 O(1) のリスト結合
まあ…… 6 個中 1 個は…… 何もないよりはマシです! 私がなぜこれを std から引きはがしたかったのか、わかりますか?
私たちのリストに「奇妙な」ものをサポートさせるつもりはありません。あれは全部アドホックでドメイン固有だからです。でも分割と結合、これはできることです!
ただし問題があります。LinkedList の k番目の要素に実際に到達するには O(k) 時間がかかります。では、任意の分割やマージを O(1) で行うことなど、いったいどうして可能なのでしょうか? そのコツは、split_at(index) のような API を持たないことです。ユーザーがリスト内の位置まで状態を持ってイテレートし、その地点で O(1) の変更を行えるような仕組みを作るのです!
おや、イテレータならすでにあります! これを使えるでしょうか? ある程度は…… ですが、それらのスーパーパワーの 1 つが邪魔になります。by-ref イテレータのライフタイムを書き出す方法では、それらが返す参照はイテレータに結び付いていない、ということを覚えているかもしれません。これにより、next を繰り返し呼び出しつつ要素を保持できます。
let mut list = ...;
let iter = list.iter_mut();
let elem1 = list.next();
let elem2 = list.next();
if elem1 == elem2 { ... }
返される参照がイテレータを借用していたら、このコードはまったく動きません。コンパイラは next の 2 回目の呼び出しに文句を言うだけです! この柔軟性は素晴らしいのですが、私たちに暗黙の制約をいくつか課します。
-
By-Mutable-Ref イテレータは、後戻りして同じ要素をもう一度 yield することは決してできません。ユーザーが同じ要素に対する 2 つの
&mutを取得できてしまい、言語の根本的なルールを破ることになるからです。 -
By-Ref イテレータは、すでに yield された参照を無効化するような形で、基底のコレクションを変更し得る追加メソッドを持つことはできません。
残念ながら、これらはどちらも、私たちの LinkedList API にやってほしいことそのものです! つまりイテレータをそのまま使うことはできず、新しいものが必要です。それがCursorsです。
カーソルとは、コンピューターでテキストを編集しているときに表示される小さく点滅する | とまったく同じものです。シーケンス(テキスト)内の位置であり、(矢印キーで)動かすことができ、何か入力するとその地点で編集が行われます。
ほら、もし私がこう
Enter を
押すと
文章全体が
真っ二つに
分かれます。
すみません、あなたは私の後ろに立って、これをタイプしているのを見ていますよね? だから完全に意味が通りますよね? ね。
さて、もし「insert」キー付きのキーボードを使うという不運に見舞われ、実際にそれを押してしまったことがあるなら、カーソルには技術的には 2 つの解釈があることを知っているでしょう。要素(文字)の間にあることも、要素の上にあることもできます。人生で「insert」を意図して押した人など誰もいないと私はかなり確信していますし、あれは純粋に苦痛ボタンとして存在しているのですから、どちらがより良く正しいかは明らかです。カーソルは要素の間に置かれるものです!
かなり盤石な論理ですね。誰も私に異論を唱えられないと思います。
え、何ですって? Rust の LinkedList に Cursors を追加する RFC が 2018 年にあった?
Cursor を使うと、リスト内を前後に移動して現在の要素を取得できます。CursorMut を使うと、前後に移動して要素への可変参照を取得でき、現在の要素の前後に要素を挿入したり削除したりできます(分割や結合など、いくつかのリスト操作も行えます)。
現在の要素? このカーソルは要素の間ではなく、要素の上にあります! 私の完全に盤石な主張を受け入れなかったなんて信じられません! というわけで、std の Cursor を使えばいいですね…… 待って、2022 年で Rust 1.60 でも Cursor はまだ unstable とマークされている?
ちょっと待って:
カーソルは常にリスト内の 2 つの要素の間に位置し、論理的に循環する形でインデックス付けされます。これに対応するため、リストの先頭と末尾の間には None を yield する「ghost」非要素があります。
ちょっと待って。これは RFC が言っていることの逆??? でも待って、メソッドのドキュメントはどれもまだ「current」要素に言及している…… 待てよ、この ghost の話、どこかで見たことがある。ああ、待って、私がプロトタイプを作った昔の linked-list フォークでやったやつじゃなかった?
カーソルは常にリスト内の 2 つの要素の間に位置し、論理的に循環する形でインデックス付けされます。これに対応するため、List の先頭と末尾の間には None を yield する「ghost」非要素があります。
ちょっと待て、何なんだこれ。これは冗談ではなく、私は今本当にドキュメントを読もうとしているんです。std は実際に私が 2015 年に提案したものとは違う設計を RFC にしたのに、その後で私のプロトタイプからドキュメントをコピペしたの??? std は、私が LinkedList をどれほど嫌いかについて本を書いていることに対して、メタクソ投稿で煽ってきてるの????? いやまあ、LinkedList を役立たずでなくすために std に追加させてもらうべく、その概念を示すためにプロトタイプを作ったわけだけど、qu’est-ce que le fuck??????????????
よし、わかりました。明らかに std は私の設計を客観的に優れたものとして祝福しているので、私たちは私の設計で行きます。あと、この章全体は文字どおり私がそのライブラリをゼロから書き直しているものなので、API を変えないというのは私としても良さそうです!
私が書いた最上位ドキュメント全文は次のとおりです。
Cursor はイテレータに似ていますが、前後へ自由に移動でき、イテレーション中に安全にリストを変更できます。これは、yield する参照のライフタイムが、基底のリストだけではなく、Cursor 自身のライフタイムに結び付いているためです。つまり、Cursor は複数の要素を同時に yield できません。
カーソルは常にリスト内の 2 つの要素の間に位置し、論理的に循環する形でインデックス付けされます。これに対応するため、List の先頭と末尾の間には None を yield する「ghost」非要素があります。
作成されたとき、カーソルは ghost とリストの先頭の間から開始します。つまり、next はリストの先頭を yield し、prev は None を yield します。もう一度 prev を呼び出すと末尾を yield します。
かわいいですね。「sentinel-node」全体は割に合わないほど面倒だという結論に至ったにもかかわらず、結局は、カーソルがリストの反対側に回り込めるように sentinel node があるかのように「見せかける」セマンティクスを持つことになります。
昔の API をもう少し流し読みする
fn splice(&mut self, other: &mut LinkedList<T>)
カーソルの直後にリスト全体の内容を挿入します。
ああそうだ、思い出してきた。これは、組合せ爆発に本気で腹を立てていたときに書いたもので、各操作のコピーが 1 つだけで済むような方法を考え出そうとしていたんだ。残念ながら、これは……意味論的に問題がある。というのも、ユーザーがあるリストを別のリストにスプライスしたいとき、カーソルがスプライスの前に来てほしい場合もあれば、後に来てほしい場合もある。挿入されるリストは任意の大きさになり得るので、片方だけを許して、挿入されたリスト全体をユーザーに走査させることを期待するのは、本当に問題になる!
結局、この設計は一からやり直さないといけない。Cursor 型には何が必要だろう? そうだな、必要なのは次のことだ。
- 2 つの要素の「間」を指す
- ちょっとした便利機能として、次の「インデックス」が何かを追跡する
- リスト自体を更新して front/back/len を変更する。
2 つの要素の間をどうやって指すのか? まあ、指さない。ただ「次の」要素を指すだけだ。つまり、そう、外には「カーソルは間に入る」という意味論を公開しているけれど、実際には「カーソルは上にある」として実装し、その位置の前または後であらゆることが起きるふりをしているだけだ。
でもこれには理由がある! スプライスのユースケースでは、ユーザーがリストの前に最終的にいるか後に最終的にいるかを選べるようにしたいのだけれど、これは……std API で表現するにはひどく複雑だ! splice_after と splice_before はあるけれど、どちらもカーソルの位置を変えないので、実際には splice_after_before と splice_after_after が必要になる……
いや待て、バカなことを言っている。std API では、最終的に乗りたいノードを選んで、それに応じて splice_after/splice_before を使えばいいだけだ。
目を細める
待て、std API って実は良いのか。
コードをざっと読む
OK、std API は実際に良い。
よし、もういい、RFC を実装する。少なくとも、その面白い部分は実装する。
std が使っている用語にはいくつか引っかかるところがあるけれど、カーソルというものは常にちょっと頭が溶けるものだ。iter().next_back() は back() を返してくれるので、そこまではよい。でもその後の各 next_back() は実際にはfront に近づいているのであり、実際、たどっているポインタはすべて「front」ポインタなのだ! この一見パラドックスのようなものについて考えすぎると頭が痛くなるので、これを避けるために別の用語を選ぶことは確かに理解できる。
std API では、「before」(front 方向)と「after」(back 方向)の前に対する操作について語っており、next と next_back の代わりに……move_next と move_prev と呼んでいる。うーん。つまり、少しイテレータ用語に寄せているわけだが、少なくとも next は front/back を想起させないし、イテレータと比べて物事がどう振る舞うかを把握する助けにはなる。
これならやっていける。
カーソルの実装
さて、不変版は実際には面白くないので、std の CursorMut だけを扱うことにします。元の設計と同じように、リストの先頭/末尾を示す None を含む「ゴースト」要素があり、それを「通り越す」ことでリストの反対側に回り込めます。これを実装するには、次のものが必要です。
- 現在のノードへのポインタ
- リストへのポインタ
- 現在のインデックス
「ゴースト」を指しているとき、インデックスはどうなるのでしょうか?
眉をひそめる … std を確認する … std の答えが気に入らない
さて、かなり妥当なことに、Cursor の index は Option<usize> を返します。std の実装は、それを Option として保持するのを避けるためにいろいろと余計なことをしていますが……こちらは連結リストなので、問題ありません。また std には cursor_front/cursor_back というものがあり、カーソルを先頭/末尾要素から開始します。これは直感的に感じますが、その結果、リストが空のときに奇妙なことをしなければならなくなります。
必要ならそのあたりを実装してもかまいませんが、私は反復的なごちゃごちゃした処理やコーナーケースを減らして、ゴーストから開始する素の cursor_mut メソッドだけを作ることにします。利用者は move_next/move_prev を使って欲しい位置へ移動できます(本当に必要なら、それを cursor_front として包むこともできます)。
始めましょう。
pub struct CursorMut<'a, T> {
cur: Link<T>,
list: &'a mut LinkedList<T>,
index: Option<usize>,
}
かなり分かりやすいですね。箇条書きの各項目に対応するフィールドが 1 つずつあります!では cursor_mut メソッドです。
impl<T> LinkedList<T> {
pub fn cursor_mut(&mut self) -> CursorMut<T> {
CursorMut {
list: self,
cur: None,
index: None,
}
}
}
ゴーストから開始するので、すべて None で始めればよく、簡潔です!次は移動です。
impl<'a, T> CursorMut<'a, T> {
pub fn index(&self) -> Option<usize> {
self.index
}
pub fn move_next(&mut self) {
if let Some(cur) = self.cur {
unsafe {
// 実際の要素上にいるので、その次(back)へ進む
self.cur = (*cur.as_ptr()).back;
if self.cur.is_some() {
*self.index.as_mut().unwrap() += 1;
} else {
// ゴーストへ到達したので、もうインデックスはない
self.index = None;
}
}
} else if !self.list.is_empty() {
// ゴーストにいて、実際の front があるので、そこへ移動する!
self.cur = self.list.front;
self.index = Some(0)
} else {
// ゴーストにいるが、それが唯一の要素……何もしない。
}
}
}
つまり、興味深いケースは 4 つあります。
- 通常ケース
- 通常ケースだが、ゴーストに到達する場合
- ゴーストケースで、リストの先頭へ移動する場合
- ゴーストケースだが、リストが空なので何もしない場合
move_prev はまったく同じロジックですが、front/back が逆になり、インデックスの変更も逆になります。
pub fn move_prev(&mut self) {
if let Some(cur) = self.cur {
unsafe {
// 実際の要素上にいるので、その前(front)へ進む
self.cur = (*cur.as_ptr()).front;
if self.cur.is_some() {
*self.index.as_mut().unwrap() -= 1;
} else {
// ゴーストへ到達したので、もうインデックスはない
self.index = None;
}
}
} else if !self.list.is_empty() {
// ゴーストにいて、実際の back があるので、そこへ移動する!
self.cur = self.list.back;
self.index = Some(self.list.len - 1)
} else {
// ゴーストにいるが、それが唯一の要素……何もしない。
}
}
次に、カーソルの周囲の要素を見るためのメソッド、current、peek_next、peek_prev を追加しましょう。非常に重要な注意: これらのメソッドは、カーソルを &mut self で借用しなければならず、結果はその借用に結び付いていなければなりません。ユーザーに可変参照のコピーを複数取得させることはできませんし、そのような参照を保持している間に insert/remove/split/splice API を使わせることもできません!
ありがたいことに、ライフタイム省略を使ったときに Rust がデフォルトで仮定するのはこれなので、デフォルトで正しいことをするだけです!
pub fn current(&mut self) -> Option<&mut T> {
unsafe {
self.cur.map(|node| &mut (*node.as_ptr()).elem)
}
}
pub fn peek_next(&mut self) -> Option<&mut T> {
unsafe {
self.cur
.and_then(|node| (*node.as_ptr()).back)
.map(|node| &mut (*node.as_ptr()).elem)
}
}
pub fn peek_prev(&mut self) -> Option<&mut T> {
unsafe {
self.cur
.and_then(|node| (*node.as_ptr()).front)
.map(|node| &mut (*node.as_ptr()).elem)
}
}
頭を空にして、Option メソッドと(省略された)コンパイラエラーがすべて考えてくれるようになりました。Option<NonNull> のあれこれには懐疑的でしたが、くそっ、本当にこのコードを自動操縦で書けるようにしてくれます。Option をまったく使えない配列ベースのコレクションを書きすぎていました。いやあ、これは素晴らしいですね!((*node.as_ptr()) は相変わらずひどいですが、それは Rust の生ポインタというものです……)
次に選択肢があります。これらの API の要点である split と splice にそのまま進むか、単一要素の insert/remove で小さな一歩を踏むかです。insert/remove は結局 split と splice の観点で実装したくなる気がするので……先にそちらをやって、どう転ぶか見てみましょう(これを書いている時点では本当に分かっていません)。
Split
まずは split_before と split_after です。これらは現在の要素の前/後ろにあるすべてを LinkedList として返します(ゴースト要素で停止します。ただし、ゴースト上にいる場合はリスト全体を返し、カーソルは空のリストを指すようになります)。
目を細める うーん、これは実際にはそれなりに自明でないロジックなので、1 ステップずつ話していく必要があります。
split_before について、興味深そうなケースは 4 つあると思います。
- 通常ケース
- 通常ケースだが、prev がゴーストの場合
- ゴーストケースで、リスト全体を返し、自分は空になる場合
- ゴーストケースだが、リストが空なので、何もせず空のリストを返す場合
まずコーナーケースから始めましょう。3 番目のケースは、単にこれだと思います。
#![allow(unused)]
fn main() {
mem::replace(self.list, LinkedList::new())
}
ですよね?こちらは空になり、リスト全体を返し、フィールドはすでに None なので更新するものはありません。いいですね。おっと、これは 4 番目のケースでも正しいことをしてくれます!
では通常ケースです……よし、これには ASCII 図が必要です。最も一般的なケースでは、次のようなものがあります。
list.front -> A <-> B <-> C <-> D <- list.back
^
cur
そして、こうしたいわけです。
list.front -> C <-> D <- list.back
^
cur
return.front -> A <-> B <- return.back
なので、cur と prev の間のリンクを切る必要があります。そして……ああ、ものすごく多くの変更が必要です。よし、自分で筋が通っていると納得できるように、これを手順に分解する必要があります。少し過剰に冗長になりますが、少なくとも自分には理解できます。
pub fn split_before(&mut self) -> LinkedList<T> {
if let Some(cur) = self.cur {
// 実在する要素を指しているので、リストは空ではない。
unsafe {
// 現在の状態
let old_len = self.list.len;
let old_idx = self.index.unwrap();
let prev = (*cur.as_ptr()).front;
// self がどうなるか
let new_len = old_len - old_idx;
let new_front = self.cur;
let new_back = self.list.back;
let new_idx = Some(0);
// 出力がどうなるか
let output_len = old_len - new_len;
let output_front = self.list.front;
let output_back = prev;
// cur と prev の間のリンクを切る
if let Some(prev) = prev {
(*cur.as_ptr()).front = None;
(*prev.as_ptr()).back = None;
}
// 結果を生成する:
self.list.len = new_len;
self.list.front = new_front;
self.list.back = new_back;
self.index = new_idx;
LinkedList {
front: output_front,
back: output_back,
len: output_len,
_boo: PhantomData,
}
}
} else {
// ゴースト位置にいるので、自分たちのリストを空のものと置き換えるだけ。
// 他の状態を変更する必要はない。
std::mem::replace(self.list, LinkedList::new())
}
}
この if-let は、「通常のケースだが、prev がゴーストである」状況を扱っていることに注意してください。
if let Some(prev) = prev {
(*cur.as_ptr()).front = None;
(*prev.as_ptr()).back = None;
}
あなたが望むなら、これらを全部まとめて、次のような最適化を適用できます。
(*cur.as_ptr()).frontへの 2 回のアクセスを、単に(*cur.as_ptr()).front.take()として畳み込む- new_back は noop であることに注目して、両方とも削除する
私にわかる限り、それ以外はすべて、別の理由でたまたま正しいことをしています。テストを書くときにわかるでしょう!(copy-paste して split_after を作る)
私はもうミスをするのにうんざりしたので、できるだけ間違えようのないコードを書こうとするだけにします。これが、私が実際にコレクションを書く方法です。つまり、物事を些細な手順とケースに分解して、自分の頭に収まり、間違えようがないと思えるまで続けます。それから、自分がそれでもなお失敗していないと納得できるまで、大量のテストを書きます。
私がやってきたコレクション関連の作業の大半は極めて unsafe なので、コンパイラがミスを捕まえてくれることに頼ることは普通できませんし、昔は miri も存在していませんでした! だから、頭が痛くなるまで問題を凝視して、絶対に絶対に絶対にミスをしないように全力を尽くす必要があります。
Unsafe Rust Code を書いてはいけません! Safe Rust のほうがずっと良いです!!!!
Splice
あともう 1 体だけボスが残っています。splice_before と splice_after です。これが全部の中でいちばんコーナーケースが簡単なものになると予想しています。この 2 つの関数は LinkedList を受け取り、その内容をこちらのリストに接ぎ木します。こちらのリストが空かもしれないし、相手のリストが空かもしれないし、ゴーストも扱わなければなりません……はあ、splice_before で一歩ずつ進めましょう。
- 相手のリストが空なら、何もする必要はありません。
- こちらのリストが空なら、こちらのリストは単に相手のリストになります。
- ゴーストを指しているなら、これは末尾に追加します(list.back を変更)
- 最初の要素 (0) を指しているなら、これは先頭に追加します(list.front を変更)
- 一般的なケースでは、非常に多くのポインタのクソ面倒な操作を行います。
一般的なケースはこれです。
input.front -> 1 <-> 2 <- input.back
list.front -> A <-> B <-> C <- list.back
^
cur
これがこうなります。
list.front -> A <-> 1 <-> 2 <-> B <-> C <- list.back
いいですか? よし。これを書き出してみましょう……大きく息を吸い込み、飛び込む:
pub fn splice_before(&mut self, mut input: LinkedList<T>) {
unsafe {
if input.is_empty() {
// 入力が空なら、何もしない。
} else if let Some(cur) = self.cur {
if let Some(0) = self.index {
// 先頭に追加している。末尾への追加を参照
(*cur.as_ptr()).front = input.back.take();
(*input.back.unwrap().as_ptr()).back = Some(cur);
self.list.front = input.front.take();
// index は入力の長さだけ前に進む
*self.index.as_mut().unwrap() += input.len;
self.list.len += input.len;
input.len = 0;
} else {
// 一般的なケース。境界はなく、内部の修正だけ
let prev = (*cur.as_ptr()).front.unwrap();
let in_front = input.front.take().unwrap();
let in_back = input.back.take().unwrap();
(*prev.as_ptr()).back = Some(in_front);
(*in_front.as_ptr()).front = Some(prev);
(*cur.as_ptr()).front = Some(in_back);
(*in_back.as_ptr()).back = Some(cur);
// index は入力の長さだけ前に進む
*self.index.as_mut().unwrap() += input.len;
self.list.len += input.len;
input.len = 0;
}
} else if let Some(back) = self.list.back {
// ゴースト上にいるが空ではないので、末尾に追加する
// 入力のポインタを `take` することも、`mem::forget` することもできる
// カスタムアロケータか、クリーンアップが必要な何かを使う場合に備えて、
// take を使うほうがより責任あるやり方!
(*back.as_ptr()).back = input.front.take();
(*input.front.unwrap().as_ptr()).front = Some(back);
self.list.back = input.back.take();
self.list.len += input.len;
// 必要ではないが、やっておくと礼儀正しい
input.len = 0;
} else {
// 空なので、入力になり、ゴースト上にとどまる
*self.list = input;
}
}
}
よし、これは本当にひどいもので、今やまさに Option<NonNull> のつらさを感じています。しかし、できるクリーンアップはたくさんあります。まず、このコードは最後まで引き出せます。常にそれを行いたいからです。私はこれを気に入っているわけではありません(もっとも、noop になることもありますし、input.len を設定するのは、今後のコード拡張に対するパラノイアのようなものです)。
self.list.len += input.len;
input.len = 0;
move された値の使用:
input
ああ、そうでした。「こちらが空」のケースではリストを move しています。これを swap に置き換えましょう。
// 空なので、入力になり、ゴースト上にとどまる
std::mem::swap(self.list, &mut input);
この場合、書き込みは無意味になりますが、それでも動作します(おそらく、この分岐で早期リターンしてコンパイラをなだめることもできるでしょう)。
この unwrap は、私がケースを逆向きに考えていた結果にすぎず、if-let に正しい質問をさせることで修正できます。
if let Some(0) = self.index {
} else {
let prev = (*cur.as_ptr()).front.unwrap();
}
インデックスの調整は分岐内で重複しているので、外に引き上げることもできます。
#![allow(unused)]
fn main() {
*self.index.as_mut().unwrap() += input.len;
}
よし、これらをすべてまとめるとこうなります。
#![allow(unused)]
fn main() {
if input.is_empty() {
// 入力は空なので、何もしない。
} else if let Some(cur) = self.cur {
// 両方のリストが空ではない
if let Some(prev) = (*cur.as_ptr()).front {
// 一般ケース、境界はなく、内部の修正だけ
let in_front = input.front.take().unwrap();
let in_back = input.back.take().unwrap();
(*prev.as_ptr()).back = Some(in_front);
(*in_front.as_ptr()).front = Some(prev);
(*cur.as_ptr()).front = Some(in_back);
(*in_back.as_ptr()).back = Some(cur);
} else {
// 前方に追加している。下の後方への追加を参照
(*cur.as_ptr()).front = input.back.take();
(*input.back.unwrap().as_ptr()).back = Some(cur);
self.list.front = input.front.take();
}
// インデックスは入力の長さだけ前方に移動する
*self.index.as_mut().unwrap() += input.len;
} else if let Some(back) = self.list.back {
// ゴースト上にいるが空ではないので、後方に追加する
// 入力のポインタを `take` することも、`mem::forget` することも
// できる。カスタムアロケータなど、クリーンアップが必要なものも
// 扱う場合に備えると、take を使う方が責任あるやり方だ!
(*back.as_ptr()).back = input.front.take();
(*input.front.unwrap().as_ptr()).front = Some(back);
self.list.back = input.back.take();
} else {
// 空なので、入力そのものになり、ゴースト上にとどまる
std::mem::swap(self.list, &mut input);
}
self.list.len += input.len;
// 必須ではないが、やっておくのが礼儀
input.len = 0;
// 入力はここで drop される
}
よし、これはまだひどいですが、主な理由は――いや待って、バグを見つけました。
#![allow(unused)]
fn main() {
(*back.as_ptr()).back = input.front.take();
(*input.front.unwrap().as_ptr()).front = Some(back);
}
input.front を take してから、次の行でそれを unwrap しています! ため息 しかも、対応するミラーケースでも同じことをしています。テストではこれを即座に捕まえられたでしょうが、今は完璧であろうとしていて、私はこれをほとんどライブでやっており、まさにこの瞬間にそれに気づいたのです。普段のように面倒くさいくらい段階を踏んでやらなかった報いですね。もっと明示的に!
#![allow(unused)]
fn main() {
// 入力のポインタを `take` することも、`mem::forget` することも
// できる。将来カスタムアロケータなど、クリーンアップも必要なものを
// 扱う場合に備えると、`take` を使う方が責任あるやり方だ!
if input.is_empty() {
// 入力は空なので、何もしない。
} else if let Some(cur) = self.cur {
// 両方のリストが空ではない
let in_front = input.front.take().unwrap();
let in_back = input.back.take().unwrap();
if let Some(prev) = (*cur.as_ptr()).front {
// 一般ケース、境界はなく、内部の修正だけ
(*prev.as_ptr()).back = Some(in_front);
(*in_front.as_ptr()).front = Some(prev);
(*cur.as_ptr()).front = Some(in_back);
(*in_back.as_ptr()).back = Some(cur);
} else {
// prev がないので、前方に追加している
(*cur.as_ptr()).front = Some(in_back);
(*in_back.as_ptr()).back = Some(cur);
self.list.front = Some(in_front);
}
// インデックスは入力の長さだけ前方に移動する
*self.index.as_mut().unwrap() += input.len;
} else if let Some(back) = self.list.back {
// ゴースト上にいるが空ではないので、後方に追加する
let in_front = input.front.take().unwrap();
let in_back = input.back.take().unwrap();
(*back.as_ptr()).back = Some(in_front);
(*in_front.as_ptr()).front = Some(back);
self.list.back = Some(in_back);
} else {
// 空なので、入力そのものになり、ゴースト上にとどまる
std::mem::swap(self.list, &mut input);
}
self.list.len += input.len;
// 必須ではないが、やっておくのが礼儀
input.len = 0;
// 入力はここで drop される
}
よし、今度のこれは、許容できます。唯一の不満は、in_front/in_back の重複を排除していないことです(おそらく条件を組み替えればできるでしょうが、まあいいでしょう)。実際これは、Option<NonNull> の厄介なもののせいで面倒になっているだけで、基本的には C で書くものと同じです。これなら受け入れられます。いや、やはりこの手のものについては raw pointer をもっと使いやすくするべきですね。ただし、それはこの本の範囲外です。
ともあれ、その後で完全に疲れ果てたので、insert や remove やその他すべての API は読者への練習問題として残しておけます。
以下は、組み合わせ論をコピペしようと試みた Cursor の最終コードです。正しくできているでしょうか? 次の章を書いてこの怪物をテストするときに初めて分かります!
pub struct CursorMut<'a, T> {
list: &'a mut LinkedList<T>,
cur: Link<T>,
index: Option<usize>,
}
impl<T> LinkedList<T> {
pub fn cursor_mut(&mut self) -> CursorMut<T> {
CursorMut {
list: self,
cur: None,
index: None,
}
}
}
impl<'a, T> CursorMut<'a, T> {
pub fn index(&self) -> Option<usize> {
self.index
}
pub fn move_next(&mut self) {
if let Some(cur) = self.cur {
unsafe {
// 実要素上にいるので、その次(back)へ進む
self.cur = (*cur.as_ptr()).back;
if self.cur.is_some() {
*self.index.as_mut().unwrap() += 1;
} else {
// ゴーストへ進んだところなので、もう index はない
self.index = None;
}
}
} else if !self.list.is_empty() {
// ゴーストにいて、実際の front があるので、そこへ移動する!
self.cur = self.list.front;
self.index = Some(0)
} else {
// ゴーストにいるが、それが唯一の要素である... 何もしない。
}
}
pub fn move_prev(&mut self) {
if let Some(cur) = self.cur {
unsafe {
// 実要素上にいるので、その前(front)へ進む
self.cur = (*cur.as_ptr()).front;
if self.cur.is_some() {
*self.index.as_mut().unwrap() -= 1;
} else {
// ゴーストへ進んだところなので、もう index はない
self.index = None;
}
}
} else if !self.list.is_empty() {
// ゴーストにいて、実際の back があるので、そこへ移動する!
self.cur = self.list.back;
self.index = Some(self.list.len - 1)
} else {
// ゴーストにいるが、それが唯一の要素である... 何もしない。
}
}
pub fn current(&mut self) -> Option<&mut T> {
unsafe {
self.cur.map(|node| &mut (*node.as_ptr()).elem)
}
}
pub fn peek_next(&mut self) -> Option<&mut T> {
unsafe {
self.cur
.and_then(|node| (*node.as_ptr()).back)
.map(|node| &mut (*node.as_ptr()).elem)
}
}
pub fn peek_prev(&mut self) -> Option<&mut T> {
unsafe {
self.cur
.and_then(|node| (*node.as_ptr()).front)
.map(|node| &mut (*node.as_ptr()).elem)
}
}
pub fn split_before(&mut self) -> LinkedList<T> {
// これは次の状態:
//
// list.front -> A <-> B <-> C <-> D <- list.back
// ^
// cur
//
//
// そしてこれを生成したい:
//
// list.front -> C <-> D <- list.back
// ^
// cur
//
//
// return.front -> A <-> B <- return.back
//
if let Some(cur) = self.cur {
// 実要素を指しているので、リストは空ではない。
unsafe {
// 現在の状態
let old_len = self.list.len;
let old_idx = self.index.unwrap();
let prev = (*cur.as_ptr()).front;
// self がどうなるか
let new_len = old_len - old_idx;
let new_front = self.cur;
let new_back = self.list.back;
let new_idx = Some(0);
// 出力がどうなるか
let output_len = old_len - new_len;
let output_front = self.list.front;
let output_back = prev;
// cur と prev の間のリンクを切る
if let Some(prev) = prev {
(*cur.as_ptr()).front = None;
(*prev.as_ptr()).back = None;
}
// 結果を生成する:
self.list.len = new_len;
self.list.front = new_front;
self.list.back = new_back;
self.index = new_idx;
LinkedList {
front: output_front,
back: output_back,
len: output_len,
_boo: PhantomData,
}
}
} else {
// ゴーストにいるので、自分のリストを空のものに置き換えるだけ。
// 他の状態を変更する必要はない。
std::mem::replace(self.list, LinkedList::new())
}
}
pub fn split_after(&mut self) -> LinkedList<T> {
// これは次の状態:
//
// list.front -> A <-> B <-> C <-> D <- list.back
// ^
// cur
//
//
// そしてこれを生成したい:
//
// list.front -> A <-> B <- list.back
// ^
// cur
//
//
// return.front -> C <-> D <- return.back
//
if let Some(cur) = self.cur {
// 実要素を指しているので、リストは空ではない。
unsafe {
// 現在の状態
let old_len = self.list.len;
let old_idx = self.index.unwrap();
let next = (*cur.as_ptr()).back;
// self がどうなるか
let new_len = old_idx + 1;
let new_back = self.cur;
let new_front = self.list.front;
let new_idx = Some(old_idx);
// 出力がどうなるか
let output_len = old_len - new_len;
let output_front = next;
let output_back = self.list.back;
// cur と next の間のリンクを切る
if let Some(next) = next {
(*cur.as_ptr()).back = None;
(*next.as_ptr()).front = None;
}
// 結果を生成する:
self.list.len = new_len;
self.list.front = new_front;
self.list.back = new_back;
self.index = new_idx;
LinkedList {
front: output_front,
back: output_back,
len: output_len,
_boo: PhantomData,
}
}
} else {
// ゴーストにいるので、自分のリストを空のものに置き換えるだけ。
// 他の状態を変更する必要はない。
std::mem::replace(self.list, LinkedList::new())
}
}
pub fn splice_before(&mut self, mut input: LinkedList<T>) {
// これは次の状態:
//
// input.front -> 1 <-> 2 <- input.back
//
// list.front -> A <-> B <-> C <- list.back
// ^
// cur
//
//
// これになる:
//
// list.front -> A <-> 1 <-> 2 <-> B <-> C <- list.back
// ^
// cur
//
unsafe {
// input のポインタを `take` するか、`mem::forget`
// するかのどちらかができる。将来、カスタムアロケータなど、
// クリーンアップが必要なものを使う場合に備えて、`take` を使う方が責任ある対応だ!
if input.is_empty() {
// input は空なので、何もしない。
} else if let Some(cur) = self.cur {
// 両方のリストが空ではない
let in_front = input.front.take().unwrap();
let in_back = input.back.take().unwrap();
if let Some(prev) = (*cur.as_ptr()).front {
// 一般的なケース。境界はなく、内部の修正だけを行う
(*prev.as_ptr()).back = Some(in_front);
(*in_front.as_ptr()).front = Some(prev);
(*cur.as_ptr()).front = Some(in_back);
(*in_back.as_ptr()).back = Some(cur);
} else {
// prev がないので、front に追加している
(*cur.as_ptr()).front = Some(in_back);
(*in_back.as_ptr()).back = Some(cur);
self.list.front = Some(in_front);
}
// index は input の長さだけ前方へ移動する
*self.index.as_mut().unwrap() += input.len;
} else if let Some(back) = self.list.back {
// ゴースト上にいるが空ではないので、back に追加する
let in_front = input.front.take().unwrap();
let in_back = input.back.take().unwrap();
(*back.as_ptr()).back = Some(in_front);
(*in_front.as_ptr()).front = Some(back);
self.list.back = Some(in_back);
} else {
// 空なので、input になり、ゴースト上に留まる
std::mem::swap(self.list, &mut input);
}
self.list.len += input.len;
// 必須ではないが、やっておくのが礼儀
input.len = 0;
// ここで input がドロップされる
}
}
pub fn splice_after(&mut self, mut input: LinkedList<T>) {
// これは次の状態:
//
// input.front -> 1 <-> 2 <- input.back
//
// list.front -> A <-> B <-> C <- list.back
// ^
// cur
//
//
// これになる:
//
// list.front -> A <-> B <-> 1 <-> 2 <-> C <- list.back
// ^
// cur
//
unsafe {
// input のポインタを `take` するか、`mem::forget`
// するかのどちらかができる。将来、カスタムアロケータなど、
// クリーンアップが必要なものを使う場合に備えて、`take` を使う方が責任ある対応だ!
if input.is_empty() {
// input は空なので、何もしない。
} else if let Some(cur) = self.cur {
// 両方のリストが空ではない
let in_front = input.front.take().unwrap();
let in_back = input.back.take().unwrap();
if let Some(next) = (*cur.as_ptr()).back {
// 一般的なケース。境界はなく、内部の修正だけを行う
(*next.as_ptr()).front = Some(in_back);
(*in_back.as_ptr()).back = Some(next);
(*cur.as_ptr()).back = Some(in_front);
(*in_front.as_ptr()).front = Some(cur);
} else {
// next がないので、back に追加している
(*cur.as_ptr()).back = Some(in_front);
(*in_front.as_ptr()).front = Some(cur);
self.list.back = Some(in_back);
}
// index は変化しない
} else if let Some(front) = self.list.front {
// ゴースト上にいるが空ではないので、front に追加する
let in_front = input.front.take().unwrap();
let in_back = input.back.take().unwrap();
(*front.as_ptr()).front = Some(in_back);
(*in_back.as_ptr()).back = Some(front);
self.list.front = Some(in_front);
} else {
// 空なので、input になり、ゴースト上に留まる
std::mem::swap(self.list, &mut input);
}
self.list.len += input.len;
// 必須ではないが、やっておくのが礼儀
input.len = 0;
// ここで input がドロップされる
}
}
}
カーソルのテスト
前のセクションで私がどれだけひどく恥ずかしい間違いを犯したのかを確認する時間です!
ああ、なんてことだ。API が std と古い実装のどちらとも違うものになってしまいました。まあいいでしょう、両方から適当に急いで寄せ集めることにします。そうですね、std からこれらのテストを「借りる」ことにしましょう。
#[test]
fn test_cursor_move_peek() {
let mut m: LinkedList<u32> = LinkedList::new();
m.extend([1, 2, 3, 4, 5, 6]);
let mut cursor = m.cursor_mut();
cursor.move_next();
assert_eq!(cursor.current(), Some(&mut 1));
assert_eq!(cursor.peek_next(), Some(&mut 2));
assert_eq!(cursor.peek_prev(), None);
assert_eq!(cursor.index(), Some(0));
cursor.move_prev();
assert_eq!(cursor.current(), None);
assert_eq!(cursor.peek_next(), Some(&mut 1));
assert_eq!(cursor.peek_prev(), Some(&mut 6));
assert_eq!(cursor.index(), None);
cursor.move_next();
cursor.move_next();
assert_eq!(cursor.current(), Some(&mut 2));
assert_eq!(cursor.peek_next(), Some(&mut 3));
assert_eq!(cursor.peek_prev(), Some(&mut 1));
assert_eq!(cursor.index(), Some(1));
let mut cursor = m.cursor_mut();
cursor.move_prev();
assert_eq!(cursor.current(), Some(&mut 6));
assert_eq!(cursor.peek_next(), None);
assert_eq!(cursor.peek_prev(), Some(&mut 5));
assert_eq!(cursor.index(), Some(5));
cursor.move_next();
assert_eq!(cursor.current(), None);
assert_eq!(cursor.peek_next(), Some(&mut 1));
assert_eq!(cursor.peek_prev(), Some(&mut 6));
assert_eq!(cursor.index(), None);
cursor.move_prev();
cursor.move_prev();
assert_eq!(cursor.current(), Some(&mut 5));
assert_eq!(cursor.peek_next(), Some(&mut 6));
assert_eq!(cursor.peek_prev(), Some(&mut 4));
assert_eq!(cursor.index(), Some(4));
}
#[test]
fn test_cursor_mut_insert() {
let mut m: LinkedList<u32> = LinkedList::new();
m.extend([1, 2, 3, 4, 5, 6]);
let mut cursor = m.cursor_mut();
cursor.move_next();
cursor.splice_before(Some(7).into_iter().collect());
cursor.splice_after(Some(8).into_iter().collect());
// check_links(&m);
assert_eq!(m.iter().cloned().collect::<Vec<_>>(), &[7, 1, 8, 2, 3, 4, 5, 6]);
let mut cursor = m.cursor_mut();
cursor.move_next();
cursor.move_prev();
cursor.splice_before(Some(9).into_iter().collect());
cursor.splice_after(Some(10).into_iter().collect());
check_links(&m);
assert_eq!(m.iter().cloned().collect::<Vec<_>>(), &[10, 7, 1, 8, 2, 3, 4, 5, 6, 9]);
/* remove_current は実装されていない
let mut cursor = m.cursor_mut();
cursor.move_next();
cursor.move_prev();
assert_eq!(cursor.remove_current(), None);
cursor.move_next();
cursor.move_next();
assert_eq!(cursor.remove_current(), Some(7));
cursor.move_prev();
cursor.move_prev();
cursor.move_prev();
assert_eq!(cursor.remove_current(), Some(9));
cursor.move_next();
assert_eq!(cursor.remove_current(), Some(10));
check_links(&m);
assert_eq!(m.iter().cloned().collect::<Vec<_>>(), &[1, 8, 2, 3, 4, 5, 6]);
*/
let mut cursor = m.cursor_mut();
cursor.move_next();
let mut p: LinkedList<u32> = LinkedList::new();
p.extend([100, 101, 102, 103]);
let mut q: LinkedList<u32> = LinkedList::new();
q.extend([200, 201, 202, 203]);
cursor.splice_after(p);
cursor.splice_before(q);
check_links(&m);
assert_eq!(
m.iter().cloned().collect::<Vec<_>>(),
&[200, 201, 202, 203, 1, 100, 101, 102, 103, 8, 2, 3, 4, 5, 6]
);
let mut cursor = m.cursor_mut();
cursor.move_next();
cursor.move_prev();
let tmp = cursor.split_before();
assert_eq!(m.into_iter().collect::<Vec<_>>(), &[]);
m = tmp;
let mut cursor = m.cursor_mut();
cursor.move_next();
cursor.move_next();
cursor.move_next();
cursor.move_next();
cursor.move_next();
cursor.move_next();
cursor.move_next();
let tmp = cursor.split_after();
assert_eq!(tmp.into_iter().collect::<Vec<_>>(), &[102, 103, 8, 2, 3, 4, 5, 6]);
check_links(&m);
assert_eq!(m.iter().cloned().collect::<Vec<_>>(), &[200, 201, 202, 203, 1, 100, 101]);
}
fn check_links<T>(_list: &LinkedList<T>) {
// これを行うとよい!
}
真実の瞬間です!
cargo test
Compiling linked-list v0.0.3
Finished test [unoptimized + debuginfo] target(s) in 1.03s
Running unittests src\lib.rs
running 14 tests
test test::test_basic_front ... ok
test test::test_basic ... ok
test test::test_debug ... ok
test test::test_iterator_mut_double_end ... ok
test test::test_ord ... ok
test test::test_cursor_move_peek ... FAILED
test test::test_cursor_mut_insert ... FAILED
test test::test_iterator ... ok
test test::test_mut_iter ... ok
test test::test_eq ... ok
test test::test_rev_iter ... ok
test test::test_iterator_double_end ... ok
test test::test_hashmap ... ok
test test::test_ord_nan ... ok
failures:
---- test::test_cursor_move_peek stdout ----
thread 'test::test_cursor_move_peek' panicked at 'assertion failed: `(left == right)`
left: `None`,
right: `Some(1)`', src\lib.rs:1079:9
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
---- test::test_cursor_mut_insert stdout ----
thread 'test::test_cursor_mut_insert' panicked at 'assertion failed: `(left == right)`
left: `[200, 201, 202, 203, 10, 100, 101, 102, 103, 7, 1, 8, 2, 3, 4, 5, 6, 9]`,
right: `[200, 201, 202, 203, 1, 100, 101, 102, 103, 8, 2, 3, 4, 5, 6]`', src\lib.rs:1153:9
failures:
test::test_cursor_move_peek
test::test_cursor_mut_insert
test result: FAILED. 12 passed; 2 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
認めますが、ここでは少し慢心していて、うまく決まったことを期待していました。だからこそテストを書くのです(でも、テストの移植がまずかっただけかも……?)。
最初の失敗は何でしょう?
let mut m: LinkedList<u32> = LinkedList::new();
m.extend([1, 2, 3, 4, 5, 6]);
let mut cursor = m.cursor_mut();
cursor.move_next();
assert_eq!(cursor.current(), Some(&mut 1));
assert_eq!(cursor.peek_next(), Some(&mut 2));
assert_eq!(cursor.peek_prev(), None);
assert_eq!(cursor.index(), Some(0));
cursor.move_prev();
assert_eq!(cursor.current(), None);
assert_eq!(cursor.peek_next(), Some(&mut 1)); // ここで死ぬ
うわぁ、かなり基本的な機能を本当にしくじっています。待ってください、
Head が空、Option メソッドと(省略された)コンパイラエラーが今やすべて考えてくれる。
まあ、私は正直であることだけは確かです。
pub fn peek_next(&mut self) -> Option<&mut T> {
unsafe {
self.cur
.and_then(|node| (*node.as_ptr()).back)
.map(|node| &mut (*node.as_ptr()).elem)
}
}
…うん、これは単に間違っています。self.cur が None の場合、ただ諦めればいいわけではなく、self.list.front もチェックする必要があります。なぜなら、私たちはゴースト上にいるからです! なので、このチェーンに or_else を追加すればいいだけです。
pub fn peek_next(&mut self) -> Option<&mut T> {
unsafe {
self.cur
.and_then(|node| (*node.as_ptr()).back)
.or_else(|| self.list.front)
.map(|node| &mut (*node.as_ptr()).elem)
}
}
pub fn peek_prev(&mut self) -> Option<&mut T> {
unsafe {
self.cur
.and_then(|node| (*node.as_ptr()).front)
.or_else(|| self.list.back)
.map(|node| &mut (*node.as_ptr()).elem)
}
}
これで直ったでしょうか?
---- test::test_cursor_move_peek stdout ----
thread 'test::test_cursor_move_peek' panicked at 'assertion failed: `(left == right)`
left: `Some(6)`,
right: `None`', src\lib.rs:1078:9
待って、今度はもっと前の方で間違っています。わかりました、peek を空撃ちして片付けるのはやめる必要があります。どうやら私が思っていたよりずっと難しいようです。こういうケースを盲目的にチェーンするのは大惨事なので、ゴーストの場合とそうでない場合について、ちゃんとした if を使いましょう。
pub fn peek_next(&mut self) -> Option<&mut T> {
unsafe {
let next = if let Some(cur) = self.cur {
// 通常ケース。cur ノードの back ポインターをたどろうとする
(*cur.as_ptr()).back
} else {
// ゴーストケース。リストの front ポインターを使おうとする
self.list.front
};
// next ノードが存在する場合は要素を返す
next.map(|node| &mut (*node.as_ptr()).elem)
}
}
pub fn peek_prev(&mut self) -> Option<&mut T> {
unsafe {
let prev = if let Some(cur) = self.cur {
// 通常ケース。cur ノードの front ポインターをたどろうとする
(*cur.as_ptr()).front
} else {
// ゴーストケース。リストの back ポインターを使おうとする
self.list.back
};
// prev ノードが存在する場合は要素を返す
prev.map(|node| &mut (*node.as_ptr()).elem)
}
}
これはかなり自信あります!
failures:
---- test::test_cursor_mut_insert stdout ----
thread 'test::test_cursor_mut_insert' panicked at 'assertion failed: `(left == right)`
left: `[200, 201, 202, 203, 10, 100, 101, 102, 103, 7, 1, 8, 2, 3, 4, 5, 6, 9]`,
right: `[200, 201, 202, 203, 1, 100, 101, 102, 103, 8, 2, 3, 4, 5, 6]`', src\lib.rs:1168:9
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
failures:
test::test_cursor_mut_insert
test result: FAILED. 13 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
よし。では残りの失敗はあとひとつ… あっ。
remove_current をテストするためにいくつかコードをコメントアウトしたところ、気づきましたか? ええ、このテストがステートフルであるという事実に注意を払っていませんでした。remove_current の部分が残していたはずの状態を持つ新しいリストを作ることにしましょう。
let mut m: LinkedList<u32> = LinkedList::new();
m.extend([1, 8, 2, 3, 4, 5, 6]);
cargo test
Compiling linked-list v0.0.3
Finished test [unoptimized + debuginfo] target(s) in 0.70s
Running unittests src\lib.rs
running 14 tests
test test::test_basic_front ... ok
test test::test_basic ... ok
test test::test_cursor_move_peek ... ok
test test::test_eq ... ok
test test::test_cursor_mut_insert ... ok
test test::test_iterator ... ok
test test::test_iterator_double_end ... ok
test test::test_ord_nan ... ok
test test::test_mut_iter ... ok
test test::test_hashmap ... ok
test test::test_debug ... ok
test test::test_ord ... ok
test test::test_iterator_mut_double_end ... ok
test test::test_rev_iter ... ok
test result: ok. 14 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Doc-tests linked-list
running 1 test
test src\lib.rs - assert_properties::iter_mut_invariant (line 803) - compile fail ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.12s
おおおお見てくださいこれ… よし、今度は不安になってきました。ちゃんと check_links を埋めて、miri でテストしましょう。
fn check_links<T: Eq + std::fmt::Debug>(list: &LinkedList<T>) {
let from_front: Vec<_> = list.iter().collect();
let from_back: Vec<_> = list.iter().rev().collect();
let re_reved: Vec<_> = from_back.into_iter().rev().collect();
assert_eq!(from_front, re_reved);
}
これが最善のやり方でしょうか? いいえ。これで十分でしょうか? はい。
$env:MIRIFLAGS="-Zmiri-tag-raw-pointers"
cargo miri test
Compiling linked-list v0.0.3
Finished test [unoptimized + debuginfo] target(s) in 0.25s
Running unittests src\lib.rs
running 14 tests
test test::test_basic ... ok
test test::test_basic_front ... ok
test test::test_cursor_move_peek ... ok
test test::test_cursor_mut_insert ... ok
test test::test_debug ... ok
test test::test_eq ... ok
test test::test_hashmap ... ok
test test::test_iterator ... ok
test test::test_iterator_double_end ... ok
test test::test_iterator_mut_double_end ... ok
test test::test_mut_iter ... ok
test test::test_ord ... ok
test test::test_ord_nan ... ok
test test::test_rev_iter ... ok
test result: ok. 14 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
Doc-tests linked-list
running 1 test
test src\lib.rs - assert_properties::iter_mut_invariant (line 803) - compile fail ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.10s
完了。
終わりです。
やりました。std にあるものと基本的に同じ機能をすべて備えた、まったくもって本番品質の LinkedList を作りました。あちこちにちょっとした便利メソッドが足りないでしょうか? もちろんです。クレートの最終公開版にそれらを追加するでしょうか? たぶん!
しかし私は、とても、とても疲れました。
だから。私たちの勝ちです。
待って、くそ。本番品質にしているんでした。よし、最後のラスボスです: clippy。
```text
cargo clippy
cargo clippy
Checking linked-list v0.0.3 (C:\Users\ninte\dev\contain\linked-list)
warning: redundant pattern matching, consider using `is_some()`
--> src\lib.rs:189:19
|
189 | while let Some(_) = self.pop_front() { }
| ----------^^^^^^^------------------- help: try this: `while self.pop_front().is_some()`
|
= note: `#[warn(clippy::redundant_pattern_matching)]` on by default
= note: this will change drop order of the result, as well as all temporaries
= note: add `#[allow(clippy::redundant_pattern_matching)]` if this is important
= help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#redundant_pattern_matching
warning: method `into_iter` can be confused for the standard trait method `std::iter::IntoIterator::into_iter`
--> src\lib.rs:210:5
|
210 | / pub fn into_iter(self) -> IntoIter<T> {
211 | | IntoIter {
212 | | list: self
213 | | }
214 | | }
| |_____^
|
= note: `#[warn(clippy::should_implement_trait)]` on by default
= help: consider implementing the trait `std::iter::IntoIterator` or choosing a less ambiguous method name
= help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#should_implement_trait
warning: redundant pattern matching, consider using `is_some()`
--> src\lib.rs:228:19
|
228 | while let Some(_) = self.pop_front() { }
| ----------^^^^^^^------------------- help: try this: `while self.pop_front().is_some()`
|
= note: this will change drop order of the result, as well as all temporaries
= note: add `#[allow(clippy::redundant_pattern_matching)]` if this is important
= help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#redundant_pattern_matching
warning: re-implementing `PartialEq::ne` is unnecessary
--> src\lib.rs:275:5
|
275 | / fn ne(&self, other: &Self) -> bool {
276 | | self.len() != other.len() || self.iter().ne(other)
277 | | }
| |_____^
|
= note: `#[warn(clippy::partialeq_ne_impl)]` on by default
= help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#partialeq_ne_impl
warning: `linked-list` (lib) generated 4 warnings
Finished dev [unoptimized + debuginfo] target(s) in 0.29s
よし clippy、やってやろうじゃないか。
指摘 1(と 3): while .is_some() の代わりに while let Some(_) = を使っている。ループは空なのでこれは本当にどうでもいいんだけど、まあいいよ、clippy、君のやり方でやるよ。
指摘 2: 実際に固有の into_iter メソッドがある。待って、std を確認する、なるほど、これは clippy に軍配だ。IntoIterator は prelude に含まれている(そして基本的には lang item でもある)ので、固有バージョンも持つ必要はない。
指摘 4: std から変なカーゴカルトをコピーしていた。肩をすくめる、まあいい、削除しよう。
cargo clippy
Finished dev [unoptimized + debuginfo] target(s) in 0.00s
いいね。production quality と呼ぶ前にやることは、あと 1 つだけ: fmt。
cargo fmt
……そう、改行がいくつか追加されて、末尾の空白がいくつか削除された。面白いことは何もない。
これで本当に本当にようやく完了です!!!!!!!!!!!!!!!!!!!!!
最終コード
私が実際に std::collections::LinkedList をゼロから再実装するところに、細々とした面倒なこだわりや途中で犯したミスも含めて、皆さんを付き合わせてしまったなんて信じられません。
やり遂げました。本は完成しました。ようやく休めます。
さて、私たちによる の完全な書き直し、全 1200 行の全貌をここに示します。これは このコミット と同じ内容のはずです。
あとで少し磨きをかけてドキュメントを戻し、0.1.0 を公開します。
#![allow(unused)]
fn main() {
use std::cmp::Ordering;
use std::fmt::{self, Debug};
use std::hash::{Hash, Hasher};
use std::iter::FromIterator;
use std::marker::PhantomData;
use std::ptr::NonNull;
pub struct LinkedList<T> {
front: Link<T>,
back: Link<T>,
len: usize,
_boo: PhantomData<T>,
}
type Link<T> = Option<NonNull<Node<T>>>;
struct Node<T> {
front: Link<T>,
back: Link<T>,
elem: T,
}
pub struct Iter<'a, T> {
front: Link<T>,
back: Link<T>,
len: usize,
_boo: PhantomData<&'a T>,
}
pub struct IterMut<'a, T> {
front: Link<T>,
back: Link<T>,
len: usize,
_boo: PhantomData<&'a mut T>,
}
pub struct IntoIter<T> {
list: LinkedList<T>,
}
pub struct CursorMut<'a, T> {
list: &'a mut LinkedList<T>,
cur: Link<T>,
index: Option<usize>,
}
impl<T> LinkedList<T> {
pub fn new() -> Self {
Self {
front: None,
back: None,
len: 0,
_boo: PhantomData,
}
}
pub fn push_front(&mut self, elem: T) {
// SAFETY: これはリンクリストだ。何を求めているんだ?
unsafe {
let new = NonNull::new_unchecked(Box::into_raw(Box::new(Node {
front: None,
back: None,
elem,
})));
if let Some(old) = self.front {
// 新しい front を古いものの前に置く
(*old.as_ptr()).front = Some(new);
(*new.as_ptr()).back = Some(old);
} else {
// front が存在しないなら、これは空のリストなので
// back も設定する必要がある。
self.back = Some(new);
}
// これは常に行う!
self.front = Some(new);
self.len += 1;
}
}
pub fn push_back(&mut self, elem: T) {
// SAFETY: これはリンクリストだ。何を求めているんだ?
unsafe {
let new = NonNull::new_unchecked(Box::into_raw(Box::new(Node {
back: None,
front: None,
elem,
})));
if let Some(old) = self.back {
// 新しい back を古いものの前に置く
(*old.as_ptr()).back = Some(new);
(*new.as_ptr()).front = Some(old);
} else {
// back が存在しないなら、これは空のリストなので
// front も設定する必要がある。
self.front = Some(new);
}
// これは常に行う!
self.back = Some(new);
self.len += 1;
}
}
pub fn pop_front(&mut self) -> Option<T> {
unsafe {
// pop する front ノードがある場合だけ処理すればよい。
self.front.map(|node| {
// Box を復活させて、その値を取り出して
// Drop できるようにする(Box は引き続き、これを魔法のように理解してくれる)。
let boxed_node = Box::from_raw(node.as_ptr());
let result = boxed_node.elem;
// 次のノードを新しい front にする。
self.front = boxed_node.back;
if let Some(new) = self.front {
// 削除されたノードへの参照をクリーンアップする
(*new.as_ptr()).front = None;
} else {
// front が null になったなら、このリストは空になったということ!
self.back = None;
}
self.len -= 1;
result
// Box はここで暗黙的に解放される。T が存在しないことを分かっている。
})
}
}
pub fn pop_back(&mut self) -> Option<T> {
unsafe {
// pop する back ノードがある場合だけ処理すればよい。
self.back.map(|node| {
// Box を復活させて、その値を取り出して
// Drop できるようにする(Box は引き続き、これを魔法のように理解してくれる)。
let boxed_node = Box::from_raw(node.as_ptr());
let result = boxed_node.elem;
// 次のノードを新しい back にする。
self.back = boxed_node.front;
if let Some(new) = self.back {
// 削除されたノードへの参照をクリーンアップする
(*new.as_ptr()).back = None;
} else {
// back が null になったなら、このリストは空になったということ!
self.front = None;
}
self.len -= 1;
result
// Box はここで暗黙的に解放される。T が存在しないことを分かっている。
})
}
}
pub fn front(&self) -> Option<&T> {
unsafe { self.front.map(|node| &(*node.as_ptr()).elem) }
}
pub fn front_mut(&mut self) -> Option<&mut T> {
unsafe { self.front.map(|node| &mut (*node.as_ptr()).elem) }
}
pub fn back(&self) -> Option<&T> {
unsafe { self.back.map(|node| &(*node.as_ptr()).elem) }
}
pub fn back_mut(&mut self) -> Option<&mut T> {
unsafe { self.back.map(|node| &mut (*node.as_ptr()).elem) }
}
pub fn len(&self) -> usize {
self.len
}
pub fn is_empty(&self) -> bool {
self.len == 0
}
pub fn clear(&mut self) {
// 見て、また drop だ
while self.pop_front().is_some() {}
}
pub fn iter(&self) -> Iter<T> {
Iter {
front: self.front,
back: self.back,
len: self.len,
_boo: PhantomData,
}
}
pub fn iter_mut(&mut self) -> IterMut<T> {
IterMut {
front: self.front,
back: self.back,
len: self.len,
_boo: PhantomData,
}
}
pub fn cursor_mut(&mut self) -> CursorMut<T> {
CursorMut {
list: self,
cur: None,
index: None,
}
}
}
impl<T> Drop for LinkedList<T> {
fn drop(&mut self) {
// 停止せざるを得なくなるまで pop する
while self.pop_front().is_some() {}
}
}
impl<T> Default for LinkedList<T> {
fn default() -> Self {
Self::new()
}
}
impl<T: Clone> Clone for LinkedList<T> {
fn clone(&self) -> Self {
let mut new_list = Self::new();
for item in self {
new_list.push_back(item.clone());
}
new_list
}
}
impl<T> Extend<T> for LinkedList<T> {
fn extend<I: IntoIterator<Item = T>>(&mut self, iter: I) {
for item in iter {
self.push_back(item);
}
}
}
impl<T> FromIterator<T> for LinkedList<T> {
fn from_iter<I: IntoIterator<Item = T>>(iter: I) -> Self {
let mut list = Self::new();
list.extend(iter);
list
}
}
impl<T: Debug> Debug for LinkedList<T> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_list().entries(self).finish()
}
}
impl<T: PartialEq> PartialEq for LinkedList<T> {
fn eq(&self, other: &Self) -> bool {
self.len() == other.len() && self.iter().eq(other)
}
}
impl<T: Eq> Eq for LinkedList<T> {}
impl<T: PartialOrd> PartialOrd for LinkedList<T> {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
self.iter().partial_cmp(other)
}
}
impl<T: Ord> Ord for LinkedList<T> {
fn cmp(&self, other: &Self) -> Ordering {
self.iter().cmp(other)
}
}
impl<T: Hash> Hash for LinkedList<T> {
fn hash<H: Hasher>(&self, state: &mut H) {
self.len().hash(state);
for item in self {
item.hash(state);
}
}
}
impl<'a, T> IntoIterator for &'a LinkedList<T> {
type IntoIter = Iter<'a, T>;
type Item = &'a T;
fn into_iter(self) -> Self::IntoIter {
self.iter()
}
}
impl<'a, T> Iterator for Iter<'a, T> {
type Item = &'a T;
fn next(&mut self) -> Option<Self::Item> {
// self.front == self.back はここでチェックしたくなる条件だが、
// 最後の要素を yield するには正しく動作しない! その種の
// ことが配列でのみ機能するのは、"one-past-the-end" ポインターがあるためだ。
if self.len > 0 {
// front を unwrap することもできるが、こちらの方が安全で簡単
self.front.map(|node| unsafe {
self.len -= 1;
self.front = (*node.as_ptr()).back;
&(*node.as_ptr()).elem
})
} else {
None
}
}
fn size_hint(&self) -> (usize, Option<usize>) {
(self.len, Some(self.len))
}
}
impl<'a, T> DoubleEndedIterator for Iter<'a, T> {
fn next_back(&mut self) -> Option<Self::Item> {
if self.len > 0 {
self.back.map(|node| unsafe {
self.len -= 1;
self.back = (*node.as_ptr()).front;
&(*node.as_ptr()).elem
})
} else {
None
}
}
}
impl<'a, T> ExactSizeIterator for Iter<'a, T> {
fn len(&self) -> usize {
self.len
}
}
impl<'a, T> IntoIterator for &'a mut LinkedList<T> {
type IntoIter = IterMut<'a, T>;
type Item = &'a mut T;
fn into_iter(self) -> Self::IntoIter {
self.iter_mut()
}
}
impl<'a, T> Iterator for IterMut<'a, T> {
type Item = &'a mut T;
fn next(&mut self) -> Option<Self::Item> {
// self.front == self.back はここでチェックしたくなる条件だが、
// 最後の要素を yield するには正しく動作しない! その種の
// ことが配列でのみ機能するのは、"one-past-the-end" ポインターがあるためだ。
if self.len > 0 {
// front を unwrap することもできるが、こちらの方が安全で簡単
self.front.map(|node| unsafe {
self.len -= 1;
self.front = (*node.as_ptr()).back;
&mut (*node.as_ptr()).elem
})
} else {
None
}
}
fn size_hint(&self) -> (usize, Option<usize>) {
(self.len, Some(self.len))
}
}
impl<'a, T> DoubleEndedIterator for IterMut<'a, T> {
fn next_back(&mut self) -> Option<Self::Item> {
if self.len > 0 {
self.back.map(|node| unsafe {
self.len -= 1;
self.back = (*node.as_ptr()).front;
&mut (*node.as_ptr()).elem
})
} else {
None
}
}
}
impl<'a, T> ExactSizeIterator for IterMut<'a, T> {
fn len(&self) -> usize {
self.len
}
}
impl<T> IntoIterator for LinkedList<T> {
type IntoIter = IntoIter<T>;
type Item = T;
fn into_iter(self) -> Self::IntoIter {
IntoIter { list: self }
}
}
impl<T> Iterator for IntoIter<T> {
type Item = T;
fn next(&mut self) -> Option<Self::Item> {
self.list.pop_front()
}
fn size_hint(&self) -> (usize, Option<usize>) {
(self.list.len, Some(self.list.len))
}
}
impl<T> DoubleEndedIterator for IntoIter<T> {
fn next_back(&mut self) -> Option<Self::Item> {
self.list.pop_back()
}
}
impl<T> ExactSizeIterator for IntoIter<T> {
fn len(&self) -> usize {
self.list.len
}
}
impl<'a, T> CursorMut<'a, T> {
pub fn index(&self) -> Option<usize> {
self.index
}
pub fn move_next(&mut self) {
if let Some(cur) = self.cur {
unsafe {
// 実要素上にいるので、次(back)へ進む
self.cur = (*cur.as_ptr()).back;
if self.cur.is_some() {
*self.index.as_mut().unwrap() += 1;
} else {
// たった今 ghost に移動したので、index はもうない
self.index = None;
}
}
} else if !self.list.is_empty() {
// ghost にいて、実際の front があるので、そこへ移動する!
self.cur = self.list.front;
self.index = Some(0)
} else {
// ghost にいるが、それが唯一の要素だ……何もしない。
}
}
pub fn move_prev(&mut self) {
if let Some(cur) = self.cur {
unsafe {
// 実要素上にいるので、前(front)へ進む
self.cur = (*cur.as_ptr()).front;
if self.cur.is_some() {
*self.index.as_mut().unwrap() -= 1;
} else {
// たった今 ghost に移動したので、index はもうない
self.index = None;
}
}
} else if !self.list.is_empty() {
// ghost にいて、実際の back があるので、そこへ移動する!
self.cur = self.list.back;
self.index = Some(self.list.len - 1)
} else {
// ghost にいるが、それが唯一の要素だ……何もしない。
}
}
pub fn current(&mut self) -> Option<&mut T> {
unsafe { self.cur.map(|node| &mut (*node.as_ptr()).elem) }
}
pub fn peek_next(&mut self) -> Option<&mut T> {
unsafe {
let next = if let Some(cur) = self.cur {
// 通常ケースでは、cur ノードの back ポインターをたどろうとする
(*cur.as_ptr()).back
} else {
// Ghost ケースでは、リストの front ポインターを使おうとする
self.list.front
};
// next ノードが存在する場合は要素を yield する
next.map(|node| &mut (*node.as_ptr()).elem)
}
}
pub fn peek_prev(&mut self) -> Option<&mut T> {
unsafe {
let prev = if let Some(cur) = self.cur {
// 通常ケースでは、cur ノードの front ポインターをたどろうとする
(*cur.as_ptr()).front
} else {
// Ghost ケースでは、リストの back ポインターを使おうとする
self.list.back
};
// prev ノードが存在する場合は要素を yield する
prev.map(|node| &mut (*node.as_ptr()).elem)
}
}
pub fn split_before(&mut self) -> LinkedList<T> {
// これは次の状態:
//
// list.front -> A <-> B <-> C <-> D <- list.back
// ^
// cur
//
//
// そして次を生成したい:
//
// list.front -> C <-> D <- list.back
// ^
// cur
//
//
// return.front -> A <-> B <- return.back
//
if let Some(cur) = self.cur {
// 実要素を指しているので、リストは空ではない。
unsafe {
// 現在の状態
let old_len = self.list.len;
let old_idx = self.index.unwrap();
let prev = (*cur.as_ptr()).front;
// self がどうなるか
let new_len = old_len - old_idx;
let new_front = self.cur;
let new_back = self.list.back;
let new_idx = Some(0);
// 出力がどうなるか
let output_len = old_len - new_len;
let output_front = self.list.front;
let output_back = prev;
// cur と prev の間のリンクを切る
if let Some(prev) = prev {
(*cur.as_ptr()).front = None;
(*prev.as_ptr()).back = None;
}
// 結果を生成する:
self.list.len = new_len;
self.list.front = new_front;
self.list.back = new_back;
self.index = new_idx;
LinkedList {
front: output_front,
back: output_back,
len: output_len,
_boo: PhantomData,
}
}
} else {
// ghost にいるので、自分のリストを空のものに置き換えるだけ。
// 他の状態を変更する必要はない。
std::mem::replace(self.list, LinkedList::new())
}
}
pub fn split_after(&mut self) -> LinkedList<T> {
// これは次の状態:
//
// list.front -> A <-> B <-> C <-> D <- list.back
// ^
// cur
//
//
// そして次を生成したい:
//
// list.front -> A <-> B <- list.back
// ^
// cur
//
//
// return.front -> C <-> D <- return.back
//
if let Some(cur) = self.cur {
// 実要素を指しているので、リストは空ではない。
unsafe {
// 現在の状態
let old_len = self.list.len;
let old_idx = self.index.unwrap();
let next = (*cur.as_ptr()).back;
// self がどうなるか
let new_len = old_idx + 1;
let new_back = self.cur;
let new_front = self.list.front;
let new_idx = Some(old_idx);
// 出力がどうなるか
let output_len = old_len - new_len;
let output_front = next;
let output_back = self.list.back;
// cur と next の間のリンクを切る
if let Some(next) = next {
(*cur.as_ptr()).back = None;
(*next.as_ptr()).front = None;
}
// 結果を生成する:
self.list.len = new_len;
self.list.front = new_front;
self.list.back = new_back;
self.index = new_idx;
LinkedList {
front: output_front,
back: output_back,
len: output_len,
_boo: PhantomData,
}
}
} else {
// ghost にいるので、自分のリストを空のものに置き換えるだけ。
// 他の状態を変更する必要はない。
std::mem::replace(self.list, LinkedList::new())
}
}
pub fn splice_before(&mut self, mut input: LinkedList<T>) {
// これは次の状態:
//
// input.front -> 1 <-> 2 <- input.back
//
// list.front -> A <-> B <-> C <- list.back
// ^
// cur
//
//
// これが次になる:
//
// list.front -> A <-> 1 <-> 2 <-> B <-> C <- list.back
// ^
// cur
//
unsafe {
// input のポインターを `take` するか、`mem::forget`
// するかのどちらかができる。将来カスタムアロケーターなど、
// 同じくクリーンアップが必要なものを扱う場合は `take` を使う方がより責任あるやり方だ!
if input.is_empty() {
// input は空なので、何もしない。
} else if let Some(cur) = self.cur {
// 両方のリストは空ではない
let in_front = input.front.take().unwrap();
let in_back = input.back.take().unwrap();
if let Some(prev) = (*cur.as_ptr()).front {
// 一般ケース。境界はなく、内部の修正だけ
(*prev.as_ptr()).back = Some(in_front);
(*in_front.as_ptr()).front = Some(prev);
(*cur.as_ptr()).front = Some(in_back);
(*in_back.as_ptr()).back = Some(cur);
} else {
// prev がないので、front に追加している
(*cur.as_ptr()).front = Some(in_back);
(*in_back.as_ptr()).back = Some(cur);
self.list.front = Some(in_front);
}
// index は input の長さ分だけ前に進む
*self.index.as_mut().unwrap() += input.len;
} else if let Some(back) = self.list.back {
// ghost 上にいるが空ではないので、back に追加する
let in_front = input.front.take().unwrap();
let in_back = input.back.take().unwrap();
(*back.as_ptr()).back = Some(in_front);
(*in_front.as_ptr()).front = Some(back);
self.list.back = Some(in_back);
} else {
// 空なので input になり、ghost 上に留まる
std::mem::swap(self.list, &mut input);
}
self.list.len += input.len;
// 必須ではないが、やっておくのが丁寧
input.len = 0;
// input はここで drop される
}
}
pub fn splice_after(&mut self, mut input: LinkedList<T>) {
// これは次の状態:
//
// input.front -> 1 <-> 2 <- input.back
//
// list.front -> A <-> B <-> C <- list.back
// ^
// cur
//
//
// これが次になる:
//
// list.front -> A <-> B <-> 1 <-> 2 <-> C <- list.back
// ^
// cur
//
unsafe {
// input のポインターを `take` するか、`mem::forget`
// するかのどちらかができる。将来カスタムアロケーターなど、
// 同じくクリーンアップが必要なものを扱う場合は `take` を使う方がより責任あるやり方だ!
if input.is_empty() {
// input は空なので、何もしない。
} else if let Some(cur) = self.cur {
// 両方のリストは空ではない
let in_front = input.front.take().unwrap();
let in_back = input.back.take().unwrap();
if let Some(next) = (*cur.as_ptr()).back {
// 一般ケース。境界はなく、内部の修正だけ
(*next.as_ptr()).front = Some(in_back);
(*in_back.as_ptr()).back = Some(next);
(*cur.as_ptr()).back = Some(in_front);
(*in_front.as_ptr()).front = Some(cur);
} else {
// next がないので、back に追加している
(*cur.as_ptr()).back = Some(in_front);
(*in_front.as_ptr()).front = Some(cur);
self.list.back = Some(in_back);
}
// index は変わらない
} else if let Some(front) = self.list.front {
// ghost 上にいるが空ではないので、front に追加する
let in_front = input.front.take().unwrap();
let in_back = input.back.take().unwrap();
(*front.as_ptr()).front = Some(in_back);
(*in_back.as_ptr()).back = Some(front);
self.list.front = Some(in_front);
} else {
// 空なので input になり、ghost 上に留まる
std::mem::swap(self.list, &mut input);
}
self.list.len += input.len;
// 必須ではないが、やっておくのが丁寧
input.len = 0;
// input はここで drop される
}
}
}
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> {}
#[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>>();
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
}
/// ```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() {}
}
#[cfg(test)]
mod test {
use super::LinkedList;
fn generate_test() -> LinkedList<i32> {
list_from(&[0, 1, 2, 3, 4, 5, 6])
}
fn list_from<T: Clone>(v: &[T]) -> LinkedList<T> {
v.iter().map(|x| (*x).clone()).collect()
}
#[test]
fn test_basic_front() {
let mut list = LinkedList::new();
// 空のリストを壊してみる
assert_eq!(list.len(), 0);
assert_eq!(list.pop_front(), None);
assert_eq!(list.len(), 0);
// 要素が 1 つのリストを壊してみる
list.push_front(10);
assert_eq!(list.len(), 1);
assert_eq!(list.pop_front(), Some(10));
assert_eq!(list.len(), 0);
assert_eq!(list.pop_front(), None);
assert_eq!(list.len(), 0);
// いろいろいじる
list.push_front(10);
assert_eq!(list.len(), 1);
list.push_front(20);
assert_eq!(list.len(), 2);
list.push_front(30);
assert_eq!(list.len(), 3);
assert_eq!(list.pop_front(), Some(30));
assert_eq!(list.len(), 2);
list.push_front(40);
assert_eq!(list.len(), 3);
assert_eq!(list.pop_front(), Some(40));
assert_eq!(list.len(), 2);
assert_eq!(list.pop_front(), Some(20));
assert_eq!(list.len(), 1);
assert_eq!(list.pop_front(), Some(10));
assert_eq!(list.len(), 0);
assert_eq!(list.pop_front(), None);
assert_eq!(list.len(), 0);
assert_eq!(list.pop_front(), None);
assert_eq!(list.len(), 0);
}
#[test]
fn test_basic() {
let mut m = LinkedList::new();
assert_eq!(m.pop_front(), None);
assert_eq!(m.pop_back(), None);
assert_eq!(m.pop_front(), None);
m.push_front(1);
assert_eq!(m.pop_front(), Some(1));
m.push_back(2);
m.push_back(3);
assert_eq!(m.len(), 2);
assert_eq!(m.pop_front(), Some(2));
assert_eq!(m.pop_front(), Some(3));
assert_eq!(m.len(), 0);
assert_eq!(m.pop_front(), None);
m.push_back(1);
m.push_back(3);
m.push_back(5);
m.push_back(7);
assert_eq!(m.pop_front(), Some(1));
let mut n = LinkedList::new();
n.push_front(2);
n.push_front(3);
{
assert_eq!(n.front().unwrap(), &3);
let x = n.front_mut().unwrap();
assert_eq!(*x, 3);
*x = 0;
}
{
assert_eq!(n.back().unwrap(), &2);
let y = n.back_mut().unwrap();
assert_eq!(*y, 2);
*y = 1;
}
assert_eq!(n.pop_front(), Some(0));
assert_eq!(n.pop_front(), Some(1));
}
#[test]
fn test_iterator() {
let m = generate_test();
for (i, elt) in m.iter().enumerate() {
assert_eq!(i as i32, *elt);
}
let mut n = LinkedList::new();
assert_eq!(n.iter().next(), None);
n.push_front(4);
let mut it = n.iter();
assert_eq!(it.size_hint(), (1, Some(1)));
assert_eq!(it.next().unwrap(), &4);
assert_eq!(it.size_hint(), (0, Some(0)));
assert_eq!(it.next(), None);
}
#[test]
fn test_iterator_double_end() {
let mut n = LinkedList::new();
assert_eq!(n.iter().next(), None);
n.push_front(4);
n.push_front(5);
n.push_front(6);
let mut it = n.iter();
assert_eq!(it.size_hint(), (3, Some(3)));
assert_eq!(it.next().unwrap(), &6);
assert_eq!(it.size_hint(), (2, Some(2)));
assert_eq!(it.next_back().unwrap(), &4);
assert_eq!(it.size_hint(), (1, Some(1)));
assert_eq!(it.next_back().unwrap(), &5);
assert_eq!(it.next_back(), None);
assert_eq!(it.next(), None);
}
#[test]
fn test_rev_iter() {
let m = generate_test();
for (i, elt) in m.iter().rev().enumerate() {
assert_eq!(6 - i as i32, *elt);
}
let mut n = LinkedList::new();
assert_eq!(n.iter().rev().next(), None);
n.push_front(4);
let mut it = n.iter().rev();
assert_eq!(it.size_hint(), (1, Some(1)));
assert_eq!(it.next().unwrap(), &4);
assert_eq!(it.size_hint(), (0, Some(0)));
assert_eq!(it.next(), None);
}
#[test]
fn test_mut_iter() {
let mut m = generate_test();
let mut len = m.len();
for (i, elt) in m.iter_mut().enumerate() {
assert_eq!(i as i32, *elt);
len -= 1;
}
assert_eq!(len, 0);
let mut n = LinkedList::new();
assert!(n.iter_mut().next().is_none());
n.push_front(4);
n.push_back(5);
let mut it = n.iter_mut();
assert_eq!(it.size_hint(), (2, Some(2)));
assert!(it.next().is_some());
assert!(it.next().is_some());
assert_eq!(it.size_hint(), (0, Some(0)));
assert!(it.next().is_none());
}
#[test]
fn test_iterator_mut_double_end() {
let mut n = LinkedList::new();
assert!(n.iter_mut().next_back().is_none());
n.push_front(4);
n.push_front(5);
n.push_front(6);
let mut it = n.iter_mut();
assert_eq!(it.size_hint(), (3, Some(3)));
assert_eq!(*it.next().unwrap(), 6);
assert_eq!(it.size_hint(), (2, Some(2)));
assert_eq!(*it.next_back().unwrap(), 4);
assert_eq!(it.size_hint(), (1, Some(1)));
assert_eq!(*it.next_back().unwrap(), 5);
assert!(it.next_back().is_none());
assert!(it.next().is_none());
}
#[test]
fn test_eq() {
let mut n: LinkedList<u8> = list_from(&[]);
let mut m = list_from(&[]);
assert!(n == m);
n.push_front(1);
assert!(n != m);
m.push_back(1);
assert!(n == m);
let n = list_from(&[2, 3, 4]);
let m = list_from(&[1, 2, 3]);
assert!(n != m);
}
#[test]
fn test_ord() {
let n = list_from(&[]);
let m = list_from(&[1, 2, 3]);
assert!(n < m);
assert!(m > n);
assert!(n <= n);
assert!(n >= n);
}
#[test]
fn test_ord_nan() {
let nan = 0.0f64 / 0.0;
let n = list_from(&[nan]);
let m = list_from(&[nan]);
assert!(!(n < m));
assert!(!(n > m));
assert!(!(n <= m));
assert!(!(n >= m));
let n = list_from(&[nan]);
let one = list_from(&[1.0f64]);
assert!(!(n < one));
assert!(!(n > one));
assert!(!(n <= one));
assert!(!(n >= one));
let u = list_from(&[1.0f64, 2.0, nan]);
let v = list_from(&[1.0f64, 2.0, 3.0]);
assert!(!(u < v));
assert!(!(u > v));
assert!(!(u <= v));
assert!(!(u >= v));
let s = list_from(&[1.0f64, 2.0, 4.0, 2.0]);
let t = list_from(&[1.0f64, 2.0, 3.0, 2.0]);
assert!(!(s < t));
assert!(s > one);
assert!(!(s <= one));
assert!(s >= one);
}
#[test]
fn test_debug() {
let list: LinkedList<i32> = (0..10).collect();
assert_eq!(format!("{:?}", list), "[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]");
let list: LinkedList<&str> = vec!["just", "one", "test", "more"]
.iter()
.copied()
.collect();
assert_eq!(format!("{:?}", list), r#"["just", "one", "test", "more"]"#);
}
#[test]
fn test_hashmap() {
// これをキーとして HashMap が動作することを確認する
let list1: LinkedList<i32> = (0..10).collect();
let list2: LinkedList<i32> = (1..11).collect();
let mut map = std::collections::HashMap::new();
assert_eq!(map.insert(list1.clone(), "list1"), None);
assert_eq!(map.insert(list2.clone(), "list2"), None);
assert_eq!(map.len(), 2);
assert_eq!(map.get(&list1), Some(&"list1"));
assert_eq!(map.get(&list2), Some(&"list2"));
assert_eq!(map.remove(&list1), Some("list1"));
assert_eq!(map.remove(&list2), Some("list2"));
assert!(map.is_empty());
}
#[test]
fn test_cursor_move_peek() {
let mut m: LinkedList<u32> = LinkedList::new();
m.extend([1, 2, 3, 4, 5, 6]);
let mut cursor = m.cursor_mut();
cursor.move_next();
assert_eq!(cursor.current(), Some(&mut 1));
assert_eq!(cursor.peek_next(), Some(&mut 2));
assert_eq!(cursor.peek_prev(), None);
assert_eq!(cursor.index(), Some(0));
cursor.move_prev();
assert_eq!(cursor.current(), None);
assert_eq!(cursor.peek_next(), Some(&mut 1));
assert_eq!(cursor.peek_prev(), Some(&mut 6));
assert_eq!(cursor.index(), None);
cursor.move_next();
cursor.move_next();
assert_eq!(cursor.current(), Some(&mut 2));
assert_eq!(cursor.peek_next(), Some(&mut 3));
assert_eq!(cursor.peek_prev(), Some(&mut 1));
assert_eq!(cursor.index(), Some(1));
let mut cursor = m.cursor_mut();
cursor.move_prev();
assert_eq!(cursor.current(), Some(&mut 6));
assert_eq!(cursor.peek_next(), None);
assert_eq!(cursor.peek_prev(), Some(&mut 5));
assert_eq!(cursor.index(), Some(5));
cursor.move_next();
assert_eq!(cursor.current(), None);
assert_eq!(cursor.peek_next(), Some(&mut 1));
assert_eq!(cursor.peek_prev(), Some(&mut 6));
assert_eq!(cursor.index(), None);
cursor.move_prev();
cursor.move_prev();
assert_eq!(cursor.current(), Some(&mut 5));
assert_eq!(cursor.peek_next(), Some(&mut 6));
assert_eq!(cursor.peek_prev(), Some(&mut 4));
assert_eq!(cursor.index(), Some(4));
}
#[test]
fn test_cursor_mut_insert() {
let mut m: LinkedList<u32> = LinkedList::new();
m.extend([1, 2, 3, 4, 5, 6]);
let mut cursor = m.cursor_mut();
cursor.move_next();
cursor.splice_before(Some(7).into_iter().collect());
cursor.splice_after(Some(8).into_iter().collect());
// check_links(&m);
assert_eq!(
m.iter().cloned().collect::<Vec<_>>(),
&[7, 1, 8, 2, 3, 4, 5, 6]
);
let mut cursor = m.cursor_mut();
cursor.move_next();
cursor.move_prev();
cursor.splice_before(Some(9).into_iter().collect());
cursor.splice_after(Some(10).into_iter().collect());
check_links(&m);
assert_eq!(
m.iter().cloned().collect::<Vec<_>>(),
&[10, 7, 1, 8, 2, 3, 4, 5, 6, 9]
);
/* remove_current は実装されていない
let mut cursor = m.cursor_mut();
cursor.move_next();
cursor.move_prev();
assert_eq!(cursor.remove_current(), None);
cursor.move_next();
cursor.move_next();
assert_eq!(cursor.remove_current(), Some(7));
cursor.move_prev();
cursor.move_prev();
cursor.move_prev();
assert_eq!(cursor.remove_current(), Some(9));
cursor.move_next();
assert_eq!(cursor.remove_current(), Some(10));
check_links(&m);
assert_eq!(m.iter().cloned().collect::<Vec<_>>(), &[1, 8, 2, 3, 4, 5, 6]);
*/
let mut m: LinkedList<u32> = LinkedList::new();
m.extend([1, 8, 2, 3, 4, 5, 6]);
let mut cursor = m.cursor_mut();
cursor.move_next();
let mut p: LinkedList<u32> = LinkedList::new();
p.extend([100, 101, 102, 103]);
let mut q: LinkedList<u32> = LinkedList::new();
q.extend([200, 201, 202, 203]);
cursor.splice_after(p);
cursor.splice_before(q);
check_links(&m);
assert_eq!(
m.iter().cloned().collect::<Vec<_>>(),
&[200, 201, 202, 203, 1, 100, 101, 102, 103, 8, 2, 3, 4, 5, 6]
);
let mut cursor = m.cursor_mut();
cursor.move_next();
cursor.move_prev();
let tmp = cursor.split_before();
assert_eq!(m.into_iter().collect::<Vec<_>>(), &[]);
m = tmp;
let mut cursor = m.cursor_mut();
cursor.move_next();
cursor.move_next();
cursor.move_next();
cursor.move_next();
cursor.move_next();
cursor.move_next();
cursor.move_next();
let tmp = cursor.split_after();
assert_eq!(
tmp.into_iter().collect::<Vec<_>>(),
&[102, 103, 8, 2, 3, 4, 5, 6]
);
check_links(&m);
assert_eq!(
m.iter().cloned().collect::<Vec<_>>(),
&[200, 201, 202, 203, 1, 100, 101]
);
}
fn check_links<T: Eq + std::fmt::Debug>(list: &LinkedList<T>) {
let from_front: Vec<_> = list.iter().collect();
let from_back: Vec<_> = list.iter().rev().collect();
let re_reved: Vec<_> = from_back.into_iter().rev().collect();
assert_eq!(from_front, re_reved);
}
}
}
ばかげたリストの寄せ集め
よし。これで終わり。すべてのリストを作りました。
あはははは
いいえ
リストはいつだってまだあります。
この章は、より突飛な連結リストと、それらが Rust とどのように 相互作用するかについての生きたドキュメントです。
- ダブル・シングル
- スタック割り当てリスト
- 自己参照アリーナリスト?
- GhostCell リスト?
二重単方向リンクリスト
双方向リンクリストでは苦労しました。所有権セマンティクスが複雑に絡み合っているからです。つまり、どのノードも他のノードを厳密には所有していません。しかし、私たちがこれに苦労したのは、リンクリストとは何であるかについての先入観を持ち込んだからです。具体的には、すべてのリンクが同じ方向に進むものだと仮定していました。
代わりに、リストを 2 つの半分に分割できます。1 つは左へ進み、もう 1 つは右へ進みます。
// lib.rs
// ...
pub mod silly1; // 新規!
// silly1.rs
use crate::second::List as Stack;
struct List<T> {
left: Stack<T>,
right: Stack<T>,
}
これで、単なる安全なスタックではなく、汎用リストが手に入りました。どちらかのスタックに push することで、リストを左方向にも右方向にも伸ばせます。また、一方の端から値を pop してもう一方へ push することで、リストに沿って「歩く」こともできます。不要なアロケーションを避けるために、安全な Stack のソースをコピーして、その private な詳細へアクセスできるようにします。
pub struct Stack<T> {
head: Link<T>,
}
type Link<T> = Option<Box<Node<T>>>;
struct Node<T> {
elem: T,
next: Link<T>,
}
impl<T> Stack<T> {
pub fn new() -> Self {
Stack { head: None }
}
pub fn push(&mut self, elem: T) {
let new_node = Box::new(Node {
elem: elem,
next: self.head.take(),
});
self.head = Some(new_node);
}
pub fn pop(&mut self) -> Option<T> {
self.head.take().map(|node| {
let node = *node;
self.head = node.next;
node.elem
})
}
pub fn peek(&self) -> Option<&T> {
self.head.as_ref().map(|node| {
&node.elem
})
}
pub fn peek_mut(&mut self) -> Option<&mut T> {
self.head.as_mut().map(|node| {
&mut node.elem
})
}
}
impl<T> Drop for Stack<T> {
fn drop(&mut self) {
let mut cur_link = self.head.take();
while let Some(mut boxed_node) = cur_link {
cur_link = boxed_node.next.take();
}
}
}
そして、push と pop を少し作り直します。
pub fn push(&mut self, elem: T) {
let new_node = Box::new(Node {
elem: elem,
next: None,
});
self.push_node(new_node);
}
fn push_node(&mut self, mut node: Box<Node<T>>) {
node.next = self.head.take();
self.head = Some(node);
}
pub fn pop(&mut self) -> Option<T> {
self.pop_node().map(|node| {
node.elem
})
}
fn pop_node(&mut self) -> Option<Box<Node<T>>> {
self.head.take().map(|mut node| {
self.head = node.next.take();
node
})
}
これで List を作れます。
pub struct List<T> {
left: Stack<T>,
right: Stack<T>,
}
impl<T> List<T> {
fn new() -> Self {
List { left: Stack::new(), right: Stack::new() }
}
}
そして、おなじみの操作ができます。
pub fn push_left(&mut self, elem: T) { self.left.push(elem) }
pub fn push_right(&mut self, elem: T) { self.right.push(elem) }
pub fn pop_left(&mut self) -> Option<T> { self.left.pop() }
pub fn pop_right(&mut self) -> Option<T> { self.right.pop() }
pub fn peek_left(&self) -> Option<&T> { self.left.peek() }
pub fn peek_right(&self) -> Option<&T> { self.right.peek() }
pub fn peek_left_mut(&mut self) -> Option<&mut T> { self.left.peek_mut() }
pub fn peek_right_mut(&mut self) -> Option<&mut T> { self.right.peek_mut() }
しかし、最も興味深いのは、動き回れることです!
pub fn go_left(&mut self) -> bool {
self.left.pop_node().map(|node| {
self.right.push_node(node);
}).is_some()
}
pub fn go_right(&mut self) -> bool {
self.right.pop_node().map(|node| {
self.left.push_node(node);
}).is_some()
}
ここでは、実際に移動できたかどうかを示すための単なる便宜として boolean を返しています。では、こいつをテストしてみましょう。
#[cfg(test)]
mod test {
use super::List;
#[test]
fn walk_aboot() {
let mut list = List::new(); // [_]
list.push_left(0); // [0,_]
list.push_right(1); // [0, _, 1]
assert_eq!(list.peek_left(), Some(&0));
assert_eq!(list.peek_right(), Some(&1));
list.push_left(2); // [0, 2, _, 1]
list.push_left(3); // [0, 2, 3, _, 1]
list.push_right(4); // [0, 2, 3, _, 4, 1]
while list.go_left() {} // [_, 0, 2, 3, 4, 1]
assert_eq!(list.pop_left(), None);
assert_eq!(list.pop_right(), Some(0)); // [_, 2, 3, 4, 1]
assert_eq!(list.pop_right(), Some(2)); // [_, 3, 4, 1]
list.push_left(5); // [5, _, 3, 4, 1]
assert_eq!(list.pop_right(), Some(3)); // [5, _, 4, 1]
assert_eq!(list.pop_left(), Some(5)); // [_, 4, 1]
assert_eq!(list.pop_right(), Some(4)); // [_, 1]
assert_eq!(list.pop_right(), Some(1)); // [_]
assert_eq!(list.pop_right(), None);
assert_eq!(list.pop_left(), None);
}
}
> cargo test
Running target/debug/lists-5c71138492ad4b4a
running 16 tests
test fifth::test::into_iter ... ok
test fifth::test::basics ... ok
test fifth::test::iter ... ok
test fifth::test::iter_mut ... ok
test fourth::test::into_iter ... ok
test fourth::test::basics ... ok
test fourth::test::peek ... ok
test first::test::basics ... ok
test second::test::into_iter ... ok
test second::test::basics ... ok
test second::test::iter ... ok
test second::test::iter_mut ... ok
test third::test::basics ... ok
test third::test::iter ... ok
test second::test::peek ... ok
test silly1::test::walk_aboot ... ok
test result: ok. 16 passed; 0 failed; 0 ignored; 0 measured
これは finger データ構造の極端な例です。このデータ構造では、構造内の何らかの finger を維持し、その結果として、位置に対する操作を finger からの距離に比例した時間でサポートできます。
finger の周辺ではリストに対して非常に高速に変更を加えられますが、finger から遠く離れた場所で変更したい場合は、そこまでずっと歩いていく必要があります。片方のスタックからもう片方のスタックへ要素を移すことで、そこまで恒久的に歩いていくこともできますし、変更を行うために一時的に &mut でリンクをたどるだけにすることもできます。しかし、&mut はリストを上へ戻ることはできませんが、私たちの finger にはそれができます!
スタック割り当てのリンクリスト
この本は主にヒープ割り当てのリンクリストに焦点を当てています。なぜなら、それらが最も一般的で実用的だからです。しかし、ヒープ割り当てを使わなければならないわけではありません。ヒープ割り当ては、メモリを動的に割り当てるのを簡単にしてくれるので便利です。この点でスタック割り当てはあまり扱いやすくありません — C の alloca のようなものは、広く「非常に呪われていて問題が多いもの」と見なされています。
そこで、簡単な方法でスタック上にメモリを割り当てましょう。関数を呼び出して、より多くの領域を持つ新しいスタックフレームを得るのです!これは私たちの問題に対する非常に馬鹿げた解決策ですが、同時に本当に実用的で有用でもあります。これは常に行われていて、実際にはリンクリストとして意識されることすらないかもしれません!
何かを再帰的に行っているときはいつでも、現在のステップの状態へのポインタを次のステップに渡すだけで済みます。そのポインタ自体が状態の一部であるなら、スタック割り当てされたリンクリストを作成したことになります!
もちろん今はこの本の馬鹿げた部分にいるので、これを馬鹿げた方法でやります。リンクリストを主役にし、ユーザーのコードをすべてコールバックの沼に住まわせるのです。みんなネストしたコールバックが大好きです!
私たちの List 型は、別の Node への参照を持つ Node にすぎません。
#![allow(unused)]
fn main() {
pub struct List<'a, T> {
pub data: T,
pub prev: Option<&'a List<'a, T>>,
}
}
そして操作は push ただ 1 つだけで、これは古いリスト、現在のノードの状態、そしてコールバックを受け取ります。新しいリストはコールバック内で生成されます。また、コールバックが任意の値を返せるようにし、push は完了時にそれを返すようにします。
impl<'a, T> List<'a, T> {
pub fn push<U>(
prev: Option<&'a List<'a, T>>,
data: T,
callback: impl FnOnce(&List<'a, T>) -> U,
) -> U {
let list = List { data, prev };
callback(&list)
}
}
これだけです!次のように使えます。
List::push(None, 3, |list| {
println!("{}", list.data);
List::push(Some(list), 5, |list| {
println!("{}", list.data);
List::push(Some(list), 13, |list| {
println!("{}", list.data);
})
})
})
美しいですね。😿
ユーザーはすでに、prev の値をたどるために while-let を使ってこのリストを走査できますが、せっかくなので、いつものようにイテレータを実装してみましょう。
impl<'a, T> List<'a, T> {
pub fn iter(&'a self) -> Iter<'a, T> {
Iter { next: Some(self) }
}
}
impl<'a, T> Iterator for Iter<'a, T> {
type Item = &'a T;
fn next(&mut self) -> Option<Self::Item> {
self.next.map(|node| {
self.next = node.prev;
&node.data
})
}
}
試してみましょう。
#[cfg(test)]
mod test {
use super::List;
#[test]
fn elegance() {
List::push(None, 3, |list| {
assert_eq!(list.iter().copied().sum::<i32>(), 3);
List::push(Some(list), 5, |list| {
assert_eq!(list.iter().copied().sum::<i32>(), 5 + 3);
List::push(Some(list), 13, |list| {
assert_eq!(list.iter().copied().sum::<i32>(), 13 + 5 + 3);
})
})
})
}
}
> cargo test
running 18 tests
test fifth::test::into_iter ... ok
test fifth::test::iter ... ok
test fifth::test::iter_mut ... ok
test fifth::test::basics ... ok
test fifth::test::miri_food ... ok
test first::test::basics ... ok
test second::test::into_iter ... ok
test fourth::test::peek ... ok
test fourth::test::into_iter ... ok
test second::test::iter_mut ... ok
test fourth::test::basics ... ok
test second::test::basics ... ok
test second::test::iter ... ok
test third::test::basics ... ok
test silly1::test::walk_aboot ... ok
test silly2::test::elegance ... ok
test second::test::peek ... ok
test third::test::iter ... ok
test result: ok. 18 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out;
さて、この時点で「ノードに格納されているデータを変更できるのかな?」と思うかもしれません。できるかもしれません!リストが共有参照ではなくミュータブル参照を使うようにしてみましょう。
#![allow(unused)]
fn main() {
pub struct List<'a, T> {
pub data: T,
pub prev: Option<&'a mut List<'a, T>>,
}
pub struct Iter<'a, T> {
next: Option<&'a List<'a, T>>,
}
impl<'a, T> List<'a, T> {
pub fn push<U>(
prev: Option<&'a mut List<'a, T>>,
data: T,
callback: impl FnOnce(&mut List<'a, T>) -> U,
) -> U {
let mut list = List { data, prev };
callback(&mut list)
}
pub fn iter(&'a self) -> Iter<'a, T> {
Iter { next: Some(self) }
}
}
impl<'a, T> Iterator for Iter<'a, T> {
type Item = &'a T;
fn next(&mut self) -> Option<Self::Item> {
self.next.map(|node| {
self.next = node.prev.as_ref().map(|prev| &**prev);
&node.data
})
}
}
}
> cargo test
error[E0521]: borrowed data escapes outside of closure
--> src\silly2.rs:47:32
|
46 | List::push(Some(list), 13, |list| {
| ----
| |
| `list` declared here, outside of the closure body
| `list` is a reference that is only valid in the closure body
47 | assert_eq!(list.iter().copied().sum::<i32>(), 13 + 5 + 3);
| ^^^^^^^^^^^ `list` escapes the closure body here
error[E0521]: borrowed data escapes outside of closure
--> src\silly2.rs:45:28
|
44 | List::push(Some(list), 5, |list| {
| ----
| |
| `list` declared here, outside of the closure body
| `list` is a reference that is only valid in the closure body
45 | assert_eq!(list.iter().copied().sum::<i32>(), 5 + 3);
| ^^^^^^^^^^^ `list` escapes the closure body here
<ad infinitum>
うーん。どうやらイテレータが気に入らないようです。私たちが何か間違えたのでしょうか?確認するために、テストを少し単純化してみましょう。
#[test]
fn elegance() {
List::push(None, 3, |list| {
assert_eq!(list.data, 3);
List::push(Some(list), 5, |list| {
assert_eq!(list.data, 5);
List::push(Some(list), 13, |list| {
assert_eq!(list.data, 13);
})
})
})
}
> cargo test
error[E0521]: borrowed data escapes outside of closure
--> src\silly2.rs:46:17
|
44 | List::push(Some(list), 5, |list| {
| ----
| |
| `list` declared here, outside of the closure body
| `list` is a reference that is only valid in the closure body
45 | assert_eq!(list.data, 5);
46 | / List::push(Some(list), 13, |list| {
47 | | assert_eq!(list.data, 13);
48 | | })
| |______^ `list` escapes the closure body here
error[E0521]: borrowed data escapes outside of closure
--> src\silly2.rs:44:13
|
42 | List::push(None, 3, |list| {
| ----
| |
| `list` declared here, outside of the closure body
| `list` is a reference that is only valid in the closure body
43 | assert_eq!(list.data, 3);
44 | / List::push(Some(list), 5, |list| {
45 | | assert_eq!(list.data, 5);
46 | | List::push(Some(list), 13, |list| {
47 | | assert_eq!(list.data, 13);
48 | | })
49 | | })
| |______________^ `list` escapes the closure body here
うーん、いや、これはまだかなりひどい代物です。
問題は、私たちのリストがうっかり(😉) 分散性に依存していることです。分散性は複雑なトピックですが、ここでは簡略化して見てみましょう。
各リストは、自分自身とまったく同じ型を持つ List への参照を含んでいます。最も内側のリストの視点からすると、それはすべてのリストが自分自身と同じライフタイムを使っていることを意味しますが、これは客観的に誤りです。リスト内の各ノードは、次のノードよりも厳密に長く生存します。なぜなら、それらは文字どおりネストしたスコープ内にあるからです!
では……なぜ共有参照を使っていたときはコードがコンパイルできたのでしょうか? 多くの場合、コンパイラは「長すぎる」寿命を持つものがあっても安全だと知っているからです! リストへの参照を次のリストに詰め込むとき、コンパイラはライフタイムを静かに「縮めて」、新しいリストが期待するものに合うようにしています。このライフタイムの縮小が分散性です。
これは、継承のある言語で、Animal(Cat のスーパータイプ)が期待される場所に Cat を渡せるのとまったく同じトリックです。直感的には、Animal が期待されているときに Cat を渡しても問題ないことが分かります。Cat は単に Animal であり、さらにそれ以上だからです。しばらく「さらにそれ以上」の部分を忘れても問題ありませんよね?
同様に、より大きなライフタイムは、より小さなライフタイムであり、さらにそれ以上です。ですから、ここでも「さらにそれ以上」を忘れて構いません!
しかしもちろん、今あなたは疑問に思っているでしょう。ではなぜ可変参照版は動かないのか!?
さて、分散性は常に安全とは限りません。もし私たちのコードがコンパイルできたなら、次のような use-after-free を書けてしまいます。
List::push(None, 3, |list| {
List::push(Some(list), 5, |list| {
List::push(Some(list), 13, |list| {
// ハハハ、すべてのライフタイムは同じだから、コンパイラは
// 親を書き換えて、自分自身への可変参照を持たせることを許してくれる!
// use-after-free を作り放題だ!!
*list.prev.as_mut().unwrap().prev = Some(list);
})
})
})
詳細を忘れることの問題は、どこか別の場所がその詳細を覚えていて、 それが真であり続けることを期待しているかもしれないことです。これは、変更を導入すると 非常に大きな問題になります。注意しないと、私たちが捨てた「さらにそれ以上」を 覚えていないコードが、「覚えていて」かつ「さらにそれ以上」がまだそこにあることを期待している 場所へ書き込んでも問題ない、と考えてしまうかもしれません。
継承の観点で言うと、このコードは不正でなければなりません。
let mut my_kitty = Cat; // Cat を作る(長いライフタイム)
let animal: &mut Animal = &mut my_kitty; // それが Cat であることを忘れる(ライフタイムを縮める)
*animal = Dog; // Dog を書き込む(短いライフタイム)
my_kitty.meow(); // 鳴く Dog!(Use After Free)
そのため、可変参照のライフタイムを縮めること自体は可能ですが、それらをネストし始めると、物事は「不変」になり、もはやライフタイムを縮めることは許されません。
具体的には、&mut &'big mut T を &mut &'small mut T に変換することはできません。ここで 'big は 'small よりも大きいものとします。より形式的に言えば、&'a mut T は 'a に関しては共変ですが、T に関しては不変です。
面白い事実として、Java は実際にはこの種のことを特に許可していますが、鳴く犬を防ぐために実行時チェックを行います。
では、データを変更するにはどうすればよいのでしょうか? 内部可変性を使います! これにより、コンパイラに対して、私たちはデータを変更できるようにしたいだけで、参照には触れないと伝えられます。
共有参照を使っていた以前のバージョンのコードに戻し、新しいテストで Cell を使うだけで済みます。
#[test]
fn cell() {
use std::cell::Cell;
List::push(None, Cell::new(3), |list| {
List::push(Some(list), Cell::new(5), |list| {
List::push(Some(list), Cell::new(13), |list| {
// リスト内のすべての値を 10 倍する
for val in list.iter() {
val.set(val.get() * 10)
}
let mut vals = list.iter();
assert_eq!(vals.next().unwrap().get(), 130);
assert_eq!(vals.next().unwrap().get(), 50);
assert_eq!(vals.next().unwrap().get(), 30);
assert_eq!(vals.next(), None);
assert_eq!(vals.next(), None);
})
})
})
}
> cargo test
running 19 tests
test fifth::test::into_iter ... ok
test fifth::test::basics ... ok
test fifth::test::iter_mut ... ok
test fifth::test::iter ... ok
test fourth::test::basics ... ok
test fourth::test::into_iter ... ok
test second::test::into_iter ... ok
test first::test::basics ... ok
test fourth::test::peek ... ok
test second::test::basics ... ok
test fifth::test::miri_food ... ok
test silly2::test::cell ... ok
test third::test::iter ... ok
test second::test::iter_mut ... ok
test second::test::peek ... ok
test silly1::test::walk_aboot ... ok
test silly2::test::elegance ... ok
test third::test::basics ... ok
test second::test::iter ... ok
test result: ok. 19 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out;
再帰的なパイのように簡単ですね! ✨