Rustにおける所有権 (Ownership)は、安全かつ効率的なメモリ管理を実現するための核となるルール群です。これはJavaやC#のような言語で一般的なガベージコレクション(Garbage Collection)に代わる仕組みです。Rustは、ランタイムに依存してメモリを解放するのではなく、コンパイル時に厳格なチェックを行うことで、メモリリーク(memory leak)やダングリングポインタ(dangling pointer)といった一般的なバグを未然に防ぎます。
所有権を深く理解するためには、まずRustの根幹をなす2つのメモリ領域、スタック(Stack)とヒープ(Heap)を明確に区別することが不可欠です。
この記事では、以下のトピックを通じて、その全体像を解説します。
- Rustがスタックとヒープをどのように管理しているか
- 所有権の3つの主要なルール
- 関連概念である借用(Borrowing)とライフタイム(Lifetimes)
これらの概念を組み合わせることで、Rustは高度な安全性とパフォーマンスを両立し、複雑で安定したアプリケーション構築に最適な言語となっています。
Rust言語におけるスタックとヒープの概要
Rustを学ぶ際、最も重要な概念の一つが所有権(Ownership)です。このメカニズムを理解するには、まずRustの基本となる2つのメモリ領域、スタック(Stack)とヒープ(Heap)を明確に区別する必要があります。
スタック(Stack)とは?
スタックは、LIFO (Last-In, First-Out)という構造を持つメモリ領域で、積み重ねられたお皿のように機能します。
- 速度: 非常に高速です。メモリの割り当てと解放は、スタックの頂点にデータを追加したり削除したりするだけで済むため、検索コストがかかりません。
- 保存されるデータ: スタックには、コンパイル時にサイズが確定している値のみが格納されます。例えば、整数型(
i32
)や真偽値(bool
)などです。加えて、ヒープ上のデータへのポインタやメタデータ(アドレス、長さなど)もスタックに置かれます。 - メモリ管理: コンパイラが自動的に管理します。変数がスコープ内で作成されるとスタックにプッシュされ、スコープから外れると自動的に削除されます。これにより、効率的なメモリ管理が実現します。
fn main() {
let x = 42; // xはスタックに置かれます
let y = true; // yもスタックに置かれます
println!("x = {}, y = {}", x, y);
}
スタック上の変数を別の変数に代入すると、Rustはデータ全体をコピーします。そのため、両方の変数は独立して有効です。
ヒープ (Heap) とは?
ヒープは、柔軟で構造を持たないメモリ領域であり、プログラムの実行中にサイズが未確定、あるいは変化するデータを保存するために使われます。
- 速度: スタックよりも遅いです。ヒープ上にメモリを確保する場合、オペレーティングシステムが適切な空き領域を探し、割り当て、その位置を指すポインタを返すというプロセスが必要になるためです。
- 保存されるデータ:
String
やVec<T>
(vector)など、サイズが動的に変化するデータ型を格納します。重要なのは、これらのデータのポインタ(アドレス、長さ、容量などの情報を含む)自体はスタックに保存される点です。
>>>関連記事:
fn main() {
let s1 = String::from("hello");
// "hello"というデータはヒープにあります。
// そのポインタであるs1はスタックにあります。
let s2 = s1; // 所有権がs1からs2に移動(move)します。
// println!("{}", s1); // コンパイルエラー:s1は所有権を失い、無効になっています。
println!("{}", s2); // 有効です。
}
上記の例では、s2 = s1
という代入が行われたとき、Rustはヒープ上のデータをコピーするのではなく、所有権をs1
からs2
へ移動させます。この仕組みにより、ヒープ上のデータは常にたった一つの所有者しか持たないことが保証され、他の言語で起こりがちなダングリングポインタ(解放されたメモリを指すポインタ)やメモリリークを防ぐことができます。
StackとHeapとの比較
特徴 | スタック (Stack) | ヒープ (Heap) |
速度 | 非常に速い | 比較的遅い |
データ型 | サイズが固定 | サイズが動的 |
メモリ管理 | スコープ(scope)による自動管理 | 所有権(Ownership)による自動管理 |
代入時の挙動 | コピー | 所有権の移動 |
用途 | ローカル変数、一時的なデータ | 文字列、動的配列、複雑なデータ |
Rustでは、スタックは高速かつシンプルなメモリ管理を小規模な固定データに提供します。一方、ヒープは大規模な動的データに柔軟性をもたらします。
この2つのメモリ領域を所有権(Ownership)モデルと組み合わせることで、Rustはガベージコレクタに頼ることなく、絶対的なメモリ安全性を保証します。これにより、プログラムは高速かつ安定して動作します。
この特性こそが、メモリ安全でないエラーを引き起こす可能性のあるC++や、ガベージコレクタを使用するJava・C#といった他の言語との大きな違いです。
>>>関連記事:
Rust所有権(Ownership)とは?
所有権(Ownership)は、Rust言語の中核をなすルールシステムであり、ガベージコレクションに頼らず、メモリを安全かつ効率的に管理するために設計されています。また、プログラマが手動でメモリを割り当てたり解放したりする必要もありません。
このシステムは、コンパイル時にRustコンパイラがチェックする一連のルールに基づいて機能します。もしこれらのルールに一つでも違反があれば、プログラムはコンパイルされません。このチェックは実行時のパフォーマンスに影響を与えないのが特徴です。
所有権の主な目的は、プログラムがメモリ関連のエラーに遭遇しないようにすることであり、これにより、より安全で効率的なコードを書くことができます。
所有権がどのように機能するかをよりよく理解するために、スコープ(scope)の概念を考えてみましょう。スコープは波括弧 {}
のセットです。変数が宣言されると、その変数は宣言されたスコープ内でのみ有効になります。
fn main() {
// ここではxはまだ宣言されていません
{ // 新しいスコープの開始
let x = 5; // ここでxが宣言されます。これ以降、xは有効です。
println!("x: {}", x);
} // このスコープの終わり。xは無効になります。
// println!("{}", x); // エラー:xはすでにスコープ外で存在しません。
}
上記の例では、プログラムが閉じ波括弧 }
に到達すると、Rustは自動的に変数 x
をメモリから解放します。これにより、存在しない変数を使用するのを防ぎ、メモリ関連のエラーを未然に防ぎます。これが、Rustがメモリを安全かつ自動的に管理するために使用する基本的な原則です。
>>>関連記事:
Rustの所有権の3大ルール
メモリの安全性を確保するため、Rustはシンプルでありながら非常に厳格な3つのルールに従います。これらのルールに違反すると、プログラムはコンパイルされません。
各値にはただ1つの「オーナー」を持つ
これは最も基本的なルールです。ヒープ上の各値は、オーナー (owner) と呼ばれる唯一の変数を持ちます。このオーナーが、値のライフサイクルを管理する責任を負います。
ある変数を別の変数に代入すると、Rustは所有権を移動 (move) させます。データはコピーされず、単に「持ち主」が変わるだけです。所有権を失った元の変数は無効になり、以降使用することはできません。
fn main() {
let s1 = String::from("hello"); // s1がオーナー
let s2 = s1; // 所有権がs1からs2に移動
// println!("{ }", s1); // コンパイルエラー:s1は所有権を失い無効
println!("{ }", s2); // s2は有効です
}
この「move」の仕組みにより、Rustは他のプログラミング言語でよく見られる、解放済みメモリの使用エラーを防ぎます。
所有者は同時に1人だけ、参照にもルールがある
Rustでは、一度に1つの参照だけがデータにアクセスできるというルールがあります。これは、以下の2つのいずれかの方法で理解できます。
- データへの可変(mutable)な参照(
&mut
)が1つ - データへの不変(immutable)な参照(
&
)が複数
これは、データ競合 (data race) を防ぐための重要なルールです。コンパイル時にこれらのルールを強制することで、Rustは並行プログラミングにおいても非常に高い安全性を実現しています。
所有者がスコープから外れると、値は解放される
Rustの各変数は、{}
で定義される特定のスコープ(scope)を持っています。変数がスコープから外れると、そのライフサイクルは終了し、Rustは自動的にその変数が所有しているデータのメモリを解放します。
- 例:
fn process_data() {
let s = String::from("Rust"); // 変数 s はここから有効になります
// ...
} // スコープが終了し、sはスコープから外れます。
// Rustは自動的に `drop` 関数を呼び出し、文字列 "Rust" のメモリを解放します。
このルールにより、メモリリーク(memory leak)が発生しないことが保証されます。Rustはいつメモリを解放すべきかを正確に知っているため、多くの言語で使用される煩雑なガベージコレクタ(garbage collector)は必要ありません。
所有権と関連する概念
借用
借用(Borrowing) とは、ある変数が他の変数の所有権を奪うことなく、そのデータにアクセスする権限を「借りる」ことです。
例:非効率的なアプローチ(clone
とタプルを返す方法)
fn main() {
let first = String::from("Ferris");
let (first, full) = add_suffix(first);
println!("{full}, originally {first}"); // first が再び有効になり、使用される
}
fn add_suffix(name: String) -> (String, String) {
let mut suffix_name = name.clone();
suffix_name.push_str(" Jr.");
(name, suffix_name)
}
このアプローチは動作しますが、不要な clone
によるリソース消費や、タプルを返すことによるコードの煩雑さといった問題があります。
- より良い解決策:借用
借用を使うと、所有権を奪うことなく元の変数への参照を持つだけで済みます。Rustでは以下のことが可能です。
- 不変な借用(&): 一度に複数の不変な参照を同時に持つことができます。この参照を介してデータを変更することはできません。
- 可変な借用(&mut): 一度に1つの可変な参照のみが許可されます。このルールは、データ競合(data race)を防ぐために非常に重要です。もし同時に複数の可変な借用を試みると、コンパイラは即座にエラーを報告します。例:同じスコープ内で、前の借用を解放せずに同じデータへの2つの
&mut
を持つことはできません。
スコープに関する注意点: 借用はオーナーのライフタイムを尊重しなければなりません。借用がスコープを超えてしまう場合(例:オーナーがドロップされた後に参照を返す)、ダングリング参照(解放されたメモリを指す参照)のエラーが発生します。Rustのボローチェッカー(borrow checker)は、コンパイル時にこれを防ぎます。
fn main() {
let s = String::from("hello");
// 不変の借用(immutable borrow)
let len = calculate_length(&s);
println!("'{}' の長さは {} です", s, len);
// 可変の借用(mutable borrow)
let mut s2 = String::from("hello");
change(&mut s2);
println!("{}", s2);
}
fn calculate_length(s: &String) -> usize {
s.len() // 不変で借用しているので、読み取りのみ可能
}
fn change(s: &mut String) {
s.push_str(", world"); // 可変で借用しているので、内容を変更できる
}
上の例では:
change
は &mut
を使ってs2
を借用し、内容を変更できるようにしています。しかし、もし同じスコープ内で別の &mut
を追加しようとすると、例えば let borrow2 = &mut s2;
と書いた場合、最初の借用が終了していないため、エラーになります:「cannot borrow s2 as mutable more than once at a time」。
下の図は、&m1
と &m2
が greet
関数に渡される様子を示しています。
関数内の g1
や g2
はあくまで参照(borrower)であり、データの所有権は依然として main
関数内の m1
と m2
にあります。

出典:2coffee.dev
ライフタイム(Lifetimes)の導入
- Lifetimes は、参照が使用される範囲内で常に有効であることを保証します → ダングリング参照が発生しません。
- 問題の例:
fn main() {
let r; // r はまだ代入されていない
{
let x = 5;
r = &x; // x はスコープを抜けるので、r は無効なメモリを指してしまう
}
// println!("{}", r); // コンパイルエラー
}
- 解説: Rust コンパイラはlifetimeを自動的にチェックして安全性を保証します。参照の関係が不明確な場合(例えば参照を返すとき)、Rust はlifetimeアノテーションの指定を要求して、参照の有効期間を明示します。
- Lifetimeアノテーションを使った例:
fn longest<'a>(s1: &'a str, s2: &'a str) -> &'a str {
if s1.len() > s2.len() {
s1
} else {
s2
}
}
fn main() {
let s1 = String::from("rust");
let s2 = "ownership";
let result = longest(s1.as_str(), s2);
println!("長い文字列は {}", result);
}
ここで 'a
は、返される参照のlifetimeが入力として渡された 2 つの参照のうち最短の lifetime と同じであることを示しています。
所有権・借用・ライフタイムの関係性まとめ
- Ownership(所有権): データの所有者は誰か?
- Borrowing(借用): 所有権を取らずにデータを借りることができる
- Lifetimes(ライフタイム): 参照が元のデータの有効範囲を超えないように保証する
これら 3 つの概念を組み合わせて安全なメモリ管理を実現します。
まとめ
RustにおけるOwnership(所有権)、Borrowing(借用)、Lifetimes(ライフタイム)は、初心者にとってはやや複雑に感じるかもしれません。しかし、これらの概念を理解することで、メモリ安全性と高いパフォーマンスを兼ね備えたプログラム を作成することができます。
- Ownership(所有権)は、各データに一人の所有者のみが存在することを保証し、データは自動的に解放されます。
- Borrowing(借用)は、所有権を奪うことなくデータにアクセスでき、安全に操作することが可能です。
- Lifetimes(ライフタイム)は、参照が無効なメモリを指すことを防ぎ、ダングリング参照を回避します。
これら三つの原則を組み合わせることで、Rustはガベージコレクターを使用せずに複雑で安定したアプリケーションを開発することを可能にします。
日本における多数の Web3、ブロックチェーン、AIプロジェクトの実績を持つRelipaの技術チームは、Rustを活用して、パフォーマンス最適化と絶対的な安全性を両立したソフトウェア を構築する自信があります。Rustによるアイデアを強力で安定、安全なプロダクトに変えてたい方は、ぜひRelipaにご相談ください。