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

基本からしっかり学ぶRust入門(2):変数でも「不変」がデフォルト――Rustの変数、データ型を理解する

Rustについて基本からしっかり学んでいく本連載。第2回は、Rustにおける変数、データ型、配列について。


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

 Rustについて基本からしっかり学んでいく本連載。連載第2回は、主にC/C++言語の経験者を意識しながら、基本的な文法(変数、データ型、配列など)について、Rustではどう書くかという観点で紹介します。

サンプルパッケージについて

 本連載ではサンプルコードを紹介して、必要に応じてコンパイルと実行まで行っていきます。サンプルコードはGitHubで公開しており、こちらから自由にダウンロードして使えるようにします。利便性を考えてサンプルコードはパッケージの形でまとめています。必要な人は、パッケージをまとめたzipファイルをダウンロードして、お手元のPCの適当なフォルダ(前回の記事で紹介したatmarkit_rustなど)に展開すれば、すぐにサンプルコードを参照/実行できます(具体的な方法は連載第1回の記事を参照してください)。

 詳しくはプロジェクト管理の回で取り上げますが、パッケージには「クレート」と呼ばれるバイナリあるいはライブラリを含むことができるようになっています。このうち、バイナリのクレートは複数を含めることができるので、パッケージ内の各サンプルはそれぞれ異なるクレートとしています。

 第1回で紹介したフォルダ構成とは若干異なりますので、注意してください。パッケージに複数のクレートを含める場合、ソースファイルの置き場所はsrc/binになり、src/main.rsは使われなくなります。例えば、sample1とsample2という2つのクレートが用意される場合、src/bin/sample1.rsとsrc/bin/sample2.rsというようにソースファイルを配置します。

 そして、これらを実行するときには、以下のようにクレートを--binオプションで明示します。指定しない場合、「デフォルトのクレートが指定されていない」というエラーになります。

% cargo run --bin sample1

 バイナリがtarget/debugに置かれるのは変わりません。同じ場所に、sample1、sample2が置かれます(Windowsの場合はsample1.exe, sample2.exe)。まとめると、以下のような構造になります。

package_name―+―src―bin―+―sample1.rs
             |         +―sample2.rs
             +―target―debug―+―sample1(.exe)
                            +―sample2(.exe)

 ソースファイルを閲覧・編集する、バイナリを直接実行するときの参考にしてください。

コメント

 Rustにおけるコメントは2種類あります。ひとつはCでおなじみの「/*~*/」の形式、もうひとつがC++/Javaなどで使える2連スラッシュ(//)形式です。

 前者は、/*から*/の範囲がコメントになります。複数行にまたがっていても、行の途中から始めて行の途中で終わっても構いません。関数の前で仕様を書いたり、ソースコードの先頭で作者や日付、バージョンを記したりするのに用いられます。

/*
作者:WINGSプロジェクト
日付:2021/9/1
バージョン:1.00
*/

 後者は、//から始まって、行末までコメントになります。行の途中から始めても構いません。文に対する説明などに用いられます。

// 処理の開始
println!("Hello, world!");  // "Hello, world!"と表示

 コメントには、このようなメモ的な用途と、ドキュメンテーションコメントというHTMLドキュメントの生成の用途があります。ドキュメンテーションコメントは本連載では取り上げません。

変数

 続いては変数を説明します。変数は制御構造とともに、プログラミング言語に欠かせない要素です。

変数の宣言

 Rustは静的に型付けをするプログラミング言語なので、変数を事前に宣言する必要があります。変数は、以下のように「let文」で宣言します。変数の宣言は、関数の内部、外部、ブロックの内部など、基本的にどこでもできるようになっています。

let x = 100;

 これで、変数xを宣言し、値として100を入れるという意味になります。なお、Rustでは「値100をxに拘束する」と表現します。

 変数のデータ型を示すものが見当たりませんが、問題ありません。Rustには「型推論」という仕組みがあり、型が省略された場合に推測するようになっています。この例では、初期化する値の100を整数と見なして、xは整数型であると推測しています(整数型にも何種類かあります。詳しくは後述します)。

変数を書き換える

 この変数xの値を書き換えたいとして、以下のように代入文を加えます。代入文は「左辺 = 右辺;」と記述します。

let x = 100;
x = 200;

 これはうまくいくでしょうか。これを確かめるために、以下のようなプログラムを用意します。この回のサンプルコードは、variablesパッケージに作成していきます。なお、main()関数のブロックおよびインデントは省略していますので、完全なソースコードは配布サンプルを参照してください。

let x = 100;
println!("{}", x);
x = 200;
println!("{}", x);
src/bin/let1.rsのソースコード

 println!()はマクロで、1番目の引数の書式に従い、2番目以降の値を埋め込んで出力します。今回はxの値を文字列化して出力します。マクロの詳細は別の回で取り上げます。

なお、ソースコードをコンパイルすると、エラーになります。

% cargo run --bin let1
   Compiling variables v0.1.0 (/Users/nao/Documents/atmarkit_rust/variables)
error[E0384]: cannot assign twice to immutable variable `x`
 --> src/bin/let1.rs:4:5
  |
2 |     let x = 100;
  |         -
  |         |
  |         first assignment to `x`
  |         help: make this binding mutable: `mut x`
3 |     println!("{}", x);
4 |     x = 200;
  |     ^^^^^^^ cannot assign twice to immutable variable
error: aborting due to previous error
For more information about this error, try `rustc --explain E0384`.
error: could not compile `variables`
To learn more, run the command again with --verbose.

 Rustのエラーメッセージはとても親切です。ソースコードの行番号付き抜粋とエラーの行内における発生箇所、エラーの内容、ヒントなど、至れり尽くせりです。しかも、最終的なエラーに至る問題の箇所も関連して報告してくれるのです。なお、以降ではエラーの定型部(後半部分)は省略していきますのでご了承ください。

 上記の実行結果を見ると、最初の代入では問題ないですが、2回目の代入で「cannot assign twice to immutable variable」というエラーが発生しています。immutableとは「不変の」という意味で、「不変の変数に2回目の代入はできません」と書かれています。

 Rustの変数は、変数と言いながら、デフォルトでは値の変更ができません(不変であるとも言います)。実は、これがRustの安全性を高める仕組みの一つです。不変の変数という言葉は変ですが「とにかくそういうことはできないんだ」と思ってください。

 これは、C/C++とは逆のアプローチです。C/C++に限った話ではないですが、一般的に変数は可変であると見なされます。値を変更できないようにするには、const修飾子を付けたり、final修飾子を付けて宣言したりします。しかし、わざわざ付けないとならないので、値を変更してはいけないのに変更できてしまう、それがバグのもとになるわけです。

変数を書き換え可能にする

 Rustでは、値を変更しても構わない変数は、指定する必要があります。ソースコードをプログラマーが見た際に、「この変数は書き換えられる可能性があるのだ」ということを明示的に伝えるのです。これには、以下のように「mut」を付けて変数を宣言します。mutはmutableの略で、変更可能という意味です。

let mut x = 100;

 これを踏まえてソースファイルを修正して、コンパイルしてみると、今度はエラーにならないことが分かります。

let mut x = 100;
println!("{}", x);      // 100
x = 200;
println!("{}", x);      // 200
src/bin/let2.rsのソースコード

 このように、Rustでは変数を文字通り変数として使いたい場合は、特別な宣言をする必要があります。

宣言と初期化を分ける

 これまでは変数の宣言と同時に初期化していました。Rustでは、これが標準的な形式になります。他言語にあるように、宣言だけしておき、後で初期化することもできます。

let x;
x = 10;

 mutがないのでxは不変変数になりますが、最初の代入だけは初期化と見なされて、エラーとなりません。つまり、不変変数は2回目の代入が許されていないだけなのです。以下のように記述すると、コンパイルエラーになります。

let x;
x = 10;
x = 20;

 この例では、xのデータ型は決まりません。最初の代入をすることで、型推論によりxの型がi32と決まります。なお、Rustの変数はシャドーイングというユニークな性質を持っています。シャドーイングは連載後半で取り上げる予定です。

定数

 変数をmutなしで宣言すれば不変にできます。しかし、定数の仕組みもきちんとあります。

const TAX_RATE: f64 = 1.1;

 f64型の定数TAX_RATEを、1.1という値で宣言しています。Rustでは、定数にも型が必要になります。関数外で宣言し、プログラム全体から参照するような使い方はC/C++と変わりません。

 「mutを付けない変数と定数は同じではないか」と思われるかも知れませんが、定数は定数式しか代入できません。変数の場合、任意の式を代入できます。おのずと使い分けられるというわけです。

変数名と定数名

 Rustでは、変数名などの識別子には、全ての英文字(大文字小文字)、数字、アンダースコア(_)が使えます(数字で始まるのは不可)。

 変数名(関数名なども)の命名規則は、スネークケースに従うことになります。スネークケースとは、全て文字は小文字にして単語ごとに区切る記法で区切り文字はアンダースコア(_)になります。

 これに対して定数の名前は、全て大文字で単語の間をアンダースコア(_)で区切ります。

 この規則に従わないからといってコンパイルエラーになることはありませんが、見た目で区別ができるので準拠しておくべきでしょう。

文と式

 Rustでは、文と式を明確に区別しています。ここではlet文を紹介しましたが、文はセミコロン(;)で終わることになっています。文は実行するためのもので、値を持ちません。

 これに対して式は、最終的に1つの値になるまで演算が行われるものです。よって、式は必ず単一の値を持ちます。式にセミコロンは必要ありません。

 通常は文と式をあまり意識する必要はありませんが、C/C++で当たり前のようにできたことができないこともあります。これは今後の連載の中で随時取り上げていきます。

データ型

 Rustは静的に型付けをするプログラミング言語なので、全ての値や変数、定数はデータ型がコンパイル時に決まる必要があります。データ型には大きく分けて「スカラー型」と「複合型」があります。まずは基本的なスカラー型から見ていきましょう。

スカラー型

 Rustには、4つのスカラー型があります。

 整数型は、小数部のない数値を表すデータ型です。表1の通り、大きさと符号の有無に応じた6つの整数型があります。

大きさ 符号付き 符号なし
8bit i8 u8
16bit i16 u16
32bit i32 u32
64bit i64 u64
128bit i128 u128
アーキテクチャ依存 isize usize
表1 整数型の種類

 符号付き整数はi(integer)で始まり、符号なし整数はu(unsigned)で始まります。大きさをbit数で続けます。アーキテクチャ依存はisize, usizeとなります。8bitから始まって128bitまでの整数を表すことができます。また基準型というものがスカラー型にはあり、整数型ではi32が基準型になります。

 浮動小数点型は、小数値を表すデータ型です。f32とf64があり、それぞれ32bit、64bitのサイズを持ち、基準型はf64です。f32は単精度浮動小数点数、f64は倍精度浮動小数点数を表現します。

 論理値型は、真偽を表すデータ型です。データ型はboolで、値はtrueとfalseしかとりません。

 最後の文字型は、文字を表すデータ型です。データ型はcharです。C/C++のcharも文字型ですが、これはASCIIコード(7bit)を表すのに対して、RustのcharはUnicodeを表します。よって、日本語などの文字も表現できます。

基準型

 さて、基準型が整数型と浮動小数点型に出てきました。基準型というのは、型注釈(後述します)のない変数宣言などで、優先して選択されるデータ型です。例えば、整数型の変数xを宣言したいとき、以下のように書かれていれば、i32すなわち32bitの符号付き整数として宣言されたと見なされます。浮動小数点型も同様です。以下では、f64が選択されます。

let x = 10;     // i32
let y = 3.14;   // f64

型注釈

 Rustでは明示的なデータ型の指定が必要と書きながら、そのデータ型を指定する方法を書いてきませんでした。Rustでは、データ型を型注釈という形で指定します。上記の変数宣言を型注釈付きで示すと以下のようになります。

let x: i32 = 10;
let y: f64 = 3.14;

 変数名の後にコロン(:)を続けて、その後にデータ型を記述します。これで、変数名、データ型、初期値、全てを指定できる文が整いました。

複合型

 複合型とは、複数のスカラー型の値をまとめて扱うためのデータ型です。複合型には、タプル(tuple)と配列(array)があります。最初に、タプル型を見ていきましょう。

タプル型

 タプル型は、複数のスカラー型の値をまとめて扱えるデータ型ですが、スカラー型がそれぞれ異なっていても扱うことができます。C/C++における構造体のように見えるかも知れませんが、それぞれの値に名前はありません。

let t = (2, 3.14, 0);
let (a, b, c) = t;
println!("a={}, b={}, c={}", a, b, c);  // a=2, b=3.14, c=0
let x = t.0;
let y = t.1;
let z = t.2;
println!("x={}, y={}, z={}", x, y, z);  // x=2, y=3.14, z=0
src/bin/tuple1.rsのソースコード

 まず、タプル型の変数tを「2」「3.14」「0」という3つの値を持つように宣言しています。型注釈がないので、i32、f64、i32がデータ型として選択されます。変数は1個なのに値が3つあります。これは他の変数群の初期化に用いたり、インデックスを用いて個別に取り出したりできます。変数a, b, cにはtの値である2, 3.14, 0が入ります。変数x, y, zについても同様です。

 個別に取り出す場合、インデックスは0から始まることに注意してください。これは、C/C++/Javaなどにおけるインデックスの考え方と同じです。またインデックスは以下の配列と同様のチェックが行われて、範囲外のインデックスが指定された場合にエラーが発生します。

配列型

 配列型には、同じデータ型の値を複数持たせることができます。aは配列型で、i32がデータ型として選択されます。要素数は5個になります。参照も同様で、大かっこ([])にインデックスを入れて指定します。

let a = [1, 2, 3, 4, 5];
let x = a[0];
let y = a[1];
let z = a[2];
println!("x={}, y={}, z={}", x, y, z);  // x=1, y=2, z=3
src/bin/array1.rsのソースコード

 一度宣言した配列の要素数を変更することはできません。この欠点を克服する手段として、コレクションライブラリが用意されています。代表的なのはベクターですが、コレクションライブラリについては別の回で紹介予定です。

 なお、配列を扱う際、有効ではないインデックスでのアクセスという問題が付きまといます。C/C++では予期しない動作となって現れますし、Java/C#では実行時に例外で捕捉されます。Rustでは、このチェックはコンパイル時と実行時に行われます。

let a = [1, 2, 3, 4, 5];
let i = 6;
let x = a[i];
let y = a[1];
let z = a[2];
println!("x={}, y={}, z={}", x, y, z);
src/bin/array2.rsのソースコード

 これをコンパイルすると、以下のようにコンパイルエラーになります。

% cargo run --bin array2
   Compiling variables v0.1.0 (/Users/nao/Documents/atmarkit_rust/variables)
error: this operation will panic at runtime
 --> src/bin/array2.rs:5:13
  |
5 |     let x = a[i];
  |             ^^^^ index out of bounds: the length is 5 but the index is 6
  |
  = note: `#[deny(unconditional_panic)]` on by default

 エラーの内容を確認すると、コンパイル時にインデックスの値が範囲外だったため、実行せずにエラーとしています。しかし、変数iの値が実行時でないと決まらない場合には、実行時にインデックスのチェックが実施されます。もし、インデックスが範囲外であれば、Rustはpanicという形でエラーを返し、プログラムを終了させます。

 この動きは、Javaなどにおける例外処理に似ています。ネイティブコンパイラ言語でありながら、実行時にインデックスの有効性をチェックしているのです。こういったエラー処理については、本連載のエラー処理の回で取り上げたいと思います。

まとめ

 第2回では、Rustの基本的な文法として、変数、データ型、定数を取り上げました。次回は、文法の要である制御構造を紹介していきます。