変数と可変性
「値を変数に保持する」の節で触れた通り、変数は標準で不変になります。これは、 Rustが提供する安全性や簡便な並行性の利点を享受する形でコードを書くための選択の1つです。 ところが、まだ変数を可変にするという選択肢も残されています。 どのように、そしてなぜRustは不変性を推奨するのか、さらには、なぜそれとは違う道を選びたくなることがあるのか見ていきましょう。
変数が不変であると、値が一旦名前に束縛されたら、その値を変えることができません。
これを具体的に説明するために、projectsディレクトリにcargo new variablesコマンドを使って、
variablesという名前のプロジェクトを生成してください。
それから、新規作成したvariablesディレクトリで、src/main.rsファイルを開き、 その中身を以下のコードに置き換えましょう。このコードはまだコンパイルできません:
ファイル名: src/main.rs
fn main() {
let x = 5;
println!("The value of x is: {x}"); // xの値は{x}です
x = 6;
println!("The value of x is: {x}");
}
これを保存し、cargo runコマンドでプログラムを走らせてください。
次の出力に示されているような、不変性に関するエラーメッセージを受け取るはずです:
$ cargo run
Compiling variables v0.1.0 (file:///projects/variables)
error[E0384]: cannot assign twice to immutable variable `x`
(不変変数`x`に2回代入できません)
--> src/main.rs:4:5
|
2 | let x = 5;
| -
| |
| first assignment to `x`
| (`x`への最初の代入)
| help: consider making this binding mutable: `mut x`
3 | println!("The value of x is: {x}");
4 | x = 6;
| ^^^^^ cannot assign twice to immutable variable
For more information about this error, try `rustc --explain E0384`.
error: could not compile `variables` (bin "variables") due to 1 previous error
この例では、コンパイラがプログラムに潜むエラーを見つけ出す手助けをしてくれることが示されています。 コンパイルエラーは、イライラすることもあるものですが、本当はまだプログラムにしてほしいことを安全に行えていないだけということなのです。 エラーが出るからといって、あなたがいいプログラマではないという意味ではありません! 経験豊富なRustaceanでも、コンパイルエラーを出すことはあります。
不変変数`x`に2回代入できませんというエラーメッセージを受け取りました。
不変なxという変数に別の値を代入しようとしたからです。
不変と指定された値を変えようとした時に、コンパイルエラーが出るのは重要なことです。 なぜなら、この状況はまさしく、バグに繋がるからです。コードのある部分は、 値が変わることはないという前提のもとに処理を行い、別の部分がその値を変更していたら、 最初の部分が目論見通りに動いていない可能性があるのです。このようなバグは、発生してしまってからでは原因が追いかけづらいものです。 特に第2のコード片が、値を時々しか変えない場合、尚更です。 Rustコンパイラは、値が不変であると宣言したら、本当に変わらないことを担保してくれるので、変更を自分で追いかける必要がなくなります。 故にコードを通して正しいことを確認するのが簡単になるのです。
しかし可変性は非常に有用で、よりコードを書きやすくしてくれることもあります。
変数は標準では不変ですが、第2章でやったように、
変数名の前にmutキーワードを付けることで、可変にできるわけです。
またmutを付けることで、コードの別の部分がこの変数の値を変えるだろうと示すことによって、
未来の読者に対してその意図を汲ませることができるのです。
例として、src/main.rsファイルを以下のように書き換えましょう:
ファイル名: src/main.rs
fn main() { let mut x = 5; println!("The value of x is: {x}"); x = 6; println!("The value of x is: {x}"); }
今、このプログラムを走らせると、以下のような出力が得られます:
$ cargo run
Compiling variables v0.1.0 (file:///projects/variables)
Finished dev [unoptimized + debuginfo] target(s) in 0.30s
Running `target/debug/variables`
The value of x is: 5 (xの値は5です)
The value of x is: 6
mutキーワードが使われると、xに束縛されている値を5から6に変更できます。
可変性を使うかどうかは最終的にはプログラマに任せられており、どちらがより明白と思えるかは個別の状況によるでしょう。
定数
不変変数のように、定数(constants)は名前に束縛され、変更することが叶わない値のことですが、 定数と変数の間にはいくつかの違いがあります。
まず、定数にはmutキーワードは使えません: 定数は標準で不変であるだけでなく、常に不変なのです。
定数はletキーワードの代わりに、constキーワードで宣言し、値の型は必ず注釈しなければなりません。
型と型注釈については次のセクション、「データ型」で講義しますので、その詳細について気にする必要はありません。
ただ単に型は常に注釈しなければならないのだと思っていてください。
定数はどんなスコープでも定義できます。グローバルスコープも含めてです。なので、 いろんなところで使用される可能性のある値を定義するのに役に立ちます。
最後の違いは、定数は定数式にしかセットできないことです。実行時に評価される値にはセットできません。
これが定数定義の例です:
#![allow(unused)] fn main() { const THREE_HOURS_IN_SECONDS: u32 = 60 * 60 * 3; }
定数の名前はTHREE_HOURS_IN_SECONDSで、その値は60(1分あたりの秒数)×60(1時間あたりの分数)×3(このプログラムで数えたい時間数)の結果にセットされています。
Rustの定数の命名規則は、全て大文字でアンダースコアで単語区切りすることです。
コンパイラはコンパイル時に一部の演算を評価することができるので、この定数に10,800という値を設定する代わりに、理解し検証しやすい方法でこの値を書き出すことを選択できます。
定数宣言内でどの演算が使用できるかについてのさらなる情報は、Rust Referenceのconstant evaluationの節をお読みください。
定数は、プログラムが走る期間、定義されたスコープ内でずっと有効です。 この性質のおかげで、定数はプログラムのいろんなところで使用される可能性のあるアプリケーション空間の値を定義するのに有用です。 例えば、ゲームでプレイヤーが取得可能なポイントの最高値や、光速度などですね。
プログラム中で使用されるハードコードされた値に対して、定数として名前付けすることは、 コードの将来的な管理者にとって値の意味を汲むのに役に立ちます。将来、ハードコードされた値を変える必要が出た時に、 たった1箇所を変更するだけで済むようにもしてくれます。
シャドーイング
第2章の数当てゲームのチュートリアルで見たように、
前に定義した変数と同じ名前の変数を新しく宣言できます。
Rustaceanはこれを、最初の変数は2番目の変数に覆い隠されたと言います。
これはその変数名を使用した際に、コンパイラは2番目の変数を見るという意味です。
2番目の変数は実質的に、最初の変数にその影を投げかけ、自身が覆い隠されるかスコープが終了するまで、
変数名の使用を自身へのものとして扱います。
以下のようにして、同じ変数名を用いて変数を覆い隠し、letキーワードの使用を繰り返します:
ファイル名: src/main.rs
fn main() { let x = 5; let x = x + 1; { let x = x * 2; println!("The value of x in the inner scope is: {x}"); } println!("The value of x is: {x}"); }
このプログラムはまず、xを5という値に束縛します。それからlet x =を繰り返すことで新しい変数xを作り、
元の値に1を加えることになるので、xの値は6になります。
次に波括弧によって作られた内側のスコープの中で、3番目のlet文もxを覆い隠して新しい変数を作り、
以前の値に2をかけることになるので、xの最終的な値は12になります。
スコープが終わるとシャドーイングは終了し、xの値は元の6に戻ります。
このプログラムを走らせたら、以下のように出力するでしょう:
$ cargo run
Compiling variables v0.1.0 (file:///projects/variables)
Finished dev [unoptimized + debuginfo] target(s) in 0.31s
Running `target/debug/variables`
The value of x in the inner scope is: 12
The value of x is: 6
シャドーイングは、変数をmutにするのとは違います。なぜなら、letキーワードを使わずに、
誤ってこの変数に再代入を試みようものなら、コンパイルエラーが出るからです。letを使うことで、
値にちょっとした加工は行えますが、その加工が終わったら、変数は不変になるわけです。
mutと上書きのもう一つの違いは、再度letキーワードを使用したら、実効的には新しい変数を生成していることになるので、
値の型を変えつつ、同じ変数名を使いまわせることです。例えば、
プログラムがユーザに何らかのテキストに対して空白文字を入力することで何個分のスペースを表示したいかを尋ねて、
そうしたらこの入力を数値として保持したいとしましょう:
fn main() { let spaces = " "; let spaces = spaces.len(); }
最初のspaces変数は文字列型であり、2番目のspaces変数は数値型です。故に、シャドーイングのおかげで、
異なる名前を思いつく必要がなくなるわけです。spaces_strとspaces_numなどですね; 代わりに、
よりシンプルなspacesという名前を再利用できるわけです。一方で、この場合にmutを使おうとすると、
以下に示した通りですが、コンパイルエラーになるわけです:
fn main() {
let mut spaces = " ";
spaces = spaces.len();
}
変数の型を可変にすることは許されていないと言われているわけです:
$ cargo run
Compiling variables v0.1.0 (file:///projects/variables)
error[E0308]: mismatched types (型が合いません)
--> src/main.rs:3:14
|
2 | let mut spaces = " ";
| ----- expected due to this value
3 | spaces = spaces.len();
| ^^^^^^^^^^^^ expected `&str`, found `usize`
| (&str型を予期しましたが、usizeが見つかりました)
|
help: try removing the method call
|
3 - spaces = spaces.len();
3 + spaces = spaces;
|
For more information about this error, try `rustc --explain E0308`.
error: could not compile `variables` (bin "variables") due to 1 previous error
さあ、変数が動作する方法を見てきたので、今度は変数が取りうるデータ型について見ていきましょう。