一般社団法人 全国個人事業主支援協会

COLUMN コラム

  • Rustで学ぶメモリ安全性:所有権システムの仕組みと活用法

なぜRustの所有権システムが重要なのか

C/C++で開発を行った経験がある方なら、メモリリーク、ダングリングポインタ、バッファオーバーフローといったメモリ関連のバグに悩まされた経験があるでしょう。Rustはこれらの問題を、ガベージコレクションを使わずにコンパイル時に検出する画期的なアプローチを採用しています。その中核が「所有権(Ownership)システム」です。

筆者はC++からRustに移行して2年ほどになりますが、所有権システムのおかげでメモリ関連のバグがほぼゼロになりましました。最初は厳しく感じるコンパイラの制約も、慣れると非常に頼もしいパートナーになります。

所有権の三つのルール

Rustの所有権システムは、以下の3つの基本ルールで成り立っています。

  • ルール1:Rustの各値は「所有者(owner)」と呼ばれる変数を持つ
  • ルール2:同時に存在できる所有者は1つだけ
  • ルール3:所有者がスコープを外れると、値は自動的に破棄されます

fn main() {
let s1 = String::from("こんにちは"); // s1がStringの所有者
let s2 = s1; // 所有権がs1からs2にムーブ
// println!("{}", s1); // コンパイルエラー!s1はもう使えない
println!("{}", s2); // OK: s2が所有者
} // s2がスコープを抜ける → メモリが自動解放

この「ムーブセマンティクス」がRustの特徴です。コピーではなく所有権の移動が行われるため、二重解放の問題が原理的に発生しません。

借用(Borrowing)と参照

所有権を移動させずに値を利用したい場合は「借用」を使います。借用には不変参照と可変参照の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の借用には厳格なルールがあり、これによりデータ競合をコンパイル時に防ぎます。

  • 不変参照は同時に何個でも作成可能
  • 可変参照は同時に1つだけ作成可能
  • 不変参照と可変参照は同時に存在できない

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]

実務での所有権設計の勘所

所有権システムを実務で使いこなすためのポイントをまとめます。

  • 最初は所有型(String, Vec等)で設計する:参照はパフォーマンスの最適化として後から導入する
  • Cloneは悪ではない:所有権の問題に悩んだら、まずcloneで解決し、後からプロファイリングで最適化する
  • 構造体のフィールドはなるべく所有型にする:ライフタイム注釈の連鎖を避けられます
  • 関数のシグネチャで借用か所有かを明確にする:APIの意図が型で伝わる
  • コンパイラのエラーメッセージを信頼する:Rustのエラーメッセージは非常に親切で、修正方法まで提案してくれます

まとめ

Rustの所有権システムは、最初は学習コストが高く感じますが、一度理解すれば「メモリ安全性が型システムで保証される」という絶大な恩恵を受けられます。CやC++で苦しんだメモリ関連のバグが原理的に発生しなくなるのは、開発者にとって大きな安心感です。所有権、借用、ライフタイムの3つの概念をしっかり理解し、まずはシンプルな所有型から始めて、必要に応じてスマートポインタや参照を導入していくアプローチをお勧めします。

この記事をシェアする

  • Twitterでシェア
  • Facebookでシェア
  • LINEでシェア