所有権 — RubyのGCがやってくれていたことを自分でやる
Rubyistが最も苦戦するRustの核心概念
Rubyでメモリ管理を気にしたことがあるだろうか?おそらくない。GCが全てやってくれるから。オブジェクトを作り、変数に入れ、メソッドに渡し、使わなくなればGCが片付ける。
RustにはGCがない。代わりに所有権という3つのルールでメモリを管理する。
ルール1:全ての値には所有者が1つ
let s1 = String::from("hello");
let s2 = s1; // s1の所有権がs2にムーブ
// println!("{}", s1); // コンパイルエラー!s1はもう有効でない
Rubyでs2 = s1すると両方とも同じオブジェクトを指す。両方使える。Rustではs1が消える。s2だけ残る。
これがRubyistにとって最もショッキングな部分だ。「変数に入れただけなのに元が消える?」
なぜこうなっているか
RubyのGCはランタイムで「このオブジェクトをまだ誰かが使っているか?」を追跡する。これがCPU時間とメモリを消費する。Rustはこの追跡をコンパイル時に行い、ランタイムコストをゼロにする。
ルール2:借用(borrow)— 参照で渡す
所有権を渡さずに貸すことができる。
fn print_len(s: &String) { // &で借用
println!("{}", s.len());
}
let s = String::from("hello");
print_len(&s); // &で貸す
println!("{}", s); // sはまだ使える!
&は「読むだけ、所有権はあなたが持って」という意味。Rubyではこの区別は不要だった。全てのオブジェクト渡しが参照渡しだから。
ルール3:可変借用は1つだけ
let mut s = String::from("hello");
let r1 = &s; // 不変参照 — OK
let r2 = &s; // もう1つの不変参照 — OK
// let r3 = &mut s; // 可変参照 — エラー!不変参照がある間は不可
「複数人が読むのはOK、1人が書くのもOK、でも読んでる人がいる時に誰かが変更するのはダメ」。このルールがデータレースをコンパイル時に防ぐ。
Clone — Rubyの.dup
所有権を渡したくなく借用も適切でなければコピーする。
let s1 = String::from("hello");
let s2 = s1.clone(); // ディープコピー
println!("s1: {}, s2: {}", s1, s2); // 両方OK
Rubyの.dup/.cloneと同じ。ただし性能コストがあるので必要な時だけ。
Copy — スタック値は例外
整数、ブール、浮動小数点のような小さい値は自動コピーされる。
let x = 42;
let y = x; // コピーされる(moveではない)
println!("{}", x); // OK!整数はCopy traitを実装
スタックにある小さい値はコピーコストがほぼないので自動コピー。Stringはヒープデータなので自動コピーされない。
実践パターン
大抵の場合、この順番で考えればいい:
- 参照(&)で借用できるか? → 大抵これで十分
- 所有権を渡す必要があるか? → 関数が値を所有すべき時だけ
- cloneが必要か? → 上の2つとも無理な時
キーポイント
全ての値に所有者1つ — let s2 = s1;で所有権がムーブ
&で貸せば所有権維持 — 大抵これで十分
不変参照複数OK、可変参照は1つだけ — データレース防止
clone()でディープコピー — Rubyの.dup
メリット
- ✓ GCなしでメモリ安全 — ランタイムコストゼロ
- ✓ データレースがコンパイル時に検出される
デメリット
- ✗ Rubyにない概念なので初期学習曲線が急
- ✗ コンパイラとの戦いに最初はかなり時間がかかる