所有権、スコープ、ムーブ

RustBeginner
オンラインで実践に進む

はじめに

Rust は所有権を使って、プログラムのどの部分が値に対する責任を持つのか、またその値をいつ解放できるのかを決定します。所有権は、特定の変数名に永久に付くラベルではなく、責任と使用許可だと考えてください。この責任は、新しい束縛へ移すことができます。これにより、ガベージコレクターを必要とせずに、いくつかのメモリエラーを防げます。

この実験では、まず小さな整数がコピーされることを確認します。次に、所有権がムーブされた後の String を意図的に使用し、コンパイラーの診断メッセージを読み取ります。その後、関数から返された所有権を使ってデータの流れを修正します。コードの枠組みはあらかじめ用意されているため、変更は小さく、目的に集中できます。

スコープとコピーされた整数を確認する

このステップでは、小さな整数を内側のスコープへコピーし、元の束縛も引き続き使用できることを確認します。

用意されているプロジェクトへ移動し、ソースファイルを開きます。

cd /home/labex/project/ownership-demo
nano src/main.rs

スコープとは、束縛が有効な範囲です。波括弧で新しい内側のスコープを作成できます。束縛 local_count は内側の波括弧の間でのみ存在し、warehouse_count は外側の main スコープに属します。

Step 1 の行で、0 だけを warehouse_count に置き換えます。

        let local_count = warehouse_count; // Step 1: replace the placeholder.

整数型 i32 は Rust の Copy 動作を実装しています。local_count に代入すると、小さな値がコピーされるため、warehouse_count は無効になりません。

Ctrl+O で保存し、Enter を押してから、Ctrl+X で終了します。プログラムを実行します。

cargo run --quiet

最初の 2 行によって、両方の束縛を使用できることが確認できます。

Local count: 3
Warehouse count: 3

内側のスコープが終了すると、local_count は無効になります。一方、外側の warehouse_count は main の終わりまで有効です。

意図的なムーブエラーを読む

このステップでは、ムーブ後の String を意図的に使用し、Rust のコンパイラー診断を読み取ります。チェックが失敗するのは想定どおりであり、このステップでの学習結果です。

String::from("PX-104") は、所有されるテキストを作成します。小さな i32 とは異なり、String はサイズを変更できる、別途確保されたテキストバッファーを管理します。Rust は、このバッファーを 1 つの所有者に任せることで、正確に 1 回だけ解放できるようにします。

record_parcel(parcel_id) の呼び出しでは、String が値として渡されます。所有権は parcel_id 束縛から関数内の引数へムーブします。関数は値を返し、呼び出し側では返された所有者を _returned_id に格納します。先頭のアンダースコアは、この一時的な束縛を現時点では意図的に使用しないことを Rust に伝えます。

値はムーブされたときに破棄されるわけではありません。元の束縛がその値を使用する権限を失うだけです。このプログラムにおける所有権の経路は次のとおりです。

parcel_id in main → parcel_id parameter in record_parcel → _returned_id in main

どの時点でも、同じテキストに対する責任を持つ現在の所有者は 1 つだけです。

ファイルを開きます。

nano src/main.rs

Step 2 のコメントの下にある行を探し、先頭の // だけを削除して、次の状態にします。

    println!("Original after move: {parcel_id}");

保存して終了します。次のコマンドを実行する前に、結果を予測してください。parcel_id はすでに所有者ではないため、コンパイラーはこの使用を拒否するはずです。

チェックを実行します。失敗するのが想定された結果です。

cargo check

診断メッセージの次の安定した部分を確認します。

error[E0382]: borrow of moved value: `parcel_id`
...
value moved here
...
value borrowed here after move

println! は値を表示するための一時的なアクセスだけを必要とするため、borrow という語が表示されます。しかし、所有権はすでにムーブされているため、parcel_id を通じてそのアクセスを行うことはできません。次のステップでは、関数から所有権を受け取った束縛を使用します。

返された所有者を使って続行する

このステップでは、record_parcel から返された値に名前を付けて使用し、ムーブエラーを修正します。

関数シグネチャ fn record_parcel(parcel_id: String) -> String は、この関数が String の所有権を受け取り、String を返すことを示しています。最後にある parcel_id 式によって、所有権が呼び出し元へ戻されます。

ファイルを開きます。

nano src/main.rs

2 か所を変更します。まず、関数呼び出しの行にある _returned_id を returned_id に変更します。これから値を使用するため、一時的なアンダースコア付きの束縛を通常の名前に変更します。

次に、エラーになる表示行を次の行に置き換えます。

    println!("Returned owner: {returned_id}");

元の parcel_id 束縛は、ムーブ後も無効なままです。_returned_id を returned_id に変更しても、元の束縛が復活するわけではありません。すでに所有権を受け取っている束縛に、通常の名前を付けるだけです。現在の所有者は returned_id なので、使用すべき正しい名前も returned_id です。

保存して終了し、修正したプログラムをチェックします。

cargo check

E0382 エラーがなく Finished と表示されれば、所有権の流れが有効であることを確認できます。プログラムを実行します。

cargo run --quiet

完全な出力は次のようになります。

Local count: 3
Warehouse count: 3
Recorded parcel: PX-104
Returned owner: PX-104

main の終わりで returned_id がスコープを抜けると、Rust は所有されている String を解放します。このデータに対する責任を持つ所有者は、引き続き 1 つだけです。

まとめ

字句スコープと値のライフタイムの関係を確認し、元の束縛を無効にせずに i32 がコピーされることを学びました。また、String が関数へムーブされる流れを追い、E0382 を読み取り、呼び出し元へ返された所有権を安全に使って処理を続けました。