ムーブセマンティクスと所有権ってなんやっけ
誤解を恐れずに言うなら、
(ほぼ)すべてのプログラムは、プロセスとしてコンピュータで実行される。
プロセス上で、プログラムは固有のメモリ領域を割り当てられ、そこにプログラムのほとんどすべての状態を記録する。
- プログラム本体
- 不変データ(定数)
- CPUレジスタ状態
- 実行中データ(関数, 変数)
- ヒープメモリ(変数)
今回の題材で関わって来るのは、この内の実行中データとヒープメモリ。
実行中データはスタックと呼ばれるメモリ形態で、ヒープと異なる。
主な特徴としては、スタックは高速で、ヒープは大容量といったところ。
つまり、大きいデータはヒープに、小さいデータはスタックに保存することが効果的だとわかる。
プログラム的に言えば、ベクトルなどの長さが不明なコレクションはヒープに、数値や定型文はスタックに置く。
で、所有権の話。
変数は名前と実体に分けられる。
どの名前がどの実体を持っているか、ということが所有ということ。
ここで、2つの異なる名前が同じ実体を持とうとしようとしたときを考えてみる。
例えば、以下のようなコード。
fn main() {
let mut a = 1;
let mut b = a;
}
a
は1
という実体も持つ名前であり、b
はa
、つまりその先の実体1
を持とうとしている。
2つが完全に同じ実体を持つ場合、この後にa = 2
というコードを書くことで、a
が持つ実体を2
に書き換えることで、b
も同様に2
という実体を持つことになる。
プログラム的には確かに正しいけれども、人間的には直感に反することになる。(a
は2
、b
は1
を持ってほしい)
そのため、1つの名前が1つの実体だけが持つ方が不具合が起きにくい。
Rustではこのようなルールを所有権と呼ぶわけである。
所有権を実現するために、先程のケースにどう対応するか?
解決方法は主に2つ考えられる。
- 実体と同じ意味のものをコピーする
- 実体を譲り渡す
1は、直感でわかりやすいものの、大きいデータだとそれだけメモリやコピーコストがかかるという問題がある。
実行速度やコストを重視する場合には、2の戦略の方が有利に働く。
そのため、Rustでは基本的戦略として2の戦略を取る。
これがいわゆる、ムーブセマンティクスと呼ばれるもの。
fn main() {
let mut a = 0;
let mut b = a; // この時点で a の実体は b に移る = a は使用できなくなる
}
一方、全てにこの戦略を適応するとほとんどのプログラムで冗長な構文を書かなければ行けなくなり、面倒さが勝る。
ここでメモリの話に戻ってみる。
スタックメモリは高速で、ヒープメモリは大容量という話をした。
1の戦略で問題になるのは、大きなメモリを使う実体では大きなコストがかかるということ。
つまり、通常スタックに入り切らないような大きいメモリを消費するヒープの方にこれを適応する方が賢い戦略じゃなかろうか。
そのため、Rustではスタックメモリに記録される実体では1の戦略を、ヒープメモリに記録される実体では2の戦略を適応する。
具体的には、
i32
, f64
, usize
, &str
などのスタックメモリに記録されるデータ型はコピー
str
, vec
, box
などのヒープメモリに記録されるデータ型はムーブセマンティックス
という戦略を分けて実装されている。
まとめ
所有権とは
変数である「名前」が「データ実体」を持つことを所有といい、1つのデータ実体は1つの名前だけが持つというルールのこと。
ムーブセマンティクスとは
変数である「名前」が「データ実体」を別の「名前」に譲り渡すこと。