なぜ二度使ってはいけないのか
一言でいうと
Rustは値の所有者を1つに強制します。 別の場所に渡すと、元の場所では使えなくなります。このたった1つの規則が、GCなしでメモリ安全を実現します。
なぜ必要なのか
C系の代表的な事故が3つあります。
- use after free: 解放したメモリをもう一度使います
- double free: 2回解放します
- data race: 2つのスレッドが同じ値を同時に書き換えます
3つとも「誰がこの値の所有者なのか」がはっきりしないために起きます。GCは最初の2つをなくしますが、停止時間とメモリの代償があり、3つ目は依然として残ります。
RustはGCとは別の答えを選びました。所有者をコンパイル時に追跡するのです。実行時のコストは0です。
規則は3つだけ
- すべての値には所有者(owner)が1つあります
- 所有者がいなくなると、値もなくなります
- 値を別の場所に渡す(move)と、元の所有者は失われます
let s = String::from("hi");
let t = s; // 주인이 t 로 옮겨졌다
println!("{}", s); // ❌ 컴파일 오류: s 는 이미 넘겨졌다
エラーメッセージがまさにそう言っています。
error[E0382]: borrow of moved value: `s`
= note: move occurs because `s` has type `String`,
which does not implement the `Copy` trait
Copyを実装した型はコピーされます。 整数・ブール値・文字などです。そのためlet a = 5; let b = a;は問題ありません。サイズが固定で、ヒープを使わないからです。
借用
渡さずに少しだけ使うには、借ります(borrow)。
fn len(s: &String) -> usize { s.len() } // 읽기만 빌림
let s = String::from("hi");
println!("{} {}", len(&s), s); // ✅ s 는 여전히 내 것
借用には、もう1つ規則があります。
読み取りの借用は同時にいくつでも可能ですが、書き込みの借用は1つだけで、その間は読み取りもできません。
let mut v = vec![1, 2, 3];
let first = &v[0]; // 읽기 빌림
v.push(4); // ❌ 쓰기 빌림이 필요한데 읽기가 살아 있다
println!("{}", first);
これが防ぐのは実際の事故です。pushがベクターを再割り当てすると、firstが指していたアドレスは無効になります。C++ではそれが黙って通り、あとで妙な値を読みます。
ライフタイム
借用は元の値より長く生きられません。コンパイラーがそれを検査します。
fn dangle() -> &String { // ❌
let s = String::from("hi");
&s // s 는 함수가 끝나면 사라진다
}
ほとんどの場合、ライフタイムは推論されます。 明示的に'aを書くことは思ったより少なく、主に、複数の参照のうちどれと結び付くかを伝えるときです。
よくある勘違い
「cloneすればいいじゃないか」。それで通ります。そして、それが正しい答えであることも多いです。所有権と戦って1日を費やすくらいなら、clone()して先に進むほうがよいです。性能が問題になったら、そのときにプロファイラーを見て直します。最初から参照で最適化しようとすることが、Rustを難しくしている最も一般的な理由です。
「unsafeは悪い」。unsafeは「ここではコンパイラーが検査できないので、自分が保証する」という表示です。標準ライブラリの中にもたくさんあります。悪いのはunsafeを使うことではなく、なぜ安全なのかを書かないことです。
所有権の問題を解く道具箱
借用チェッカーと戦う代わりに、状況に合った道具を選べば、たいてい解決します。
| 状況 | 道具 | 代償 |
|---|---|---|
| 少し読むだけ | &T |
なし |
| 少し書き換える | &mut T |
その間、ほかの参照は不可 |
| 所有権を渡す | 値そのまま | 元の変数を使えない |
| 複製しても安い | .clone() |
コピーのコスト |
| 複数の場所が共同で所有 | Rc<T>(シングルスレッド) |
参照カウント、循環によるリーク |
| 複数のスレッドが共有 | Arc<T> |
アトミックなカウント(少し遅い) |
| 共有しながら書き換える | Arc<Mutex<T>> |
ロックのコスト、デッドロックの可能性 |
初心者が.clone()を使うのは、間違いではありません。 まず動くようにして、プロファイラーがそのコピーを指摘したら、そのとき参照に変えます。最初からライフタイム注釈と戦って諦めるよりも、はるかによいです。
コンパイルエラーを読む順序
Rustのエラーメッセージは長いですが、構造は一定です。
error[E0502]: cannot borrow `v` as mutable because it is also borrowed as immutable
--> src/main.rs:4:5
|
3 | let first = &v[0]; ← 여기서 불변으로 빌렸고
| - immutable borrow occurs here
4 | v.push(4); ← 여기서 가변으로 빌리려 한다
| ^^^^^^^^^ mutable borrow occurs here
5 | println!("{}", first); ← 불변 빌림이 여기까지 살아 있다
| ----- immutable borrow later used here
3本の矢印を順番に読みます。 どこで借り、どこで衝突し、なぜまだ生きているのかです。最後の行が核心です。firstを使わなければ、借用がpushの前に終わってエラーが消えます(NLL、non-lexical lifetimes)。
rustc --explain E0502で、そのエラーの解説の全文を見られます。コードごとに例が付いているので、初めて出会うエラーはこれから読むのが早いです。
いつunsafeを使うのか
ほとんど使いません。ただしunsafeは「検査を切る」ではなく、「不変条件を守るとコンパイラーに約束する」という意味です。追加で許可されるのは5つの操作だけで(生ポインターの参照外し、unsafe関数の呼び出しなど)、残りの検査はそのままです。
実務で出会う場面は3つです。Cライブラリを呼ぶとき(FFI)、データ構造を自分で実装するとき(双方向連結リスト)、そして性能が証明されたボトルネックのときです。
unsafeブロックはできるだけ狭くし、その上に安全なAPIをかぶせます。 そうすれば、そのブロックだけ人がレビューすればよく、ユーザーは安全なインターフェースだけを見ます。標準ライブラリがまさにこの構造です。
実務で本当に大切なこと
エラーメッセージを最後まで読む習慣が、Rust学習の半分です。rustcはたいてい直し方まで教えてくれます。
help: consider cloning the value if the performance cost is acceptable
|
5 | let t = s.clone();
| ++++++++
そしてcargo clippyは、コンパイルは通るけれどもっとよい方法がある箇所を指摘してくれます。学んでいる間はclippyを有効にしておくことが、本をもう1冊読むのに似ています。