最終更新日:2023/10/21 原本2021-10-01

基本からしっかり学ぶRust入門(3):Rustの制御構造、演算子とは

Rustについて基本からしっかり学んでいく本連載。第3回はリテラルと制御構文(条件分岐、繰り返し)について。


連載:基礎からしっかり学ぶRust入門

 本連載のサンプルコードをGitHubで公開しています。こちらからダウンロードしてみてください。具体的な利用方法は連載第1回を参考にしてください。


 Rustについて基本からしっかり学んでいく本連載。連載第3回は、数値、文字列リテラルと制御構文(条件分岐、繰り返し)についてRustでどう書くか紹介します。

リテラル

 連載第2回では、変数とデータ型を取り上げました。変数の初期化に数値を利用しましたがこれを「リテラル(literal)」と呼びます。リテラルは「見たままのもの」という意味ですが、数値や文字などの値そのものを指します。Rustでは、リテラルをどのように記述するのか紹介します。

数値リテラル

 数値リテラルは、文字通り数値を表します、表1は、数値リテラルの一覧です。数値リテラルには必要に応じて接頭辞と接尾辞を付けることで、形式とデータ型を明示させることができます。

数値リテラル 接頭辞 接尾辞
整数(10進数) なし i8, i16, i32, i64, i128, isize, u8, u16, u32, u64, u128, usize 123 369i16 45_678_u32
整数(16進数) 0x 0xffff 0x88 0xaa_u8
整数(8進数) 0o 0o11 0o55 0o77_u8
整数(2進数) 0b 0b00001111 0b1111_0000 0b01010101_u8
整数(バイト) b b'A' b'9'
浮動小数点数 なし f32, f64 1.23 9.87f32, 4.56_f64
表1 数値リテラル

 整数の10進数、16進数、8進数の接頭辞は、C/C++でもおなじみの記述方式です。Rustでは2進数も表現でき、その場合には接頭辞は「0b」となります(bはbinaryの意)。バイトというのはu8型としてのみ使用できて、ASCII文字に接頭辞として「b」を付けて表現します。また、型接尾辞を付けることができ、その場合にはデータ型を明示できます(バイト以外)。さらに、数値の場合には、区切りとしてアンダースコア(_)を適宜挿入することができます。例えば、10進数で3桁ごとに区切りを入れて見やすくできます。

文字列リテラル

 文字列リテラルは、文字列を表します。第2回でデータ型を取り上げましたが、そこには文字列型はありませんでした。Rustには、言語仕様に文字列型というものはありません。詳しくは本連載の所有権の回で取り上げますが、文字列はStringというライブラリで実装されます。では文字列リテラルとは何かというと、難しく考える必要はありません、ダブルクォーテーションで囲まれた文字の連続です。

let str = "Hello, world!";

 この場合、文字列リテラルが初期化に使われたので、変数strは文字列リテラルへの参照になります(参照も本連載の所有権の回で取り上げます)。

 文字を表す'A'、'B'や、論理値を表すtrueとfalseも、それぞれchar型、bool型のリテラルとして使用可能です。

式と演算子

 続いて、制御構造において重要となる式と演算子について取り上げます。式も演算子も、C/C++と大きく変わりません。第2回で触れたように、Rustにおいても、式は評価され、最終的な演算結果を返します。演算子は、複合代入演算子を含む代入演算子、算術演算子、論理演算子、ビット演算子などC/C++で使えたものをほぼそのまま使うことができます。表2は、代表的な演算子です。

演算の種類 演算子 説明
代入演算 = var = expr 代入
数値演算 + expr + expr 加算
+= var += expr 加算後に代入
- - expr 算術否定
- expr - expr 減算
-= var -= expr 減算後に代入
* expr * expr 乗算
*= var *= expr 乗算後に代入
/ expr / expr 除算
/= var /= expr 除算後に代入
% expr % expr 剰余演算
%= var %= expr 剰余演算後に代入
比較演算 == expr == expr 等価比較
!= expr != expr 非等価比較
< expr < expr 小なり比較
<= expr <= expr 以下比較
> expr > expr 大なり比較
>= expr >= expr 以上比較
論理演算 ! !expr ビット反転、または論理反転
&& expr && expr 論理AND
|| expr || expr 論理OR
ビット演算 & expr & expr ビットAND
&= var &= expr ビットAND後に代入
| expr | expr ビットOR
|= var |= expr ビットOR後に代入
^ expr ^ expr ビットXOR
^= var ^= expr ビットXOR後に代入
<< expr << expr 左シフト
<<= var <<= expr 左シフト後に代入
>> expr >> expr 右シフト
>>= var >>= expr 右シフト後に代入
表2 Rustの主な演算子

 Rustには、他言語で見るようなインクリメント演算子(++)とデクリメント演算子(--)はありません。複合演算子(+=, -=)で代替可能です。なお、演算子がない正確な理由は不明ですが、前置と後置で評価結果が変わり、バグの原因になりやすいことから実装していないようです。

 また注意が必要なのは、上記にある演算子でも、コンテキストによってはRust独自の演算子として異なった意味に解釈される可能性があることです。!演算子は論理否定に用いられますが、識別子の後にident!と続けるとidentが表すマクロの展開になります。また、&と*はC/C++でポインタ関連の演算となるように、Rustでは参照と参照外しといった意味で使われます。

条件分岐(if式)

 さて、Rustの制御構造も見ていきましょう。制御構造は一般的に逐次実行、条件分岐、繰り返しの3つがあります。逐次実行は第2回でやってきたような上から下に順番に実行する構造です。逐次実行は省略して条件分岐を取り上げます。なお、今回のサンプルコードは、全てcontrolsパッケージに作成していきます。

if式、else式、else if式

 Rustでは、条件分岐はif式を利用します。「式」とあるように、条件分岐の実行結果全体が最終的に評価されて値を持ちます。if式の返す値が不要であれば、それは一般的なif文と同様になります。

let age = 15;   // 年齢
if age >= 25 {                                  // 条件式はfalseとなる
    println!("選挙権と被選挙権があります。");   // 実行されない
} else if age >= 18 {                           // 条件式はfalseとなる
    println!("選挙権のみがあります。");         // 実行されない
} else {
    println!("選挙権も被選挙権もありません。"); // 実行される
}
src/bin/if1.rsのソースコード

 ifキーワードに続く「age >= 25」は条件式で小かっこは不要です。条件式の評価結果が論理値のtrueなら、続くブロックの中身が実行されます。falseになれば、else式に続くブロックの中身が実行されます。上記のようにelse式でなくelse if式になっている場合は、さらに条件式「age >= 18」が評価され、同様にどちらかのブロックの中身が実行されます。

 if式には中かっこ({~})で囲まれたブロックが必要です。ブロックは、すでにmain()関数のブロックとして登場してきていましたが、複数の文をまとめたものです。文が1個でもブロックは省略できません。

if式の値を利用する

 if式が値を返す場合を見てみましょう。値を返す場合はそれぞれのブロックの最後にif式の値とする式を置きます。

let age = 15;   // 年齢
let s = if age >= 25 {  // sをif式の結果で初期化。条件式はfalseとなる
    "選挙権と被選挙権があります。"      // if式の値にならない
} else if age >= 18 {                   // 条件式はfalseとなる
    "選挙権のみがあります。"            // if式の値にならない
} else {
    "選挙権も被選挙権もありません。"    // if式の値になる
};                                      // セミコロンが必要
println!("{}", s);                      // 「選挙権も被選挙権もありません。」
src/bin/if2.rsのソースコード

 値を返すif式にする場合は、以下の点に気を付ける必要があります。

条件式

 Rustでは条件式は必ず論理値を返すbool型の結果にする必要があります。C/C++では0か0以外かという判定をしていましたが、これには利点もある反面、さまざまなバグの原因となるものでした。Rustはそこが厳格化され、必ず論理値にならなくてはならないというようになっています。従って、以下のようなコードはコンパイルしてもエラーが出力されます。

let age = 20;
if age {     // 論理型に評価できないのでエラーになる
    println!("0歳児ではありません。");
}
src/bin/if3.rsのソースコード
error[E0308]: mismatched types
 --> src/bin/if3.rs:4:8
  |
4 |     if age {
  |        ^^^ expected `bool`, found integer   // boolなのにintegerがあった
src/bin/if3.rsの実行結果

条件分岐(match式)

 条件分岐には、match式というものもあります。match式は、C/C++などのswitch文に似た動きをする構文で、与えられた変数に対してパターンマッチングをして、合致すれば指定された文を実行します。下記のコードはchar型の変数の値でメッセージの表示を切り替える例です。match式も値を返すことができるので、それを含めた例にしています。

let letter = 'S';
let str = match letter {                                // letterでマッチングする
    'Z' => "アルファベット最後の文字",                  // 単一値のマッチング
    'S' | 'M' | 'L' => "サイズを表すアルファベット",    // 複数値のマッチング
    '0'..='9' | 'A'..='F' => "16進数で使う文字",        // 範囲を指定したマッチング
    _ => "いずれでもない文字",                          // いずれにもマッチしなかった場合
};
println!("{}は{}です。", letter, str);
src/bin/match1.rsのコード

 match式におけるパターンマッチングの主要な方法である単一値、複数値、値の範囲、そしていずれにもマッチしなかった場合を紹介しました。match式では、パターンは全てのケースを網羅するようにしなければなりません。アンダースコア(_)によるパターンがあれば取りあえず満たせますが、そうでない場合はくまなくパターンを記述する必要があります。

 また「パターン => 実行する文および式」はカンマ(,)で区切り、文および式が複数になる場合には中かっこ({ })で囲ってブロックにします。さらに、パターンは定数である必要がなく、式も指定できます。この場合でも、全てのケースを網羅するようにしなければなりません。

繰り返し

 条件分岐に続く制御構造は繰り返し(ループ)です。ループには3種類の構文が用意されています。while式、for式、それにloop式です。if式と同様に、繰り返しの構文も条件付きですが値を返します。順番に見てみましょう。

while式

 while式もif式と同様に、条件式がtrueに評価されればブロック内を実行します。こちらも条件式に小かっこは不要ですが、ブロックは必要です。while式を使って、1から変数maxまでの数の総計を求めて表示してみましょう。

let max = 10;
let mut count = 1;
let mut sum = 0;
while count <= max {
    sum += count;
    count += 1;
}
println!("{}までの合計は{}です。", max, sum);   // 「10までの合計は55です。」
src/bin/while1.rsのソースコード

 if式とmatch式は条件分岐の結果としての値を返すことができましたが、while式では値を返すことはできません。正確には、空のタプルである()が返されます。

for式

 Rustのfor式は、配列などのコレクションに対して利用します。C/C++などで伝統的に使われている初期化、条件、繰り返し前処理のような構文はありません。このような構文は、while式で記述します。for文を使って配列scoresの要素を1つずつ取り出し、表示してみます。

let scores = [90, 80, 85, 70, 95];      // 点数の配列
for score in scores.iter() {                    // for式で回す
    println!("点数は{}点です。", score);        // 配列の要素数だけ実行される
}
src/bin/for1.rsのソースコード
% cargo run --bin for1
点数は90点です。
点数は80点です。
点数は85点です。
点数は70点です。
点数は95点です。
src/bin/for1.rsの実行結果

 コレクションのiter()メソッドを呼び出すことに注意してください。これはイテレータ(繰り返し記述子)で、コレクションの中身を順番に変数に与えるために使用します。配列の内容を順番に表示するだけなら、while式も利用できます。しかし、while式だと配列の各要素に対するインデックスを正確に指定してあげなければならず、しばしばバグの原因となります。for式も、while式同様に値を返すことはできません。正確には、空のタプルである()が返されます。

loop式

 繰り返しの最後はloop式です。これは最もシンプルな繰り返しの構文です。

loop {                          // 永遠に続くのでCtrl+Cを入力して止める
    println!("線路は続くよ、どこまでも。");
}
src/bin/loop1.rsのソースコード

 見て分かるように条件も何もなく、ひたすら繰り返すだけです。止め方は後で考えるので取りあえず実行してほしいというケースで使います(終了条件が明確なのにloop式を安易に利用するのは非推奨です)。

 無限ループなので、どこかで止めないとプログラムは実行し続けます。[Ctrl]+[C]キーか、あるいは次で取り上げるbreak式を使って終了させることができます。loop式は値を返すことができ、値を返すにもbreak式を使います。

break式

 繰り返しを強制的に止める(繰り返しから抜ける)break式を使うと、繰り返しをその時点で止められる他、loop式として値を返すことができます。配列scoresから値を探し、見つかればインデックスを繰り返しの値として返してみましょう。なお、このソースコードには欠陥があり、もし値が見つからない場合はインデックスが範囲外になりpanicとなります。scoresの値を書き換えて試してみてください。

let scores = [90, 20, 100, 40, 60];
let mut i = 0;
let f = loop {
    if scores[i] == 100 {     // 配列に100が見つかれば中断してインデックスを返す
        break i;
    }
    i += 1;
};
println!("満点が要素{}にありました。", f);      // 「満点が要素2にありました。」
src/bin/break1.rsのソースコード

 break式の値は省略できます。省略した場合、単純にループを中断するだけになりますが、loop式が値を返すことはできなくなります。なお、while式もfor式もbreak式を利用できますが、loop式のように値を返すことはできません。中断のみの用途になります。

continue式

 continue式は、ブロックの途中から次の繰り返しに強制的に移動させます。continue式を使ってみましょう。

let scores = [90, 20, 100, 40, 60];
let mut i = 5;
while i > 0 {
    i -= 1;
    if scores[i] == 100 {     // 配列に100が見つかれば次の繰り返しに移す
        continue;
    }
    println!("満点でない点数{}が要素{}にありました。", scores[i], i);
}
src/bin/continue1.rsのソースコード
% cargo run --bin continue1
満点でない点数60が要素4にありました。
満点でない点数40が要素3にありました。
満点でない点数20が要素1にありました。
満点でない点数90が要素0にありました。
src/bin/continue1.rsの実行結果

 配列scoresの要素が100であれば、その値だけ表示しないプログラムです。なお、continue式は値を返すためには使えません。また値を指定するとコンパイルエラーになります。

まとめ

 今回は、Rustのリテラルと制御構文(条件分岐、繰り返し)を紹介しました。次回は、基本的な文法の最後として、関数を紹介します。