C/C++で開発を行った経験がある方なら、メモリリーク、ダングリングポインタ、バッファオーバーフローといったメモリ関連のバグに悩まされた経験があるでしょう。Rustはこれらの問題を、ガベージコレクションを使わずにコンパイル時に検出する画期的なアプローチを採用しています。その中核が「所有権(Ownership)システム」です。
筆者はC++からRustに移行して2年ほどになりますが、所有権システムのおかげでメモリ関連のバグがほぼゼロになりましました。最初は厳しく感じるコンパイラの制約も、慣れると非常に頼もしいパートナーになります。
Rustの所有権システムは、以下の3つの基本ルールで成り立っています。
fn main() {
let s1 = String::from("こんにちは"); // s1がStringの所有者
let s2 = s1; // 所有権がs1からs2にムーブ
// println!("{}", s1); // コンパイルエラー!s1はもう使えない
println!("{}", s2); // OK: s2が所有者
} // s2がスコープを抜ける → メモリが自動解放
この「ムーブセマンティクス」がRustの特徴です。コピーではなく所有権の移動が行われるため、二重解放の問題が原理的に発生しません。
所有権を移動させずに値を利用したい場合は「借用」を使います。借用には不変参照と可変参照の2種類があります。
fn calculate_length(s: &String) -> usize {
s.len()
// sは借用しているだけなので、ここで解放されない
}
fn main() {
let s1 = String::from("Rust");
let len = calculate_length(&s1); // 不変参照を渡す(借用)
println!("'{}'の長さ: {}", s1, len); // s1はまだ使える
}
Rustの借用には厳格なルールがあり、これによりデータ競合をコンパイル時に防ぎます。
fn main() {
let mut s = String::from("hello");
let r1 = &s; // OK: 不変参照1
let r2 = &s; // OK: 不変参照2
println!("{}, {}", r1, r2);
// r1, r2はここ以降使われないのでスコープ終了
let r3 = &mut s; // OK: 可変参照(不変参照はもう使われていない)
r3.push_str(", world");
println!("{}", r3);
}
このルールは「Non-Lexical Lifetimes (NLL)」により、変数が最後に使用された時点でスコープ終了と見なされます。以前のRustではもっと厳しい制約がありましたが、NLLの導入で自然なコードが書けるようになりましました。
ライフタイムは、参照が有効な期間をコンパイラに伝える仕組みです。多くの場合はコンパイラが自動推論してくれますが、明示的な指定が必要な場面もあります。
// ライフタイム注釈が必要な例
fn longest(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() {
x
} else {
y
}
}
fn main() {
let string1 = String::from("長い文字列");
let result;
{
let string2 = String::from("短い");
result = longest(string1.as_str(), string2.as_str());
println!("最長: {}", result); // OK: string2はまだスコープ内
}
// println!("{}", result); // エラー: string2が解放済み
}
ライフタイム注釈'aは「この関数が返す参照は、引数の参照と同じかそれより短い期間だけ有効」とコンパイラに伝えています。
構造体の設計でも所有権は重要な判断ポイントです。フィールドに値を所有させるか、参照を持たせるかで設計が大きく変わります。
// 値を所有するパターン(一般的)
struct User {
name: String,
email: String,
age: u32,
}
// 参照を持つパターン(ライフタイム注釈が必要)
struct UserView {
name: &'a str,
email: &'a str,
}
impl User {
fn as_view(&self) -> UserView {
UserView {
name: &self.name,
email: &self.email,
}
}
}
筆者の経験則として、データの永続化や長期保持には所有型を、一時的な読み取りビューには参照型を使うのが良い設計です。
所有権システムだけでは表現しにくいパターンのために、Rustにはスマートポインタが用意されています。
use std::rc::Rc;
use std::cell::RefCell;
// Box: ヒープ上に配置(再帰的データ構造に必須)
enum List {
Cons(i32, Box),
Nil,
}
// Rc: 参照カウント(複数の所有者が必要な場合)
let shared = Rc::new(String::from("共有データ"));
let clone1 = Rc::clone(&shared);
let clone2 = Rc::clone(&shared);
println!("参照カウント: {}", Rc::strong_count(&shared)); // 3
// RefCell: 実行時の借用チェック(内部可変性)
let data = RefCell::new(vec![1, 2, 3]);
data.borrow_mut().push(4);
println!("{:?}", data.borrow()); // [1, 2, 3, 4]
所有権システムを実務で使いこなすためのポイントをまとめます。
Rustの所有権システムは、最初は学習コストが高く感じますが、一度理解すれば「メモリ安全性が型システムで保証される」という絶大な恩恵を受けられます。CやC++で苦しんだメモリ関連のバグが原理的に発生しなくなるのは、開発者にとって大きな安心感です。所有権、借用、ライフタイムの3つの概念をしっかり理解し、まずはシンプルな所有型から始めて、必要に応じてスマートポインタや参照を導入していくアプローチをお勧めします。