本文へスキップ
BecomeCoder

Rustコース · 第4章 所有権 · レッスン16

ムーブ(move)― 所有権が移る

ブラウザで完結

導入

第3章のレッスン13で、let b = a;a が数値)は値がコピーされ、ab も使えました。ところが aString だと、まったく違うことが起きます。ここが 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 にコピーします。ヒープの実データは複製しません(重いから)。

すると、s1s2両方が同じヒープデータを指す状態になります。

graph LR
    subgraph stack["スタック"]
        s1["s1: ptr ●(無効化される)"]
        s2["s2: ptr ●"]
    end
    subgraph heap["ヒープ"]
        d["h | e | l | l | o"]
    end
    s1 -.->|かつて指していた| d
    s2 -->|指す| d

もしこのまま両方を有効にしておくと、スコープ終わりに s1s2両方が同じヒープを解放しようとします——これが「二重解放」バグです。所有権ルール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() を使います。

実際に動かしてみよう

下のエディタにRustを書いて「実行」を押すと、学習用シミュレータが println! の出力を表示します(本物のrustcではなく、教材の範囲を再現した軽量エンジンです)。struct・enum・match・トレイト・Vec/String/HashMap・Option/Result なども動きます。本文の例を書き換えて動かしてみましょう(所有権・借用チェッカーのコンパイルエラーは再現しないため、動きの確認用として使ってください)。

Rust — ブラウザ内で実行(学習用シミュレータ)

Rustの教材サブセットを動かす学習用シミュレータを読み込みます(本物のrustcではなく、動きを再現した軽量な自作エンジンです)。
スクロールして表示された時点でも自動で読み込まれます。