分解
前回は Rust のデータ型について見ました。構造体の中に何らかのデータがあるなら、そのデータを取り出したくなるでしょう。構造体については、Rust には C++ と同様にフィールドアクセスがあります。タプル、タプル構造体、列挙型については、分解を使わなければなりません(ライブラリにはさまざまな便利関数がありますが、それらは内部的に分解を使っています)。データ構造の分解は C++ では C++17 以降にしか存在しないため、おそらく Python やさまざまな関数型言語のような言語でおなじみでしょう。その考え方は、ローカル変数のまとまりからデータをフィールドに埋めてデータ構造を初期化できるのと同じように、データ構造からデータを取り出してローカル変数のまとまりを埋めることができる、というものです。この単純な出発点から、分解は Rust の最も強力な機能の 1 つになりました。別の言い方をすると、分解はパターンマッチングとローカル変数への代入を組み合わせたものです。
分解は主に let 文と match 文を通じて行います。match 文は、分解される構造が(列挙型のように)異なるバリアントを持ち得る場合に使います。let 式は変数を現在のスコープに取り出すのに対し、match は新しいスコープを導入します。比較すると次のようになります。
#![allow(unused)] fn main() { fn foo(pair: (int, int)) { let (x, y) = pair; // これで foo のどこでも x と y を使える match pair { (x, y) => { // x と y はこのスコープ内でのみ使える } } } }
どちらの場合も、パターン(上の例で let の後、=> の前に使われているもの)の構文は(ほぼ)同じです。これらのパターンは、関数宣言の引数位置でも使えます。
#![allow(unused)] fn main() { fn foo((x, y): (int, int)) { } }
(これはタプルよりも、構造体やタプル構造体でより有用です)。
ほとんどの初期化式は分解パターンの中に現れることができ、任意に複雑にできます。これには参照やプリミティブリテラルだけでなく、データ構造も含められます。たとえば次のとおりです。
#![allow(unused)] fn main() { struct St { f1: int, f2: f32 } enum En { Var1, Var2, Var3(int), Var4(int, St, int) } fn foo(x: &En) { match x { &Var1 => println!("最初のバリアント"), &Var3(5) => println!("数値 5 を持つ 3 番目のバリアント"), &Var3(x) => println!("数値 {}(5 ではない)を持つ 3 番目のバリアント", x), &Var4(3, St { f1: 3, f2: x }, 45) => { println!("埋め込まれた構造体を分解し、f2 に {} が見つかった", x) } &Var4(_, ref x, _) => { println!("f1 に {}、f2 に {} を持つ別の Var4", x.f1, x.f2) } _ => println!("その他 (Var2)") } } }
パターンの中で & を使うことで参照を通して分解していること、またリテラル(5、3、St { ... })、ワイルドカード(_)、変数(x)を混在させて使っていることに注目してください。
パターン内の単一の項目を無視したい場合、変数が期待される場所ならどこでも _ を使えます。そのため、整数に関心がなければ &Var3(_) を使うこともできました。最初の Var4 アームでは埋め込まれた構造体を分解しており(ネストしたパターン)、2 番目の Var4 アームでは構造体全体を変数に束縛しています。タプルや構造体のすべてのフィールドを表すものとして .. を使うこともできます。したがって、列挙型の各バリアントごとに何かをしたいが、バリアントの内容には関心がない場合は、次のように書けます。
#![allow(unused)] fn main() { fn foo(x: En) { match x { Var1 => println!("最初のバリアント"), Var2 => println!("2 番目のバリアント"), Var3(..) => println!("3 番目のバリアント"), Var4(..) => println!("4 番目のバリアント") } } }
構造体を分解するとき、フィールドは順番どおりである必要はなく、残りのフィールドを省くために .. を使えます。たとえば次のとおりです。
#![allow(unused)] fn main() { struct Big { field1: int, field2: int, field3: int, field4: int, field5: int, field6: int, field7: int, field8: int, field9: int, } fn foo(b: Big) { let Big { field6: x, field3: y, ..} = b; println!("{} と {} を取り出した", x, y); } }
構造体では省略記法として、フィールド名だけを使うことができ、その名前を持つローカル変数が作られます。上の例の let 文は、x と y という 2 つの新しいローカル変数を作りました。代わりに、次のように書くこともできます。
#![allow(unused)] fn main() { fn foo(b: Big) { let Big { field6, field3, .. } = b; println!("{} と {} を取り出した", field3, field6); } }
これで、フィールドと同じ名前を持つローカル変数が作られます。この場合は field3 と field6 です。
Rust の分解には、さらにいくつかの技巧があります。パターン内の変数への参照が欲しいとしましょう。& は参照を作るのではなく参照にマッチする(したがってオブジェクトをデリファレンスする効果を持つ)ため、使えません。たとえば次のとおりです。
#![allow(unused)] fn main() { struct Foo { field: &'static int } fn foo(x: Foo) { let Foo { field: &y } = x; } }
ここで、y の型は int であり、x のフィールドのコピーです。
パターン内の何かへの参照を作るには、ref キーワードを使います。たとえば次のとおりです。
#![allow(unused)] fn main() { fn foo(b: Big) { let Big { field3: ref x, ref field6, ..} = b; println!("{} と {} を取り出した", *x, *field6); } }
ここで、x と field6 はどちらも型 &int を持ち、b 内のフィールドへの参照です。
分解に関する最後の技巧として、複雑なオブジェクトを分解している場合、個々のフィールドだけでなく中間のオブジェクトにも名前を付けたいことがあります。以前の例に戻ると、&Var4(3, St{ f1: 3, f2: x }, 45) というパターンがありました。このパターンでは構造体の 1 つのフィールドに名前を付けましたが、構造体オブジェクト全体にも名前を付けたいかもしれません。&Var4(3, s, 45) と書けば構造体オブジェクトを s に束縛できますが、その場合、フィールドにはフィールドアクセスを使う必要があります。あるいは、フィールド内の特定の値とだけマッチさせたい場合は、ネストした match を使わなければならないでしょう。それは楽しくありません。Rust では @ 構文を使ってパターンの一部に名前を付けられます。たとえば &Var4(3, s @ St{ f1: 3, f2: x }, 45) と書くと、フィールド(f2 に対する x)と構造体全体(s)の両方に名前を付けられます。
これで Rust のパターンマッチングで使える選択肢はほぼ網羅できました。ベクターのマッチングなど、ここで扱っていない機能はいくつかありますが、match と let の使い方や、それらでできる強力なことの一端を理解してもらえたなら幸いです。次回は、Rust を学んでいるときにかなりつまずいた、match と借用の間にある微妙な相互作用について取り上げます。