Rustをはじめよう その3(基本文法編)

三菱総研DCS 技術戦略部の加藤です。 この記事では、プログラミング言語 Rust(ラスト)の基本的な文法についてまとめます。

前書き

記事概要

  • この記事では、他のプログラミング言語にも共通する、Rust の基本的な文法を紹介します。
  • 前々回前回の記事で構築した開発環境を元に説明しますので、これらの記事を読まれていない方はぜひご一読いただければと思います。

変数、定数

以下でご紹介するプログラムは、前回の記事で作成した hello_cargo プロジェクトの「main.rs」に上書きして実行しても OK です。
hello_cargo プロジェクトとは別に新規のプロジェクトを作成する場合は、以下のコマンドで作成してください。例えば、新規に「rust_practice」という名前のプロジェクトを作成する場合は、コマンドプロンプトでプロジェクトを作成するフォルダに移動した上で、以下のコマンドを実行します。
その後、生成された rust_practice フォルダ配下の src フォルダ内にある「main.rs」にプログラムを記述します。

cargo new rust_practice


まずは、変数の定義と同時に初期値を代入し、変数に代入した値をコンソール出力してみましょう。
以下のプログラムは、変数 x に数値を代入し、変数に代入した値をコンソール出力する例です。

main.rs

fn main() {
    let x = 123;
    println!("x = {}", x);
}


出力結果

x = 123


変数 x に代入した123という数値を、画面に表示することができました。

上記のプログラムについて、2 つの部分に分けて説明します。

  • let x = 123;

    この行では、全体として x という変数を宣言し、変数 x に値 123 を代入しています。
    let は、Rust において変数を宣言する際に使用するキーワードです。let の後ろには変数名を記述します。Rust では、変数の宣言時に変数のデータ型を指定する必要はなく、変数に代入された値のデータ型に応じて変数のデータ型も決まります。
    また、=の右辺に x に代入する値を記述しています。こうすることで、x という変数を宣言すると同時に値を代入することができます。

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

    この行では、println マクロを呼び出し、コンソール画面に文字を出力しています。マクロ名の後ろに!を付けてマクロを呼び出すことは、前回の記事で説明したとおりです。
    println マクロの引数部分ですが、第一引数(,の前)には、x = {}という文字列を指定しています。この中で、{}はプレースホルダと呼ばれ、第二引数に指定した値を{}の位置に埋め込む働きをします(複数のプレースホルダが存在する場合は、第二引数、第三引数...の順に埋め込まれる)。よって、上記のプログラムは「x =」という文字列に続けて、第二引数に指定した値が画面に表示される、という挙動になります。
    マクロの第二引数には、変数 x を指定しています。これで、プレースホルダ部分には x に代入されている値 123 が表示されることになります。

変数は不変(immutable)

Rust における変数の重要な性質として、変数はデフォルトで不変(immutable)であるということが挙げられます。つまり、一度宣言した変数に対して値を再代入できないということです。
例えば、プログラムを以下のように書き換えて、変数に値を再代入すると、コンパイル時にエラーが発生します。

main.rs

fn main() {
    let x = 123;
    println!("x = {}", x);

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


出力結果

error[E0384]: cannot assign twice to immutable variable `x`
 --> src\main.rs:5:5
  |
2 |     let x = 123;
  |         -
  |         |
  |         first assignment to `x`
  |         help: make this binding mutable: `mut x`
...
5 |     x = 456;
  |     ^^^^^^^ cannot assign twice to immutable variable
(以下略)


エラーメッセージを読むと、確かに再代入の箇所がエラーとして指摘され(コードの下に^^^^^^^が引かれている箇所がエラー)、cannot assign twice to immutable variable(不変の変数へ二度代入することはできません)と表示されます。

変数が不変(immutable)であることにはメリットがあります。それは、変数の値が書き換わっていないことをコンパイラが保証する点です。長いプログラムを作成する場合に、変数が書き換わっていないか確認することは手間がかかる場合がありますが、Rust では書き換わっている場合にはコンパイルが通らなくなります。よって、意図せず変数が書き換わったことによる潜在的なバグを含むコードが実行されることを未然に防ぐことができます。

では、プログラムを以下のように書き換えて、x という変数を「再宣言」することはできるでしょうか。結論としては、以下のプログラムは問題なく実行することができます。

main.rs

fn main() {
    let x = 123;
    println!("x = {}", x);

    let x = 456;
    println!("x = {}", x);
}


出力結果

x = 123
x = 456


以上のことから、Rust ではlet で宣言した変数は再代入不可、再宣言可ということがご理解いただけたと思います。
なお、変数の再宣言のことをシャドーイングとも呼びます。最初に宣言した変数を、後から宣言した同名の変数で覆い隠す(シャドーイング)、というイメージです。

【参考】JavaScript(ES2015 以降) では、let で宣言した変数は再代入可、再宣言不可となります。Rust の let とは性質が異なっているため、JavaScript に慣れている方は混同しないようご注意ください。

変数を再宣言した場合、以前の変数と再宣言した変数は無関係なので、代入する値のデータ型を変えることも可能です。
以下のサンプルコードでは、まず変数 x に数値型の値を代入し、その後に再宣言した変数 x には文字列型の値を代入していますが、このプログラムは正常に動作します。

main.rs

fn main() {
    let x = 123;
    println!("x = {}", x);

    let x = "abc";
    println!("x = {}", x);
}


出力結果

x = 123
x = abc


変数を可変(mutable)にする方法

ここまで、Rust の変数は不変(immutable)であることを説明してきました。一方で、コーディングを行う際には可変(mutable)な変数が必要となる場合もあります。
Rust では、可変(mutable)な変数を作成することができます。以下のサンプルコードのように、変数宣言の let の後ろにmutを付けることで、変数 x は可変(mutable)な変数となり、再代入が可能となります。

main.rs

fn main() {
    let mut x = 123;
    println!("x = {}", x);

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


出力結果

x = 123
x = 456


では、可変(mutable)の変数に対して、最初に代入した値のデータ型と異なる型の値を代入することは可能でしょうか。結論としては、異なるデータ型の再代入は許されません。
以下のサンプルコードでは、可変(mutable)な変数に異なるデータ型の値を代入していますが、これが原因でコンパイルエラーが発生します。

main.rs

fn main() {
    let mut x = 123;
    println!("x = {}", x);

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


出力結果

error[E0308]: mismatched types
 --> src\main.rs:5:9
  |
5 |     x = "abc";
  |         ^^^^^ expected integer, found `&str`
(以下略)


上記のように、エラーメッセージとしてmismatched types(データ型の不一致)が表示され、エラー発生箇所の下線部にはexpected integer, found &str(integer を予想しましたが、&str(ここでは文字列型と考えてください)が見つかりました)と表示されます。
以上のことからわかるように、Rust では可変(mutable)な変数に対して、最初に代入した値のデータ型と異なるデータ型の値を代入することはできません。つまり、最初に代入したデータ型に基づいて可変(mutable)な変数が生成され、これを後から変更することはできない、ということです。

【参考】Rust がこのような仕組みになっているのは、変数に値を入れた段階で、そのデータ型の値を保存するために必要な固定のメモリ領域を確保するからです。仮に異なる型の代入を許す場合、代入のたびに必要なメモリ領域を計算して確保し直すか、データ型のうちで最大サイズのものに合わせてメモリ領域を確保する必要があり、これが原因で処理のオーバーヘッドや無駄なメモリ領域の確保が発生します。このようなオーバーヘッドや無駄を避ける目的で、変数のために確保したメモリ領域の変更を許していないため、この結果として異なるデータ型の値を代入することができない仕組みとなっているのです。

定数(constants)

Rust の定数は、再代入不可、再宣言(シャドーイング)不可な値です。定数は let で宣言した変数と似ていますが、変数は再宣言可能な一方で、定数は再宣言も不可であるという点が異なります。
このような性質を持つため、定数は主にプログラムの中で何度も呼び出される固定値を設定する用途に使用します。

以下に、定数を宣言し、定数に代入した値を画面に表示するサンプルコードを示します。

main.rs

fn main() {
    const CONSTANT: i32 = 123;
    println!("x = {}", CONSTANT);
}


出力結果

x = 123


定数の宣言には、変数宣言のletに代わってconstを使用します。また、定数は宣言時にデータ型も指定する必要があります。データ型は、定数名(サンプルコードの CONSTANT)の後に: データ型名の形で記述します。サンプルコードでは、i32 型(32bit 整数型で、他の言語の integer に相当)の定数を宣言しています。
定数宣言時には、必ず定数に値を代入する必要があります。代入しなかった場合は、エラーが発生します。
定数の使用の仕方は変数と同様です。サンプルコードでは、println マクロで定数の値を画面に表示していますが、当該処理は変数の場合と同じように記述しています。

では、上記のコードを定数を再宣言(シャドーイング)する形に書き換えてみましょう。変数と異なり、定数は再宣言できないため、以下のプログラムを実行するとコンパイルエラーが発生します。

main.rs

fn main() {
    const CONSTANT: i32 = 123;
    println!("x = {}", CONSTANT);

    const CONSTANT: i32 = 456;
    println!("x = {}", CONSTANT);
}


出力結果

error[E0428]: the name `CONSTANT` is defined multiple times
 --> src\main.rs:5:5
  |
2 |     const CONSTANT: i32 = 123;
  |     --------------------------- previous definition of the value `CONSTANT` here
...
5 |     const CONSTANT: i32 = 456;
  |     ^^^^^^^^^^^^^^^^^^^^^^^^^^^ `CONSTANT` redefined here
  |
  = note: `CONSTANT` must be defined only once in the value namespace of this block
(以下略)


エラーメッセージを読むと、確かに再宣言の箇所がエラーとして指摘され、the name CONSTANT is defined multiple times(CONSTANT という名前が再定義(宣言)されています)と表示されます。
このように、定数は再代入することも、再宣言することもできません。

制御文

次に、プログラムには欠かせない制御文(分岐、反復)の記述方法について説明します。
Rust の制御文の書き方は、他のプログラミング言語とよく似ているため、特に違和感なく覚えられると思います。はじめに条件分岐に使用する if について説明し、その後に反復に使用する loop、while、for を紹介します。

if

はじめに、以下のサンプルコードを示します。以下のコードは、変数 number の値が負、0、正のいずれかを判定し、判定結果をコンソール出力するプログラムです。

main.rs

fn main() {
    let number = 2;

    if number < 0 {
        println!("number is negative value.");
    } else if number == 0 {
        println!("number is zero.");
    } else {
        println!("number is positive value.");
    }
}


出力結果

number is positive value.


上記のコードを見ると、条件分岐の構文は他の言語とよく似ていることがわかります。
まず、条件分岐を表すキーワードのifがあり、その後ろに条件式を記述します。条件式は bool 型である必要があるので、例えばif number(数値型の値)のようにすることはできません。
条件式の後ろには{}があり、その内側に条件式が成立した場合に実行する処理を記述します。なお、Java などの言語では、実行する処理が一行の場合に{}を省略できますが、Rust では常に{}を記述する必要があるので注意してください。
複数の条件分岐を扱う場合は、if 条件式 {}の後ろにelse if 条件式 {}を続けます。また、else {}を末尾に記述することで、それまでの条件式にマッチしなかった場合の処理を記述できます。

条件分岐を使用すると、条件に応じて変数に代入する値を切り替えるプログラムを記述できます。
以下のサンプルコードは、変数 number の値に応じて変数 message に代入する文字列を切り替え、message の値をコンソール出力するプログラムです。

main.rs

fn main() {
    let number = 2;

    let message = if number < 0 {
        "number is negative value."
    } else if number == 0 {
        "number is zero."
    } else {
        "number is positive value."
    };

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


出力結果

number is positive value.


上記のプログラムのように、変数宣言の右辺に条件分岐を記述することで、左辺の変数に対して代入する値を切り替えることができます。
ただし、この方法が使用できるのは、代入する変数の型が全て同一である場合だけです。例えば、ある分岐では数値型、ある分岐では文字列型というように、分岐ごとに代入する変数の型が異なる条件分岐は実装できません。

loop

Rust には反復処理に使用するキーワードがいくつかありますが、その中でも loop キーワードは最も単純な動作をします。loop キーワードを使用すると、処理を明示的に停止するまで、何度も繰り返し実行することができます(いわゆる無限ループ)。

loop キーワードを使用したサンプルコードを、以下に示します。サンプルコードでは、変数 counter の値をコンソール出力して counter に 1 を加算する、という処理を繰り返しています。
なお、Rust にはインクリメント(++)やデクリメント(--)の演算子はありません。そのため、それぞれ+=(右辺の値を左辺に加算)、-=(右辺の値を左辺から減算)の各演算子を使用します。

【参考】よくある質問 · プログラミング言語 Rust (Why doesn't Rust have increment and decrement operators?)

以下のプログラムを実行すると、処理が無限に繰り返されます。プログラムを停止する場合は Ctrl + C(コマンドプロンプトから実行した場合)、もしくは Shift + F5(VSCode 上で実行した場合)で止めて下さい。

main.rs

fn main() {
    let mut counter = 1;
    loop {
        println!("{}", counter);
        counter += 1;
    }
}


出力結果

1
2
3
4
5
...(以下、停止するまで表示される)


loop キーワードを使用した反復処理から抜け出す別の方法としては、break キーワードがあります。break キーワードは、反復処理を中止し、{}から抜け出して後続の処理へと遷移する働きがあります。

上記のサンプルコードの counter が 100 より大きくなった時点で処理を中止するように、break キーワードを使用して書き換えたコードを以下に示します。

main.rs

fn main() {
    let mut counter = 1;
    loop {
        if counter > 100 {
            break;
        } else {
            println!("{}", counter);
            counter += 1;
        }
    }
}


出力結果

1
2
3
...(中略)
100


while

while キーワードを使用すると、条件式が満たされる間のみ反復処理を実行できます。
以下のサンプルコードは、loop キーワードの説明の最後で紹介したコードを while キーワードを使用して書き換えたものです。loop キーワードの場合に使用していた条件分岐の if やループを抜け出すための break が不要となり、コードが読みやすくなったことがわかると思います。

main.rs

fn main() {
    let mut counter = 0;
    while counter <= 100 {
        println!("{}", counter);
        counter += 1;
    }
}


出力結果

1
2
3
...(中略)
100


for

for キーワードも、他のプログラミング言語と同様の機能を持っており、主に指定した回数だけ処理を反復したり、配列などのコレクションの各要素をすべて参照したりする用途に使用します。

まず、指定した回数だけ処理を反復するサンプルコードを以下に示します。loop と while で紹介したサンプルコードと同様に、1 から 100 までの数をコンソール出力するプログラムです。

main.rs

fn main() {
    for number in 0..100 {
        println!("{}", number + 1);
    }
}


出力結果

1
2
3
...(中略)
100


上記のサンプルコードのfor number in 0..100 {~}の意味は、「変数 number を 0 から 100 まで 1 ずつインクリメントし、処理を反復する。number が 100 になったら反復を停止する」となります。
このような記述の仕方をする理由ですが、反復回数を一目で理解できるようにするためだと思われます。たとえば、「10 回だけ処理を反復したい」場合に「0..10」と書けば、反復回数の「10」がコードからも容易に読み取れます。
ここで注意が必要なのは、number in n..mと記述した場合にnumber = mのときは反復処理が実行されないという点です。これは、C や Java で言うところのfor (int i = 0; i < 100; i++)i=100 のときに反復処理が実行されないのと同様です。
今回のサンプルコードを同じ結果を得るためにfor number in 1..101 {println!("{}", number)}という記述の仕方も可能ですが、これだと反復回数がわかりにくくなります。そのため、基本的には「0..反復回数」という書き方をおすすめします。

上記のサンプルコードでは数値をインクリメントしましたが、反対にデクリメントすることもできます。やり方は、「0..100」を()でくくった上で、その後ろに「.rev()」を付けるだけです。これで逆順(100,99,...,1,0)となります。
実は、「n..m」と記述すると Range 型というデータ型の値が生成されています。この型が持つ (正確には Iterator トレイト(*)で定義され、Range 型で実装している) rev()というメソッドを使用すると、逆順になった Range 型の値が返却されます。この中身を number へ順番に取り出していくことで、100~0 までのデクリメントが実現できます。

(*) トレイトは、他言語で言うところのインタフェースに似た役割を持つ機能です。詳細については、以下をご参照下さい。
トレイト: 共通の振る舞いを定義する - The Rust Programming Language

ここで注意したいのは、デクリメントの場合でも、インクリメントの場合と同様にnumber = 100のときは処理が実行されず、実際にはnumberが0~99の間だけ処理が繰り返される点です。numberに値が入る順番は逆順になりますが、処理に使用されるnumberの範囲(0~99)や反復回数(100回)はインクリメントのforと同様である、と理解して下さい。

main.rs

fn main() {
    for number in (0..100).rev() {
        println!("{}", number + 1);
    }
}


出力結果

100
99
98
...(中略)
1


for 文は、コレクション(配列など)の内容をすべて参照する際にもよく用いられます。
for 取り出された配列の要素 in 配列名.iter()の形で、配列の各要素を順番に取り出すことができます。また、逆順で取り出したい場合は、後ろに「.rev()」を付けます。
以下のサンプルコードは、まず配列の内容を順番にコンソール出力します。その後、逆順でコンソール出力します。

main.rs

fn main() {
    let array = ["a", "b", "c"];

    println!("---in order---");

    for elem in array.iter() {
        println!("{}", elem);
    }

    println!("---in reverse order---");

    for elem in array.iter().rev() {
        println!("{}", elem);
    }
}


出力結果

---in order---
a
b
c
---in reverse order---
c
b
a


コメント

最後に、コメントの書き方について説明します。
コメントの書き方も、他のプログラミング言語とよく似ています。1 行のコメントには、コメントの頭に「//」を付けます。また、複数行のコメント(ブロックコメント)は「/* ~ */」でくくります。その他にはドキュメンテーションコメント(Java で言うところの Javadoc コメント)がありますが、本記事では割愛します。

以下のサンプルコードでは、一行のコメントと複数行のコメントを記述しています。コメントは、プログラム自体の動作に一切影響を与えません。

main.rs

fn main() {
    // これは一行コメントです
    let message = "Message";

    /*
    これはブロックコメントです
    */

    print!("{}", message);
}


出力結果

Message


まとめ

本記事では、Rust の基本的な文法(変数・定数、制御文、コメント)をご紹介しました。
次回は、Rust の特徴的な機能である「所有権」について解説する予定です。

参考文献

  • 「The Rust Programming Language: 2nd Edition」, https://doc.rust-jp.rs/book-ja-pdf/book.pdf 2020 年 2 月 26 日アクセス
  • κeen,河野 達也,小松礼人(2019)『実践 Rust 入門[言語仕様から開発手法まで]』 技術評論社
その2 |  その3 | その4