30歳からのプログラミング

30歳無職から独学でプログラミングを開始した人間の記録。

Drop トレイトと Deref トレイトについて

この記事では、Rust のDropトレイトとDerefトレイトについて説明していく。
それぞれdropメソッドとderefのメソッドを必須としているが、これらのメソッドを明示的に呼び出すことは稀で、多くの場合暗黙的に呼び出される。

トレイトそのものの初歩については以下を参照。

Drop トレイト

以下のコードでは、MyBoxというタプル構造体にDropトレイトを実装している。
Dropuseを使ってインポートする必要はなく、そのまま使える。
そしてDropトレイトを実装している型はdropメソッドが必須であり、このメソッドはselfへのミュータブルな参照を引数として取る。

struct MyBox(String);

impl Drop for MyBox {
    fn drop(&mut self) {
        println!("drop {}", self.0);
    }
}

dropメソッドは、スコープを抜けたときに実行される。そのため以下のような結果になる。

struct MyBox(String);

impl Drop for MyBox {
    fn drop(&mut self) {
        println!("drop {}", self.0);
    }
}

fn main() {
    let _x = MyBox(String::from("X"));
    {
        let _y = MyBox(String::from("Y"));
    } // drop Y
} // drop X

この例ではprintln!を実行しているだけだが、Dropトレイトは主に、不要になったリソース(メモリなど)を解放するために使われる。
スコープを抜けたということはその値が使われることはもうないため、このタイミングでリソースを解放すると都合がよい。また、スコープを抜けたときに自動的に呼び出されるため、リソースを解放し忘れる恐れもない。

dropメソッドを手動で呼び出そうとすると、コンパイルエラーになる。

fn main() {
    let x = MyBox(String::from("X"));
    x.drop(); // explicit destructor calls not allowed
}

std::mem::drop関数を使うことで、任意のタイミングでdropメソッドを呼び出すことができる。
この関数もDropトレイトと同様、useを使ってインポートする必要はない。

以下のコードを実行するとdrop Xendの順番で表示されるので、std::mem::drop関数を実行したタイミングでdropメソッドが呼び出されていることがわかる。

fn main() {
    let x = MyBox(String::from("X"));
    drop(x);
    println!("end")
}

ドロップしたあとに値を使おうとするとコンパイルエラーになる。

fn main() {
    let x = MyBox(String::from("X"));
    drop(x);
    println!("{}", x.0); // value borrowed here after move
}

ドロップする順番

複数の値が同時にスコープを抜けた場合、宣言された順番とは逆の順番でドロップしていく。

struct MyBox(String);

impl Drop for MyBox {
    fn drop(&mut self) {
        println!("drop {}", self.0);
    }
}

fn main() {
    let _x = MyBox(String::from("X"));
    let _y = MyBox(String::from("Y"));
    let _z = MyBox(String::from("Z"));
}
// drop Z
// drop Y
// drop X

Dropトレイトを実装した型が入れ子になっている場合は、まず親がドロップされる。子については、宣言された順番にドロップされる。

struct Child(i32);
struct Parent(Child, Child);

impl Drop for Child {
    fn drop(&mut self) {
        println!("drop {}", self.0);
    }
}

impl Drop for Parent {
    fn drop(&mut self) {
        println!("drop Parent");
    }
}

fn main() {
    let _x = Parent(Child(4), Child(5));
}
// drop Parent
// drop 4
// drop 5

Deref トレイト

*演算子を使うと、参照が指している値にアクセスすることができる。これを「参照外し」(dereference)という。

fn main() {
    let x = 1;
    let y = &x;
    println!("{}", x == *y); // true
    println!("{}", x == y); // can't compare `{integer}` with `&{integer}`
}

構造体に対しては参照外しできない。

struct MyBox {}

fn main() {
    let x = MyBox {};
    let y = *x; // type `MyBox` cannot be dereferenced
}

Derefトレイトを使うことで、構造体に対して参照外しができるようになる。

まずはシンプルな例を示す。

struct MyBox {}

use std::ops::Deref;

impl Deref for MyBox {
    type Target = i32;

    fn deref(&self) -> &Self::Target {
        &9
    }
}

fn main() {
    let x = MyBox {};
    let y = *x;
    println!("{}", y); // 9
    println!("{}", y == 9); // true
}

MyBoxのインスタンスであるxに対して参照外しをしたところ、9を得られた。

Deref トレイトの実装方法

DerefDropと異なり、useでインポートする必要がある。

use std::ops::Deref;

Derefトレイトを実装する型にはtype Targetderefメソッドが必須。

まずtype Targetだが、参照外しを行った際に得られる型を設定する。先程の例ではtype Target = i32;と書いたので、参照外しするとi32型の値が得られることになる。
そしてderefメソッドで、実際に得られる値の参照を返す。値ではなく参照を返すので、先程の例では9ではなく&9を返している。

構造体がジェネリックを使っている場合は以下のように書く。

struct MyBox<T> {
    value: T,
}

use std::ops::Deref;

impl<T> Deref for MyBox<T> {
    type Target = T;

    fn deref(&self) -> &T {
        &self.value
    }
}

fn main() {
    let x = MyBox { value: 9 };
    let y = MyBox { value: "abc" };
    println!("{}", *x); // 9
    println!("{}", *y); // abc
}

なぜ Deref トレイトを実装すると参照外しできるようになるのか

xDerefを実装してるとき、xに対して参照外しを行おうとすると、つまり*xと書くと、コンパイラはそれを*(x.deref())に変換する。Derefトレイトは必ずderefメソッドを持っているためそれが実行され、値を得られるのである。

xが以下のMyBoxのインスタンスであるとき、*x*(x.deref())となり、derefメソッドが実行された結果*(&9)となり、それは最終的に9になる。

impl Deref for MyBox {
    type Target = i32;

    fn deref(&self) -> &Self::Target {
        &9
    }
}
fn main() {
    let x = MyBox {};
    println!("{}", x.deref() == &9); // true
    println!("{}", *(&9) == 9); // true
    let y = *x;
    println!("{}", y == 9); // true
}

参照外し型強制

Derefトレイトを実装している型の参照を関数に渡すと、「参照外し型強制」と呼ばれる処理が発生することがある。

以下のis_foois_i32はそれぞれ、Fooi32の参照を引数として受け取る。そのためis_i32Fooの参照を渡せば、当然コンパイルエラーになる。

#[derive(Debug)]
struct Foo {}

fn is_foo(arg: &Foo) {
    println!("{:?}", arg);
}

fn is_i32(arg: &i32) {
    println!("{}", arg);
}

fn main() {
    is_i32(&7); // 7
    let x = Foo {};
    is_foo(&x); // Foo
    is_i32(&x); // expected `i32`, found struct `Foo`
}

FooDerefトレイトを実装してderefメソッドが&9を返すようにすると、is_i32Fooの参照を渡せるようになる。
コンパイラが暗黙的にderefを呼び出して&Foo&9に変換しているため、このような結果になる。この挙動が、参照外し型強制である。

#[derive(Debug)]
struct Foo {}

use std::ops::Deref;

impl Deref for Foo {
    type Target = i32;

    fn deref(&self) -> &Self::Target {
        println!("deref executed!");
        &9
    }
}

fn is_foo(arg: &Foo) {
    println!("{:?}", arg);
}

fn is_i32(arg: &i32) {
    println!("{}", arg);
}

fn main() {
    is_i32(&7); // 7
    let x = Foo {};
    is_foo(&x); // Foo
    is_i32(&x); // 9
}

上記を実行してみると、deref executed!は一度しか表示されないため、参照外し型強制は必要なときにしか発生しないことが分かる。
そのため、&Foois_fooに渡したときは参照外し型強制は発生せず、is_fooはそのまま&Fooを受け取る。

整理すると、参照外し型強制は以下の条件を全て満たしたときに発生する。

  1. 何らかの参照を関数に渡す
  2. 関数の引数の型が、渡された型と一致しない
  3. 渡された型がDerefトレイトを実装している

参照外し型強制は、必要に応じて何度も繰り返される。
そのため以下のケースでは、&Foois_i32に渡した際に参照外し型強制が 2 回発生している。まず&Foo&Barに変換され、さらに&Bar&9に変換される。

#[derive(Debug)]
struct Foo {}

#[derive(Debug)]
struct Bar {}

use std::ops::Deref;

impl Deref for Foo {
    type Target = Bar;

    fn deref(&self) -> &Self::Target {
        println!("deref executed in Foo!");
        &Bar {}
    }
}

impl Deref for Bar {
    type Target = i32;

    fn deref(&self) -> &Self::Target {
        println!("deref executed in Bar!");
        &9
    }
}

fn is_foo(arg: &Foo) {
    println!("{:?}", arg);
}

fn is_bar(arg: &Bar) {
    println!("{:?}", arg);
}

fn is_i32(arg: &i32) {
    println!("{}", arg);
}

fn main() {
    let x = Foo {};

    // Foo
    is_foo(&x);

    // deref executed in Foo!
    // Bar
    is_bar(&x);

    // deref executed in Foo!
    // deref executed in Bar!
    // 9
    is_i32(&x);
}

String のケース

Stringも、Derefトレイトを実装している型のひとつ。
そのため以下のis_deref関数に渡すことができる。boolDerefトレイトを実装していないため、渡すとコンパイルエラーになる。

struct Foo {}

use std::ops::Deref;

impl Deref for Foo {
    type Target = i32;

    fn deref(&self) -> &Self::Target {
        &9
    }
}

fn is_deref<T: Deref>(_: T) {
    println!("is deref!");
}

fn main() {
    is_deref(Foo {}); // is deref!
    is_deref(String::from("abc")); // is deref!
    is_deref(true); // expected an implementor of trait `std::ops::Deref`
}

StringDerefトレイトの実装はtype Target = strになっているため、derefメソッドは&strを返す。
そのため、&strを引数として受け取る関数hello&Stringを渡すことができる。

fn hello(name: &str) {
    println!("Hello, {}!", name);
}

fn main() {
    let x = String::from("abc");
    hello(&x); // Hello, abc!
}

DerefMut

Derefトレイトに加えてDerefMutトレイトも実装すると、ミュータブルな参照も返せるようになる。
DerefMutトレイトを実装する型には、deref_mutメソッドが必須となる。

#[derive(Debug)]
struct MyBox {
    value: i32,
}

use std::ops::Deref;
use std::ops::DerefMut;

impl Deref for MyBox {
    type Target = i32;

    fn deref(&self) -> &Self::Target {
        &self.value
    }
}

impl DerefMut for MyBox {
    fn deref_mut(&mut self) -> &mut Self::Target {
        &mut self.value
    }
}

fn main() {
    let mut x = MyBox { value: 9 };
    *x = 8;
    println!("{:?}", x); // MyBox { value: 8 }
    println!("{}", *x == 8); // true
}

状況に応じてderefderef_mutのいずれかを呼び出す。

#[derive(Debug)]
struct MyBox {
    value: i32,
}

use std::ops::Deref;
use std::ops::DerefMut;

impl Deref for MyBox {
    type Target = i32;

    fn deref(&self) -> &Self::Target {
        println!("deref");
        &self.value
    }
}

impl DerefMut for MyBox {
    fn deref_mut(&mut self) -> &mut Self::Target {
        println!("deref_mut");
        &mut self.value
    }
}

fn main() {
    let mut x = MyBox { value: 9 };
    *x = 8; // deref_mut
    let _y = *x; // deref
}