Rust入門(4)「制御構文を学ぼう!」

スポンサーリンク
Rust

今回は、「Rust入門(3)「関数とスコープを学ぼう!」」の続編である。
まだ、GitHub Codespacesで環境構築をしていない方は、第1回「GitHub Codespaces で Rust を始めよう!」を先にお読みください。

はじめに

前回は、Rustの関数とスコープについて学んだ。

今回は、条件によって処理を分けたり、同じ処理を繰り返したりするための制御構文を学んでいこう。

今回学ぶこと

  • if式(条件分岐)
  • loop(無限ループ)
  • while(条件付きループ)
  • for(繰り返し処理)
  • match式(パターンマッチ)

準備:体験用プロジェクトを開く

作業場所: trials/04_control_flow/
前回と同じように、このディレクトリで自由にコードを試してみよう。

# リポジトリを最新の状態にする
cd /workspaces/rust-practice
git pull

# 体験用プロジェクトに移動
cd trials/04_control_flow

# VSCodeでファイルを開く
code src/main.rs

# 動作確認
cargo run

学習の進め方

1. 体験する(trials/)

  • ブログ記事を読みながら trials/04_control_flow/ で試す
  • 自由にコードを書いて実験
  • エラーを恐れず、どんどん試す

2. 練習する(exercises/)

  • 体験が終わったら、docs/04_control_flow.md(プレビュー表示)を参考にして、 exercises/04_control_flow/ で練習する
  • 構造化された問題を順番に解く
  • 解答例も用意されている

if式

基本のif

fn main() {
    let number = 7;

    if number % 2 == 0 {
        println!("{}は偶数である", number);
    } else {
        println!("{}は奇数である", number);
    }
}
# 出力
7は奇数である

ポイント

  • 条件式はbool型でなければならない。他の言語のようにif 1のような書き方はできない
  • 条件を()で囲む必要はない(囲んでもエラーにはならないが、Rustの流儀ではない)

条件を数値と間違えるとエラー

fn main() {
    let number = 3;

    if number { // ← エラー!
        println!("何か");
    }
}
error[E0308]: mismatched types
(一部抜粋)expected `bool`, found integer

C言語では「0以外は真」として扱われるが、Rustではこの自動変換が行われない。
number != 0のように、明示的にbool型の式を書く必要がある。

else if で複数の条件

fn main() {
    let score = 75;

    if score >= 90 {
        println!("評価: A");
    } else if score >= 70 {
        println!("評価: B");
    } else if score >= 50 {
        println!("評価: C");
    } else {
        println!("評価: D");
    }
}
# 出力
評価: B

条件は上から順に評価され、最初にtrueになったブロックだけが実行される。

練習問題 1

以下のプログラムを作成してみよう。

  1. nという変数(整数)を用意する
  2. nが3で割り切れて、かつ5でも割り切れるなら「FizzBuzz」
  3. 3でだけ割り切れるなら「Fizz」
  4. 5でだけ割り切れるなら「Buzz」
  5. どちらでもなければ、その数自体を出力

解答例を見る

fn main() {
    let n = 15;

    if n % 3 == 0 && n % 5 == 0 {
        println!("FizzBuzz");
    } else if n % 3 == 0 {
        println!("Fizz");
    } else if n % 5 == 0 {
        println!("Buzz");
    } else {
        println!("{}", n);
    }
}

&&は「かつ」(AND)、||は「または」(OR)を表す。

ifは式である

let文でifの結果を受け取る

前回、ブロック({ })は式になると学んだ。実はifも式であり、値を返せる。

fn main() {
    let number = 7;

    let description = if number % 2 == 0 {
        "偶数"
    } else {
        "奇数"
    };

    println!("{}は{}である", number, description);
}
# 出力
7は奇数である

ポイント

  • ifとelse、両方のブロックが同じ型の値を返す必要がある
  • 各ブロックの最後の式にセミコロンを付けない(前回学んだ「式」のルールと同じ)
  • 他の言語の三項演算子(条件 ? 値1 : 値2)に近い使い方だが、Rustには三項演算子自体は存在しない

型が合わないとエラー

fn main() {
    let number = 7;

    let result = if number % 2 == 0 {
        "偶数" // 文字列
    } else {
        0 // 整数
    };

    println!("{}", result);
}
error[E0308]: `if` and `else` have incompatible types
(略)

ポイントは、resultという変数は1つしかないということである。
しかし、このif式は「条件によって、文字列を返すこともあれば、整数を返すこともある」という書き方になっている。

Rustでは、if式全体として1つの型に決まる必要がある。
このコードでは、if側が&str、else側がi32となっており、分岐によって異なる型の値を返している。そのため、if式の型を1つに決めることができず、コンパイルエラーになる。

メモリの視点で考えると分かりやすい

&str(文字列)とi32(整数)は、メモリ上での表現がまったく違う。

型メモリ上のサイズ・中身
i324バイトの整数値そのもの
&strポインタ+長さ(合計16バイト、中身は別の場所にある文字データを指す)

resultという変数のために、コンパイラは事前にメモリの箱を用意する。しかし、箱のサイズも中身の解釈のしかたも、i32と&strではまったく違う。メモリ上の表現が異なるため、「同じ変数に条件によって&strとi32を入れる」という扱いはできない。

Cとの違い

Cをご存じなら、こう考えるとわかりやすいかもしれない。

  • Cでは、void*や共用体(union)を使えば、型を無視して同じ変数にいろいろな値を詰め込むことは可能である(安全性はプログラマの責任)
  • Rustでは、コンパイラがすべての変数の型を1つに確定させることを保証する。
    この保証があるからこそ、実行時に「思っていたのと違う型として読んでしまった」という事故は起きない

Pythonのような動的型付け言語なら、resultが文字列になったり整数になったりしても問題なく動く。しかし、Rustは静的型付け言語であり、letで作られる変数は宣言された瞬間(正確にはコンパイル時)に型が1つに決まる。

直すとしたら

型を揃える必要がある。たとえば、両方ともString型(所有権を持つ文字列)に揃えるなら、こう書ける。

fn main() {
    let number = 7;

    let result = if number % 2 == 0 {
        String::from("偶数")
    } else {
        0.to_string() // 整数を文字列に変換
    };

    println!("{}", result);
}

0.to_string()で、整数の0を文字列"0"に変換している。これで、ifとelseのどちらのブロックもString型を返すようになり、コンパイルできる。

まとめ

  • if式全体は、1つの型の値になる必要がある
  • これは、変数の型をコンパイル時に確定させるという、Rustの型システムの根本ルールから来ている
  • 型を揃えられない場合は、片方の型をもう片方に変換する(今回なら.to_string()など)

練習問題 2

以下のプログラムを作成してみよう。

  1. aとb、2つの整数を用意する
  2. if式を使って、大きいほうの値をlargerという変数に代入する
  3. largerを出力する

解答例を見る

fn main() {
    let a = 12;
    let b = 25;

    let larger = if a > b { a } else { b };

    println!("大きいほうの値: {}", larger);
}

loop:無限ループ

基本のloop

loopは、明示的に止めるまで無限に繰り返す構文である。止めるにはbreakを使う。

fn main() {
    let mut count = 0;

    loop {
        count += 1;
        println!("count = {}", count);

        if count == 3 {
            break;
        }
    }

    println!("ループ終了");
}
# 出力
count = 1
count = 2
count = 3
ループ終了

loopは値を返せる

breakの後ろに値を書くと、その値がloop全体の値になる。これも、loopが式であることの現れである。

fn main() {
    let mut count = 0;

    let result = loop {
        count += 1;

        if count == 10 {
            break count * 2;
        }
    };

    println!("result = {}", result);
}
# 出力
result = 20

countが10になった時点でbreak count * 2;が実行され、20がresultに代入される。

練習問題 3

以下のプログラムを作成してみよう。

  1. nという変数を1で初期化する
  2. loopを使って、nを2倍し続ける
  3. nが100を超えたら、その時点のnの値をbreakで返す
  4. 結果を出力

解答例を見る

fn main() {
    let mut n = 1;

    let result = loop {
        n *= 2;

        if n > 100 {
            break n;
        }
    };

    println!("100を超えた最初の値: {}", result);
}

while:条件付きループ

基本のwhile

条件がtrueの間だけ繰り返したいときは、whileを使う。

fn main() {
    let mut count = 3;

    while count > 0 {
        println!("{}", count);
        count -= 1;
    }

    println!("発射!");
}
# 出力
3
2
1
発射!

loopとの違い

  • loopは無限ループで、breakで自分で止める
  • whileは、条件を毎回チェックして、falseになったら自動的に止まる

条件判定が先にある分、whileのほうが「何回繰り返すか」が読み取りやすい場面が多い。

練習問題 4

以下のプログラムを作成してみよう。

  1. nという変数を10で初期化する
  2. whileを使って、nが0より大きい間、nを出力しながら1ずつ減らす
  3. ただし、3の倍数のときだけは出力をスキップする(continueを使う)

ヒント: continueは、ループの残りをスキップして次の周回に進む構文である。

解答例を見る

fn main() {
    let mut n = 10;

    while n > 0 {
        if n % 3 == 0 {
            n -= 1;
            continue;
        }

        println!("{}", n);
        n -= 1;
    }
}

for:繰り返し処理

範囲(Range)を使う

決まった回数を繰り返すときは、forと範囲(開始..終了)を組み合わせるのが最も一般的である。

fn main() {
    for i in 1..5 {
        println!("i = {}", i);
    }
}
# 出力
i = 1
i = 2
i = 3
i = 4

ポイント

  • 1..5は「1以上5未満」を表す(5は含まれない)
  • 5を含めたい場合は、1..=5(=を付ける)と書く(下のコード)
fn main() {
    for i in 1..=5 {
        println!("i = {}", i);
    }
}
# 出力
i = 1
i = 2
i = 3
i = 4
i = 5

逆順に繰り返す

.rev()を付けると、範囲を逆順にできる。

fn main() {
    for i in (1..=5).rev() {
        println!("{}", i);
    }
    println!("発射!");
}
# 出力
5
4
3
2
1
発射!

先ほどwhileで書いたカウントダウンは、forを使うとこのように簡潔に書ける。

配列を繰り返す

forは、配列や前回学んだVecの要素を順番に取り出すこともできる。

fn main() {
    let numbers = [10, 20, 30, 40, 50];

    for n in numbers {
        println!("{}", n);
    }
}
# 出力
10
20
30
40
50

whileで配列を扱う場合、添字(インデックス)の範囲を自分で管理する必要があり、範囲を間違えるとエラーになる可能性がある。forはその心配がなく安全である。

練習問題 5

以下のプログラムを作成してみよう。

  1. 1から30までの整数について、forで繰り返す
  2. 3の倍数のときは「Fizz」
  3. 5の倍数のときは「Buzz」
  4. 両方の倍数のときは「FizzBuzz」
  5. それ以外はその数自体を出力

これは、練習問題1で作った条件分岐を、forで30回分繰り返すFizzBuzzである。

解答例を見る

fn main() {
    for n in 1..=30 {
        if n % 3 == 0 && n % 5 == 0 {
            println!("FizzBuzz");
        } else if n % 3 == 0 {
            println!("Fizz");
        } else if n % 5 == 0 {
            println!("Buzz");
        } else {
            println!("{}", n);
        }
    }
}

match式

matchの基本

matchは、値がどのパターンに一致するかで処理を分ける構文である。ifのelse ifが増えすぎて読みにくいときに、特に効果を発揮する。

fn main() {
    let day = 3;

    let name = match day {
        1 => "月曜日",
        2 => "火曜日",
        3 => "水曜日",
        4 => "木曜日",
        5 => "金曜日",
        6 => "土曜日",
        7 => "日曜日",
        _ => "無効な値",
    };

    println!("{}", name);
}
# 出力
水曜日

ポイント

  • matchも式であり、値を返せる(if式と同じ考え方)
  • _は「それ以外のすべて」を表すワイルドカードである
  • すべてのパターンを網羅する必要がある。_を書き忘れると、コンパイルエラーになる

網羅していないとエラー

fn main() {
    let day = 3;

    let name = match day {
        1 => "月曜日",
        2 => "火曜日",
        3 => "水曜日",
        // _ がない!
    };

    println!("{}", name);
}
error[E0004]: non-exhaustive patterns: `i32::MIN..=0_i32` and `4_i32..=i32::MAX` not covered

dayはi32型なので、1、2、3以外にも無数の値がありうる。すべてを書ききれないため、_で「それ以外」を必ず受け止める必要がある。このチェックのおかげで、パターンの書き漏れをコンパイル時に発見できる。

範囲でパターンを書く

matchのパターンには、範囲(1..=5のような書き方)も使える。

fn main() {
    let score = 75;

    let grade = match score {
        90..=100 => "A",
        70..=89 => "B",
        50..=69 => "C",
        _ => "D",
    };

    println!("評価: {}", grade);
}
# 出力
評価: B

先ほどifとelse ifで書いた成績判定は、matchを使うとこのように書ける。条件の候補が増えても見通しがよい。

複数の値を1つのパターンにまとめる

|(パイプ)を使うと、複数の値を1つのパターンにまとめられる。

fn main() {
    let n = 4;

    match n {
        1 | 3 | 5 | 7 | 9 => println!("{}は奇数(1桁)", n),
        0 | 2 | 4 | 6 | 8 => println!("{}は偶数(1桁)", n),
        _ => println!("{}は1桁の数字ではない", n),
    }
}
# 出力
4は偶数(1桁)

練習問題 6

以下のプログラムを作成してみよう。matchを使うこと。

  1. monthという変数(1〜12の整数)を用意する
  2. 3〜5月なら「春」
  3. 6〜8月なら「夏」
  4. 9〜11月なら「秋」
  5. 12、1、2月なら「冬」
  6. それ以外の値なら「無効な月」

解答例を見る

fn main() {
    let month = 8;

    let season = match month {
        3..=5 => "春",
        6..=8 => "夏",
        9..=11 => "秋",
        12 | 1 | 2 => "冬",
        _ => "無効な月",
    };

    println!("{}月は{}である", month, season);
}

|で複数の値をまとめられるので、12月・1月・2月のように連続しない値も1つのパターンにできる。

関数と組み合わせる

制御構文は、前回学んだ関数と組み合わせることで、より実践的なコードになる。

fn main() {
    for n in 1..=5 {
        println!("{}は素数? {}", n, is_prime(n));
    }
}

fn is_prime(n: u32) -> bool {
    if n < 2 {
        return false;
    }

    let mut i = 2;
    while i * i <= n {
        if n % i == 0 {
            return false;
        }
        i += 1;
    }

    true
}
# 出力
1は素数? false
2は素数? true
3は素数? true
4は素数? false
5は素数? true

is_prime関数の中で、whileループとreturn(前回学んだ、途中で値を返す構文)を組み合わせている。2で割り切れる、3で割り切れる……と、iを増やしながら順番に調べ、途中で割り切れる数が見つかった時点でfalseを返している。

総合練習問題

ここまでの内容を理解できたら、総合練習問題に挑戦しよう!

練習問題の場所

cd /workspaces/rust-practice/exercises/04_control_flow

# 練習問題一覧を表示
cargo run

# 個別の練習問題を実行
cargo run --bin ex01_if
cargo run --bin ex02_loop
cargo run --bin ex03_match
cargo run --bin challenge01_fizzbuzz
cargo run --bin challenge02_prime
cargo run --bin challenge03_grade

練習問題のファイルには、todo!()と書かれた部分がある(ex01_ifは出力を予想する問題なのでtodo!()はない)。ここを自分のコードに書き換えて完成させよう。完成したかどうかは、次のコマンドでテストして確認できる。

# 例:ex02_loopのテストを実行
cargo test --bin ex02_loop

test ... okと表示されれば正解である。



GitHubに保存しよう

学習内容をGitHubに保存しよう。

cd /workspaces/rust-practice
git add .
git commit -m "Complete control flow practice"
git push

(参考)筆者のGitHub: https://github.com/Rocky-Seven/rust-practice

次回予告

次回は、Rustの最大の特徴である所有権について学ぶ予定である。

  • 所有権のルール
  • ムーブとクローン
  • 借用と参照
  • スライス

C言語のポインタ操作に慣れている方には、特に興味深い回になるはずである。

コメント

タイトルとURLをコピーしました