Derefトレイトでスマートポインタを普通の参照のように扱う
Derefトレイトを実装することで参照外し演算子の*(掛け算やグロブ演算子とは対照的に)の振る舞いをカスタマイズすることができます。
スマートポインタを普通の参照のように扱えるようにDerefを実装することで、
参照に対して処理を行うコードを書き、そのコードをスマートポインタとともに使用することもできます。
まずは、参照外し演算子が普通の参照に対して動作するところを見ましょう。それからBox<T>のように振る舞う独自の型を定義し、
参照外し演算子が新しく定義した型に対して参照のように動作しない理由を確認しましょう。
Derefトレイトを実装することでスマートポインタが参照と似た方法で動作するようにできる方法を探求します。
そして、Rustの参照外し型強制機能と、それにより参照やスマートポインタに取り掛かる方法を見ます。
参照外し演算子で値までポインタを追いかける
普通の参照は1種のポインタであり、ポインタの捉え方の一つが、どこか他の場所に格納された値への矢印としてです。
リスト15-6で、i32値への参照を生成し、それから参照外し演算子を使用して参照をデータまで追いかけています:
ファイル名: src/main.rs
fn main() { let x = 5; let y = &x; assert_eq!(5, x); assert_eq!(5, *y); }
リスト15-6: 参照外し演算子を使用して参照をi32値まで追いかける
変数xはi32値の5を保持しています。yをxへの参照にセットします。xは5に等しいとアサートできます。
しかしながら、yの値に関するアサートを行いたい場合、*yを使用して参照を指している値まで追いかけなければなりません(そのため参照外しです)。
一旦、yを参照外ししたら、yが指している5と比較できる整数値にアクセスできます。
代わりにassert_eq!(5, y);と書こうとしたら、こんなコンパイルエラーが出るでしょう:
error[E0277]: the trait bound `{integer}: std::cmp::PartialEq<&{integer}>` is
not satisfied
(エラー: トレイト境界`{integer}: std::cmp::PartialEq<&{integer}>`は満たされていません)
--> src/main.rs:6:5
|
6 | assert_eq!(5, y);
| ^^^^^^^^^^^^^^^^^ can't compare `{integer}` with `&{integer}`
|
= help: the trait `std::cmp::PartialEq<&{integer}>` is not implemented for
`{integer}`
(助言: トレイト`std::cmp::PartialEq<&{integer}>`は`{integer}`に対して実装されていません)
参照と数値は異なる型なので、比較することは許容されていません。参照外し演算子を使用して、 参照を指している値まで追いかけなければならないのです。
Box<T>を参照のように使う
リスト15-6のコードを参照の代わりにBox<T>を使うように書き直すことができます;
参照外し演算子は、リスト15-7に示したように動くでしょう:
ファイル名: src/main.rs
fn main() { let x = 5; let y = Box::new(x); assert_eq!(5, x); assert_eq!(5, *y); }
リスト15-7: Box<i32>に対して参照外し演算子を使用する
リスト15-7とリスト15-6の唯一の違いは、ここでは、xの値を指す参照ではなく、
xの値を指すボックスのインスタンスにyをセットしていることです。
最後のアサートで参照外し演算子を使用してyが参照だった時のようにボックスのポインタを追いかけることができます。
次に、独自のボックス型を定義することで参照外し演算子を使用させてくれるBox<T>について何が特別なのかを探求します。
独自のスマートポインタを定義する
標準ライブラリが提供しているBox<T>型に似たスマートポインタを構築して、スマートポインタは規定で、
どう異なって参照に比べて振る舞うのか経験しましょう。それから、参照外し演算子を使う能力を追加する方法に目を向けましょう。
Box<T>型は究極的に1要素のタプル構造体として定義されているので、リスト15-8は、同じようにMyBox<T>型を定義しています。
また、Box<T>に定義されたnew関数と合致するnew関数も定義しています。
ファイル名: src/main.rs
# #![allow(unused_variables)] #fn main() { struct MyBox<T>(T); impl<T> MyBox<T> { fn new(x: T) -> MyBox<T> { MyBox(x) } } #}
リスト15-8: MyBox<T>型を定義する
MyBoxという構造体を定義し、ジェネリック引数のTを宣言しています。自分の型にどんな型の値も保持させたいからです。
MyBox型は、型Tを1要素持つタプル構造体です。MyBox::new関数は型Tの引数を1つ取り、
渡した値を保持するMyBoxインスタンスを返します。
試しにリスト15-7のmain関数をリスト15-8に追加し、Box<T>の代わりに定義したMyBox<T>型を使うよう変更してみてください。
コンパイラはMyBoxを参照外しする方法がわからないので、リスト15-9のコードはコンパイルできません。
ファイル名: src/main.rs
fn main() {
let x = 5;
let y = MyBox::new(x);
assert_eq!(5, x);
assert_eq!(5, *y);
}
リスト15-9: 参照とBox<T>を使ったのと同じようにMyBox<T>を使おうとする
こちらが結果として出るコンパイルエラーです:
error[E0614]: type `MyBox<{integer}>` cannot be dereferenced
(エラー: 型`MyBox<{integer}>`は参照外しできません)
--> src/main.rs:14:19
|
14 | assert_eq!(5, *y);
| ^^
MyBox<T>に参照外しの能力を実装していないので、参照外しできません。*演算子で参照外しできるようにするには、
Derefトレイトを実装します。
Derefトレイトを実装して型を参照のように扱う
第10章で議論したように、トレイトを実装するには、トレイトの必須メソッドに実装を提供する必要があります。
Derefトレイトは標準ライブラリで提供されていますが、selfを借用し、
内部のデータへの参照を返すderefという1つのメソッドを実装する必要があります。リスト15-10には、
MyBoxの定義に追記するDerefの実装が含まれています:
ファイル名: src/main.rs
# #![allow(unused_variables)] #fn main() { use std::ops::Deref; # struct MyBox<T>(T); impl<T> Deref for MyBox<T> { type Target = T; fn deref(&self) -> &T { &self.0 } } #}
リスト15-10: MyBox<T>にDerefを実装する
type Target = T;という記法は、Derefトレイトが使用する関連型を定義しています。関連型は、
ジェネリック引数を宣言する少しだけ異なる方法ですが、今は気にする必要はありません; 第19章でより詳しく講義します。
derefメソッドの本体を&self.0で埋めているので、derefは*演算子でアクセスしたい値への参照を返します。
リスト15-9のMyBox<T>に*を呼び出すmain関数はこれでコンパイルでき、アサートも通ります!
Derefがなければ、コンパイラは&参照しか参照外しできなくなります。derefメソッドによりコンパイラは、
Derefを実装するあらゆる型の値を取り、derefメソッドを呼び出して参照外しの仕方を知っている&参照を得る能力を獲得するのです。
リスト15-9に*yを入力した時、水面下でコンパイラは、実際にはこのようなコードを走らせていました:
*(y.deref())
コンパイラは、*演算子をderefメソッド、それから何の変哲もない参照外しの呼び出しに置き換えるので、
derefメソッドを呼び出す必要があるかどうかを考える必要はないわけです。このRustの機能により、
普通の参照かDerefを実装した型があるかどうかと等しく機能するコードを書くことができます。
derefメソッドが値への参照を返し、*(y.deref())のかっこの外の何の変哲もない参照外しがそれでも必要な理由は、
所有権システムです。derefメソッドが値への参照ではなく、値を直接返したら、値はselfから外にムーブされてしまいます。
今回の場合や、参照外し演算子を使用する多くの場合にはMyBox<T>の中の値の所有権を奪いたくはありません。
*演算子はderefメソッドの呼び出し1回とコードで*打つたび、ただ1回の*演算子の呼び出しに置き換えられることに注意してください。
*演算子の置き換えは、無限に繰り返されないので、型i32に行き着き、リスト15-9でassert_eq!の5と合致します。
関数やメソッドで暗黙的な参照外し型強制
参照外し型強制は、コンパイラが関数やメソッドの実引数に行う便利なものです。参照外し型強制は、
Derefを実装する型への参照をDerefが元の型を変換できる型への参照に変換します。参照外し型強制は、
特定の型の値への参照を関数やメソッド定義の引数型と一致しない引数として関数やメソッドに渡すときに自動的に発生します。
一連のderefメソッドの呼び出しが、提供した型を引数が必要とする型に変換します。
参照外し型強制は、関数やメソッド呼び出しを書くプログラマが&や*で多くの明示的な参照や参照外しとして追加する必要がないように、
Rustに追加されました。また、参照外し型強制のおかげで参照あるいはスマートポインタのどちらかで動くコードをもっと書くことができます。
参照外し型強制が実際に動いていることを確認するため、リスト15-8で定義したMyBox<T>と、
リスト15-10で追加したDerefの実装を使用しましょう。リスト15-11は、
文字列スライス引数のある関数の定義を示しています:
ファイル名: src/main.rs
# #![allow(unused_variables)] #fn main() { fn hello(name: &str) { println!("Hello, {}!", name); } #}
リスト15-11: 型&strの引数nameのあるhello関数
hello関数は、文字列スライスを引数として呼び出すことができます。例えば、hello("Rust")などです。
参照外し型強制により、helloを型MyBox<String>の値への参照とともに呼び出すことができます。リスト15-12のようにですね:
ファイル名: src/main.rs
# use std::ops::Deref; # # struct MyBox<T>(T); # # impl<T> MyBox<T> { # fn new(x: T) -> MyBox<T> { # MyBox(x) # } # } # # impl<T> Deref for MyBox<T> { # type Target = T; # # fn deref(&self) -> &T { # &self.0 # } # } # # fn hello(name: &str) { # println!("Hello, {}!", name); # } # fn main() { let m = MyBox::new(String::from("Rust")); hello(&m); }
リスト15-12: helloをMyBox<String>値とともに呼び出し、参照外し型強制のおかげで動く
ここで、hello関数を引数&mとともに呼び出しています。この引数は、MyBox<String>値への参照です。
リスト15-10でMyBox<T>にDerefトレイトを実装したので、コンパイラはderefを呼び出すことで、
&MyBox<String>を&Stringに変換できるのです。標準ライブラリは、Stringに文字列スライスを返すDerefの実装を提供していて、
この実装は、DerefのAPIドキュメンテーションに載っています。コンパイラはさらにderefを呼び出して、
&Stringを&strに変換し、これはhello関数の定義と合致します。
Rustに参照外し型強制が実装されていなかったら、リスト15-12のコードの代わりにリスト15-13のコードを書き、
型&MyBox<String>の値でhelloを呼び出さなければならなかったでしょう。
ファイル名: src/main.rs
# use std::ops::Deref; # # struct MyBox<T>(T); # # impl<T> MyBox<T> { # fn new(x: T) -> MyBox<T> { # MyBox(x) # } # } # # impl<T> Deref for MyBox<T> { # type Target = T; # # fn deref(&self) -> &T { # &self.0 # } # } # # fn hello(name: &str) { # println!("Hello, {}!", name); # } # fn main() { let m = MyBox::new(String::from("Rust")); hello(&(*m)[..]); }
リスト15-13: Rustに参照外し型強制がなかった場合に書かなければならないであろうコード
(*m)がMyBox<String>をStringに参照外ししています。そして、&と[..]により、
文字列全体と等しいStringの文字列スライスを取り、helloのシグニチャと一致するわけです。
参照外し型強制のないコードは、これらの記号が関係するので、読むのも書くのも理解するのもより難しくなります。
参照外し型強制により、コンパイラはこれらの変換を自動的に扱えるのです。
Derefトレイトが関係する型に定義されていると、コンパイラは、型を分析し必要なだけDeref::derefを使用して、
参照を得、引数の型と一致させます。Deref::derefが挿入される必要のある回数は、コンパイル時に解決されるので、
参照外し型強制を活用する実行時の代償は何もありません。
参照外し型強制が可変性と相互作用する方法
Derefトレイトを使用して不変参照に対して*をオーバーライドするように、R
DerefMutトレイトを使用して可変参照の*演算子をオーバーライドできます。
以下の3つの場合に型やトレイト実装を見つけた時にコンパイラは、参照外し型強制を行います:
T: Deref<Target=U>の時、&Tから&UT: DerefMut<Target=U>の時、&mut Tから&mut UT: Deref<Target=U>の時、&mut Tから&U
前者2つは、可変性を除いて一緒です。最初のケースは、&Tがあり、Tが何らかの型UへのDerefを実装しているなら、
透過的に&Uを得られると述べています。2番目のケースは、同じ参照外し型強制が可変参照についても起こることを述べています。
3番目のケースはもっと巧妙です: Rustはさらに、可変参照を不変参照にも型強制するのです。ですが、逆はできません: 不変参照は、絶対に可変参照に型強制されないのです。借用ルールにより、可変参照があるなら、 その可変参照がそのデータへの唯一の参照に違いありません(でなければ、プログラムはコンパイルできません)。 1つの可変参照を1つの不変参照に変換することは、借用ルールを絶対に破壊しません。 不変参照を可変参照にするには、そのデータへの不変参照がたった1つしかないことが必要ですが、 借用ルールはそれを保証してくれません。故に、不変参照を可変参照に変換することが可能であるという前提を敷けません。