今回はRustにおける条件分岐処理についてまとめました。
Rustではif
などの基本的な構文からif let
, match
といった特有の処理など様々な方法を使用して条件分岐させることができます。
そんな条件分岐処理の数々をサンプルコード多数で紹介しますのでぜひご覧になってください!
- 基本的な条件分岐処理
- Rust特有の条件分岐処理
- 条件分岐で使用できるパターン
if
まずはじめに基本的な条件分岐処理であるif
を見ていきます。
基本形
基本的な記述は以下のようになります。
fn sample() {
let x = 20;
if x < 10 {
println!("x is less then 10");
} else if x > 20 {
println!("x is greater then 20");
} else {
println!("x is between 10 and 20");
}
}
他言語との違い
Rustのif
は戻り値を持ちます。
そのため以下のようなコードで条件に合わせた値を変数へ束縛することができます。
fn sample() {
let x = 20;
let y = if x > 10 { 30 } else { 50 };
println!("{y}");
// -> 30
}
Rustには三項演算子が存在しません。
同様の処理を行う場合は上記のlet if
や後述のmatch
を使用します。
match
Rustには他言語に存在するswtich case
構文が存在しません。
厳密には異なりますが、似た処理としてmatch
という構文が使用できます。
基本形
基本的な記述は以下のようになります。
fn sample() {
let x = 20;
match x {
10 => println!("ten"),
20 => println!("twenty"),
_ => println!("else"), // _(アンダースコア)は上記で記述したパターン以外のすべてにマッチする
};
// -> twenty
}
match
内の各条件分岐(パターン => 式
)部分はマッチアームもしくはアームと言います。
match
の注意点として、マッチさせる変数(上記のサンプルコードではx
)が取りうるすべての値のパターンをアームとして記述する必要があります。すべてのパターンが網羅されていない場合はコンパイルエラーとなります。
fn sample() {
let x: i32 = 20;
// 下記のmatchではマッチする値が10と20のパターンしかない。
// i32型が取りうる他の値について網羅されていないためコンパイルエラーとなる。
match x {
10 => println!("ten"),
20 => println!("twenty"),
};
}
一部のパターンのみマッチさせたいという場合は後述するif letが適しています。
戻り値
match
も前述のif
と同様に戻り値を持ちます。
そのためlet
と組み合わせることで条件に応じた変数束縛することができます。
fn sample() {
let x = 20;
let y = match x {
10 => 30,
20 => 50,
_ => -1,
};
println!("{y}");
// -> 50
}
変数マッチと値の束縛
match
の良いところとして、パターンに変数を指定することで特定のリテラル値ではなく包括的な値をマッチできるということが挙げられます。
具体的にはOption
やResult
といった値を内包する型にマッチさせながら、内包する値を取り出してアーム内で使用することができます。
fn sample() {
let some = Some(5);
match some {
// Someの中身がどんな値であろうとここにマッチする。さらに中身の値を変数xに束縛する。
Some(x) => {
// xはこのアームの式で使用することができる
let y = x * x;
println!("{y}"); // -> 25
}
None => {
println!("None");
}
}
}
複雑な条件分岐
前述の変数マッチはすべての値にマッチしてしまいます。
マッチする値を絞り込みたい場合は次のマッチガードやバインディングを使用できます。
マッチガード
各アームにはマッチガードと呼ばれる追加条件を指定することができます。
マッチガードは各アームの条件式部分にif
を追加することで実現できます。
fn sample() {
let some = Some(11);
match some {
Some(x) if x > 10 => {
// xが10より大きい場合はこのブロックが実行される
println!("x > 10");
}
Some(x) if x < 0 => {
// xが上記のマッチガードに当てはまらない場合かつ、0より小さい場合はこのブロックが実行される
println!("x < 0");
}
Some(x) => {
// xが上記のマッチガードに当てはまらない場合はこのブロックが実行される
println!("{x}");
}
None => {
println!("None");
}
}
}
バインディング
バインディングと呼ばれる手法でもマッチする値を絞り込めます。
バインディングは以下のように変数の後ろに@
を記述する形式となります。
fn sample() {
let some = Some(10);
match some {
Some(x @ 10..=20) => {
// xが10~20の場合はこのブロックが実行される
println!("x = {x}");
}
Some(x) => {
// xが上記のマッチガードに当てはまらない場合はこのブロックが実行される
println!("{x}");
}
None => {
println!("None");
}
}
}
マッチガードとバインディングの違い
マッチガードとバインディングはどう使い分ければ良いのでしょうか?
そんな疑問に少しでも役に立てればと思い、大きな違いを3つほどまとめました。
- 網羅性
-
- マッチガード
-
マッチガードによる条件が網羅的であっても、
match
構文としての網羅性としては考慮されない。そのためマッチガードで対象の値が取りうるすべての値のパターンについて網羅されていても、match
構文としてのパターンが不十分であればコンパイルエラーとなる。 - バインディング
-
@
で適用される条件がmatch
構文の網羅性として考慮される。そのためバインディングが網羅的であればmatch
構文として成り立ち、逆に網羅的でなければmatch
構文として不十分となるのでコンパイルエラーとなる。
fn sample() { let some: Option<i32> = Some(10); match some { Some(v) if v > 0 => println!("{v}"), Some(v) if v <= 0 => println!("{v}"), // 上記まででSome(v)が取りうるすべての値は網羅されている。 // しかし次のアーム(すべての値にマッチするもの)がないとコンパイルエラーとなる。 Some(_) => println!("never reach"), None => println!("None"), } match some { // 1以上の数値を対象とするアーム Some(v @ 1..) => println!("{v}"), // 0以下の数値を対象とするアーム Some(v @ i32::MIN..=0) => println!("{v}"), None => println!("None"), // 上記まででOption<i32>が取りうるすべての値を網羅しているため問題なし。 } }
- 変数の束縛
-
- マッチガード
-
マッチガード内で新たに変数束縛することはできない。
- バインディング
-
バインディングで新たに変数束縛することができる。
fn sample() { let some: Option<i32> = Some(10); match some { // マッチガード(if)以降で新たな変数(t)を束縛することはできない。 v if Some(t).unwrap() > 10 => println!("{:?}", v), Some(v) => println!("{v}"), None => println!("None"), } match some { // バインディング(@)以降で新たな変数(t)を束縛することができる。 v @ Some(t @ ..=10) => println!("{:?},{:?}", v, t), None => println!("None"), _ => println!("other"), } }
- 複雑な条件
-
- マッチガード
-
マッチと関係ない条件が記述できる
- バインディング
-
マッチと関係ない条件は記述できない
fn sample() { let some: Option<i32> = Some(10); let is_enable = false; match some { // マッチガード(if)にて束縛変数(v)とは無関係の条件を記述できる。 Some(v) if v > 10 && is_enable => println!("{v}"), None => println!("None"), _ => println!("other"), } match some { // バインディング(@)では束縛変数(v)と無関係の条件は記述できない。 v @ Some(t) && is_enable => println!("{t}"), None => println!("None"), _ => println!("other"), } }
if let
match
では対象の値が取りうるパターンすべてを網羅する必要がありました。
そのため一部のみマッチしたい場合などは冗長なコードとなることもあります。
そんな場合はif let
を使用することで簡潔に記述することができます。
fn sample() {
let some = Some(35);
match some {
Some(3) => println!("3"), // 直接マッチ(単一パターン)
Some(10 | 20) => println!("10 or 20"), // 直接マッチ(複数パターン)
Some(20..=30) => println!("21 to 30"), // 直接マッチ(範囲)
Some(x @ 35) => println!("{x}"), // バインディング(単一パターン)
Some(x @ (36 | 37)) => println!("{x}"), // バインディング(複数パターン)
Some(x @ 38..=80) => println!("{x}"), // バインディング(範囲)
Some(x) => println!("{:?}", x * x), // 変数マッチ(すべての値)
_ => println!("other"), // 上記以外のすべての値
}
}
let else
次にRust1.65にて新たに導入された構文であるlet else
を見てみましょう。
基本的な使い方は以下のようになります。
fn sample() {
let option: Option<i32> = Some(10);
// Some(v)のパターンにマッチするのでsome_valueに値が束縛され、elseは実行されない
let Some(some_value) = option else {
println!("else");
return
};
println!("{:?}", some_value); // -> 10
// Noneのパターンにマッチしないのでelseが実行される
let None = option else {
println!("else");
return;
};
// 上記のelseでreturnしているのでここには到達しない
println!("never reach");
}
if letとの違い
上記のサンプルコードを見るとわかりますが、条件にマッチして束縛した変数(サンプルコードのsome_value
)をそのまま現在のスコープで使用しています。
これに対してif let
では束縛した変数は分岐処理内のブロックでしか使用できません。
fn sample() {
let some: Option<i32> = Some(10);
if let Some(i) = some {
// 束縛した変数iはこのブロック内でしか使えない
println!("{:?}", i);
}
// 以下のようにしても変数iは分岐処理外のスコープには存在しないためコンパイルエラーとなる。
println!("{:?}", i);
}
let ifとの違い
let else
と似た構文にlet if
というものがありました。(サンプルコード)
これらの違いとしてはパターンマッチを使用した変数束縛ができるか否かというところです。
fn sample() {
let option: Option<i32> = Some(10);
// Someの中身を取り出すことができる。Noneの場合はelseが実行される。
let Some(some_value) = option else {
println!("else");
return
};
// 以下のようには記述できない。
let Some(v) = if true { Some(2) } else { Some(5) };
}
値の絞り込み
バインディングを使用することでlet else
でもマッチさせたい値を絞り込むことができます。
fn sample() {
let option: Option<i32> = Some(0);
// Someの中身が10~20の場合は変数束縛が実行され、それ以外はelseが実行される
let Some(some_value @ 10..=20) = option else {
println!("else");
return
};
}
elseの戻り値
else
ブロックからの戻り値はlet else
が存在する関数の戻り値と同じ型か!
型(never
型)である必要があります。
例:関数の戻り値がi32
の場合
fn main() {
let x = sample();
}
fn sample() -> i32 {
let option: Option<i32> = None;
let Some(_x) = option else {
return -1
};
let Some(_y) = option else {
panic!("else");
};
0
}
例:関数の戻り値がない(()
型となる)場合
fn main() {
sample();
}
fn sample() {
let option: Option<i32> = None;
let Some(_x) = option else {
return
};
let Some(_y) = option else {
panic!("else");
};
}
while let
while let
という構文を用いることで、ループ構造であるwhile
においても変数マッチを行うことができます。
fn sample() {
let mut vec = vec![1, 2, 3];
while let Some(x) = vec.pop() {
println!("{:?}", x);
// -> 3
// -> 2
// -> 1
}
}
使用可能なパターンの例
最後に、今回紹介したmatch
,if let
,let else
,while let
で使用できるパターンをまとめてみました。
fn sample() {
let some = Some(35);
match some {
Some(3) => println!("3"), // 直接マッチ(単一パターン)
Some(10 | 20) => println!("10 or 20"), // 直接マッチ(複数パターン)
Some(20..=30) => println!("21 to 30"), // 直接マッチ(範囲)
Some(x @ 35) => println!("{x}"), // バインディング(単一パターン)
Some(x @ (36 | 37)) => println!("{x}"), // バインディング(複数パターン)
Some(x @ 38..=80) => println!("{x}"), // バインディング(範囲)
Some(x) => println!("{:?}", x * x), // 変数マッチ(すべての値)
_ => println!("other"), // 上記以外のすべての値
}
}
最後に
様々な条件分岐処理があり、それぞれの違いやニュアンスを理解するのが大変でした。。。
しかしRustの分岐処理はコードを簡潔にできるとともにスコープも限定されてとても良いです。
バージョンアップを重ねてさらに便利な条件分岐処理も増えています。
より簡潔かつ堅牢なコードを書くためにもキャッチアップを続けていきたいところです。