はじめに
制御フローへようこそ。この実験は、Rust Bookの一部です。LabExでRustのスキルを練習できます。
この実験では、Rustの制御フローに焦点を当てます。これには、条件に基づいてコードを実行し、条件が真の間コードを繰り返すためにif式とループを使用することが含まれます。
This tutorial is from open-source community. Access the source code
💡 このチュートリアルは英語版からAIによって翻訳されています。原文を確認するには、 ここをクリックしてください
制御フローへようこそ。この実験は、Rust Bookの一部です。LabExでRustのスキルを練習できます。
この実験では、Rustの制御フローに焦点を当てます。これには、条件に基づいてコードを実行し、条件が真の間コードを繰り返すためにif式とループを使用することが含まれます。
条件がtrue
であるかどうかに応じてコードを実行し、条件がtrue
の間コードを繰り返し実行する機能は、ほとんどのプログラミング言語の基本的な構成要素です。Rustコードの実行フローを制御するための最も一般的な構文は、if
式とループです。
if
式を使うと、条件に応じてコードを分岐させることができます。条件を指定して、「この条件が満たされた場合、このコードブロックを実行します。条件が満たされない場合、このコードブロックを実行しません。」と述べます。
if
式を調べるために、project
ディレクトリに新しいプロジェクトbranches
を作成しましょう。src/main.rs
ファイルに次のコードを入力します:
cd ~/project
cargo new branches
ファイル名: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
式の条件に関連付けられたコードブロックは、「比較的推測する秘密の数」で議論したmatch
式のアームのように、時には「アーム」と呼ばれます。
任意ですが、ここではelse
式も含めることができます。これにより、条件がfalse
の場合に実行する代替のコードブロックをプログラムに与えることができます。else
式を提供せず、条件がfalse
の場合、プログラムはif
ブロックをスキップし、次のコードに進みます。
このコードを実行してみましょう。以下の出力が表示されるはずです:
$ cargo run
Compiling branches v0.1.0 (file:///projects/branches)
Finished dev [unoptimized + debuginfo] target(s) in 0.31s
Running `target/debug/branches`
condition was true
number
の値を条件がfalse
になる値に変更して、何が起こるか見てみましょう:
let number = 7;
再度プログラムを実行して、出力を見てみましょう:
$ cargo run
Compiling branches v0.1.0 (file:///projects/branches)
Finished dev [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
このエラーは、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
を組み合わせることで、複数の条件を使うことができます。たとえば:
ファイル名: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 [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
条件のブロックのみを実行し、見つけるとそれ以降をチェックしないからです。
else if
式をたくさん使うとコードが混乱するので、1つ以上ある場合はコードをリファクタリングする必要があるかもしれません。第6章では、これらのケースに対してmatch
と呼ばれる強力なRustの分岐構文について説明しています。
if
は式なので、let
文の右辺で使用して結果を変数に割り当てることができます。例えば、リスト3-2のようにです。
ファイル名:src/main.rs
fn main() {
let condition = true;
let number = if condition { 5 } else { 6 };
println!("The value of number is: {number}");
}
リスト3-2:if
式の結果を変数に割り当てる
number
変数は、if
式の結果に基づいて値に束縛されます。このコードを実行して、何が起こるか見てみましょう:
$ cargo run
Compiling branches v0.1.0 (file:///projects/branches)
Finished dev [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
if
ブロック内の式は整数を評価し、else
ブロック内の式は文字列を評価します。これは機能しません。なぜなら、変数は単一の型を持たなければならず、Rustはコンパイル時にnumber
変数がどの型であるかを明確に知る必要があるからです。number
の型を知ることで、コンパイラはnumber
を使用するすべての場所で型が有効であることを検証できます。number
の型が実行時にのみ決定される場合、Rustはそれを行うことができません。コンパイラはより複雑になり、任意の変数に対して複数の仮想型を追跡する必要がある場合、コードに対する保証も少なくなります。
コードブロックを複数回実行することは、多くの場合便利です。このタスクのために、Rustはいくつかの「ループ」を提供しています。これらのループは、ループ本体の中のコードを最後まで実行し、その後直ちに最初から再開します。ループを試すために、新しいプロジェクト「loops」を作成しましょう。
Rustには3種類のループがあります。loop
、while
、およびfor
です。それぞれを試してみましょう。
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 [unoptimized + debuginfo] target(s) in 0.29s
Running `target/debug/loops`
again!
again!
again!
again!
^Cagain!
^C
の記号は、ctrl-Cを押した場所を表しています。割り込み信号を受け取ったときにループ内のどこにコードがあったかに応じて、^C
の後にagain!
という単語が表示されるかどうかは異なります。
幸いなことに、Rustはコードを使ってループから抜ける方法も提供しています。break
キーワードをループ内に配置することで、プログラムにループの実行を停止するタイミングを伝えることができます。「正解を当てた後に終了する」のゲームでは、ユーザーが正解を当ててゲームに勝ったときにプログラムを終了するために、この方法を使っていました。
また、ゲームではcontinue
も使っていました。これは、ループ内でプログラムに対して、このループのこの反復で残りのコードをスキップして、次の反復に進むように指示します。
loop
の用途の1つは、失敗する可能性がある操作を再試行することです。たとえば、スレッドがその作業を完了したかどうかを確認することです。また、その操作の結果をループからコードの残りの部分に渡す必要がある場合もあります。これを行うには、ループを停止するために使用する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
です。
ループの中にループがある場合、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;
文は、外側のループを終了します。このコードは以下のように出力されます:
Compiling loops v0.1.0 (file:///projects/loops)
Finished dev [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
プログラムでは、ループ内で条件を評価する必要がよくあります。条件がtrue
の間、ループは実行されます。条件がtrue
でなくなったとき、プログラムはbreak
を呼び出してループを停止します。このような動作を、loop
、if
、else
、およびbreak
を組み合わせて実装することができます。もし好きなら、今すぐプログラムで試してみてください。しかし、このパターンは非常に一般的なので、Rustにはこれに対応する組み込みの言語構文があり、while
ループと呼ばれます。リスト3-3では、while
を使ってプログラムを3回ループさせ、毎回カウントダウンし、その後、ループの後にメッセージを表示して終了します。
ファイル名:src/main.rs
fn main() {
let mut number = 3;
while number!= 0 {
println!("{number}!");
number -= 1;
}
println!("LIFTOFF!!!");
}
リスト3-3:条件がtrue
の間、コードを実行するためのwhile
ループの使用
この構文は、loop
、if
、else
、およびbreak
を使う場合に必要となる多くのネストを排除し、より明確になります。条件がtrue
の間、コードは実行されます。そうでなければ、ループを抜けます。
配列などのコレクションの要素を反復処理する際には、while
構文を使うことができます。たとえば、リスト3-4のループは配列a
の各要素を出力します。
ファイル名:src/main.rs
fn main() {
let a = [10, 20, 30, 40, 50];
let mut index = 0;
while index < 5 {
println!("the value is: {}", a[index]);
index += 1;
}
}
リスト3-4:while
ループを使ったコレクションの各要素の反復処理
ここでは、コードは配列の要素をインクリメントしながら反復処理します。インデックスは0
から始まり、配列の最後のインデックスに達するまで(つまり、index < 5
がもはやtrue
でなくなるまで)ループします。このコードを実行すると、配列のすべての要素が表示されます:
$ cargo run
Compiling loops v0.1.0 (file:///projects/loops)
Finished dev [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のコードのようになります。
ファイル名:src/main.rs
fn main() {
let a = [10, 20, 30, 40, 50];
for element in a {
println!("the value is: {element}");
}
}
リスト3-5:for
ループを使ったコレクションの各要素の反復処理
このコードを実行すると、リスト3-4と同じ出力が表示されます。より重要なことは、コードの安全性が向上し、配列の末尾を超えたり、十分に進まなかって項目を見落としたりすることから生じるバグの可能性が排除されたことです。
for
ループを使えば、配列の値の数を変更した場合にも、リスト3-4で使用した方法のように、他のコードを変更する必要がありません。
for
ループの安全性と簡潔さのため、これはRustで最も一般的に使用されるループ構文になっています。たとえば、リスト3-3でwhile
ループを使ったカウントダウンの例のように、特定の回数だけコードを実行したい場合でも、ほとんどのRustプログラマーはfor
ループを使います。その方法は、標準ライブラリによって提供されるRange
を使うことで、ある数から始まり、別の数の前まで順番にすべての数を生成することです。
以下は、for
ループとまだ説明していない別のメソッドであるrev
を使って、範囲を逆順にすることでカウントダウンを行った場合のコードです:
ファイル名:src/main.rs
fn main() {
for number in (1..4).rev() {
println!("{number}!");
}
println!("LIFTOFF!!!");
}
このコードはもっと綺麗ですよね?
おめでとうございます!コントロールフローの実験を完了しました。技術力を向上させるために、LabExでさらに実験を行って練習してください。