スマートポインタを通常の参照のように扱う
Deref トレイトを実装すると、デリファレンス演算子 * の動作をカスタマイズできます(乗算演算子やグロブ演算子と混同しないでください)。スマートポインタが通常の参照のように扱えるような形で Deref を実装すれば、参照に対して動作するコードを書き、そのコードをスマートポインタに対しても使えるようになります。
まず、通常の参照に対してデリファレンス演算子がどのように動作するのかを見ていきましょう。次に、Box<T> のように振る舞う独自の型を定義し、新しく定義した型に対してデリファレンス演算子が参照と同じようには機能しない理由を確認します。さらに、Deref トレイトを実装することで、スマートポインタが参照に似た方法で動作できるようになる仕組みを見ていきます。その後、Rust の deref coercion 機能と、それによって参照とスマートポインタのどちらも扱えるようになる方法を見ていきます。
参照をたどって値に到達する
通常の参照はポインタの一種であり、ポインタは別の場所に格納されている値を指し示す矢印のようなものだと考えることができます。リスト 15-6 では、i32 の値への参照を作成し、その後デリファレンス演算子を使って参照をたどり、その値に到達します。
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 を使って参照をたどり、それが指している値に到達する必要があります(したがって、dereference です)。そうすることで、コンパイラは実際の値を比較できます。y をデリファレンスすると、y が指している整数値にアクセスでき、それを 5 と比較できます。
代わりに assert_eq!(5, y); と書こうとすると、次のコンパイルエラーが発生します。
$ cargo run
Compiling deref-example v0.1.0 (file:///projects/deref-example)
error[E0277]: can't compare `{integer}` with `&{integer}`
--> src/main.rs:6:5
|
6 | assert_eq!(5, y);
| ^^^^^^^^^^^^^^^^ no implementation for `{integer} == &{integer}`
|
= help: the trait `PartialEq<&{integer}>` is not implemented for `{integer}`
= note: this error originates in the macro `assert_eq` (in Nightly builds, run with -Z macro-backtrace for more info)
For more information about this error, try `rustc --explain E0277`.
error: could not compile `deref-example` (bin "deref-example") due to 1 previous error
数値と、その数値への参照は型が異なるため、比較することはできません。参照が指している値に到達するには、デリファレンス演算子を使う必要があります。
Box<T> を参照のように使う
リスト 15-6 のコードは、参照の代わりに Box<T> を使うように書き換えることができます。リスト 15-7 で Box<T> に対して使われているデリファレンス演算子は、リスト 15-6 で参照に対して使われていたものと同じように機能します。
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> 型に似たラッパー型を作ってみましょう。その後、デリファレンス演算子を使えるようにする方法を見ていきます。
注: これから作る
MyBox<T>型と実際のBox<T>には、大きな違いが 1 つあります。私たちのバージョンは、そのデータをヒープに格納しません。この例ではDerefに焦点を当てているため、データが実際にどこに格納されるかは、ポインタのような振る舞いほど重要ではありません。
Box<T> 型は最終的には 1 つの要素を持つタプル構造体として定義されているため、リスト 15-8 でも同じように MyBox<T> 型を定義します。また、Box<T> に定義されている new 関数に対応する new 関数も定義します。
struct MyBox<T>(T);
impl<T> MyBox<T> {
fn new(x: T) -> MyBox<T> {
MyBox(x)
}
}
fn main() {}
MyBox という名前の構造体を定義し、任意の型の値を保持できるように、ジェネリックパラメータ T を宣言しています。MyBox 型は、型 T の 1 つの要素を持つタプル構造体です。MyBox::new 関数は、型 T の引数を 1 つ受け取り、その渡された値を保持する MyBox インスタンスを返します。
それでは、リスト 15-7 の main 関数をリスト 15-8 に追加し、Box<T> の代わりに、私たちが定義した MyBox<T> 型を使うように変更してみましょう。リスト 15-9 のコードはコンパイルされません。なぜなら、Rust は MyBox をどのようにデリファレンスすればよいかを知らないからです。
struct MyBox<T>(T);
impl<T> MyBox<T> {
fn new(x: T) -> MyBox<T> {
MyBox(x)
}
}
fn main() {
let x = 5;
let y = MyBox::new(x);
assert_eq!(5, x);
assert_eq!(5, *y);
}
その結果のコンパイルエラーは次のとおりです。
$ cargo run
Compiling deref-example v0.1.0 (file:///projects/deref-example)
error[E0614]: type `MyBox<{integer}>` cannot be dereferenced
--> src/main.rs:14:19
|
14 | assert_eq!(5, *y);
| ^^ can't be dereferenced
For more information about this error, try `rustc --explain E0614`.
error: could not compile `deref-example` (bin "deref-example") due to 1 previous error
MyBox<T> 型は、その機能を型に実装していないため、デリファレンスできません。* 演算子によるデリファレンスを有効にするには、Deref トレイトを実装します。
Deref トレイトを実装する
第 10 章の [「型にトレイトを実装する」][impl-trait] で説明したように、トレイトを実装するには、そのトレイトが要求するメソッドの実装を提供する必要があります。標準ライブラリが提供する Deref トレイトでは、self を借用し、内部のデータへの参照を返す deref という名前のメソッドを 1 つ実装する必要があります。リスト 15-10 には、MyBox<T> の定義に追加する Deref の実装が含まれています。
use std::ops::Deref;
impl<T> Deref for MyBox<T> {
type Target = T;
fn deref(&self) -> &Self::Target {
&self.0
}
}
struct MyBox<T>(T);
impl<T> MyBox<T> {
fn new(x: T) -> MyBox<T> {
MyBox(x)
}
}
fn main() {
let x = 5;
let y = MyBox::new(x);
assert_eq!(5, x);
assert_eq!(5, *y);
}
type Target = T; という構文は、Deref トレイトが使う関連型を定義しています。関連型は、ジェネリックパラメータを宣言するための少し異なる方法ですが、今のところ気にする必要はありません。これについては第 20 章でさらに詳しく扱います。
`deref`メソッドの本体を`&self.0`で埋めることで、`deref`は`*`演算子でアクセスしたい値への参照を返すようになります。[第5章の[「タプル構造体で異なる型を作成する」][tuple-structs]]<!--
無視 -->で見たように、`.0`はタプル構造体の最初の値にアクセスします。これで、リスト15-9の`MyBox<T>`値に対して`*`を呼び出している`main`関数はコンパイルされ、アサーションも通ります!
`Deref`トレイトがなければ、コンパイラがデリファレンスできるのは`&`参照だけです。`deref`メソッドは、`Deref`を実装する任意の型の値を受け取り、`deref`メソッドを呼び出して、コンパイラがデリファレンス方法を知っている参照を得る能力をコンパイラに与えます。
リスト15-9で`*y`と入力したとき、Rustは実際には裏で次のコードを実行していました。
```rust,ignore
*(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トレイトを実装した型への参照を、別の型への参照に変換します。たとえば、Stringは&strを返すようにDerefトレイトを実装しているので、Deref型強制は&Stringを&strに変換できます。Deref型強制は、Rustが関数やメソッドへの引数に対して行う便利な機能であり、Derefトレイトを実装している型に対してのみ働きます。これは、ある特定の型の値への参照を、関数またはメソッドの定義にあるパラメータ型と一致しない形で引数として渡したときに自動的に発生します。derefメソッドの呼び出しを連続して行うことで、私たちが渡した型が、パラメータが必要とする型へと変換されます。
Deref型強制は、関数呼び出しやメソッド呼び出しを書くプログラマが、&や*による明示的な参照やデリファレンスをそれほど多く追加しなくて済むようにするためにRustに追加されました。Deref型強制の機能により、参照でもスマートポインタでも動作する、より多くのコードを書けるようにもなります。
Deref型強制が実際にどう動くかを見るために、リスト15-8で定義したMyBox<T>型と、リスト15-10で追加したDerefの実装を使いましょう。リスト15-11は、文字列スライスをパラメータに取る関数の定義を示しています。
fn hello(name: &str) {
println!("Hello, {name}!");
}
fn main() {}
たとえば、引数として文字列スライスを使ってhello関数を呼び出すことができ、hello("Rust");のように書けます。Deref型強制により、リスト15-12に示すように、MyBox<String>型の値への参照を使ってhelloを呼び出すことも可能になります。
use std::ops::Deref;
impl<T> Deref for MyBox<T> {
type Target = T;
fn deref(&self) -> &T {
&self.0
}
}
struct MyBox<T>(T);
impl<T> MyBox<T> {
fn new(x: T) -> MyBox<T> {
MyBox(x)
}
}
fn hello(name: &str) {
println!("Hello, {name}!");
}
fn main() {
let m = MyBox::new(String::from("Rust"));
hello(&m);
}
ここでは、引数&mでhello関数を呼び出しています。これはMyBox<String>値への参照です。リスト15-10でMyBox<T>に対してDerefトレイトを実装したので、Rustはderefを呼び出すことで&MyBox<String>を&Stringに変換できます。標準ライブラリはStringに対するDerefの実装を提供しており、それは文字列スライスを返します。このことはDerefのAPIドキュメントにあります。Rustはさらにもう一度derefを呼び出して&Stringを&strに変換し、これがhello関数の定義に一致します。
もしRustがDeref型強制を実装していなければ、&MyBox<String>型の値を使ってhelloを呼び出すには、リスト15-12のコードではなく、リスト15-13のコードを書かなければならないでしょう。
use std::ops::Deref;
impl<T> Deref for MyBox<T> {
type Target = T;
fn deref(&self) -> &T {
&self.0
}
}
struct MyBox<T>(T);
impl<T> MyBox<T> {
fn new(x: T) -> MyBox<T> {
MyBox(x)
}
}
fn hello(name: &str) {
println!("Hello, {name}!");
}
fn main() {
let m = MyBox::new(String::from("Rust"));
hello(&(*m)[..]);
}
(*m)はMyBox<String>をデリファレンスしてStringにします。次に、&と[..]はString全体に等しいStringの文字列スライスを取り出し、helloのシグネチャに一致させます。Deref型強制がないこのコードは、これらすべての記号が関わるため、読むのも、書くのも、理解するのも難しくなります。Deref型強制により、Rustはこれらの変換を自動的に処理してくれます。
関係する型に対してDerefトレイトが定義されている場合、Rustは型を解析し、パラメータの型に一致する参照を得るために必要な回数だけDeref::derefを使います。Deref::derefを何回挿入する必要があるかはコンパイル時に解決されるため、Deref型強制を活用しても実行時のペナルティはありません!
可変参照に対するDeref型強制の扱い
不変参照に対してDerefトレイトを使って*演算子をオーバーライドするのと同様に、可変参照に対してDerefMutトレイトを使って*演算子をオーバーライドできます。
Rustは、次の3つのケースで型とトレイト実装を見つけるとDeref型強制を行います。
T: Deref<Target=U>のとき、&Tから&UへT: DerefMut<Target=U>のとき、&mut Tから&mut UへT: Deref<Target=U>のとき、&mut Tから&Uへ
最初の2つのケースは、2番目が可変性を実装するという点を除けば同じです。最初のケースは、&Tを持っていて、Tが何らかの型UへのDerefを実装しているなら、透過的に&Uを得られることを述べています。2番目のケースは、同じDeref型強制が可変参照でも起こることを述べています。
3つ目のケースはさらに厄介です。Rust は可変参照を不変参照にも
型強制します。しかし、逆は _不可能_ です。不変参照が可変参照に
型強制されることは決してありません。借用規則のため、可変参照を
持っているなら、その可変参照はそのデータへの唯一の参照でなければ
なりません(そうでなければ、プログラムはコンパイルされません)。
1つの可変参照を1つの不変参照に変換しても、借用規則が破られることは
決してありません。不変参照を可変参照に変換するには、最初の不変参照がその
データへの唯一の不変参照である必要がありますが、借用規則はそれを
保証しません。したがって、Rust は不変参照を可変参照に変換できると
仮定することはできません。
[impl-trait]: ch10-02-traits.html#implementing-a-trait-on-a-type
[tuple-structs]: ch05-01-defining-structs.html#creating-different-types-with-tuple-structs