この記事について
RustのDerefトレイトについて、ドキュメントだけでは挙動がよく分からなかったので、実際にサンプルを書きながら色々試して挙動を確認してみましたので、その結果について書いています。
主に参考にしたのは公式ドキュメントのDerefに関するページとDerefに関するAPI referenceです。
確認した環境やバージョンは以下の通りです。
- MacOSX 10.11.6 (El Capitan)
- rust 1.5.1
Derefトレイトとは
まず、RustのDerefトレイトの役割について確認しておきます。
Derefには以下の2つの機能が備わっています。
-
*による参照のデリファレンスをオーバーロードする - 逆変換による型の自動変換
それでは、ひとつずつサンプルを通して確認します。
デリファレンス演算のオーバーロード
通常の参照のデリファレンスは&などで取得した参照から*で参照の内容をデリファレンスするためのものですが、Derefトレイトを実装することにより、このデリファレンス演算子(*)を使ってTargetに指定した型の値を取得することができます。
以下、サンプルです。
use std::ops::Deref;
# [derive(Debug)]
struct Parent {
value: String,
}
impl Deref for Parent {
type Target = String;
fn deref(&self) -> &String {
&self.value
}
}
fn main() {
let parent = Parent{
value: "parent".to_string(),
}
assert_eq!(*parent, "parent")
}
String型のvalueフィールドを持った独自の構造体Parentを作り、Derefトレイトを実装しています。
DerefトレイトのderefメソッドはParentが持つvalueを返すように実装します。
これによって参照ではないparentに対してデリファレンス(*)した場合にparentが持つvalueを取得することができます。
逆変換(deref coercions)による型の自動変換
逆変換(deref coercions)とは型UがDeref<Target=T>を実装している場合、&Uが&Tに自動変換されるルールです。
自動変換は型Tへの参照であることが自明である場合に実施され、型Tが見つかるまで再帰的に行われます。
具体的には以下のような時にこの自動変換が行われます。
- 型Tを明示した変数への代入
- 型Tを引数に持つメソッドの呼び出し
- 型Uから型Tが持つメソッドの呼び出し
それぞれサンプルを通して確認してみます。
型Tを明示した変数への代入
use std::ops::Deref;
# [derive(Debug)]
struct Parent {
value: String,
child: Child,
}
impl Deref for Parent {
type Target = Child;
fn deref(&self) -> &Child {
&self.child
}
}
# [derive(Debug)]
struct Child {
value: String,
grandchild: Grandchild,
}
impl Deref for Child {
type Target = Grandchild;
fn deref(&self) -> &Grandchild {
&self.grandchild
}
}
# [derive(Debug)]
struct Grandchild {
value: String,
}
impl Deref for Grandchild {
type Target = String;
fn deref(&self) -> &String {
&self.value
}
}
fn main() {
let grandchild = Grandchild{
value: "grandchild".to_string(),
};
let child = Child{
value: "child".to_string(),
grandchild: grandchild,
};
let parent = Parent{
value: "parent".to_string(),
child: child,
};
// 検証1: 自動変換(Parent->Child)
let ref_child: &Child = &parent;
println!("{:?}", ref_child); // Child { value: "child", grandchild: Grandchild { value: "grandchild" } }
// 検証2: 再帰的に自動変換(Parent->Child->Grandchild)
let ref_grandchild: &Grandchild = &parent;
println!("{:?}", ref_grandchild); // Grandchild { value: "grandchild" }
// 検証3: 再帰的に自動変換(Parent->Child->Grandchild->String)
let ref_value: &String = &parent;
println!("{:?}", ref_value); // "grandchild"
// 検証4: 型を明示しない場合は通常の参照取得
let ref_parent = &parent;
println!("{:?}", ref_parent); // Parent { value: "parent", chile: Child { value: "child", grandchild: Grandchild { value: "grandchild" } } }
}
再帰的に逆参照されることを確認する為に、Parent、Child、Grandchildと3つの構造体を作り、Parent -> Child -> Grandchildとネストして持たせています。
ParentはDeref<Target=Child>、ChildはDeref<Target=Grandchild>、GrandchildはDeref<Target=String>です。
検証1が逆参照による自動変換が行われているところです。
「型UがDeref<Target=T>を実装している場合、&Uが&Tに自動変換される」というルール通り、型ParentはDeref<Target=Child>を実装しているので&Parentが&Childに自動変換されているというわけです。
検証2も正しく動作します。Parentの逆参照でChildを見つけ、Childの逆参照でGrandchildを見つけます。
検証3も同様に動作します。Parent -> Child -> Grandchild -> Stringと自動変換されます。
検証4のように型を明示しない場合は自動変換は行われず、通常通り参照を取得します。
型Tを引数に持つメソッドの呼び出し
(構造体やDerefの定義部分は同じ)
fn check_parent(p: &Parent, expect: &str) {
assert_eq!(p.value, expect);
}
fn check_child(c: &Child, expect: &str) {
assert_eq!(c.value, expect);
}
fn check_grandchild(gc: &Grandchild, expect: &str) {
assert_eq!(gc.value, expect);
}
fn check_value(v: &String, expect: &str) {
assert_eq!(v, expect);
}
fn main() {
let grandchild = Grandchild{
value: "grandchild".to_string(),
};
let child = Child{
value: "child".to_string(),
grandchild: grandchild,
};
let parent = Parent{
value: "parent".to_string(),
child: child,
};
// 検証1: 普通のメソッド呼び出し
check_parent(&parent, "parent");
// 検証2: 自動変換(Parent->Child)
check_child(&parent, "child");
// 検証3: 再帰的に自動変換(Parent->Child->Grandchild)
check_grandchild(&parent, "grandchild");
// 検証4: 再帰的に自動変換(Parent->Child->Grandchild->String)
check_value(&parent, "grandchild");
}
先程のサンプルに引数に&Parent、&Child、&Grandchild、&Stringを持つメソッドをそれぞれ追加します。メソッドの中身は単純にvalueを検証するアサーションです。
各メソッドを&parentを渡して呼び出してみました。
検証1は&Parentを要求する引数に対して&parentを渡している普通の呼び出しです。
これは問題なくOKです。
検証2は&Childを要求する引数に対して&parentを渡しています。
これは逆参照による型の自動変換(Parent->Child)が適用されるのでOKになります。
検証3は&Grandchildを要求する引数に対して&parentを渡しています。
これも再帰的な逆参照による型変換(Parent->Child->Grandchild)が適用されてOKになります。
検証4は&Stringを要求する引数に対して&parentを渡しています。
これも同様に型変換(Parent->Child->Grandchild->String)されてOKです。
型Uから型Tが持つメソッドの呼び出し
(構造体やDerefの定義部分は同じ)
impl Parent {
fn assert(&self, expend: &str) {
assert_eq!(&self.value, expend);
}
}
impl Child {
fn assert2(&self, expend: &str) {
assert_eq!(&self.value, expend);
}
}
impl Grandchild {
fn assert3(&self, expend: &str) {
assert_eq!(&self.value, expend);
}
}
fn main() {
let grandchild = Grandchild{
value: "grandchild".to_string(),
};
let child = Child{
value: "child".to_string(),
grandchild: grandchild,
};
let parent = Parent{
value: "parent".to_string(),
child: child,
};
// 検証1: &Parentのメソッド呼び出し
parent.assert("parent");
// 検証2: &Childのメソッド呼び出し
parent.assert2("child");
// 検証3: &Grandchildのメソッド呼び出し
parent.assert3("grandchild");
}
構造体に紐づくメソッドは引数に&selfを持っています。ということは型Tを引数に持つメソッドの呼び出しと同様に逆参照による型の自動変換が適用されるのではないかと思い、検証してみました。
構造体やDerefの実装は今までのサンプルと同じで、それぞれの構造体に紐づくメソッドを別途定義します。
そして、それらのメソッドをparentから呼び出します。
検証1はParentが持つメソッドを呼び出しているだけなので問題なくOKでした。
検証2はChildが持つメソッドの呼び出しですが、OKになりました。
予想通りDerefによる型の自動変換が行われているようです。
検証3も同じくOKでした。
再帰的な自動変換も行われています。
[Note]
自動変換による型Tが持つメソッドの呼び出しは、同じメソッド名の場合最初に解決した型のメソッドが呼び出されます。
つまり、このサンプルのParentにassert3と追加すると今までOKだった検証3がleft: "parent", right: "grandchild"となりNGになります。
まとめ
Derefは標準ライブラリのStringやVec、Box、Arcなどなど、多くの主要な構造体でも実装されているとてもよく見るトレイトの1つです。また、Derefによる型の自動変換はRustでも数少ない自動変換の1つらしいので、このルールを理解しておくことはコードリーディングにおいてもとても重要だと思われます(実際に私もDerefを理解するまでは暗黙的な型の変換に気持ち悪さを感じていました)。
公式のドキュメントよりも詳細にサンプルコードを書いて確認してみたので、誰かの理解のための一助となれば幸いです。