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の唯一の違いは、ここでは、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) } } #}
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);
}
こちらが結果として出るコンパイルエラーです:
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 } } #}
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>
の中の値の所有権を奪いたくはありません。
*
演算子は、コードで*
を打つたびに、ただ1回、deref
メソッドの呼び出し、そして*
演算子の呼び出しに置き換えられることに注意してください。
*
演算子の置き換えは、無限に繰り返されないので、型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); } #}
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つしかないことが必要ですが、 借用規則はそれを保証してくれません。故に、不変参照を可変参照に変換することが可能であるという前提を敷けません。