はじめに
Deref を使って通常の参照と同じようにスマートポインタを扱うへようこそ。この実験は、Rust Bookの一部です。LabEx で Rust のスキルを練習することができます。
この実験では、Deref トレイトを実装することでスマートポインタを通常の参照と同じように扱う方法と、Rust の deref 強制機能が参照またはスマートポインタのどちらでも動作するようにする方法を探ります。
Deref を使って通常の参照と同じようにスマートポインタを扱う
Derefトレイトを実装することで、参照演算子*の動作をカスタマイズできます(乗算演算子やグロブ演算子と混同しないでください)。スマートポインタを通常の参照と同じように扱えるようにDerefを実装することで、参照で動作するコードを書き、それをスマートポインタでも使うことができます。
まず、通常の参照で参照演算子がどのように機能するか見てみましょう。次に、Box<T>と同じように動作するカスタム型を定義し、新しく定義した型で参照と同じように参照演算子が機能しない理由を見てみましょう。Derefトレイトを実装することでスマートポインタが参照と同じように動作するようになる方法を探ります。そして、Rust のderef 強制機能と、それが参照またはスマートポインタのどちらでも動作させる方法を見てみましょう。
注:これから作成する
MyBox<T>型と本物のBox<T>には大きな違いがあります。私たちのバージョンはデータをヒープに格納しません。この例ではDerefに焦点を当てているため、データが実際に格納されている場所は、ポインタのような動作よりも重要ではありません。
ポインタを辿って値を取得する
通常の参照はポインタの一種であり、ポインタを考える一つの方法は、他の場所に格納されている値への矢印として考えることです。リスト 15-6 では、i32型の値に対する参照を作成し、その参照を辿って値を取得するために参照演算子を使用しています。
ファイル名:src/main.rs
fn main() {
1 let x = 5;
2 let y = &x;
3 assert_eq!(5, x);
4 assert_eq!(5, *y);
}
リスト 15-6: 参照演算子を使ってi32型の値に対する参照を辿る
変数xはi32型の値5を保持しています[1]。yをxへの参照に等しく設定します[2]。xが5に等しいことをアサートできます[3]。しかし、yに格納されている値についてアサートを行う場合、参照演算子*yを使用して参照先の値を辿る必要があります(したがって「参照解除」)。そうすることでコンパイラが実際の値を比較できるようになります[4]。yの参照を解除すると、yが指し示す整数値にアクセスでき、それを5と比較できるようになります。
代わりにassert_eq!(5, y);と書こうとすると、次のコンパイルエラーが発生します。
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}`
数値と数値への参照を比較することはできません。なぜなら、それらは異なる型だからです。参照先の値を辿るためには、参照演算子を使用する必要があります。
Box<T> を参照のように使う
リスト 15-6 のコードを、参照ではなくBox<T>を使って書き直すことができます。リスト 15-7 でBox<T>に対して使用される参照演算子は、リスト 15-6 の参照に対して使用される参照演算子と同じように機能します。
ファイル名:src/main.rs
fn main() {
let x = 5;
1 let y = Box::new(x);
assert_eq!(5, x);
2 assert_eq!(5, *y);
}
リスト 15-7: Box<i32> に対して参照演算子を使用する
リスト 15-7 とリスト 15-6 の主な違いは、ここではyをxのコピーされた値を指すボックスのインスタンスに設定している点で、xの値を指す参照ではない点です[1]。最後のアサーションでは[2]、yが参照のときと同じように、参照演算子を使ってボックスのポインタを辿ることができます。次に、独自のボックス型を定義することで、Box<T>に何が特別なのかを調べます。これにより、参照演算子を使用できるようになります。
独自のスマートポインタを定義する
標準ライブラリに提供されているBox<T>型に似たスマートポインタを作成して、デフォルトではスマートポインタが参照とどのように異なる動作をするかを体験しましょう。そして、参照演算子を使用できる機能を追加する方法を見てみましょう。
Box<T>型は最終的には 1 つの要素を持つタプル構造体として定義されているので、リスト 15-8 では同じようにMyBox<T>型を定義します。また、Box<T>に定義されているnew関数に合わせてnew関数も定義します。
ファイル名:src/main.rs
1 struct MyBox<T>(T);
impl<T> MyBox<T> {
2 fn new(x: T) -> MyBox<T> {
3 MyBox(x)
}
}
リスト 15-8: MyBox<T>型を定義する
MyBoxという構造体を定義し、ジェネリックパラメータTを宣言します[1]。なぜなら、任意の型の値を保持するようにしたいからです。MyBox型は型Tの 1 つの要素を持つタプル構造体です。MyBox::new関数は型Tの 1 つのパラメータを受け取り[2]、渡された値を保持するMyBoxインスタンスを返します[3]。
リスト 15-7 のmain関数をリスト 15-8 に追加し、Box<T>の代わりに定義したMyBox<T>型を使うように変更してみましょう。リスト 15-9 のコードはコンパイルされません。なぜなら、Rust はMyBoxを参照解除する方法を知らないからです。
ファイル名: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
--> src/main.rs:14:19
|
14 | assert_eq!(5, *y);
| ^^
MyBox<T>型は参照解除できません。なぜなら、その型にその機能を実装していないからです。*演算子で参照解除を可能にするには、Derefトレイトを実装します。
Deref トレイトを実装する
「型に対するトレイトの実装」で説明したように、トレイトを実装するには、トレイトの必要なメソッドの実装を提供する必要があります。標準ライブラリによって提供されるDerefトレイトでは、derefという名前の 1 つのメソッドを実装する必要があります。このメソッドはselfを借用し、内部データへの参照を返します。リスト 15-10 には、MyBox<T>の定義に追加するDerefの実装が含まれています。
ファイル名:src/main.rs
use std::ops::Deref;
impl<T> Deref for MyBox<T> {
1 type Target = T;
fn deref(&self) -> &Self::Target {
2 &self.0
}
}
リスト 15-10: MyBox<T>にDerefを実装する
type Target = T;の構文[1]は、Derefトレイトが使用する関連型を定義します。関連型は、ジェネリックパラメータを宣言する少し異なる方法ですが、今は心配する必要はありません。第 19 章で詳細を説明します。
derefメソッドの本体を&self.0で埋めます。これにより、derefは*演算子でアクセスしたい値への参照を返します[2]。「名前付きフィールドなしのタプル構造体を使って異なる型を作成する」で学んだように、.0はタプル構造体の最初の値にアクセスします。リスト 15-9 のmain関数でMyBox<T>値に対して*を呼び出すと、今はコンパイルされ、アサーションが通過します!
Derefトレイトがなければ、コンパイラは&参照のみを参照解除できます。derefメソッドにより、コンパイラはDerefを実装する任意の型の値を取り、derefメソッドを呼び出して、参照解除できる&参照を取得することができます。
リスト 15-9 で*yを入力したとき、裏で Rust は実際にこのコードを実行しました。
*(y.deref())
Rust は*演算子をderefメソッドの呼び出しに置き換え、その後単純な参照解除を行うため、derefメソッドを呼び出す必要があるかどうかを考える必要がありません。この Rust の機能により、通常の参照でもDerefを実装する型でも、同じように機能するコードを書くことができます。
derefメソッドが値への参照を返し、*(y.deref())の括弧外の単純な参照解除がまだ必要な理由は、所有権システムに関係しています。derefメソッドが値そのものではなく値への参照を返した場合、値はselfから移動されてしまいます。この場合や、参照解除演算子を使用するほとんどの場合では、MyBox<T>内の内部値の所有権を取得したくありません。
コード内で*を使用するたびに、*演算子はderefメソッドの呼び出しに置き換えられ、その後*演算子の呼び出しに置き換えられます。*演算子の置き換えが無限に再帰しないため、最終的にはi32型のデータが得られ、これはリスト 15-9 のassert_eq!の5と一致します。
関数とメソッドによる暗黙的な参照解除強制
参照解除強制は、Derefトレイトを実装する型への参照を別の型への参照に変換します。たとえば、参照解除強制は&Stringを&strに変換できます。なぜなら、StringはDerefトレイトを実装しており、&strを返すからです。参照解除強制は、Rust が関数とメソッドの引数に対して行う便利な機能であり、Derefトレイトを実装する型にのみ機能します。関数またはメソッドの定義におけるパラメータ型と一致しない型の値への参照を関数またはメソッドの引数として渡すときに、自動的に行われます。derefメソッドへの一連の呼び出しにより、提供した型をパラメータが必要とする型に変換します。
参照解除強制は Rust に追加されたため、関数とメソッド呼び出しを書くプログラマは、&と*を使った明示的な参照と参照解除をそれほど多く追加する必要がなくなりました。参照解除強制機能により、参照またはスマートポインタのどちらでも動作するコードをより多く書くことができます。
参照解除強制の動作を見てみましょう。リスト 15-8 で定義したMyBox<T>型と、リスト 15-10 で追加したDerefの実装を使います。リスト 15-11 は、文字列スライスパラメータを持つ関数の定義を示しています。
ファイル名:src/main.rs
fn hello(name: &str) {
println!("Hello, {name}!");
}
リスト 15-11: 型&strのパラメータnameを持つhello関数
たとえば、hello("Rust");のように、文字列スライスを引数としてhello関数を呼び出すことができます。参照解除強制により、MyBox<String>型の値への参照を使ってhelloを呼び出すことが可能になります。これはリスト 15-12 に示されています。
ファイル名:src/main.rs
fn main() {
let m = MyBox::new(String::from("Rust"));
hello(&m);
}
リスト 15-12: 参照解除強制のおかげで動作する、MyBox<String>型の値への参照を使ってhelloを呼び出す
ここでは、引数&mでhello関数を呼び出しています。これはMyBox<String>型の値への参照です。リスト 15-10 でMyBox<T>にDerefトレイトを実装したため、Rust はderefを呼び出すことで&MyBox<String>を&Stringに変換することができます。標準ライブラリはStringに対してDerefの実装を提供しており、文字列スライスを返します。これはDerefの API ドキュメントに記載されています。Rust はさらにderefを呼び出して&Stringを&strに変換し、これがhello関数の定義と一致します。
Rust が参照解除強制を実装していなかった場合、&MyBox<String>型の値でhelloを呼び出すには、リスト 15-13 のコードのように書かなければなりません。
ファイル名:src/main.rs
fn main() {
let m = MyBox::new(String::from("Rust"));
hello(&(*m)[..]);
}
リスト 15-13: Rust に参照解除強制がなかった場合に書かなければならないコード
(*m)はMyBox<String>をStringに参照解除します。その後、&と[..]は、文字列全体と等しいStringの文字列スライスを取得して、helloのシグネチャに一致させます。これらの記号がすべて含まれるため、参照解除強制がないこのコードは読みにくく、書きにくく、理解しにくいです。参照解除強制により、Rust がこれらの変換を自動的に処理してくれます。
関係する型に対してDerefトレイトが定義されている場合、Rust は型を分析し、パラメータの型と一致する参照を取得するために必要なだけDeref::derefを何度も使用します。Deref::derefを挿入する必要のある回数はコンパイル時に解決されるため、参照解除強制を利用することによる実行時のペナルティはありません!
参照解除強制が不変性とどのように相互作用するか
不変参照における*演算子をオーバーライドするためにDerefトレイトを使用する方法と同様に、可変参照における*演算子をオーバーライドするためにDerefMutトレイトを使用することができます。
Rust は、3 つのケースで型とトレイトの実装を見つけたときに参照解除強制を行います。
T: Deref<Target=U>の場合、&Tから&UT: DerefMut<Target=U>の場合、&mut Tから&mut UT: Deref<Target=U>の場合、&mut Tから&U
最初の 2 つのケースは、2 番目のケースが可変性を実装している点を除いて同じです。最初のケースは、&Tがあり、Tがある型Uに対してDerefを実装している場合、透明に&Uを取得できることを述べています。2 番目のケースは、可変参照に対しても同じ参照解除強制が行われることを述べています。
3 番目のケースはややこしいです。Rust はまた、可変参照を不変参照に強制変換します。しかし、逆は不可能です。不変参照は決して可変参照に強制変換されません。借用規則により、可変参照がある場合、その可変参照はそのデータへの唯一の参照でなければなりません(そうでなければ、プログラムはコンパイルされません)。1 つの可変参照を 1 つの不変参照に変換することは、借用規則を決して破壊しません。不変参照を可変参照に変換するには、最初の不変参照がそのデータへの唯一の不変参照である必要がありますが、借用規則はそれを保証しません。したがって、Rust は不変参照を可変参照に変換することが可能であると仮定することができません。
まとめ
おめでとうございます!「Deref を使って通常の参照のようにスマートポインタを扱う」実験を完了しました。さらにスキルを向上させるために、LabEx でさらに実験を行って練習してください。