導入
第3章のレッスン13で、let b = a;(a が数値)は値がコピーされ、a も b も使えました。ところが a が String だと、まったく違うことが起きます。ここが Rust 初学者が最初に「?」となる、そして超えると世界が変わるポイントです。
説明
String を別の変数に代入してみます。
fn main() {
let s1 = String::from("hello");
let s2 = s1; // 所有権が s1 から s2 へ「ムーブ」する
println!("{s2}"); // OK
// println!("{s1}"); // ← コンパイルエラー! s1 はもう使えない
}
なぜ s1 が使えなくなるのでしょうか。第3章で見たとおり、String は「スタックにポインタ+長さ+容量、ヒープに実データ」という構造でした。let s2 = s1; のとき Rust は、スタック上の管理情報(ポインタなど)だけを s2 にコピーします。ヒープの実データは複製しません(重いから)。
すると、s1 と s2 の両方が同じヒープデータを指す状態になります。
graph LR
subgraph stack["スタック"]
s1["s1: ptr ●(無効化される)"]
s2["s2: ptr ●"]
end
subgraph heap["ヒープ"]
d["h | e | l | l | o"]
end
s1 -.->|かつて指していた| d
s2 -->|指す| d
もしこのまま両方を有効にしておくと、スコープ終わりに s1 と s2 の両方が同じヒープを解放しようとします——これが「二重解放」バグです。所有権ルール2(所有者は1人だけ)に真っ向から反します。
そこで Rust は、let s2 = s1; の時点で s1 を無効化します。これを ムーブ(move) と呼びます。「コピー」ではなく「所有権が引っ越した」のです。以後 s1 を使おうとすると、コンパイラが borrow of moved value: s1(ムーブ済みの値を使おうとしている)とエラーで止めてくれます。
- ヒープを持つ型(
String,Vecなど)の代入・関数渡しは、既定でムーブ。元の変数は無効になる。 - スタック完結型(
i32などCopy型)は、前章のとおりコピー。元の変数もそのまま使える。
「代入したら元が使えなくなる」は最初こそ驚きますが、これこそが二重解放をコンパイル時に根絶している仕組みです。実行してからクラッシュするのではなく、書いた瞬間に気づけます。
試すには
let s2 = s1; のあとで println!("{s1}") を書くと、コンパイラが「value borrowed here after move」と、ムーブが起きた行まで示して教えてくれます。もし本当に両方使いたいなら、次のレッスンの clone() を使います。