2026年03月12日
こんにちは。新入社員のY・Dです。
入社してもうすぐ1年になります。業務の中で Rust に触れる機会をいくつかいただいたので、今回は Ownership(所有権)について、いまの理解を整理して共有します。
Ownership は、Rust のメモリ管理を支える中核的な仕組みです。GC に頼らず、コンパイル時にリソース管理の責務をはっきりさせることで、安全性と実行性能の両立を狙っています。
最初はコンパイルエラーが多く感じられますが、見方を変えると「実行前に不具合の芽を摘む仕組み」だと捉えられて、学習もしやすくなりました。
Ownership の位置づけがつかみやすいように、GC付き言語とGCなし言語を比較します。
| 観点 | Rust(Ownership) | GC言語(Go / Java など) | GCなし言語(C/C++ など) |
|---|---|---|---|
| 解放タイミング | スコープ終了時に自動解放(責務はコンパイル時に確定) | ランタイムの GC が回収 | 基本はスコープ終了で解放、設計によって手動管理も混在 |
| 安全性の担保 | 借用規則をコンパイル時に厳密チェック | 解放ミスは減らせるが、停止タイミングは GC の挙動に依存 | 自由度は高いが、未定義動作リスクの管理が必要 |
| 実行時特性 | GC 停止がなく、レイテンシを予測しやすい | 生産性は高い一方で、GC の影響を受ける場合がある | 最適化余地は大きいが、運用ルールの徹底が前提 |
| 実行時コスト | GC 実行コストは基本的に発生せず、所有権チェックはコンパイル時に実施 | GC の実行負荷と停止時間(Pause)がワークロード次第で発生 | GC コストはないが、設計やメモリ管理方針によって運用コストが増えやすい |
Rust は「実行時コストを抑えるために、コンパイル時にしっかり検証する」という設計思想が強い言語だと感じています。
Ownership は基本的には次の3つのルールで説明できます。
すべての値は owner(所有者)を1つ持つ。
同時に存在できる owner は1つだけ。*
owner がスコープを抜けると値は drop される。
まずは最小の例です。
fn main() {
let s = String::from("hello");
// このスコープ内では s は有効
} // スコープ終了時に自動解放される
*厳密に言えば、Rc や Arc を利用することで、複数の owner を持つことも可能です。
String のようなヒープデータでは、代入時に deep copy ではなく move(所有権移動)が起こります。
fn main() {
let s1 = String::from("hello");
let s2 = s1; // 所有権が s1 から s2 へ移動
// println!("{}", s1); // コンパイルエラー: s1 は無効
println!("{}", s2);
}
この仕様のおかげで、double free のような問題を防ぎやすくなっています。
値を実際に複製したいときは、clone() を明示的に使います。
fn main() {
let s1 = String::from("hello");
let s2 = s1.clone();
println!("s1 = {}, s2 = {}", s1, s2);
}
一方、整数のように Copy を実装する型は値コピーされるため、元の変数もそのまま使えます。
fn main() {
let x = 42;
let y = x;
println!("x = {}, y = {}", x, y);
}
関数に String をそのまま渡すと所有権が移るので、読み取り中心の処理では参照を使います。
fn len_of(s: &String) -> usize {
s.len()
}
fn main() {
let s = String::from("ownership");
let len = len_of(&s);
println!("{} -> {}", s, len);
}
&s は不変借用なので、読み取りはできますが変更はできません。
同一時点では「可変参照は1つ」または「不変参照を複数」のどちらかしか許可されません。
fn main() {
let mut s = String::from("hello");
let r1 = &mut s;
r1.push_str(" rust");
println!("{}", r1);
}
この制約が data race(データ競合)を防ぐ重要な仕組みです。
実務で Ownership を扱うときは、個別テクニックよりも考える順序をそろえたほうが判断しやすいと感じています。いま意識しているのは次の5点です。
まず、どの境界(関数・モジュール・スレッド)で所有権を受け渡すかを先に決める。
境界の内側は借用(&T / &mut T)を優先し、不要な ownership 移動を増やさない。
データの生存期間を意識し、一時的に使う値はローカルに閉じ、長く保持する状態だけを構造体で管理する。
API では「参照する(&T)」「更新する(&mut T)」「所有する(T)」を型で明確に分ける。
clone() は必要最小限に留め、所有権エラーが出たら場当たり対応ではなくデータフローを見直す。
本記事では、Ownership の基本ルール、GC付き言語・GCなし言語との違い、そして実務で考えるときの整理ポイントを簡単にまとめました。
Rust が Ownership を採用している背景には、実行時の不確実なコストを減らし、コンパイル時に安全性を先に検証しておくことで、予測しやすい性能と堅牢性を両立したい狙いがあるのだと理解しています。
いわば Rust は「実行時に払うコストを、コンパイル時に先払いする」思想を徹底している言語だと感じます。
私自身もまだ学習の途中なので、理解が十分でない点や誤りが含まれているかもしれません。その際はご容赦いただけると幸いです。