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);
}

リスト15-6: 参照外し演算子を使用して参照をi32値まで追いかける

変数xi32値の5を保持しています。yxへの参照として設定します。x5に等しいとアサートできます。 しかしながら、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: Box<i32>に対して参照外し演算子を使用する

リスト15-7とリスト15-6の唯一の違いは、ここではyが、xの値を指す参照ではなく、 xの値を指すボックスのインスタンスとして設定されている点にあります。 最後のアサートでは、参照外し演算子を使ってボックスのポインタを辿ることができます。これはyが参照だった時と同じやり方です。 参照外し演算子が使える以上Box<T>には特別な何かがあるので、次はそれについて調べることにします。そのために、独自にボックス型を定義します。

独自のスマートポインタを定義する

標準ライブラリが提供しているBox<T>型に似たスマートポインタを作りましょう。そうすれば、スマートポインタがそのままだと 参照と同じ様には振る舞わないことがわかります。それから、どうすれば参照外し演算子を使えるようになるのか見てみましょう。

Box<T>型は突き詰めると(訳註:データがヒープに置かれることを無視すると)1要素のタプル構造体のような定義になります。なのでリスト15-8ではそのようにMyBox<T>型を定義しています。 また、Box<T>に定義されたnew関数に対応するnew関数も定義しています。

ファイル名: src/main.rs


#![allow(unused)]
fn main() {
struct MyBox<T>(T);

impl<T> MyBox<T> {
    fn new(x: T) -> MyBox<T> {
        MyBox(x)
    }
}
}

リスト15-8: MyBox<T>型を定義する

MyBoxという構造体を定義し、ジェネリック引数のTを宣言しています。この型にどんな型の値も持たせたいからです。 MyBox型は型Tの要素を1つ持つタプル構造体です。MyBox::new関数は型Tの引数を1つ取り、 渡した値を持つMyBoxのインスタンスを返します。

試しにリスト15-7のmain関数をリスト15-8に追加し、定義したMyBox<T>型をBox<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);
}

リスト15-9: 参照とBox<T>を使ったのと同じようにMyBox<T>を使おうとする

こちらが結果として出るコンパイルエラーです。

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トレイトは標準ライブラリで提供されており、derefという1つのメソッドの実装を要求します。derefselfを借用し、 内部のデータへの参照を返すメソッドです。 リスト15-10には、MyBoxの定義に付け足すDerefの実装が含まれています。

ファイル名: src/main.rs


#![allow(unused)]
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
    }
}
}

リスト15-10: MyBox<T>Derefを実装する

type Target = T;という記法は、Derefトレイトが使用する関連型を定義しています。関連型はまた少し違ったやり方でジェネリック引数を宣言するためのものですが、今は気にする必要はありません。第19章でより詳しく扱います。

derefメソッドの本体は&self.0だけなので、derefが返すのは私達が*演算子でアクセスしたい値への参照なわけです。 リスト15-9のMyBox<T>*を呼び出すmain関数はこれでコンパイルでき、アサートも通ります!

Derefトレイトがないと、コンパイラは&参照しか参照外しできません。 derefメソッドのおかげで、コンパイラはDerefを実装している型の値を取り、derefメソッドを呼ぶことで、参照外しが可能な&参照を得られるようになります。

リスト15-9に*yを入力した時、水面下でRustは実際にはこのようなコードを走らせていました。

*(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メソッドの呼び出しが、提供した型を引数が必要とする型に変換します。

参照外し型強制は、関数やメソッド呼び出しを書くプログラマが&*を多くの明示的な参照や参照外しとして追記する必要がないように、 Rustに追加されました。また、参照外し型強制のおかげで参照あるいはスマートポインタのどちらかで動くコードをもっと書くことができます。

参照外し型強制が実際に動いていることを確認するため、リスト15-8で定義したMyBox<T>と、 リスト15-10で追加したDerefの実装を使用しましょう。リスト15-11は、 文字列スライス引数のある関数の定義を示しています:

ファイル名: src/main.rs


#![allow(unused)]
fn main() {
fn hello(name: &str) {
    println!("Hello, {}!", name);
}
}

リスト15-11: 型&strの引数nameのあるhello関数

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);
}

リスト15-12: helloMyBox<String>値とともに呼び出し、参照外し型強制のおかげで動く

ここで、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)[..]);
}

リスト15-13: Rustに参照外し型強制がなかった場合に書かなければならないであろうコード

(*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つしかないことが必要ですが、 借用規則はそれを保証してくれません。故に、不変参照を可変参照に変換することが可能であるという前提を敷けません。

関連キーワード:  参照, MyBox, Deref, リスト, 実装, 定義, deref, 強制, 関数, メソッド