制御フロー
条件が true であるかどうかに応じて何らかのコードを実行する機能や、条件が true である間に何らかのコードを繰り返し実行する機能は、ほとんどのプログラミング言語における基本的な構成要素です。Rust コードの実行フローを制御するためのもっとも一般的な構文は、if 式とループです。
if 式
if 式を使うと、条件に応じてコードを分岐できます。条件を指定し、「この条件が満たされたら、このコードブロックを実行する。条件が満たされなければ、このコードブロックは実行しない」と記述します。
if 式を試すために、projects ディレクトリに branches という新しいプロジェクトを作成してください。src/main.rs ファイルに、次の内容を入力します。
ファイル名: src/main.rs
fn main() {
let number = 3;
if number < 5 {
println!("condition was true");
} else {
println!("condition was false");
}
}
すべての if 式は、キーワード if で始まり、その後に条件が続きます。この場合、条件は変数 number の値が 5 未満かどうかをチェックしています。条件が true のときに実行するコードブロックは、中かっこで囲んで条件の直後に置きます。if 式の条件に関連付けられたコードブロックは、「推測と秘密の数字を比較する」 で第2章で説明した match 式のアームと同じように、アーム と呼ばれることがあります。
必要に応じて、ここで行っているように else 式を含めることもできます。これにより、条件が false と評価された場合に実行する別のコードブロックをプログラムに与えられます。else 式を用意せず、条件が false の場合、プログラムは単に if ブロックをスキップして次のコードへ進みます。
このコードを実行してみてください。次のような出力が表示されるはずです。
$ cargo run
Compiling branches v0.1.0 (file:///projects/branches)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.31s
Running `target/debug/branches`
condition was true
では、条件が false になる値に number の値を変更して、何が起こるか見てみましょう。
fn main() {
let number = 7;
if number < 5 {
println!("condition was true");
} else {
println!("condition was false");
}
}
もう一度プログラムを実行し、出力を見てみてください。
$ cargo run
Compiling branches v0.1.0 (file:///projects/branches)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.31s
Running `target/debug/branches`
condition was false
このコードの条件は、必ず bool でなければならないことにも注意してください。条件が bool でなければ、エラーになります。たとえば、次のコードを実行してみてください。
ファイル名: src/main.rs
fn main() {
let number = 3;
if number {
println!("number was three");
}
}
今回は if の条件が値 3 に評価されるため、Rust はエラーを返します。
$ cargo run
Compiling branches v0.1.0 (file:///projects/branches)
error[E0308]: mismatched types
--> src/main.rs:4:8
|
4 | if number {
| ^^^^^^ expected `bool`, found integer
For more information about this error, try `rustc --explain E0308`.
error: could not compile `branches` (bin "branches") due to 1 previous error
このエラーは、Rust が bool を期待していたのに整数を受け取ったことを示しています。Ruby や JavaScript のような言語とは異なり、Rust はブール値でない型をブール値へ自動的に変換しようとはしません。常に明示的に、if に条件としてブール値を与える必要があります。たとえば、数値が 0 と等しくないときだけ if のコードブロックを実行したいなら、if 式を次のように変更できます。
ファイル名: src/main.rs
fn main() {
let number = 3;
if number != 0 {
println!("number was something other than zero");
}
}
このコードを実行すると、number was something other than zero と表示されます。
else if で複数の条件を扱う
if と else を組み合わせた else if 式を使うことで、複数の条件を扱えます。たとえば、次のようになります。
ファイル名: src/main.rs
fn main() {
let number = 6;
if number % 4 == 0 {
println!("number is divisible by 4");
} else if number % 3 == 0 {
println!("number is divisible by 3");
} else if number % 2 == 0 {
println!("number is divisible by 2");
} else {
println!("number is not divisible by 4, 3, or 2");
}
}
このプログラムには、取りうる経路が 4 つあります。実行すると、次のような出力が表示されるはずです。
$ cargo run
Compiling branches v0.1.0 (file:///projects/branches)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.31s
Running `target/debug/branches`
number is divisible by 3
このプログラムが実行されると、各 if 式を順番にチェックし、条件が true と評価された最初の本体を実行します。6 は 2 でも割り切れるにもかかわらず、出力に number is divisible by 2 は表示されず、else ブロックの number is not divisible by 4, 3, or 2 というテキストも表示されないことに注目してください。これは、Rust が最初に true になった条件に対応するブロックだけを実行し、1 つ見つけた時点で残りは確認すらしないからです。
else if 式を使いすぎるとコードが煩雑になることがあるため、複数ある場合はコードのリファクタリングを検討するとよいでしょう。こうしたケースのための強力な Rust の分岐構文である match については、第6章で説明します。
let 文で if を使う
if は式なので、リスト 3-2 のように let 文の右辺で使って、その結果を変数に代入できます。
fn main() {
let condition = true;
let number = if condition { 5 } else { 6 };
println!("The value of number is: {number}");
}
変数 number は、if 式の結果に応じた値に束縛されます。このコードを実行して、何が起こるか見てみましょう。
$ cargo run
Compiling branches v0.1.0 (file:///projects/branches)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.30s
Running `target/debug/branches`
The value of number is: 5
コードブロックはその中の最後の式に評価され、数値それ自体も式であることを思い出してください。この場合、if 式全体の値は、どのコードブロックが実行されるかに依存します。つまり、if の各アームから結果として返されうる値は、同じ型でなければなりません。リスト 3-2 では、if アームと else アームの両方の結果は i32 整数でした。次の例のように型が一致しない場合は、エラーになります。
ファイル名: src/main.rs
fn main() {
let condition = true;
let number = if condition { 5 } else { "six" };
println!("The value of number is: {number}");
}
このコードをコンパイルしようとすると、エラーになります。if アームと else アームは互換性のない値の型を持っており、Rust はプログラム内のどこに問題があるかを正確に示してくれます。
$ cargo run
Compiling branches v0.1.0 (file:///projects/branches)
error[E0308]: `if` and `else` have incompatible types
--> src/main.rs:4:44
|
4 | let number = if condition { 5 } else { "six" };
| - ^^^^^ expected integer, found `&str`
| |
| expected because of this
For more information about this error, try `rustc --explain E0308`.
error: could not compile `branches` (bin "branches") due to 1 previous error
if ブロック内の式は整数に評価され、else ブロック内の式は文字列に評価されます。これはうまくいきません。変数は単一の型を持たなければならず、Rust はコンパイル時に number 変数の型が何であるかを明確に知っている必要があるからです。number の型が分かっていれば、コンパイラは number を使うあらゆる場所でその型が正しいことを検証できます。もし number の型が実行時にしか決まらないとしたら、Rust はそれを行えません。どの変数についても複数の仮の型を追跡しなければならないなら、コンパイラはより複雑になり、コードに対して与えられる保証も少なくなってしまいます。
ループによる繰り返し
コードのブロックを複数回実行したいことはよくあります。このために、 Rust にはいくつかの_ループ_が用意されており、ループ本体の中のコードを 最後まで実行してから、すぐに先頭に戻って再び開始します。ループを試して みるために、loops という名前の新しいプロジェクトを作成しましょう。
Rust には 3 種類のループがあります: loop、while、for です。それぞれを試してみましょう。
loop でコードを繰り返す
loop キーワードは、コードのブロックを何度も繰り返し実行するよう Rust に
指示します。明示的に停止するよう伝えるまで、あるいは永遠に実行されます。
例として、loops ディレクトリ内の src/main.rs ファイルを次のように変更して ください:
ファイル名: src/main.rs
fn main() {
loop {
println!("again!");
}
}
このプログラムを実行すると、手動で停止するまで again! が途切れることなく
繰り返し表示されます。ほとんどのターミナルは、ループし続けている
プログラムを中断するためのキーボードショートカット
ctrl-C をサポートしています。試してみてください:
$ cargo run
Compiling loops v0.1.0 (file:///projects/loops)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.08s
Running `target/debug/loops`
again!
again!
again!
again!
^Cagain!
記号 ^C は、ctrl-C を押した位置を表しています。
割り込みシグナルを受け取ったときにコードがループのどこを実行していたかに
よっては、^C の後に again! が表示されることもあれば、表示されないこともあります。
幸い、Rust にはコードを使ってループを抜ける方法もあります。ループの中に
break キーワードを置くことで、いつループの実行を停止するかをプログラムに
指示できます。第 2 章の
「正しい予想をした後に終了する」 節で、ユーザーが正しい数を当ててゲームに勝ったときにプログラムを終了するために
これを行ったことを思い出してください。
推測ゲームでは continue も使いました。これはループ内で、その反復にある
残りのコードをスキップして、次の反復へ進むようプログラムに指示します。
ループから値を返す
loop の用途のひとつは、スレッドが処理を完了したかどうかを確認する場合の
ように、失敗する可能性があると分かっている操作を再試行することです。また、
その操作の結果をループの外に出して、コードの残りの部分へ渡す必要があるかも
しれません。これを行うには、ループを停止するために使う break 式の後に
返したい値を追加します。その値はループの外へ返されるので、次のように
利用できます:
fn main() {
let mut counter = 0;
let result = loop {
counter += 1;
if counter == 10 {
break counter * 2;
}
};
println!("The result is {result}");
}
ループの前で、counter という名前の変数を宣言し、0 で初期化します。
次に、ループから返される値を保持するための result という名前の変数を宣言します。
ループの各反復で、counter 変数に 1 を加え、その後 counter が 10 と
等しいかどうかを確認します。そうなったら、counter * 2 という値とともに
break キーワードを使います。ループの後では、result に値を代入する文を
セミコロンで終えます。最後に、result の値を表示します。この場合は 20 です。
ループの内側から return することもできます。break は現在のループだけを
抜けますが、return は常に現在の関数を抜けます。
ループラベルで対象を明確にする
ループの中にさらにループがある場合、break と continue はその位置で最も
内側にあるループに適用されます。必要に応じて、ループに_ループラベル_を
指定できます。そうすると、そのラベルを break や continue と一緒に使って、
それらのキーワードが最も内側のループではなく、ラベル付きのループに適用
されることを指定できます。ループラベルはシングルクォートで始める必要が
あります。次は 2 つの入れ子になったループの例です:
fn main() {
let mut count = 0;
'counting_up: loop {
println!("count = {count}");
let mut remaining = 10;
loop {
println!("remaining = {remaining}");
if remaining == 9 {
break;
}
if count == 2 {
break 'counting_up;
}
remaining -= 1;
}
count += 1;
}
println!("End count = {count}");
}
外側のループには 'counting_up というラベルがあり、0 から 2 まで数え上げます。
ラベルのない内側のループは 10 から 9 まで数え下げます。ラベルを指定しない
最初の break は内側のループだけを抜けます。break 'counting_up; 文は外側のループを抜けます。このコードは次を表示します:
$ cargo run
Compiling loops v0.1.0 (file:///projects/loops)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.58s
Running `target/debug/loops`
count = 0
remaining = 10
remaining = 9
count = 1
remaining = 10
remaining = 9
count = 2
remaining = 10
End count = 2
while を使った条件付きループの簡潔化
プログラムでは、ループ内で条件を評価する必要が生じることがよくあります。
条件が true である間はループが実行されます。条件が true でなくなると、
プログラムは break を実行してループを停止します。このような振る舞いは、
loop、if、else、break を組み合わせても実装できます。望むなら、
今ここでプログラムとして試してみてもよいでしょう。しかし、このパターンは
非常によく使われるため、Rust には while ループと呼ばれる組み込みの言語構文が
あります。リスト 3-3 では、while を使ってプログラムを 3 回繰り返し、
毎回カウントダウンし、その後ループの後でメッセージを表示して終了します。
fn main() {
let mut number = 3;
while number != 0 {
println!("{number}!");
number -= 1;
}
println!("LIFTOFF!!!");
}
この構文により、loop、if、else、break を使う場合に必要になる多くの
入れ子構造が不要になり、より明確になります。条件が true と評価されている
間はコードが実行され、それ以外の場合はループを抜けます。
for でコレクションを反復処理する
配列のようなコレクションの要素を順に処理するために、while 構文を使うことも
できます。たとえば、リスト 3-4 のループは配列 a の各要素を表示します。
fn main() {
let a = [10, 20, 30, 40, 50];
let mut index = 0;
while index < 5 {
println!("the value is: {}", a[index]);
index += 1;
}
}
ここでは、コードは配列の要素を順に数え上げながら進みます。インデックス
0 から始まり、配列の最後のインデックスに達するまでループします(つまり、
index < 5 がもはや true でなくなるまでです)。このコードを実行すると、
配列内のすべての要素が表示されます:
$ cargo run
Compiling loops v0.1.0 (file:///projects/loops)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.32s
Running `target/debug/loops`
the value is: 10
the value is: 20
the value is: 30
the value is: 40
the value is: 50
予想どおり、5 つの配列の値がすべてターミナルに表示されます。index
はいつか 5 になりますが、ループは配列から 6 つ目の値を取得しようとする前に
実行を停止します。
しかし、このアプローチはエラーを起こしやすく、インデックス値やテスト条件が正しくない場合、プログラムをパニックさせてしまう可能性があります。たとえば、a 配列の定義を4要素に変更したにもかかわらず、条件を while index < 4 に更新し忘れた場合、このコードはパニックします。また、これには速度面の問題もあります。というのも、コンパイラはループの各反復ごとに、インデックスが配列の境界内にあるかどうかを条件チェックするための実行時コードを追加するからです。
より簡潔な代替手段として、for ループを使い、コレクション内の各要素に対してコードを実行できます。for ループはリスト3-5のコードのようになります。
fn main() {
let a = [10, 20, 30, 40, 50];
for element in a {
println!("the value is: {element}");
}
}
このコードを実行すると、リスト3-4と同じ出力が表示されます。さらに重要なのは、コードの安全性が向上し、配列の末尾を超えてしまったり、十分に進まずにいくつかの要素を見逃したりすることで生じるバグの可能性を排除できたことです。for ループから生成されるマシンコードは、各反復でインデックスを配列の長さと比較する必要がないため、より効率的になる場合もあります。
for ループを使えば、リスト3-4で使った方法とは異なり、配列内の値の個数を変更したときに、ほかのコードを変更し忘れる心配がありません。
for ループの安全性と簡潔さにより、これは Rust で最も一般的に使われるループ構文になっています。リスト3-3で while ループを使ったカウントダウンの例のように、あるコードを一定回数実行したい状況であっても、たいていの Rustacean は for ループを使うでしょう。その方法は、標準ライブラリが提供する Range を使うことです。これは、ある数から始まり、別の数の直前で終わるまでのすべての数を順に生成します。
for ループと、まだ説明していない別のメソッドである rev を使って範囲を逆順にすると、カウントダウンは次のようになります。
ファイル名: src/main.rs
fn main() {
for number in (1..4).rev() {
println!("{number}!");
}
println!("LIFTOFF!!!");
}
このコードのほうが、少し見やすいでしょう?
まとめ
やり切りました! かなり大きな章でした。変数、スカラー型と複合型のデータ型、関数、コメント、if 式、そしてループについて学びました! この章で取り上げた概念を練習するために、次のことを行うプログラムを作ってみてください。
- 華氏と摂氏の間で温度を変換する。
- フィボナッチ数列の第 n 項を生成する。
- クリスマスキャロル “The Twelve Days of Christmas” の歌詞を出力する。その際、歌の繰り返しを活用すること。
先に進む準備ができたら、Rust における、他のプログラミング言語には一般的には 存在しない 概念、所有権について説明します。