ピーク
さて、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
素晴らしい!