Rust の制御フローの基本

RustRustBeginner
今すぐ練習

This tutorial is from open-source community. Access the source code

💡 このチュートリアルは英語版からAIによって翻訳されています。原文を確認するには、 ここをクリックしてください

はじめに

制御フローへようこそ。この実験は、Rust Bookの一部です。LabExでRustのスキルを練習できます。

この実験では、Rustの制御フローに焦点を当てます。これには、条件に基づいてコードを実行し、条件が真の間コードを繰り返すためにif式とループを使用することが含まれます。


Skills Graph

%%%%{init: {'theme':'neutral'}}%%%% flowchart RL rust(("Rust")) -.-> rust/FunctionsandClosuresGroup(["Functions and Closures"]) rust(("Rust")) -.-> rust/BasicConceptsGroup(["Basic Concepts"]) rust(("Rust")) -.-> rust/DataTypesGroup(["Data Types"]) rust(("Rust")) -.-> rust/ControlStructuresGroup(["Control Structures"]) rust/BasicConceptsGroup -.-> rust/variable_declarations("Variable Declarations") rust/BasicConceptsGroup -.-> rust/mutable_variables("Mutable Variables") rust/DataTypesGroup -.-> rust/boolean_type("Boolean Type") rust/DataTypesGroup -.-> rust/string_type("String Type") rust/ControlStructuresGroup -.-> rust/for_loop("for Loop") rust/FunctionsandClosuresGroup -.-> rust/function_syntax("Function Syntax") rust/FunctionsandClosuresGroup -.-> rust/expressions_statements("Expressions and Statements") subgraph Lab Skills rust/variable_declarations -.-> lab-100391{{"Rust の制御フローの基本"}} rust/mutable_variables -.-> lab-100391{{"Rust の制御フローの基本"}} rust/boolean_type -.-> lab-100391{{"Rust の制御フローの基本"}} rust/string_type -.-> lab-100391{{"Rust の制御フローの基本"}} rust/for_loop -.-> lab-100391{{"Rust の制御フローの基本"}} rust/function_syntax -.-> lab-100391{{"Rust の制御フローの基本"}} rust/expressions_statements -.-> lab-100391{{"Rust の制御フローの基本"}} end

制御フロー

条件がtrueであるかどうかに応じてコードを実行し、条件がtrueの間コードを繰り返し実行する機能は、ほとんどのプログラミング言語の基本的な構成要素です。Rustコードの実行フローを制御するための最も一般的な構文は、if式とループです。

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を使った複数の条件の処理

else if式でifelseを組み合わせることで、複数の条件を使うことができます。たとえば:

ファイル名: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の分岐構文について説明しています。

let文でのifの使用

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}");
}

このコードをコンパイルしようとすると、エラーが発生します。ifelseのアームは互換性のない値の型を持っており、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種類のループがあります。loopwhile、および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 [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を加え、その後counter10と等しいかどうかを確認します。等しい場合、値counter * 2breakキーワードを使用します。ループの後、セミコロンを使用して、resultに値を割り当てる文を終了します。最後に、resultの値を出力します。この場合、それは20です。

複数のループ間を区別するためのループラベル

ループの中にループがある場合、breakcontinueはその時点で最内側のループに適用されます。任意で、ループに「ループラベル」を指定することができます。その後、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

whileによる条件付きループ

プログラムでは、ループ内で条件を評価する必要がよくあります。条件がtrueの間、ループは実行されます。条件がtrueでなくなったとき、プログラムはbreakを呼び出してループを停止します。このような動作を、loopifelse、および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ループの使用

この構文は、loopifelse、およびbreakを使う場合に必要となる多くのネストを排除し、より明確になります。条件がtrueの間、コードは実行されます。そうでなければ、ループを抜けます。

forを使ったコレクションの反復処理

配列などのコレクションの要素を反復処理する際には、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でさらに実験を行って練習してください。