数日前に久しぶりに記事を書いたけど,あれを書くだけで午前中全部使った気がする.毎日ブログを書いたりQiitaに沢山投稿する人はすごい(小並感).
その数日前の記事でC++でOSを作成中という事を書いたが,なんかの拍子でRustに目を向けた.Rustはmut
をつけなければ基本的に変数は不変らしいが,他にも様々良さそうな機能があり,急にRustでOSを書き換えたくなった.My C++ OSなんて名前つけるべきじゃなかった.
で,早速Rustの日本語版チュートリアルを進めることにしたが,どうもRustには不変変数以外に定数も存在するらしい.そもそも不変変数ってなんだい.矛盾じゃねえか.
そんなわけで早速混乱することになったので,調べた結果をまとめてみる.
const
- 正式名称はconstant item.ただしconstant valueとも称されるらしい.
- メモリの特定のアドレスに結び付けられるわけではない.
- 実質的にはその定数が使用される場所にインライン化される. つまりCやC++の
#define
文で定義したマクロみたいなもの.CやC++はプリプロセッサがマクロを値で置換するが. - 型注釈が必須.
- 生存期間の指定がなければ
static
生存期間を持つ.つまり,プログラムが動いている間はずっと値が保持される.
mutなし変数
let
を使えば実質的に値を変えられる.もう訳わからん.これをシャドーイングというらしい.
日本語版Rustチュートリアルのシャドーイングの項よりコードを引用.
fn main() { let x = 5; let x = x + 1; let x = x * 2; println!("The value of x is: {}", x); }
このコードは問題なく実行され,最後にxの値が12であると出力される.
更に言うと,違う型の値を束縛することも可能らしい.次のコードは動作する.
fn main() { let x = 5; println!("The value of x is: {}", x); let x = "This is a pen."; println!("The value of x is: {}", x); }
これもう普通の変数じゃん.
ちなみに,mut
つき変数は,let
を使用しなければ違う型の値には変更できない.よって次のコードはコンパイルできない.
fn main() { let mut x = 5; println!("The value of x is: {}", x); x = "This is a pen."; // error: mismatched types println!("The value of x is: {}", x); }
それでもlet
を使うと違う型の値を束縛できる.
fn main() { let mut x = 5; // warning: variable does not need to be mutable println!("The value of x is: {}", x); let x = "This is a pen."; println!("The value of x is: {}", x); }
ただしこの場合,コード内のコメントのように,変数は可変である必要はないぞという警告がなされる.
ところで,文中に束縛と変更という2つの言葉が出てきたが,これらは異なる意味を持つ.
まずRustにおいて,変数には値を代入するものではなく,値を束縛するものとされている1.さっき登場したシャドーイングというものは言葉の意味の通り,それまで存在した変数を覆い隠す,つまりそれまで存在した変数と,新たにlet
を用いて定義した変数は別物ということになるが,これはつまり,2回目以降同じ変数に対してlet
を用いて値を束縛した場合,1回目に値が束縛された変数や束縛した値を弄るのではなく,別に新たに同名の変数を生成することになる.そしてそれ以降その名前の変数を指すと,最後にlet
で値が束縛された変数を指すことになる.非常ににややこしい.
もう一度さっきのコードを見てみる.
fn main() { let x = 5; let x = x + 1; let x = x * 2; println!("The value of x is: {}", x); }
最初,let x = 5;
でx
という変数に5
が束縛される.次のlet x = x + 1;
で新たに生成されたx
という変数にx + 1
の値が束縛され(つまり6
),スタック領域に積まれる.この時最初に登場したx
に束縛した5
という値はスタック領域上に残ることになる2.そして次のlet x = x * 2;
で,また新たにx
を生成して,x * 2
つまり12
を生成したx
に束縛し,スタック領域に積む.そしてそれまでx
に束縛されていた6
という値はスタック領域に残ることになる.スタック領域に残った,覆い隠された値を含むすべての変数は,スコープ外に動作が移った瞬間に解放される.つまり,関数内で束縛された値などは処理が関数外に移ると解放される.これはCやC++と同じ.
mut
付き変数では値は変更される.この変更というのは,let
を使ったときのように新たに変数を生成するのではなく,今まで束縛されていた値自体を変更する.例えば次のコード.
fn main() { let x = 5; x = 6; }
この時,x = 6
が実行されても,新しいx
に6
が束縛されるのではなく,既存の変数(つまり最初のlet x = 5;
のx
)の値自体が(この場合は6
に)変化する.
mut
をつけて,変数が束縛している値を変更するのには,例えば大きなデータ構造を扱う際に利点があるらしい.以下,日本語版Rustチュートリアルの変数と可変性より引用.
考えるべきトレードオフはバグの予防以外にも、いくつかあります。例えば、大きなデータ構造を使う場合などです。 インスタンスを可変にして変更できるようにする方が、いちいちインスタンスをコピーして新しくメモリ割り当てされたインスタンスを返すよりも速くなります。 小規模なデータ構造なら、新規インスタンスを生成して、もっと関数型っぽいコードを書く方が通して考えやすくなるため、 低パフォーマンスは、その簡潔性を得るのに足りうるペナルティになるかもしれません。
さっき,mut
付き変数は,let
なしの値の変更は型を超えた変更はできないといったことを書いたが,これはC++のauto
で宣言された変数の型を後に変更できないのと似ている.次のようなC++のコードはコンパイルできない.auto
で宣言された変数に,型の異なる値を代入しようとしたからだ.
int main() { auto x = 3; x = "This is a pen."; // invalid conversion from 'const char*' to 'int' }
Rustでの変数の値の変え方を整理すると,
let
を使用して新たに同名の変数を定義する.(mut
付きでもなしでも可能)ただし,値そのものは変わらない.同名の変数を定義することで既存の変数を隠す(シャドーイング)let
を使用せずvar = foo;
のように値を変更する.(mut
付きでのみ可能)こちらは値そのものを変更する.ただし型を超えた変更はできない.
ここまで書いて気づいたが,結局let
を使えば値が実質的に変わってしまうのであれば,Rustが持つ変数の不変性は果たして本当に不変性なのか.例えばCやC++のconst
で宣言された定数は,宣言後もずっと値が変わることがないためデバッグ中に変数の値の変化に注意する必要がなくなるため安心できるが,Rustではmut
をつけまいが,結局let
でシャドーイングしてしまえば値は実質的に変わってしまう.この言語設計はプログラムを書く人にとってメリットがあるのだろうか,謎は深まるばかりである.
参考
変数と可変性 - The Rust Programming Language
Constant items - The Rust Reference
Does Rust free up the memory of overwritten variables? - Stack Overflow
-
日本語チュートリアルの変数を値に保持するの項では,一貫して束縛という言葉が使用されており,代入という言葉は使用されていない.↩
-
Newbie question: memory leaks by shadowing? - The Rust Programming Language Forum↩