分解 pt2 - match と借用
分解を行うとき、借用に関してはいくつか思いがけないことがあります。借用参照を本当に十分理解していれば、驚くことは何もないはずですが、議論する価値はあります(私が理解するまでにはしばらくかかったことは確かです。実際、このブログ記事の最初のバージョンで間違えていたので、自分で思っていたよりも長くかかっていました)。
何らかの &Enum 型の変数 x があるとします(ここで Enum は何らかの enum 型です)。選択肢は 2 つあります。*x を match してすべてのバリアントを列挙する(Variant1 => ... など)か、x を match してバリアントパターンへの参照を列挙する(&Variant1 => ... など)かです。(スタイルとしては、構文上のノイズが少ないため、可能な場合は前者の形式を好むべきです)。x は借用参照であり、借用参照をどのようにデリファレンスできるかには厳格なルールがあります。これらは match 式と、思いがけない形で相互作用します(少なくとも私にとっては思いがけないものでした)。特に、既存の enum を一見無害に見える方法で変更したところ、どこかの match でコンパイラが爆発するような場合です。
match 式の詳細に入る前に、Rust の値渡しに関するルールを振り返っておきましょう。C++ では、値を変数に代入したり関数に渡したりするとき、選択肢は 2 つあります。値渡しと参照渡しです。前者がデフォルトのケースであり、コピーコンストラクタまたはビット単位のコピーを使って値がコピーされることを意味します。パラメータ渡しや代入の宛先に & を付けると、値は参照渡しされます。つまり、値へのポインタだけがコピーされ、新しい変数を操作すると、古い値も操作していることになります。
Rust にも参照渡しの選択肢がありますが、Rust では宛先だけでなくソースにも & を付ける必要があります。Rust の値渡しには、さらに 2 つの選択肢があります。コピーとムーブです。コピーは C++ のセマンティクスと同じです(ただし Rust にはコピーコンストラクタはありません)。ムーブは値をコピーしますが、古い値を破棄します。Rust の型システムにより、古い値にはもはやアクセスできないことが保証されます。例として、i32 はコピーセマンティクスを持ち、Box<i32> はムーブセマンティクスを持ちます。
#![allow(unused)] fn main() { fn foo() { let x = 7i32; let y = x; // x はコピーされる println!("x is {}", x); // OK let x = Box::new(7i32); let y = x; // x はムーブされる //println!("x is {}", x); // エラー: ムーブされた値 `x` の使用 } }
ユーザー定義型についても、Copy トレイトを実装することでコピーセマンティクスを持たせることができます。そのための単純な方法の 1 つは、struct の定義の前に #[derive(Copy)] を追加することです。すべてのユーザー定義型が Copy トレイトを実装できるわけではありません。型のすべてのフィールドが Copy を実装していなければならず、その型はデストラクタを持っていてはいけません。デストラクタについては、おそらくそれ自体で 1 つの記事が必要ですが、今のところ、Rust のオブジェクトは Drop トレイトを実装している場合にデストラクタを持つ、としておきます。C++ と同様に、デストラクタはオブジェクトが破棄される直前に実行されます。
さて、借用されたオブジェクトがムーブされないことは重要です。そうでないと、もはや有効ではない古いオブジェクトへの参照を持つことになります。これは、スコープを抜けた後に破棄されたオブジェクトへの参照を保持することと同じであり、一種のダングリングポインタです。あるオブジェクトへのポインタを持っている場合、そのオブジェクトには他の参照が存在する可能性があります。したがって、オブジェクトがムーブセマンティクスを持ち、そのオブジェクトへのポインタを持っている場合、そのポインタをデリファレンスするのは安全ではありません。(オブジェクトがコピーセマンティクスを持つ場合、デリファレンスによりコピーが作成され、古いオブジェクトもまだ存在するため、他の参照は問題ありません)。
では、match 式に戻りましょう。先ほど述べたように、&T 型の何らかの x を match したい場合、match 節で一度デリファレンスするか、match 式の各アームで参照を match することができます。例を示します。
#![allow(unused)] fn main() { enum Enum1 { Var1, Var2, Var3 } fn foo(x: &Enum1) { match *x { // 選択肢 1: ここで deref する。 Enum1::Var1 => {} Enum1::Var2 => {} Enum1::Var3 => {} } match x { // 選択肢 2: 各アームで 'deref' する。 &Enum1::Var1 => {} &Enum1::Var2 => {} &Enum1::Var3 => {} } } }
この場合、Enum1 はコピーセマンティクスを持つため、どちらのアプローチも取ることができます。それぞれのアプローチをもう少し詳しく見てみましょう。最初のアプローチでは、x を Enum1 型の一時変数へデリファレンスし(これにより x 内の値がコピーされます)、それから Enum1 の 3 つのバリアントに対して match を行います。これは「1 レベル」の match です。値の型の奥深くまでは入らないからです。2 つ目のアプローチでは、デリファレンスはありません。&Enum1 型の値を、各バリアントへの参照と match します。この match は 2 レベル深く進みます。型(常に参照)を match し、その型の内側を見て参照先の型(これは Enum1 です)を match します。
どちらの場合でも、私たち(つまりコンパイラ)は、ムーブと参照に関する Rust の不変条件を尊重しなければなりません。参照されている場合、そのオブジェクトのどの部分もムーブしてはいけません。match される値がコピーセマンティクスを持つ場合、それは自明です。ムーブセマンティクスを持つ場合は、どの match アームでもムーブが起こらないようにしなければなりません。これは、ムーブされるデータを無視するか、それへの参照を作る(つまりムーブ渡しではなく参照渡しにする)ことで実現されます。
#![allow(unused)] fn main() { enum Enum2 { // Box にはデストラクタがあるため、Enum2 はムーブセマンティクスを持つ。 Var1(Box<i32>), Var2, Var3 } fn foo(x: &Enum2) { match *x { // ネストされたデータを無視しているため、これは OK Enum2::Var1(..) => {} // 他のアームに変更はない。 Enum2::Var2 => {} Enum2::Var3 => {} } match x { // ネストされたデータを無視しているため、これは OK &Enum2::Var1(..) => {} // 他のアームに変更はない。 &Enum2::Var2 => {} &Enum2::Var3 => {} } } }
どちらのアプローチでも、ネストされたデータを一切参照していないため、そのどれもムーブされません。最初のアプローチでは、x は参照されているものの、デリファレンスのスコープ(つまり match 式)内でその内部には触れないため、何も外へ逃げることはできません。また、値全体を束縛する(つまり *x を変数に束縛する)こともしていないため、オブジェクト全体をムーブすることもできません。
2 つ目の match では任意のバリアントへの参照を取ることができますが、デリファレンスしたバージョンではできません。したがって、2 つ目のアプローチでは 2 番目のアームを a @ &Var2 => {} に置き換えても問題ありません(a は参照です)が、最初のアプローチでは a @ Var2 => {} と書くことはできません。なぜなら、それは *x を a にムーブすることを意味するからです。ref a @ Var2 => {} と書くことはできます(この場合も a は参照です)が、これはあまり頻繁に見る構文ではありません。
では、Var1 の内側にネストされたデータを使いたい場合はどうでしょうか。次のようには書けません。
#![allow(unused)] fn main() { match *x { Enum2::Var1(y) => {} _ => {} } }
または
#![allow(unused)] fn main() { match x { &Enum2::Var1(y) => {} _ => {} } }
どちらの場合も、x の一部を y にムーブすることを意味するからです。ref キーワードを使えば、Var1 内のデータへの参照を得ることができます。&Var1(ref y) => {} です。これは問題ありません。なぜなら、これでどこでもデリファレンスしておらず、したがって x のどの部分もムーブしていないからです。代わりに、x の内部を指すポインタを作成しています。
Alternatively、Box を分解することもできます(この match は 3 階層深くまで入っています): &Var1(box y) => {}(box パターン構文は rustc 1.58 時点で実験的であり、rustc の nightly バージョンでのみ利用可能であることに注意してください)。
これは問題ありません。なぜなら i32 はコピーセマンティクスを持ち、y は Var1 の内側にある Box の内側にある i32 のコピーだからです(これは借用された参照の「内側」にあります)。i32 はコピーセマンティクスを持つため、x のどの部分もムーブする必要はありません。int をコピーするのではなく、それへの参照を作ることもできます:
&Var1(box ref y) => {}。繰り返しますが、これも問題ありません。なぜなら、デリファレンスを一切行わないため、x のどの部分もムーブする必要がないからです。Box の内容がムーブセマンティクスを持っていた場合、&Var1(box y) => {} と書くことはできず、参照版を使わざるを得ません。同様のテクニックを最初のマッチング方法でも使うことができ、その場合は最初の & がないだけで同じように見えます。たとえば、Var1(box ref y) => {} です。
では、もう少し複雑にしてみましょう。enum 値への参照のペアに対してマッチしたいとします。この場合、最初の方法はまったく使えません:
#![allow(unused)] fn main() { fn bar(x: &Enum2, y: &Enum2) { // エラー: x と y がムーブされています。 // match (*x, *y) { // (Enum2::Var2, _) => {} // _ => {} // } // OK。 match (x, y) { (&Enum2::Var2, _) => {} _ => {} } } }
最初の方法が不正なのは、マッチされる値が、x と y をデリファレンスしてから、それらの両方を新しいタプルオブジェクトへムーブすることで作られるためです。そのため、この状況では 2 つ目の方法だけが機能します。そしてもちろん、x と y の一部をムーブしないようにするため、上記のルールに従う必要は依然としてあります。
何らかのデータへの参照しか取得できず、その値自体が必要になった場合、そのデータをコピーする以外に選択肢はありません。通常、それは clone() を使うことを意味します。そのデータが Clone を実装していない場合は、さらに分解して手動でコピーするか、自分で Clone を実装する必要があります。
では、ムーブセマンティクスを持つ値への参照ではなく、その値自体を持っている場合はどうでしょうか。この場合、ムーブは問題ありません。なぜなら、その値への参照を他の誰も持っていないことが分かっているからです(もし持っているなら、その値を使えないようにコンパイラが保証します)。たとえば、
#![allow(unused)] fn main() { fn baz(x: Enum2) { match x { Enum2::Var1(y) => {} _ => {} } } }
それでも、注意すべきことがいくつかあります。第一に、ムーブできる先は 1 か所だけです。上の例では、x の一部を y へムーブしており、残りは忘れることになります。もし a @ Var1(y) => {} と書いた場合、x 全体を a へムーブし、さらに x の一部を y へムーブしようとしていることになります。それは許可されておらず、そのようなアームは不正です。a または y のどちらか一方を参照にする(ref a などを使う)ことも選択肢にはなりません。その場合、参照を保持しながらムーブするという、上で説明した問題が起こるからです。a と y の両方を参照にすれば問題ありません。どちらもムーブしていないため、x はそのまま残り、全体とその一部へのポインターを持つことになります。
同様に(そしてより一般的に)、ネストされたデータを複数持つバリアントがある場合、あるデータへの参照を取り、別のデータをムーブすることはできません。たとえば Var4(Box<int>, Box<int>) として宣言された Var4 がある場合、両方を参照する match アーム(Var4(ref y, ref z) => {})や、両方をムーブする match アーム(Var4(y, z) => {})を持つことはできますが、一方をムーブし、もう一方を参照する match アーム(Var4(ref y, z) => {})を持つことはできません。これは、部分的なムーブであってもオブジェクト全体が破棄されるため、その参照が無効になるからです。