前回までの関数・制御構文で、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つである。
- それぞれの値は、所有者(owner)と呼ばれる変数を1つだけ持つ
- 所有者がスコープを抜けると、値は破棄される
- 値は、一度に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で一覧表示)。
次回予告
次回は、同じ値を何度も使うためのクローンと、所有権を移動せずに値を貸し借りする借用(参照)について学ぶ。


コメント