変数とはなにか?
変数とは何かという問いは、実はそれほど自明ではない。
より踏み込むなら、変数が何を指すのかは言語の設計思想によって大きく異なる。
多くの場合、変数は「値を入れる箱」と説明される。しかし実際には、変数の意味は宣言によって決まるというよりも、それに対する代入や操作によってどのような変化が起こるかによって理解されることが多い。
以下の例を見て、結果を予想してみてほしい。
JavaScript
let a = "hello";
let b = a;
b = "goodbye";
// このとき a は?
let c = [];
let d = c;
d.push("hello");
// このとき c は?
Python
a = "hello"
b = a
b = "goodbye"
# このとき a は?
a = []
b = a
b.append("hello")
# このとき a は?
Go
a := 1
b := a
b++
// このとき a は?
c := []int{1}
d := c
d = append(d, 2)
// このとき c は?
C
int a = 1;
int b = a;
b++;
// このとき a は?
int* c = malloc(sizeof(int) * 4);
int* d = c;
d[1] = 2;
// このとき c は?
C++
int a = 1;
int b = a;
b++;
int& c = a;
c++;
// このとき a は?
これらの結果をすべて説明するのは、意外と難しいのではないだろうか?
C, C++, Go では基本的に値のコピーが行われるため比較的理解しやすいが、Goのスライスは内部で配列を共有するため、値を渡したつもりが実際には同じデータを共有している、という状況が起こる。
JavaScript や Python では、プリミティブ値はコピーのように振る舞い、オブジェクトは共有されるように見えるため、挙動の違いが直感に反することがある。
ただし厳密には、Pythonではすべてがオブジェクトであり、整数などがコピーのように見えるのは immutable だからである。
このように、変数の意味は単なる「名前」以上のものであり、どのようにデータと結びつくかという点に大きく依存している。
GCがある場合の変数
特に重要なのは、変数が指すデータの寿命がどのように管理されるかである。
GCを備えた言語では、データの削除は runtime に任せられる。
そのため、
変数が参照を保持している限り、それが指すデータも存在する
ことが基本的に保証される。
この性質により、変数はデータに貼られたラベルのように振る舞うと言えるだろう。
a ─┐
├→ object
b ─┘
言い換えれば、
変数とデータの結びつきが強い
手動メモリ管理の場合の変数
一方、手動でメモリ管理を行う言語では、変数が存在していても、その指す先のデータが有効であるとは限らない。
例えばCでは:
int* p = malloc(sizeof(int));
free(p);
この時点で
p は存在するが、指しているデータは無効
となる。
メモリは明示的に解放する必要があり、変数の寿命とデータの寿命は一致しない。
そのため、変数は単なるラベルというよりも、メモリアドレスを直接扱うものとして意識されやすい。
pointer → memory address
この場合、
変数とデータの結びつきは弱い
変数の振る舞いのパターン
整理すると、変数の振る舞いにはいくつかの典型的なパターンがあると言える。
データとの結びつきが強い変数(ラベル型)
変数の存在がデータの存在を保証する。
例:
- Python
- JavaScript
- Java
- C#
特徴:
- GCが寿命を管理
- 参照している限りデータは消えない
データとの結びつきが弱い変数(アドレス型)
変数が存在してもデータの有効性は保証されない。
例:
- C
- 一部のC++
特徴:
- 手動でメモリを解放する必要がある
- ダングリングポインタが起こりうる
- メモリリークが発生しうる
GCがある言語では、オブジェクトの寿命は参照の有無によって決まる。そのため、どこかに参照が残っている限りデータは解放されない。これは安全性を高める一方で、「もう使っていないつもりのデータ」が意図せず保持され続ける原因にもなる。例えば、コレクションやクロージャがオブジェクトへの参照を保持し続けることで、不要になったデータが解放されずメモリリークが発生する。
また、オブジェクトが参照として共有される場合、複数の変数が同じデータを指すことになる。その結果、一方で行った変更が他方にも影響する。これは意図的な共有であれば便利だが、コピーされたと思っていた値が実際には同じオブジェクトを指していた場合、予期しない共有が起こる。特にミュータブルなデータ構造では、変更がどこまで影響するかを把握するのが難しくなる。
一方、手動メモリ管理の言語では、データの寿命はプログラマが明示的に管理する必要がある。変数が存在していても、その指す先のメモリがすでに解放されている可能性があるため、ダングリングポインタが発生する。また、解放を忘れればメモリリークが起こる。さらに、ポインタを通じて同じメモリ領域を共有している場合、どこで変更が行われたかを追跡するのが難しくなり、変更の伝播がバグの原因になる。
このように変数の在り方が、意図しないバグの温床になりえる。
好ましい変数とはなにか
ここまで見てきたように、変数とデータの結びつき方にはいくつかのパターンが存在するが、ではどのような性質を持つ変数が望ましいのだろうか。
直感的には、次の2つを同時に満たす性質が望ましいと考えられる。
- 変数が存在する限りデータの有効性が保証されること
- 意図しない共有が起こらないこと
前者は安全性に関係する。変数が参照しているデータがいつのまにか解放されてしまうと、ダングリングポインタや use-after-free といった問題が発生する。
後者は可読性と保守性に関係する。値をコピーしたつもりが実際には同じオブジェクトを共有していた場合、どこで変更が行われたのかを追跡するのが難しくなる。
例えば JavaScript では次のようなコードが成立する。
const a = { x: 1 };
const b = a;
b.x = 10;
console.log(a.x); // 10
一見コピーのように見えるが、実際には同じオブジェクトを共有している。
このような「意図しないシャローコピー」は、バグの原因になることがある。
特にミュータブルなデータ構造では
- どこで変更されたのか
- 変更がどこに影響するのか
を追跡するのが難しくなる。
つまり望ましいのは
変数がラベルとして振る舞いつつ、値としての独立性も保てること
である。
多くのGC言語では参照が基本となるため共有は容易に行えるが、意図しない共有が発生することがある。
一方Cのようにすべてを値として扱う場合は
- コピーコストが大きくなる
- メモリ管理を手動で行う必要がある
という問題がある。
つまり単純に
- すべて参照
- すべてコピー
のどちらかだけでは扱いづらい。
必要なのは
データの寿命は保証しつつ、共有は明示的に行える
という性質である。
Rustの登場
Rustはこの問題に対して独特なアプローチをとった。
Rustでは、変数は単なる名前ではなく、データの所有者として扱われる。
let s = String::from("hello");
このとき
s owns "hello"
という関係が成立する。
所有者である変数が存在する限り、対応するデータの有効性が保証される。
そして所有者がスコープを抜けるとき、データは自動的に解放される。
{
let s = String::from("hello");
} // ここで drop が呼ばれる
move(所有権の移動)
let a = String::from("hello");
let b = a;
これはコピーではない。
所有権が移動する。
a → b
そのため
println!("{}", a); // エラー
borrow(参照)
fn length(s: &String) -> usize {
s.len()
}
let s = String::from("hello");
let len = length(&s);
所有権は移動しない。
s owns data
length temporarily borrows data
Rustが実現したこと
Rustは
- GCを使わず
- メモリ安全を保証し
- 変数をラベルのように扱える
という性質を実現している。
所有権はしばしば「メモリ安全のための仕組み」として説明されるが、別の見方をすれば、
変数とデータの結びつきを強くした仕組み
とも言える。
さらにRustでは
- move によってデータの独立性を保ち
- borrow によって必要な場合のみ共有する
という形で
ラベル的な扱いやすさと値としての安全性
の両立を実現している。
この考え方が他の言語にほとんど見られないため、多くの人が最初に混乱するが、非常に合理的な設計なのである。
まとめ
多くの言語では、変数は単なる名前や参照として扱われる。
しかしRustでは、
変数はリソースの責任主体
として扱われる。
この設計によってRustは
- 実行速度
- 安全性
- 抽象化のしやすさ
を同時に実現している。
Rustを理解する鍵は、
メモリ管理
ではなく
変数の意味
にあるのかもしれない。