Rust
20
どのような問題がありますか?

この記事は最終更新日から3年以上が経過しています。

投稿日

更新日

RustのDerefトレイトによる挙動を確認してみた

この記事について

RustDerefトレイトについて、ドキュメントだけでは挙動がよく分からなかったので、実際にサンプルを書きながら色々試して挙動を確認してみましたので、その結果について書いています。
主に参考にしたのは公式ドキュメントの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を明示した変数への代入

型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を引数に持つメソッドの呼び出し

型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が持つメソッドの呼び出し

型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が持つメソッドの呼び出しは、同じメソッド名の場合最初に解決した型のメソッドが呼び出されます。
つまり、このサンプルのParentassert3と追加すると今までOKだった検証3left: "parent", right: "grandchild"となりNGになります。

まとめ

Derefは標準ライブラリのStringVecBoxArcなどなど、多くの主要な構造体でも実装されているとてもよく見るトレイトの1つです。また、Derefによる型の自動変換はRustでも数少ない自動変換の1つらしいので、このルールを理解しておくことはコードリーディングにおいてもとても重要だと思われます(実際に私もDerefを理解するまでは暗黙的な型の変換に気持ち悪さを感じていました)。
公式のドキュメントよりも詳細にサンプルコードを書いて確認してみたので、誰かの理解のための一助となれば幸いです。

ユーザー登録して、Qiitaをもっと便利に使ってみませんか。
  1. あなたにマッチした記事をお届けします
    ユーザーやタグをフォローすることで、あなたが興味を持つ技術分野の情報をまとめてキャッチアップできます
  2. 便利な情報をあとで効率的に読み返せます
    気に入った記事を「ストック」することで、あとからすぐに検索できます
emonuh
しがないシステムエンジニアです。
この記事は以下の記事からリンクされています
wass80@githubRustとC++を比較からリンク

コメント

リンクをコピー
このコメントを報告

Derefによる型の自動変換はRustで唯一の自動変換らしいので

によれば

「唯一」ではなく「数少ない箇所の一つ」なのだそうです。

0
リンクをコピー
このコメントを報告

@scivola
ご指摘ありがとうございます!
修正させて頂きました!

0
どのような問題がありますか?
あなたもコメントしてみませんか :)
ユーザー登録
すでにアカウントを持っている方はログイン
記事投稿イベント開催中
新人プログラマ応援 - みんなで新人を育てよう!
~
データに関する記事を書こう!
~
20
どのような問題がありますか?
ユーザー登録して、Qiitaをもっと便利に使ってみませんか

この機能を利用するにはログインする必要があります。ログインするとさらに下記の機能が使えます。

  1. ユーザーやタグのフォロー機能であなたにマッチした記事をお届け
  2. ストック機能で便利な情報を後から効率的に読み返せる
ユーザー登録ログイン
ストックするカテゴリー