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); }
変数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とリスト15-6の唯一の違いは、ここではy
が、x
の値を指す参照ではなく、
x
の値を指すボックスのインスタンスとして設定されている点にあります。
最後のアサートでは、参照外し演算子を使ってボックスのポインタを辿ることができます。これはy
が参照だった時と同じやり方です。
参照外し演算子が使える以上Box<T>
には特別な何かがあるので、次はそれについて調べることにします。そのために、独自にボックス型を定義します。
独自のスマートポインタを定義する
標準ライブラリが提供しているBox<T>
型に似たスマートポインタを作りましょう。そうすれば、スマートポインタがそのままだと
参照と同じ様には振る舞わないことがわかります。それから、どうすれば参照外し演算子を使えるようになるのか見てみましょう。
Box<T>
型は突き詰めると(訳註:データがヒープに置かれることを無視すると)1要素のタプル構造体のような定義になります。なのでリスト15-8ではそのようにMyBox<T>
型を定義しています。
また、Box<T>
に定義されたnew
関数に対応するnew
関数も定義しています。
ファイル名: src/main.rs
#![allow(unused)] fn main() { struct MyBox<T>(T); impl<T> MyBox<T> { fn new(x: T) -> MyBox<T> { MyBox(x) } } }
MyBox
という構造体を定義し、ジェネリック引数のT
を宣言しています。この型にどんな型の値も持たせたいからです。
MyBox
型は型T
の要素を1つ持つタプル構造体です。MyBox::new
関数は型T
の引数を1つ取り、
渡した値を持つMyBox
のインスタンスを返します。
試しにリスト15-7のmain
関数をリスト15-8に追加し、定義したMyBox<T>
型をBox<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);
}
こちらが結果として出るコンパイルエラーです。
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
トレイトは標準ライブラリで提供されており、deref
という1つのメソッドの実装を要求します。deref
はself
を借用し、
内部のデータへの参照を返すメソッドです。
リスト15-10には、MyBox
の定義に付け足すDeref
の実装が含まれています。
ファイル名: src/main.rs
#![allow(unused)] 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 } } }
type Target = T;
という記法は、Deref
トレイトが使用する関連型を定義しています。関連型はまた少し違ったやり方でジェネリック引数を宣言するためのものですが、今は気にする必要はありません。第19章でより詳しく扱います。
deref
メソッドの本体は&self.0
だけなので、deref
が返すのは私達が*
演算子でアクセスしたい値への参照なわけです。
リスト15-9のMyBox<T>
に*
を呼び出すmain
関数はこれでコンパイルでき、アサートも通ります!
Deref
トレイトがないと、コンパイラは&
参照しか参照外しできません。
deref
メソッドのおかげで、コンパイラはDeref
を実装している型の値を取り、deref
メソッドを呼ぶことで、参照外しが可能な&
参照を得られるようになります。
リスト15-9に*y
を入力した時、水面下でRustは実際にはこのようなコードを走らせていました。
*(y.deref())
Rustが*
演算子をderef
メソッドの呼び出しと普通の参照外しへと置き換えてくれるので、
私達はderef
メソッドを呼び出す必要があるかどうかを考えなくて済むわけです。このRustの機能により、
普通の参照かDeref
を実装した型であるかどうかに関わらず、等しく機能するコードを書くことができます。
deref
メソッドが値への参照を返し、*(y.deref())
のかっこの外にある普通の参照外しがそれでも必要になるのは、
所有権システムがあるからです。deref
メソッドが値への参照ではなく値を直接返したら、値はself
から外にムーブされてしまいます。
今回もそうですが、参照外し演算子を使用するときはほとんどの場合、MyBox<T>
の中の値の所有権を奪いたくはありません。
*
演算子がderef
メソッドの呼び出しと*
演算子の呼び出しに置き換えられるのは、コード内で*
を打つ毎にただ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)] fn main() { fn hello(name: &str) { println!("Hello, {}!", name); } }
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); }
ここで、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)[..]); }
(*m)
がMyBox<String>
をString
に参照外ししています。そして、&
と[..]
により、
文字列全体と等しいString
の文字列スライスを取り、hello
のシグニチャと一致するわけです。
参照外し型強制のないコードは、これらの記号が関係するので、読むのも書くのも理解するのもより難しくなります。
参照外し型強制により、コンパイラはこれらの変換を自動的に扱えるのです。
Deref
トレイトが関係する型に定義されていると、コンパイラは、型を分析し必要なだけDeref::deref
を使用して、
参照を得、引数の型と一致させます。Deref::deref
が挿入される必要のある回数は、コンパイル時に解決されるので、
参照外し型強制を活用するための実行時の代償は何もありません。
参照外し型強制が可変性と相互作用する方法
Deref
トレイトを使用して不変参照に対して*
をオーバーライドするように、
DerefMut
トレイトを使用して可変参照の*
演算子をオーバーライドできます。
以下の3つの場合に型やトレイト実装を見つけた時にコンパイラは、参照外し型強制を行います:
T: Deref<Target=U>
の時、&T
から&U
T: DerefMut<Target=U>
の時、&mut T
から&mut U
T: Deref<Target=U>
の時、&mut T
から&U
前者2つは、可変性を除いて一緒です。最初のケースは、&T
があり、T
が何らかの型U
へのDeref
を実装しているなら、
透過的に&U
を得られると述べています。2番目のケースは、同じ参照外し型強制が可変参照についても起こることを述べています。
3番目のケースはもっと巧妙です: Rustはさらに、可変参照を不変参照にも型強制するのです。ですが、逆はできません: 不変参照は、絶対に可変参照に型強制されないのです。借用規則により、可変参照があるなら、 その可変参照がそのデータへの唯一の参照に違いありません(でなければ、プログラムはコンパイルできません)。 1つの可変参照を1つの不変参照に変換することは、借用規則を絶対に破壊しません。 不変参照を可変参照にするには、そのデータへの不変参照がたった1つしかないことが必要ですが、 借用規則はそれを保証してくれません。故に、不変参照を可変参照に変換することが可能であるという前提を敷けません。