Progrust Library.

// book chapter

動かして学ぶRustプログラミング問題集

第3章 制御フロー.

この章では、処理の流れを操作する制御フローを学びます。
条件によって処理を分けるif式と、同じ処理を繰り返すloop式・while式・for式を10問で身につけます。

第2章までのコードは、書いた順に上から下へ実行されるだけでした。分岐と繰り返しが使えるようになると、書ける処理の幅が一気に広がります。

進め方は第1章第2章と同じです。各問題の冒頭に関連する辞書へのリンクを挙げているので、まずはリンク先で必要な知識を確認してから取り組んでください。

01 - if式で分岐する

if式に関する問題です。
気温に応じて、異なるメッセージを出力してください。25度以上なら「暑いですね。」、そうでなければ「過ごしやすいですね。」と出力します。

期待する出力
暑いですね。
「Playgroundで開く」をクリックして修正・実行してください
fn main() {
    let temperature = 28;

    // temperatureが25以上なら「暑いですね。」、そうでなければ「過ごしやすいですね。」を出力せよ

}
Playgroundで開く
解答例と解説
fn main() {
    let temperature = 28;

    // temperatureが25以上なら「暑いですね。」、そうでなければ「過ごしやすいですね。」を出力せよ
    if temperature >= 25 { 
        println!("暑いですね。"); 
    } else { 
        println!("過ごしやすいですね。"); 
    } 
}
Playgroundで開く

if 条件 { ... } else { ... }と書くと、条件がtrueのときは前のブロック、falseのときはelseのブロックが実行されます。条件には第2章で学んだ論理値型の値を生む式(temperature >= 25など)を書きます。

他の言語との違いが2つあります。

  • 条件を括弧で囲む必要はありません(if (temperature >= 25)と書いてもコンパイルは通りますが、警告が出ます)
  • ブロックの{}は省略できません(1行しか書かない場合でも必須です)

temperatureの値を20などに書き換えて実行すると、else側が実行されることも確認できます。

02 - else ifで多分岐

if式に関する問題です。
テストの点数に応じて、3段階の評価を出力してください。80点以上なら「優」、60点以上80点未満なら「良」、60点未満なら「可」です。

期待する出力
評価: 良
「Playgroundで開く」をクリックして修正・実行してください
fn main() {
    let score = 72;

    // scoreが80以上なら「評価: 優」、60以上なら「評価: 良」、それ未満なら「評価: 可」を出力せよ

}
Playgroundで開く
解答例と解説
fn main() {
    let score = 72;

    // scoreが80以上なら「評価: 優」、60以上なら「評価: 良」、それ未満なら「評価: 可」を出力せよ
    if score >= 80 { 
        println!("評価: 優"); 
    } else if score >= 60 { 
        println!("評価: 良"); 
    } else { 
        println!("評価: 可"); 
    } 
}
Playgroundで開く

分岐が3つ以上になるときはelse ifで条件をつなげます。条件は上から順に判定され、最初にtrueになったブロックだけが実行されて、残りの分岐は評価されません。

ここで大事なのは条件を書く順番です。2番目の条件はscore >= 60としか書いていませんが、80点以上のケースは1番目の条件ですでに処理されているため、ここに到達する時点で「80点未満」であることは確定しています。そのためscore >= 60 && score < 80のように上限を書く必要はありません。

逆に、条件をゆるい順に並べてif score >= 60 { "良" } else if score >= 80 { "優" }と書いてしまうと、90点でも「良」と判定されてしまいます。多分岐では厳しい条件から順に並べるのが定石です。

scoreの値をいろいろ書き換えて、3つの分岐すべてを確認してみてください。

03 - ifは式

if式に関する問題です。
次のコードは在庫数に応じたメッセージを変数に束縛しようとしていますが、コンパイルエラーになります。エラーメッセージを読み、println!の行は変えずにコンパイルが通るようにしてください。(エラー箇所は自由に変えて大丈夫です。)

期待する出力
お知らせ: 残りわずかです。
「Playgroundで開く」をクリックして修正・実行してください
fn main() {
    let stock = 3;

    let message = if stock == 0 {
        "在庫切れです。"
    } else if stock < 5 {
        "残りわずかです。"
    } else {
        stock
    };

    println!("お知らせ: {}", message);
}
Playgroundで開く
解答例と解説
fn main() {
    let stock = 3;

    let message = if stock == 0 {
        "在庫切れです。"
    } else if stock < 5 {
        "残りわずかです。"
    } else {
        stock 
        "在庫は十分にあります。" // 他の分岐と同じ&str型に揃える
    };

    println!("お知らせ: {}", message);
}
Playgroundで開く

エラーメッセージは「if and else have incompatible types」、続けて「expected &str, found integer」でした。つまり「先の分岐は文字列を返しているのに、最後の分岐は整数を返している」という指摘です。

第1章で学んだとおり、ブロックは末尾のセミコロンなしの式の値を返します。if式の値は「実行されたブロックの値」になるため、let message = if ... { ... } else { ... };のように結果をそのまま変数に束縛できます。Rustに三項演算子(条件 ? a : b)がないのは、if自体が値を返す式だからです。

ただし値として使う以上、どの分岐が実行されても同じ型でなければなりません。実行してみるまで型が決まらない変数は作れないためです。注目したいのは、今回stockが3なので最後の分岐は実行されないにもかかわらずエラーになる点です。型の検査は実行前のコンパイル時に、すべての分岐に対して行われます。

なお、この問題では末尾のセミコロンにも注意が必要です。"残りわずかです。";のようにセミコロンを付けると値を捨てるになり、そのブロックの値は()になってしまいます。

04 - loop式とbreak

loop式に関する問題です。
次のコードはloop式でカウントを繰り返していますが、このままでは永遠に終わりません。3回目の出力でループを抜けるようにコードを追記してください。

期待する出力
1回目のループです。
2回目のループです。
3回目のループです。
ループを抜けました。
「Playgroundで開く」をクリックして修正・実行してください
fn main() {
    let mut count = 0;

    loop {
        count += 1;
        println!("{}回目のループです。", count);

        // countが3になったらループを抜けよ

    }

    println!("ループを抜けました。");
}
Playgroundで開く
解答例と解説
fn main() {
    let mut count = 0;

    loop {
        count += 1;
        println!("{}回目のループです。", count);

        // countが3になったらループを抜けよ
        if count == 3 { 
            break; 
        } 
    }

    println!("ループを抜けました。");
}
Playgroundで開く

loop式は本体のブロックを無限に繰り返すループです。終了条件を持たないため、抜けるにはbreakを書く必要があります。breakに到達した時点でループが終了し、続きの処理へ進みます。

繰り返し回数や継続条件を事前に決められない処理(通信のリトライ、入力待ちなど)に向いた構文です。回数が決まっているなら、この後の問題で扱うwhile式やfor式のほうが素直に書けます。

05 - breakで値を返す

loop式に関する問題です。
1円を毎日2倍にしていったとき、金額が1000円を超えるのは何日目かを求めてください。breakに値を渡してloop式全体の値として取り出します。

期待する出力
10日目に1000円を超えました。
「Playgroundで開く」をクリックして修正・実行してください
fn main() {
    let mut amount = 1;
    let mut days = 0;

    let result = loop {
        amount *= 2;
        days += 1;

        // amountが1000を超えたら、そのときのdaysをloop式の値として返しつつループを抜けよ

    };

    println!("{}日目に1000円を超えました。", result);
}
Playgroundで開く
解答例と解説
fn main() {
    let mut amount = 1;
    let mut days = 0;

    let result = loop {
        amount *= 2;
        days += 1;

        // amountが1000を超えたら、そのときのdaysをloop式の値として返しつつループを抜けよ
        if amount > 1000 { 
            break days; 
        } 
    };

    println!("{}日目に1000円を超えました。", result);
}
Playgroundで開く

break 値;と書くと、渡した値がloop式全体の値になります。ここではbreak days;days(10)がresultに束縛されます。amountは2, 4, 8, … と倍々に増え、10日目に1024となって1000を超えます。

3種類のループのうち、breakで値を返せるのはloop式だけです。while式やfor式のブロックでbreak 値;と書くとコンパイルエラー(E0571)になります。この点でloopは「値を生み出す式」としての性格が最もはっきりしたループだと言えます。

なおamount *= 2;amount = amount * 2;の省略形です。第1章で使った+=と同じ仲間で、-=/=%=もあります。

06 - while式

while式に関する問題です。
3からのカウントダウンを出力してください。ループの継続条件をwhile式で書きます。

期待する出力
3
2
1
発射!
「Playgroundで開く」をクリックして修正・実行してください
fn main() {
    let mut count = 3;

    // countが0より大きい間、countを出力してから1減らす処理を繰り返せ

    println!("発射!");
}
Playgroundで開く
解答例と解説
fn main() {
    let mut count = 3;

    // countが0より大きい間、countを出力してから1減らす処理を繰り返せ
    while count > 0 { 
        println!("{}", count); 
        count -= 1; 
    } 

    println!("発射!");
}
Playgroundで開く

while 条件 { ... }は、条件がtrueの間だけ本体を繰り返すループです。本体を1回実行するたびに条件を再評価し、falseになった時点で終了します。

loop式とifbreakを組み合わせれば同じ動きは書けますが、「この条件の間だけ続ける」という意図はwhileのほうが一目で伝わります。

注意したいのは、条件に使う値を本体の中で必ず変化させることです。count -= 1;を書き忘れるとcountはずっと3のままで、条件がfalseにならず無限ループになります。

なお、while式の値は常にユニット型()です。問題05のloop式のようにbreakで値を取り出すことはできません。

07 - 範囲式

範囲式に関する問題です。
....=の2つの記法で範囲を作り、それぞれに含まれる整数の個数を出力してください。どちらも1から5までの範囲ですが、終端の5を含むかどうかが違います。

期待する出力
終端を含まない範囲の要素は4個です。
終端を含む範囲の要素は5個です。
「Playgroundで開く」をクリックして修正・実行してください
fn main() {
    // 1以上5未満を表す範囲式をexclusiveに束縛せよ

    // 1以上5以下を表す範囲式をinclusiveに束縛せよ

    println!("終端を含まない範囲の要素は{}個です。", exclusive.count());
    println!("終端を含む範囲の要素は{}個です。", inclusive.count());
}
Playgroundで開く
解答例と解説
fn main() {
    // 1以上5未満を表す範囲式をexclusiveに束縛せよ
    let exclusive = 1..5; // 1, 2, 3, 4

    // 1以上5以下を表す範囲式をinclusiveに束縛せよ
    let inclusive = 1..=5; // 1, 2, 3, 4, 5

    println!("終端を含まない範囲の要素は{}個です。", exclusive.count());
    println!("終端を含む範囲の要素は{}個です。", inclusive.count());
}
Playgroundで開く

範囲式は開始..終端の形で「範囲」を表す値を作る式です。..は終端を含まず..=は終端を含みます。開始側はどちらの記法でも含まれます。

記法 含まれる値 1..5 / 1..=5の場合
a..b a以上b未満 1, 2, 3, 4
a..=b a以上b以下 1, 2, 3, 4, 5

0から数え始めるなら0..5が5個ぶんの範囲になるため、..のほうが素直に書ける場面が多くあります。一方「1から5まで」のように終端を含めたいことが明らかな場面では..=を使います。

count()は範囲に含まれる値の個数を数えるメソッドです。ここでは違いを確認するために使いましたが、範囲式の主な用途は次の問題で扱うfor式による繰り返しと、第5章で扱う配列の一部の切り出しです。

08 - for式で範囲を走査する

for式範囲式に関する問題です。
かけ算の「3の段」を1倍から5倍まで出力してください。

期待する出力
3 × 1 = 3
3 × 2 = 6
3 × 3 = 9
3 × 4 = 12
3 × 5 = 15
「Playgroundで開く」をクリックして修正・実行してください
fn main() {
    // for式で1から5までを順に走査し、3の段を出力せよ

}
Playgroundで開く
解答例と解説
fn main() {
    // for式で1から5までを順に走査し、3の段を出力せよ
    for i in 1..=5 { 
        println!("3 × {} = {}", i, i * 3); 
    } 
}
Playgroundで開く

for 変数名 in 範囲 { ... }と書くと、範囲から値が1つずつ取り出されて変数に束縛され、その値ごとに本体が実行されます。1..=5ならiが1, 2, 3, 4, 5と変化しながら5回繰り返されます。

while式と違ってカウンタ変数を自分で用意して増やす必要がありません。終了条件の書き間違いや更新の書き忘れによるバグが起きにくいため、繰り返し回数が決まっている処理ではまずfor式を選びます。3種類のループの中で最も使用頻度の高い構文です。

なおimutを付けていませんが、繰り返しのたびに新しい値が束縛されるだけなので問題ありません(ループの中でi自体を書き換えることはできません)。

09 - ループラベル

ループラベルfor式に関する問題です。
次のコードは、かけ算の答えが4を超えた時点で探索をやめようとしていますが、期待する出力より1行多く出力されてしまいます。原因を考え、breakの書き方を変えて期待する出力に合わせてください。

期待する出力
1 × 1 = 1
1 × 2 = 2
1 × 3 = 3
2 × 1 = 2
2 × 2 = 4
探索を終了しました。
「Playgroundで開く」をクリックして修正・実行してください
fn main() {
    for i in 1..=3 {
        for j in 1..=3 {
            if i * j > 4 {
                break;
            }
            println!("{} × {} = {}", i, j, i * j);
        }
    }

    println!("探索を終了しました。");
}
Playgroundで開く
解答例と解説
fn main() {
    for i in 1..=3 { 
    'outer: for i in 1..=3 { 
        for j in 1..=3 {
            if i * j > 4 {
                break; 
                break 'outer; // 内側だけでなく外側のforごと抜ける
            }
            println!("{} × {} = {}", i, j, i * j);
        }
    }

    println!("探索を終了しました。");
}
Playgroundで開く

修正前の余分な1行は3 × 1 = 3です。ラベルなしのbreakが抜けるのは最も内側のループだけなので、iが2のときに内側のループを抜けても外側のループは続き、iが3の周回が始まってしまいます。

ループの前に'名前:の形でループラベルを付けると、break '名前でそのループを直接指定して抜けられます。ここでは外側のfor'outerというラベルを付け、内側からbreak 'outerで二重ループごと脱出しています。

ラベルはforだけでなくloopwhileにも付けられます。また、breakと同じくcontinue '名前で「指定したループの次の周回に進む」こともできます。

10 - 応用: FizzBuzz

第3章の総復習として、for式if式数値演算を組み合わせた問題です。
1から15までの数を順に出力してください。ただし、3の倍数のときは数の代わりにFizz、5の倍数のときはBuzz、3と5の両方の倍数のときはFizzBuzzと出力します。

期待する出力
1
2
Fizz
4
Buzz
Fizz
7
8
Fizz
Buzz
11
Fizz
13
14
FizzBuzz
「Playgroundで開く」をクリックして修正・実行してください
fn main() {
    for number in 1..=15 {
        // numberが3と5の両方の倍数なら「FizzBuzz」、3の倍数なら「Fizz」、
        // 5の倍数なら「Buzz」、それ以外なら数値そのものを出力せよ

    }
}
Playgroundで開く
解答例と解説
fn main() {
    for number in 1..=15 {
        // numberが3と5の両方の倍数なら「FizzBuzz」、3の倍数なら「Fizz」、
        // 5の倍数なら「Buzz」、それ以外なら数値そのものを出力せよ
        if number % 3 == 0 && number % 5 == 0 { 
            println!("FizzBuzz"); 
        } else if number % 3 == 0 { 
            println!("Fizz"); 
        } else if number % 5 == 0 { 
            println!("Buzz"); 
        } else { 
            println!("{}", number); 
        } 
    }
}
Playgroundで開く

プログラミングの練習問題として有名なFizzBuzzです。使っているのはこの章までに学んだ要素だけで、「倍数かどうか」の判定は第2章の剰余演算子%で行います。number % 3 == 0は「3で割った余りが0」、つまり3の倍数であることを表します。

ポイントは問題02と同じ条件の順番です。FizzBuzzの条件を先頭に置かないと、15のときに手前の「3の倍数」の分岐に入ってFizzだけが出力されてしまいます。複数の条件が重なるときは、より限定的な条件から先に判定します。

3と5の両方の倍数であることは「15の倍数」と言い換えられるので、最初の条件はnumber % 15 == 0と書いても同じ結果になります。

これで第3章は終わりです。分岐と繰り返しがそろい、書ける処理の幅がぐっと広がりました。次の章では、処理をひとまとまりの部品として切り出す関数に進みます。