my.code(); Logomy.code();
Rust-3.所有権

my.code(); Logomy.code();

  • C++
    • 0.C++の世界へようこそ
    • 1.型システムと制御構造
    • 2.データ集合とモダンな操作
    • 3.ポインタとメモリ管理
    • 4.関数と参照渡し
    • 5.プロジェクトの分割とビルド
    • 6.クラスの基礎
    • 7.クラスを使いこなす
    • 8.継承とポリモーフィズム
    • 9.テンプレート
    • 10.STL ①:コンテナ
    • 11.STL ②:アルゴリズムとラムダ式
    • 12.RAIIとスマートポインタ
  • JavaScript
    • 0.JavaScriptへようこそ
    • 1.基本構文とデータ型
    • 2.制御構文
    • 3.関数とクロージャ
    • 4.'this'の正体
    • 5.オブジェクトとプロトタイプ
    • 6.クラス構文
    • 7.配列とイテレーション
    • 8.非同期処理①: Promise
    • 9.非同期処理②: Async/Await
  • Python
    • 0.環境構築と基本思想
    • 1.基本構文とデータ型
    • 2.リスト、タプル、辞書、セット
    • 3.制御構文と関数
    • 4.モジュールとパッケージ
    • 5.オブジェクト指向プログラミング
    • 6.ファイルの入出力とコンテキストマネージャ
    • 7.例外処理
    • 8.ジェネレータとデコレータ
  • Ruby
    • 0.rubyの世界へようこそ
    • 1.基本構文とデータ型
    • 2.制御構造とメソッド定義
    • 3.すべてがオブジェクト
    • 4.コレクション (Array, Hash, Range)
    • 5.ブロックとイテレータ
    • 6.クラスとオブジェクト
    • 7.モジュールとMix-in
    • 8.Proc, Lambda, クロージャ
    • 9.標準ライブラリの活用
    • 10.テスト文化入門
    • 11.メタプログラミング入門
  • Rust
    • 0.Rustの世界へようこそ
    • 1.基本構文と「不変性」
    • 2.関数と制御フロー
    • 3.所有権
    • 4.借用とスライス
    • 5.構造体とメソッド構文
    • 6.列挙型とパターンマッチ
    • 7.モジュールシステムとパッケージ管理
    • 8.コレクションと文字列
    • 9.エラーハンドリング
    • 10.ジェネリクスとトレイト
    • 11.ライフタイム
  • TypeScript
    • 0.TypeScriptへようこそ
    • 1.基本的な型と型推論
    • 2.オブジェクト、インターフェース、型エイリアス
    • 3.関数の型定義
    • 4.型を組み合わせる
    • 5.ジェネリクス
    • 6.クラスとアクセス修飾子
    • 7.非同期処理とユーティリティ型
my.code(); Logomy.code();

環境構築不要、その場で実践。

ut-code / my-code

Copyright © 2026 ut.code();

my.code(); について
コード実行環境についてAI質問機能についてライセンスお問い合わせ
ut.code(); について
公式ウェブサイト公式 𝕏 アカウント

第3章: 所有権(Ownership)システム

Rustへようこそ。ここからがRustの学習における最大の山場であり、同時に最大の特徴である「所有権(Ownership)」システムについて解説します。

他のプログラミング言語(Python, Java, C++など)の経験がある方にとって、この概念は最も馴染みがなく、直感に反する場合があるかもしれません。しかし、所有権こそがガベージコレクション(GC)なしでメモリ安全性を保証するRustの魔法の源です。

本章では、参照(借用)の概念に入る前に、基礎となる「所有権の移動(ムーブ)」とメモリ管理の仕組みを理解します。

スタックとヒープのメモリ管理

所有権を理解するには、計算機科学の基礎である「スタック(Stack)」と「ヒープ(Heap)」の違いを意識する必要があります。多くの高水準言語ではこれを意識しなくてもコードが書けますが、システムプログラミング言語であるRustでは、値がどこに配置されるかが言語の挙動に直結します。

スタック(Stack)

  • 特徴: Last In, First Out (LIFO)。高速。
  • データ: コンパイル時にサイズが既知のデータ(i32, bool, 固定長配列など)が置かれます。
  • 動作: 関数呼び出し時にローカル変数がプッシュされ、関数終了時にポップされます。

ヒープ(Heap)

  • 特徴: 任意の順序で確保・解放が可能。スタックより低速(ポインタ経由のアクセスが必要)。
  • データ: コンパイル時にサイズが不明、または可変長のデータ(String, Vecなど)が置かれます。
  • 動作: メモリ管理が必要。OSにメモリを要求し、そのアドレス(ポインタ)を受け取ります。ポインタ自体はスタックに置かれます。

Rustの所有権システムは、主にこの「ヒープデータの管理」を自動化・安全化するためのルールセットです。

所有権の3つのルール

Rustのコンパイラは、以下の厳格なルールに基づいてメモリ管理を行います。これを破るとコンパイルエラーになります。

所有権のルール

  1. Rustの各値は、所有者(Owner)と呼ばれる変数を持つ。
  2. いかなる時も、所有者は一人だけである。
  3. 所有者がスコープから外れると、値は破棄(ドロップ)される。

変数のスコープ

まずは単純なスコープの例を見てみましょう。これは他の言語とほぼ同じです。

ファイルを編集:scope_example.rs
fn main() {
    {                      // s はここで宣言されていないので無効
        let s = "hello";   // s はここから有効になる
        println!("{}", s); // s を使用できる
    }                      // ここでスコープ終了。s は無効になる
}
ブラウザ上で動作するrustの実行環境です。
左上の実行ボタンを押して、このページ内のscope_example.rsに書かれている内容を実行します。
hello

ここで重要なのは、スコープを抜けた瞬間にRustが自動的にメモリを解放する処理(drop関数)を呼び出すという点です。これはC++のRAII (Resource Acquisition Is Initialization) パターンと同様です。

ムーブセマンティクス(Move vs Copy)

ここからがRust独自の挙動です。データ型によって、「代入」の意味が変わります。

Copyトレイト:スタックのみのデータ

整数型のような単純な値は、サイズが固定でスタック上にあります。この場合、変数を代入すると値がコピーされます。

let x = 5;
let y = x; // xの値(5)がコピーされてyに入る
// ここでは x も y も両方有効

Move(移動):ヒープデータの場合

String型のようにヒープにメモリを確保する型を見てみましょう。

let s1 = String::from("hello");
let s2 = s1; 

C++などの経験があると、これは「ポインタのコピー(浅いコピー)」あるいは「ディープコピー」のどちらかだと思うかもしれません。 しかしRustでは、これは所有権の移動(Move)とみなされます。

  1. s1 はヒープ上の "hello" を指すポインタ、長さ、容量をスタックに持っています。
  2. s2 = s1 を実行すると、スタック上のデータ(ポインタ等)のみが s2 にコピーされます。
  3. 重要: この瞬間、Rustは s1 を無効とみなします。

なぜなら、もし s1 も有効なままだと、スコープを抜けた時に s1 と s2 が同じヒープメモリを2回解放しようとしてしまう(二重解放エラー)からです。

以下のコードを実行して確認してみましょう。

ファイルを編集:move_error_demo.rs
fn main() {
    let s1 = String::from("hello");
    let s2 = s1; // 所有権が s1 から s2 へ移動(ムーブ)

    // s1 はもう無効なので、以下の行はコンパイルエラーになる
    // println!("{}, world!", s1); 

    println!("s1 is moved.");
    println!("s2 is: {}", s2);
}
ブラウザ上で動作するrustの実行環境です。
左上の実行ボタンを押して、このページ内のmove_error_demo.rsに書かれている内容を実行します。
s1 is moved.
s2 is: hello

もし println!("{}", s1) のコメントアウトを外すと、value borrowed here after move という有名なコンパイルエラーが発生します。

Clone:ディープコピー

もしヒープ上のデータも含めて完全にコピーしたい場合は、明示的に .clone() メソッドを使用します。

ファイルを編集:clone_example.rs
fn main() {
    let s1 = String::from("hello");
    let s2 = s1.clone(); // ヒープデータごとコピーする(コストは高い)

    println!("s1 = {}, s2 = {}", s1, s2);
}
ブラウザ上で動作するrustの実行環境です。
左上の実行ボタンを押して、このページ内のclone_example.rsに書かれている内容を実行します。
s1 = hello, s2 = hello

所有権と関数

関数に変数を渡す動作も、代入と同様に機能します。つまり、関数へ値を渡すと所有権が移動します(Copy型を除く)。

ファイルを編集:function_ownership.rs
fn main() {
    let s = String::from("hello");  // s がスコープに入る

    takes_ownership(s);             // s の値が関数にムーブされる
                                    // ここで s はもう有効ではない!

    let x = 5;                      // x がスコープに入る
    makes_copy(x);                  // x も関数に移動するが、
                                    // i32はCopyトレイトを持つので、
                                    // この後も x を使って問題ない

} // ここで x がスコープアウト。s もスコープアウトだが、
  // 所有権は既に移動しているので何も起きない。

fn takes_ownership(some_string: String) { // some_string に所有権が移る
    println!("{}", some_string);
} // ここで some_string がスコープアウトし、`drop` が呼ばれる。メモリ解放。

fn makes_copy(some_integer: i32) { // some_integer に値がコピーされる
    println!("{}", some_integer);
} // ここで some_integer がスコープアウト。何も起きない。
ブラウザ上で動作するrustの実行環境です。
左上の実行ボタンを押して、このページ内のfunction_ownership.rsに書かれている内容を実行します。
hello
5

戻り値と所有権

関数から値を返すことで、所有権を呼び出し元に戻すことができます。

ファイルを編集:return_ownership.rs
fn main() {
    let s1 = gives_ownership();         // 戻り値の所有権が s1 に移動
    let s2 = String::from("hello");     // s2 スコープイン
    let s3 = takes_and_gives_back(s2);  // s2 は関数にムーブされ、
                                        // 戻り値が s3 にムーブされる
    println!("s1: {}, s3: {}", s1, s3);
}

fn gives_ownership() -> String {
    let some_string = String::from("yours");
    some_string                              // 所有権を呼び出し元に返す
}

// 文字列を受け取り、それをそのまま返す関数
fn takes_and_gives_back(a_string: String) -> String {
    a_string  // 所有権を返す
}
ブラウザ上で動作するrustの実行環境です。
左上の実行ボタンを押して、このページ内のreturn_ownership.rsに書かれている内容を実行します。
s1: yours, s3: hello

この章のまとめ

  • 所有権のルール: 値の所有者は常に一人。所有者がスコープを抜けると値は破棄される。
  • スタック vs ヒープ: サイズ固定のデータはスタック(高速)、可変長データはヒープ(管理が必要)に置かれる。
  • Move(ムーブ): ヒープデータを変数に代入(または関数渡し)すると、所有権が移動し、元の変数は使用不能になる。
  • Copy: 整数などの単純な型は、ムーブではなく自動的にコピーされる。
  • Clone: ヒープデータをディープコピーしたい場合は clone() を使う。

このシステムのおかげで、Rustは実行時のガベージコレクションによる停止を避けつつ、ダングリングポインタ(無効なメモリを指すポインタ)や二重解放のバグをコンパイル時に完全に防ぐことができます。

しかし、「関数に値を渡すたびに所有権がなくなってしまい、使えなくなる」のは不便だと感じたでしょう。値を一時的に関数に使わせたいだけなのに、いちいち所有権を返してもらうのは面倒です。

次章の「借用(Borrowing)」では、所有権を渡さずに値を参照する方法を学びます。

練習問題 1: ムーブの回避

以下のコードは、s1 の所有権が s2 に移動してしまったためコンパイルエラーになります。s1 と s2 の両方を表示できるように修正してください(cloneを使用する方法と、新しい文字列を作る方法のどちらでも構いません)。

ファイルを編集:practice4_1.rs
fn main() {
    let s1 = String::from("Rust");
    let s2 = s1;

    println!("Original: {}", s1); // ここでエラー
    println!("New: {}", s2);
}
ブラウザ上で動作するrustの実行環境です。
左上の実行ボタンを押して、このページ内のpractice4_1.rsに書かれている内容を実行します。

練習問題 2: 所有権のリレー

以下の process_string 関数は文字列を受け取って長さを出力しますが、戻り値がないため、呼び出し元で元の文字列が使えなくなってしまいます。 process_string を修正して、受け取った文字列の所有権を呼び出し元に返すようにし、main 関数でその後の処理ができるようにしてください。

ファイルを編集:practice4_2.rs
fn main() {
    let s = String::from("ownership");
    
    // ここで s を渡して、処理後に戻ってきた所有権を new_s で受け取るように修正する
    process_string(s); 
    
    // 修正後は以下のコメントを解除しても動作するようにする
    // println!("String is still valid: {}", new_s);
}

// 戻り値の型と実装を修正してください
fn process_string(input: String) {
    println!("Length is: {}", input.len());
}
ブラウザ上で動作するrustの実行環境です。
左上の実行ボタンを押して、このページ内のpractice4_2.rsに書かれている内容を実行します。
前のページ« 関数と制御フロー
次のページ借用とスライス »