Learning Rust - If and Loops

ひさびさのRustです。 というか最近更新サボり気味ですね。

Syntax and Semantics

引き続いてRustのsyntaxとsemanticsを学びます。

If

条件分岐ですね。以下のようなsyntaxです。

let x = 5;

if x == 5 {
    println!("x is five!");
} else if x == 6 {
    println!("x is six!");
} else {
    println!("x is not five or six :(");
}

条件式の()はつけないが、then節、else節には{}が必要なようです。 もちろんelse節は省略できます。

実はifはexpressionなので、値を返します。 例えば、

let x = 5;
let y = if x == 5 { 10 } else { 15 }; // y = 10

となります。 then節、else節にstatementを書くと空tuple()が返るようです。 もちろんthen節とelse節に違う型のexpressionを書くことはできません。

現時点でのドキュメントだと、

An if without an else always results in () as the value.

と書いてあったのですが、 Rust v1.4でif-expressionの値を代入するとき、 else節を省略するとコンパイルエラーになりました。

Loop

条件式とならんで制御構文を代表する繰り返し構文です。 Rustには3つの方法があります。 loop, while, forです。

(untilunlessがあるとよかったのですが、かえってわかりにくいでしょうか)

loop

loopキーワードは無限ループを作ります。 breakreturnで抜けることができます。

loop {
    println!("Loop forever!");
}

while

おなじみwhileは、条件が満たされるまで繰り返します。

let mut x = 5;
let mut done = false;

while !done {
    x += x - 3;
    println!("{}", x);

    if x % 5 == 0 {
        done = true;
    }
}

上述のloopwhile trueと同値でしょうか? 実は(少なくともv1.4の)Rustでは、 loopwhile trueの単なる置き換えではありません。 例えば、

let mut a;
loop {
    a = 1;
    break;
}
println!("{}", a);

は正しいコードですが、loopwhile trueで置き換えると、 aが未初期化であるとのコンパイルエラーになります。 というのは、loopキーワードを使うと、 必ずその中身が実行されることがわかっているので、 aがちゃんと初期化されることがわかるから、ということらしいです。(参考: https://github.com/rust-lang/rust/issues/12975 )

でも、コンパイラがwhile trueを見つけたら loopとみなすのではダメなんでしょうか?

for

forはある範囲を走査しながら繰り返す構文です。 syntaxは以下のようになっています。

for var in expression {
    code
}

C++とかにもある形ですが、 var, expressionに適用できるものパターンが強力になっています。

まずはC++にもある、数値を動かしていくもの。

for i in 0..10 {
    println!("{}", i);
}

は、想像通り0から9を順に出力します。

vectorの要素を辿っていくこともできます。

let nums = vec![1, 2, 3];

for num in &nums {
    println!("{}", num);
}

便利ですね。 もっとも、C++11にもrange-based-forがあって同じ書き方はできますが。

Enumerate

「何番目まで走査しているか」を知りたいときは、enumerate()関数が使えます。 rangeに対しては、

for (i, j) in (5..10).enumerate() {
    println!("The {}st number is {}.", i, j)
}

で、0: 5などの出力が得られ、

let lines = "hello\nworld!".line();
for (linenumber, line) in lines.enumerate() {
    println!("{}: {}", linenumber, line);
}

は、

0: hello
1: world!

を出力します。

Ending iteration early

上述の通り、breakreturnを使えばループを抜けられます。 よって、上に挙げたwhileの例は以下のように書き換えられます。

let mut x = 5;

loop {
    x += x - 3;
    println!("{}", x);

    if x % 5 == 0 { break; }
}

continueは次のイテレーションに進みます。 よって次のコードは奇数のみ出力します。

for x in 0..10 {
    if x % 2 == 0 { continue; }

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

break, continueどちらもご存知のとおりでしたね。

Loop label

最後はRust特有の機能でしょうか。私は見たことがありませんでした。

入れ子になったループでbreak, continueすると、 デフォルトではそれが実行された最も内側のループに対して働きますが、 もっと上層のループを抜けたいときもあります。

そこでRustでは、ループに名前をつけることができます。 以下のように、outerinnerというラベルをつけ、continue 'outer のようにすれば、外側のループを次に進めることができます。

'outer: for x in 0..10 {
    'inner: for y in 0..10 {
        if x % 2 == 0 { continue 'outer; } // continues the loop over x
        if y % 2 == 0 { continue 'inner; } // continues the loop over y
        println!("x: {}, y: {}", x, y);
    }
}

今回はここまでです。 若干syntaxが違うもののsemanticsはほぼおなじ、条件分岐とループの話でした。

最後のLoop labelは便利ですね。 「終了フラグをたててすべてのループを順に抜ける」のようなコードを書かなくてすみます。 (関数にしてreturnすべしという話はありますが)