備忘録やめた

備忘録として使用していたけどやめた.このブログに載せてあるコードのライセンスは別途記載がない限りWTFPL OR NYSLです.

Rustの変数と定数

数日前に久しぶりに記事を書いたけど,あれを書くだけで午前中全部使った気がする.毎日ブログを書いたり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が実行されても,新しいx6が束縛されるのではなく,既存の変数(つまり最初の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での変数の値の変え方を整理すると,

  1. letを使用して新たに同名の変数を定義する.(mut付きでもなしでも可能)ただし,値そのものは変わらない.同名の変数を定義することで既存の変数を隠す(シャドーイング

  2. letを使用せずvar = foo;のように値を変更する.(mut付きでのみ可能)こちらは値そのものを変更する.ただし型を超えた変更はできない.

ここまで書いて気づいたが,結局letを使えば値が実質的に変わってしまうのであれば,Rustが持つ変数の不変性は果たして本当に不変性なのか.例えばCやC++constで宣言された定数は,宣言後もずっと値が変わることがないためデバッグ中に変数の値の変化に注意する必要がなくなるため安心できるが,Rustではmutをつけまいが,結局letシャドーイングしてしまえば値は実質的に変わってしまう.この言語設計はプログラムを書く人にとってメリットがあるのだろうか,謎は深まるばかりである.

参考

変数と可変性 - The Rust Programming Language

Constant items - The Rust Reference

Static - Rust By Example

Does Rust free up the memory of overwritten variables? - Stack Overflow


  1. 日本語チュートリアル変数を値に保持するの項では,一貫して束縛という言葉が使用されており,代入という言葉は使用されていない.

  2. Newbie question: memory leaks by shadowing? - The Rust Programming Language Forum