変数と可変性
第2章で触れた通り、変数は標準で不変になります。これは、 Rustが提供する安全性や簡便な並行性の利点を享受する形でコードを書くための選択の1つです。 ところが、まだ変数を可変にするという選択肢も残されています。 どのように、そしてなぜRustは不変性を推奨するのか、さらには、なぜそれとは違う道を選びたくなることがあるのか見ていきましょう。
変数が不変であると、値が一旦名前に束縛されたら、その値を変えることができません。
これを具体的に説明するために、projectsディレクトリにcargo new --bin variables
コマンドを使って、
variablesという名前のプロジェクトを生成しましょう。
それから、新規作成したvariablesディレクトリで、src/main.rsファイルを開き、 その中身を以下のコードに置き換えましょう。このコードはまだコンパイルできません:
ファイル名: src/main.rs
fn main() {
let x = 5;
println!("The value of x is: {}", 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` due to 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
キーワードで宣言し、値の型は必ず注釈しなければなりません。
型と型注釈については次のセクション、「データ型」で講義しますので、その詳細について気にする必要はありません。
ただ単に型は常に注釈しなければならないのだと思っていてください。
定数はどんなスコープでも定義できます。グローバルスコープも含めてです。なので、 いろんなところで使用される可能性のある値を定義するのに役に立ちます。
最後の違いは、定数は定数式にしかセットできないことです。関数呼び出し結果や、実行時に評価される値にはセットできません。
定数の名前がMAX_POINTS
で、値が100,000にセットされた定数定義の例をご覧ください。(Rustの定数の命名規則は、
全て大文字でアンダースコアで単語区切りすることです):
#![allow(unused)] fn main() { const MAX_POINTS: u32 = 100_000; }
定数は、プログラムが走る期間、定義されたスコープ内でずっと有効です。従って、 プログラムのいろんなところで使用される可能性のあるアプリケーション空間の値を定義するのに有益な選択肢になります。 例えば、ゲームでプレイヤーが取得可能なポイントの最高値や、光速度などですね。
プログラム中で使用されるハードコードされた値に対して、定数として名前付けすることは、 コードの将来的な管理者にとって値の意味を汲むのに役に立ちます。将来、ハードコードされた値を変える必要が出た時に、 たった1箇所を変更するだけで済むようにもしてくれます。
シャドーイング
第2章の数当てゲームのチュートリアル、「予想と秘密の数字を比較する」節で見たように、前に定義した変数と同じ名前の変数を新しく宣言でき、
新しい変数は、前の変数を覆い隠します。Rustaceanはこれを最初の変数は、
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が見つかりました)
For more information about this error, try `rustc --explain E0308`.
error: could not compile `variables` due to previous error
さあ、変数が動作する方法を見てきたので、今度は変数が取りうるデータ型について見ていきましょう。