分解(造語:非構造化)
前回は、Rustのデータ型について見た。
一旦、何かしらデータ構造を手に入れたら、そこからデータを取り出したくなるだろう。
構造体について、RustではC++と全く同じようにフィールドアクセスができる。しかし、タプルやタプル構造体、enumについては、分解を行わねばならない(ライブラリには様々な便利関数があるが、それらも内部的には非構造化機能を使用している)。
データ構造の分解は、C++にはない機能だ。しかし、Pythonや種々の関数型言語で馴染み深いかもしれない。その根底にあるのは、ローカル変数でフィールド値を初期化し、データ構造を作成できるように、ローカル変数をデータ構造の値で初期化できるはずだという理論である。この単純な理論に端を発して、非構造化はRustで最も強力な機能の一つになった。
別の言い方をすると、非構造化は、パターンマッチングとローカル変数への代入を組み合わせたものである。
非構造化は、主にlet文かmatch文で行われる。対象のデータ構造が複数の状態を持つ(enumなど)場合は、match文を使用する。let式は現在のスコープに変数を展開するが、match式はそれ自身スコープを持っている。比較してみよう。
fn foo(pair: (int, int)) {
let (x, y) = pair;
// これでfoo内のどこからでもxとyを使用できる
match pair {
(x, y) => {
// こちらのxとyは、このスコープ内でしか使用できない
}
}
}
パターン(上記の例では、letキーワードの後と、=>記号の前に見られる)の書き方は、どちらのケースでも全く同じだ。このパターンは、関数定義の引数の位置でも使用することができる。fn foo((x, y): (int, int)) {
}
(この書き方は、タプルよりも構造体やタプル構造体に対して効果を発揮する)非構造化パターンは、ほとんどの初期化式内に書くことができ、無限に複雑化できる。この中には、データ構造のみならず、参照やリテラルも含まれる。
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"),
&Var3(x) => println!("三つ目 数字の{}", x),
&Var4(3, St { f1: 3, f2: x }, 45) => {
println!("構造体入り。f2の中身は{}", x)
}
&Var4(_, x, _) => {
println!("別のVar4 f1の中身は{} f2の中身は{}", x.f1, x.f2)
}
_ => println!("その他(Var2)")
}
}
パターン内に&記号を使用して参照で分解する方法と、リテラル(5, 3, St{ ... })やワイルドカード(_)、変数(x)を組み合わせる方法に注目してほしい。パターン内でアイテムを一つ無視したい場合は、変数の代わりに_を記述すればいい。なので、中身の整数がどうでもよければ、&Var3(_)と書いてもいい。
最初のVar4項で中身の構造体を分解(ネストしたパターン)している。また、二番目のVar4項では、構造体全体を変数に束縛している。
その他、..と書くとタプルや構造体の全フィールドを表すことができる。したがって、enumの状態に応じて処理を行う必要はあるが、中身はどうでもいい場合は以下のようにも書くことができる。
fn foo(x: En) {
match x {
Var1 => println!("一つ目"),
Var2 => println!("二つ目"),
Var3(..) => println!("三つ目"),
Var4(..) => println!("四つ目")
}
}
構造体を分解する際、フィールドは定義時の順番通りに記述する必要はなく、..で残りのフィールドを省略することもできる。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);
}
構造体の省略記法として、フィールド名を記述すれば、同名のローカル変数を定義してくれる。上記の例では、xとyという新しい変数を定義していたが、次のようにも書ける。fn foo(b: Big) {
let Big { field6, field3, ..} = b;
println!("{}と{}を取り出したよ", field3, field6);
}
今度は、フィールドと同じ名前のローカル変数を定義している。今回の場合は、field3とfield6ね。これ以外にもRustの分解機能にはテクニックが必要なものがある。
例えば、パターン内で変数を参照する必要が出たとしよう。この場合、&演算子は使用できない。これだと、参照を作成するんじゃなくて、参照にマッチしちゃうからね(ゆえに、オブジェクトを被参照することになる)。コードで表すとこう。
struct Foo {
field: &'static int
}
fn foo(x: Foo) {
let Foo { field: &y } = x;
}
ここで、変数yのタイプはintとなり、変数xのフィールドをコピーしている。パターン内で参照を作成するには、refキーワードを使う。
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)というパターンがあった。このパターン内では、あるフィールドだけに名前付けをしていたが、構造体オブジェクト全体に名前付けをする必要が出てくる可能性もある。
もちろん、&Var4(3, s, 45)と書けば、変数sに構造体オブジェクトを束縛できるが、こうしたら、フィールドアクセスにはもう1段階処理が必要になってしまうし、またフィールドが特定の値だった場合だけにマッチさせたいときにはmatch式をネストする必要が出てくる。これでは面白くない。
そこで、Rustでは、パターンの一部を@記号で名前付けすることができるようになっている。例として、&Var4(3, s @ St{f1: 3, f2: x }, 45)と書けば、フィールド(f2フィールドに対して変数x)と構造体オブジェクト全体(変数s)を変数に束縛できるのだ。
これにて、Rustのパターンマッチングの機能はほぼ網羅し終えた。まだベクターコンテナマッチングなどカバーしていない機能もあるけど、match式とlet文の使い方や強力な機能の一部でも理解してもらえてればありがたい。
次回は、match式と無所有権参照の些事たる関連性について解説する。これがまた、Rustを学習する上で間違いやすいんだよな〜。
原文: https://github.com/nrc/r4cppp/blob/master/destructuring.md
0 件のコメント:
コメントを投稿
なにか意見や感想、質問などがあれば、ご自由にお書きください。