プログラミング言語Rustのパターンマッチを完全に理解しました!!!
記念にここにメモしておこうと思います!
let文について
let文ってありますよね?こういうのです。
1 | let x = 1; |
タプルや構造体ならこんな感じ。
1 | struct Foo { |
これが基本系ですね。
これが基本?そうじゃない!
let 変数名 = 式;
と書くのが基本・・・そう思っていましたが、違ったんです。変数名
ではなく 右辺の型
と同じ構造を書くのが基本、そう考えましょう。
つまり、タプルや構造体なら下のように。
1 | struct Foo { |
参照なら、右辺と同じように左辺にも&を付ける。1
2let &(x, y) = &(1, 2);
let &mut (x, y) = &mut (1, 2);
さらに・・・より複雑なら型なら同じように複雑に。1
2
3
4
5
6struct Bar {
a: (i32, i32),
b: i32,
};
let &Foo { a: (x, y), b: z } = &Foo { a: (1, 2), b: 3 };
右辺と同じ構造を左辺にも書く。それが基本と考えましょう。
こうやって書くと左辺の変数(x,y,z)に右辺の対応する値(1,2,3)が入ります。
値をまとめる
右辺では値を直接書かなくても変数で構造体やタプルを一度に指定できますよね?
こんな感じです。
1 | struct Bar { |
8行目の右辺で(1, 2)と書く代わりに、変数lを使用しました。
この記法は左辺でも使えます。
1 | struct Bar { |
これでタプル (1, 2)
がまとまって変数 x
に入りました。
もっとまとめる
構造体もまとめてみましょう。
1 | struct Bar { |
これで構造体がまるごと変数xに入りました。
もっともっとまとめる
参照もまとめてしまいましょう。
1 | struct Bar { |
これでxに構造体の参照が入りました。
おや・・・このパターンは左辺が変数名だけ・・・最初に基本形だと思っていたパターンですね。
そうなんです、基本形だと思っていたパターンは値をまとめ尽くした最終形態だったのです。
forループ
パターンが使えるのはlet文だけではありません。forループでも同じようにパターンが使えます。
少し例を見てみましょう。
1 | let xs = &[(1, 2), (3, 4), (5, 6)]; |
xs
は IntoIterator<&(i32,i32)>
なので &(i32,i32)
のパターン &(a, b)
を使っています。これが基本と考えます。
そして、必要に応じて値をまとめたり、&を付けずに参照そのまま束縛して使いましょう。
束縛方法を変える
もう一つ例を見てみましょう。&(String, i32)
型の式をそのままパターンにしてみます。1
2
3let s = &(String::from("A"), 1);
let &(a, b) = s;
1 | error[E0507]: cannot move out of borrowed content |
コンパイルエラーとなってしまいました。何故でしょうか?
まずは a
の型が何になるか考えてみてください。
右辺の型は &(String, i32)
なので、&(a, b)
の a
は String
になりますよね?
これってどう見ても無理じゃないですか。所有権的に考えて。String
を作るには所有権を奪わないといけないけれど &(String, i32)
の所有権は借り物で奪えない。
このような場合はパターンは使えないのでしょうか?
いいえ、そんなことはありません。
ref - 参照を束縛する
こんな時に使えるのが ref
です。
変数名に ref を付けると、値そのものを束縛するのではなく、値の参照を束縛することができるのです。
試してみましょう。1
2
3let s = &(String::from("A"), 1);
let &(ref a, b) = s;
a
の型は &String
となり、コンパイルエラーも出なくなりました。
なお、ミュータブル参照を取得するには ref mut
を使います。1
2
3let s = &mut (String::from("A"), 1);
let &mut (ref mut a, b) = s;
_ - 束縛しない
先ほどは参照を束縛しましたが、そもそも束縛しないという選択肢もあります。
その為に使えるのが _
です。
試してみましょう。1
2
3let s = &(String::from("A"), 1);
let &(_, b) = s;
束縛しないので所有権は必要なく、コンパイルエラーも出ません。
なお、所有権を奪わないのでこんなこともできます。1
2
3
4
5
6
7
8
9let s = String::from("A");
let _ = s;
let _ = s; // 運が良かったな。普通の変数ならここでエラーになっていた所だ。
let _ = s;
let _ = s;
let _ = s;
let _ = s;
let _ = s;
コピー不可能な型の値を何度でも代入することができます。意味はありませんが。
また、変数への代入のような値の寿命を延ばしてdropのタイミングを遅らせる効果もありません。
次のコードを実行してみましょう。1
2
3
4
5
6
7
8
9struct Foo(u32);
impl Drop for Foo {
fn drop(&mut self) {
println!("drop {}", self.0);
}
}
let _ = Foo(1); // Foo(1) はすぐに破棄される
let x = Foo(2); // Foo(2) はxのスコープが終了するときに破棄される
println!("3");
すると・・・結果は次のようになります。1
2
3drop 1
3
drop 2
お判りいただけたでしょうか?Foo(1)
の戻り値は _
に代入されましたが、スコープの終了を待たず、すぐにdropされています。
変数を使わないからといって変数を _
に変更すると、プログラムの意味が変わってしまう場合があるので注意が必要です。
変数未使用の警告を消したいならば、_x
のようなアンダーバーから始まる変数名を使用することで警告を回避できます。1
let _x = 1; // これなら変数未使用の警告は出ない
.. - 複数の値を束縛しない
_
を使えば必要な値だけを束縛でき無駄がなくて良いですね。
しかし、不必要な値がたくさんあった場合はどうでしょう? _
を沢山書くのは面倒です。
そんな時に役立つのが ..
です。..
を使うと使わないフィールドを省略することができます。1
2
3
4
5
6
7
8
9
10
11struct Foo {
a: String,
b: i32,
c: i32,
}
let x = Foo {
a: String::from("A"),
b: 1,
c: 2,
};
let &Foo { b: b, .. } = &x;
今回は Foo::b
だけ束縛してみました。
..
はタプルでも使えます。1
2
3
4
5
6let x = (1, 2, 3, 4);
let (a, ..) = x;
let (.., d) = x;
let (a, .., d) = x;
let (..) = x;
一致するとは限らないパターン
列挙型
今まで構造体、タプル、参照のパターンを見てきました。
次は列挙型のパターンを見てみましょう。
選択肢が一つの列挙型ならば、構造体と同じようにlet文で分解できます。1
2
3
4
5
6enum Foo {
A(u32),
};
let a = Foo::A(0);
let Foo::A(y) = a;
println!("{}", y); // 0
しかし。選択肢が複数の列挙型では・・・1
2
3
4
5
6enum Foo {
A(u32),
B(u32),
};
let a = Foo::A(0);
let Foo::A(y) = a;
1 | error[E0005]: refutable pattern in local binding: `B(_)` not covered |
コンパイルエラーが出てしまいました。何故でしょうか?
それは、右辺が左辺のパターンにマッチしない可能性があるからです。
右辺の型はFoo型なので Foo::A
Foo::B
の可能性があります。
しかし、左辺のパターンはFoo::A(y)
・・・つまり、Foo::B
の場合の処理が不可能なのです。
そのため、コンパイルエラーとなってしまったわけですね。
if let 式
列挙型のようなマッチしない可能性のあるパターンを使えるのが if let 式
です。
例を見てみましょう。1
2
3
4
5
6
7
8
9
10
11
12enum Foo {
A(u32),
B(u32),
};
let a = Foo::A(0);
if let Foo::A(y) = a {
// ここは実行される
}
if let Foo::B(y) = a {
// ここは実行されない
}
if let 式
はパターンにマッチした時だけ値を変数に束縛し、ブロックを実行するのです。
まさに if let
の名にふさわしい機能ですね。
定数とのマッチ
さて「確実にマッチするパターン」 だけでなく 「マッチしないかもしれないパターン」 というものが存在することがわかりました。
ここでは「マッチしないかもしれないパターン」 でのみ使える構文をいくつか紹介しましょう。
まず始めは 1
2
3
のような 定数 です。
左辺に 1
2
3
のような定数を記述すると、その値が右辺の対応する位置の値と一致するときのみマッチするパターンになります。1
2
3
4
5
6if let (1, x) = (1, 1) {
// ここは実行される
}
if let (1, x) = (2, 1) {
// ここは実行されない
}
数字以外に文字列も使用できます。1
2
3
4
5
6if let ("abc", x) = ("abc", 1) {
println!("A");
}
if let ("xyz", x) = ("abc", 1) {
println!("X");
}
値の範囲とのマッチ
次に紹介するのは値の範囲です。1...5
のように記述することで、特定の値の範囲と一致する場合のみマッチするパターンになります。
1 | if let (1...5, x) = (1, 1) { |
値の範囲のイテレータを作成する構文(例:1..5
) と異なり、
- ピリオドの数が3つ
- 最後の値を含む(上の例では5を含む)
事に注意してください。
@パターン
値の範囲とのマッチができるようになったのは良いのですが・・・ちょっと次の例を見てください。
1 | fn foo(n: Option<i32>) { |
0から5までのうち、どの数字にマッチしたのかがわかりませんね。
いや n.unwrap()
と書けば確かに数字は取り出せますよ? でも unwrap
は使いどころを間違えてもコンパイラが警告してくれないので、できれば避けたいところです。
そんな時に役立つのが @
です。 変数名 @ パターン
と書く事で、そのパターンにマッチした値を変数に束縛することができます。1
2
3
4
5fn foo(n: Option<i32>) {
if let Some(i @ 0...5) = n {
println!("{}", i);
}
}
これで値のチェックと束縛が同時に行えるようになりましたね。
else if let 節, else if 節, else 節
if let 式
には else if let 節
, else if 節
, else 節
を追加することができます。
使い方はその名前から想像できると思うので省略します。
気になる方はこのページの最後を見てください。
while let 式
条件式のパターンとマッチする間、何度もマッチとブロックの実行を繰り返す while let 式
もあります。Iterator
を実装しなくても Iterator::next
相当の関数だけで for
相当のループが書けるので便利です。
次の例はVecをスタックと見立てて、スタックが空になるまでループを回しています。1
2
3
4
5fn foo(q: Vec<u8>) {
while let Some(item) = q.pop() {
// ここに処理を書く
}
}
match式
先ほどの列挙型は選択肢が二つだけでした。
今度はもっと選択肢の多い列挙型で if let
を使うことを考えてみましょう。
1 | enum Foo { |
う~ん、なんだか記述が冗長ですね。他の言語の switch
文のような物はないのでしょうか?
はぁい!ありまぁす!
Rustには match
式がありまぁす!
上のコードは match
式を使うと次のように書くことができます。
1 | enum Foo { |
さて、match
式には if let
式と異なる重要な特性があります。
それは、いずれかのパターンに必ずマッチしなければならない という点です。
試しに、列挙型の選択肢を1つ増やしてみましょう。
1 | enum Foo { |
1 | error[E0004]: non-exhaustive patterns: `D` not covered |
コンパイルエラーになってしまいました。
エラーメッセージに pattern `D` not covered
とありますね。
そうです。この match
式では追加した Foo::D
にマッチしないため、100%マッチする状況ではなくなり、エラーとなってしまったのです。
では、match
式に足りない Foo::D
を追加してましょう。
1 | enum Foo { |
無事コンパイルエラーはなくなりました。
しかし、常に match
式ですべての選択肢を使うとは限りません。
一部の選択肢を省略することもできます。
100%確実にマッチさせなければならない・・・ならば、100%確実にマッチするパターンを書いてしまえばよいのです。1
2
3
4
5
6
7
8
9
10
11
12
13
14
enum Foo {
A,
B,
C,
D,
}
fn bar(x: Foo) {
match x {
Foo::A => println!("AAAAAAAAAA!!!"),
y => println!("x は {:?} です", y),
}
}
let y = x;
で y
が100%確実にマッチするパターンだったことを思い出してください。
ここでも同じように y
は100%確実にマッチするため、 match
式全体でも100%確実にマッチするようになり、エラーは出なくなります。
ところで、上の例では Foo::A
の時は
println!("AAAAAAAAAA!!!")
println!("x は {:?} です", y)
の両方が実行されるのでしょうか?それとも、前者のみが実行されるのでしょうか?
答えは、前者のみが実行される です。
match
式では最初にマッチしたパターンのブランチのみが実行されるため、100%マッチするパターンを最後に書くことで、他のブランチにマッチしなかった場合に実行するブランチになります。
他の言語の switch
の default
のように使えますね。
なお、変数が必要なければ、代わりに _
を使うこともできます。1
2
3
4
5
6
7
8
9
10
11
12
13enum Foo {
A,
B,
C,
D,
}
fn bar(x: Foo) {
match x {
Foo::A => println!("AAAAAAAAAA!!!"),
_ => println!("xの値は教えぬ!"),
}
}
match式だけで使える構文
if let
式で使えるパターンの構文は全て match
式でも使うことができます。
一方、match
式だけで使えて、if let
式では使えない構文がいくつかあります。
複数のパターンを指定
|
で区切って複数のパターンを記述することで、いずれかのパターンにマッチする場合に実行されるブランチになります。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16enum Foo {
A,
B,
C,
D,
}
fn bar(x: Foo) {
match x {
Foo::A | Foo::B => println!("A又はB"),
Foo::C | Foo::D => {
println!("C!");
println!("いや、Dかも!");
}
}
}
ガード
パターンの後に if 条件式
を付けることで、条件式に一致した場合のみ実行されるブランチになります。1
2
3
4
5
6
7
8fn bar(x: u32, y: u32) {
match (x, y) {
(vx @ 0...10, vy @ 0...10) if vx != vy => {
println!("xとyが10以下。ただしx==yの時は除く。")
}
_ => {}
}
}
まとめ
今まで色々なパターンを見てきました。まとめると・・・
- パターンは値の束縛、分解、マッチングを同時に行う構文である
- パターンは値を構築するときと同じ構文が使える
- 100%マッチするパターンとマッチしない可能性があるパターンがあり、使える場面が異なる
理解すると実にシンプルですね。
なお、Rustの公式ドキュメントの第二版にパターンの詳しい説明があります。(英語)
ぜひこちらも見てください。
https://doc.rust-lang.org/book/second-edition/ch18-00-patterns.html
第一版の物足りない説明と比べて大幅に進化しています。
最後にパターンの構文と使える場所をまとめておきたいと思います。
パターンの構文一覧
構文の例 | 名前 |
---|---|
x |
変数への束縛 |
(x, y, z) |
タプル |
Foo { a: x, b: y, c: z } |
構造体 |
Foo(x, y, z) |
タプル構造体 |
Foo::A |
enum |
_ |
無視 |
.. |
タプル・構造体の一部を無視 |
&x |
共有参照の展開 |
&mut x |
ミュータブル参照の展開 |
ref x |
共有参照の束縛 |
ref mut x |
ミュータブル参照の束縛 |
5 |
定数 |
2...7 |
値の範囲 |
x @ 2...7 |
@による変数の束縛 |
p | p |
複数パターン (match 式のみ) |
x if x > 5 |
ガード (match 式のみ) |
パターンの使える場所
値を変数に束縛できる場所で使えます。
具体的には、次の表の例で p
と書かれた部分で使えます。
網羅性が 必要
となっている部分では100%マッチするパターンを書く必要があります。
場所 | 例 | 複数パターン | 網羅性 |
---|---|---|---|
let文 | let p = a; |
× | 必要 |
関数の仮引数 | fn (p : t) { } |
× | 必要 |
クロージャの仮引数 | |p| { } |
× | 必要 |
for ループ | for p in expr { } |
× | 必要 |
while let ループ | while let p = expr { } |
× | 不要 |
if let 式 | if let p = expr { } |
〇 (*1) | 不要 |
match 式 | match expr { p => { }, p => { } } |
〇 | 必要 (*2) |
- *1 : else if let 節を追加することで複数パターンを利用可能
- *2 : 複数パターンの合計で網羅性が必要