Rust入門(5)「所有権のルールとムーブ」

スポンサーリンク
Rust

前回までの関数・制御構文で、Rustの基本的な書き方は一通り学んだ。
今回からは、Rust最大の特徴である所有権について、3回に分けて学んでいく。

  • 第5回(今回):所有権のルールとムーブ
  • 第6回:クローンと借用(参照)
  • 第7回:スライス

学習の進め方

Rust入門シリーズは、2段階で進める構成になっている。
まだ、GitHub Codespacesで環境構築をしていない方は、第1回「GitHub Codespaces で Rust を始めよう!」を先にお読みください。

1. 体験する(trials/)

  • ブログ記事を読みながら、trials/05_ownership/で自由にコードを試す
  • 自分で書き換えたり、わざとエラーを起こしたりして実験する
  • エラーを恐れず、どんどん試してみよう

2. 練習する(exercises/)

  • 体験が終わったら、exercises/05_ownership/で構造化された練習問題に挑戦する
  • todo!()の部分を自分のコードに書き換えて完成させる
  • 解答例も用意されている

準備

# リポジトリを最新の状態にする
cd /workspaces/rust-practice
git pull
# 体験用プロジェクトに移動
cd trials/05_ownership
# VSCodeでファイルを開く
code src/main.rs
# 動作確認
cargo run

所有権とは何か

Rustには、ガベージコレクション(GCによる自動メモリ管理)がない。その代わりに、所有権というルールをコンパイラがチェックすることで、メモリを安全に管理している。
ルールは3つである。

  1. それぞれの値は、所有者(owner)と呼ばれる変数を1つだけ持つ
  2. 所有者がスコープを抜けると、値は破棄される
  3. 値は、一度に1つの変数しか所有できない(代入すると、所有権は移動する)

ムーブ:所有権の移動

i32のような単純な値と、Stringのような値とで、代入の挙動が違う。

fn main() {
    let x = 5;
    let y = x; // xの値がコピーされる
    println!("x = {}, y = {}", x, y); // 両方使える
}
x = 5, y = 5

一方、Stringでは事情が異なる。下記のコードを実行するとエラーになる。

fn main() {
    let s1 = String::from("こんにちは");
    let s2 = s1; // 所有権がs1からs2へ移動する

    println!("{}", s2); // OK
    println!("{}", s1); // ← エラー!
}
error[E0382]: borrow of moved value: `s1`
(略)

s2 = s1の時点で、s1が持っていた所有権はs2に移動(ムーブ)する。
前述のルール3「値は一度に1つの変数しか所有できない」により、元のs1はもう使えなくなる。

なぜ挙動が違うのか

i32は固定サイズで、値そのものがスタックに積まれるため、コピーしても軽い。これらの型にはCopyトレイトが付いており、代入のたびに自動でコピーされる。

Stringは、文字データの実体をヒープ(可変長のメモリ領域)に置き、変数自身はそこへのポインタを持つ構造である。もし代入のたびに中身までコピーしていたら、長い文字列ほど重くなってしまう。かといって、s1とs2の両方が同じヒープ領域を指したまま両方使えてしまうと、片方がスコープを抜けて解放したときに、もう片方が無効な領域を指すことになる(Cで実際に起こりうる不具合である)。

Rustは「両方使える」を諦める代わりに、「所有者は常に1つ」を保証することで、この種の不具合をコンパイル時に防いでいる。

関数に渡すときもムーブする

下記のコードを実行するとエラーになる。

fn main() {
    let s = String::from("hello");
    takes_ownership(s);

    println!("{}", s); // ← エラー! sはもう使えない
}

fn takes_ownership(text: String) {
    println!("{}", text);
}
error[E0382]: borrow of moved value: `s`
(略)

関数にStringを渡すと、所有権は関数のパラメータに移動する。
関数が終わるとパラメータはスコープを抜けて破棄されるため、呼び出し元のsはもう使えない。

練習問題

次のコードはエラーになる。なぜか説明したうえで、エラーを直してみよう(ヒント:Stringを2つ用意する)。

fn main() {
    let name = String::from("太郎");
    greet(name);
    greet(name); // ← ここでエラー
}

fn greet(name: String) {
    println!("こんにちは、{}さん", name);
}

解答例

fn main() {
    let name1 = String::from("太郎");
    let name2 = String::from("太郎");
    greet(name1);
    greet(name2);
}

fn greet(name: String) {
    println!("こんにちは、{}さん", name);
}

1回目のgreet(name)で所有権が関数に移動するため、2回目のgreet(name)はもう存在しない値を使おうとしてエラーになる。
今回は2つ用意したが、次回学ぶ「クローン」や「借用」を使えば、1つのStringを何度も使い回すこともできる。

保存して次に進もう

ひととおり試したら、学習内容を保存しておこう。

cd /workspaces/rust-practice
git add .
git commit -m "Complete lesson 5: ownership and move"
git push

体験が済んだら、exercises/05_ownership/の練習問題にも挑戦してみよう(cargo runで一覧表示)。

次回予告

次回は、同じ値を何度も使うためのクローンと、所有権を移動せずに値を貸し借りする借用(参照)について学ぶ。

コメント

タイトルとURLをコピーしました