この記事では、Rust のDrop
トレイトとDeref
トレイトについて説明していく。
それぞれdrop
メソッドとderef
のメソッドを必須としているが、これらのメソッドを明示的に呼び出すことは稀で、多くの場合暗黙的に呼び出される。
トレイトそのものの初歩については以下を参照。
Drop トレイト
以下のコードでは、MyBox
というタプル構造体にDrop
トレイトを実装している。
Drop
はuse
を使ってインポートする必要はなく、そのまま使える。
そして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 X
、end
の順番で表示されるので、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 トレイトの実装方法
Deref
はDrop
と異なり、use
でインポートする必要がある。
use std::ops::Deref;
Deref
トレイトを実装する型にはtype Target
とderef
メソッドが必須。
まず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 トレイトを実装すると参照外しできるようになるのか
x
がDeref
を実装してるとき、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_foo
とis_i32
はそれぞれ、Foo
とi32
の参照を引数として受け取る。そのためis_i32
にFoo
の参照を渡せば、当然コンパイルエラーになる。
#[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` }
Foo
にDeref
トレイトを実装してderef
メソッドが&9
を返すようにすると、is_i32
にFoo
の参照を渡せるようになる。
コンパイラが暗黙的に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!
は一度しか表示されないため、参照外し型強制は必要なときにしか発生しないことが分かる。
そのため、&Foo
をis_foo
に渡したときは参照外し型強制は発生せず、is_foo
はそのまま&Foo
を受け取る。
整理すると、参照外し型強制は以下の条件を全て満たしたときに発生する。
- 何らかの参照を関数に渡す
- 関数の引数の型が、渡された型と一致しない
- 渡された型が
Deref
トレイトを実装している
参照外し型強制は、必要に応じて何度も繰り返される。
そのため以下のケースでは、&Foo
をis_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
関数に渡すことができる。bool
はDeref
トレイトを実装していないため、渡すとコンパイルエラーになる。
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` }
String
のDeref
トレイトの実装は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 }
状況に応じてderef
と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 { 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 }