Use Cases (更新: 2026/6/7)

Rust入門でcloneだらけになる前に:所有権・借用・Resultの考え方

Rustの所有権・借用・ライフタイムをモノの貸し借りで理解し、Result/Optionでエラーを安全に扱う。コピペで動くCLIと、いつRustを選ぶかの判断軸まで。

Rust入門でcloneだらけになる前に:所有権・借用・Resultの考え方

Rustを始めて3日目、僕のコードは .clone() だらけになっていました。

コンパイラが「value がここで move された後に使われている」と怒る。意味はよく分からない。でも .clone() を付けると赤い波線が消える。だから、エラーが出るたびに .clone() を足していった。動きはする。けれど、なぜ動くのかは一つも分かっていませんでした。

これ、Rust初心者の通過儀礼みたいなものです。コンパイラの声を「うるさい門番」だと思っているうちは、ずっとこのループから抜けられません。

でも、所有権と借用を「モノの貸し借り」として一度ちゃんと飲み込むと、世界が反転します。あのうるさい門番は、本番でメモリ事故を起こす前に止めてくれる味方だった、と気づくんです。この記事では、その飲み込み方を、小さなCLIを実際に作りながら共有します。

この記事の要点

  • 所有権は「このデータの持ち主は一人だけ」というルール。持ち主が変わる(move)と、元の変数はもう使えない。
  • **借用(&)**は「持ち主を変えずに、ちょっと貸す」こと。これを覚えると .clone() 地獄から抜けられる。
  • エラー処理は ResultOption で型に乗せる。null も例外も投げない。「失敗するかも」が型に書いてある。
  • まず cargo new で小さく作り、cargo testcargo clippy でふるまいを固定してから育てる。
  • Rustが向くのは速度・省メモリ・壊れにくさが効く場面。スクリプト的な使い捨てには重い。
  • コンパイラエラーは敵じゃなく、本番事故を前倒しで見せてくれる無料のレビュアー。

Rustが「むずかしい」と言われる本当の理由

Rustが難しいのは、文法が奇抜だからではありません。他の言語が裏でこっそりやっていたメモリ管理を、表に出して書かせるからです。

PythonやJavaScriptには、ガベージコレクタという「使い終わったメモリを後ろで片付ける係」がいます。便利な反面、いつ片付くかは制御できず、たまに動作が一瞬止まる。一方Cやc++はその係がいないので、片付けを手書きします。書き忘れればメモリリーク、二重に片付ければクラッシュ。自由ですが、地雷原です。

Rustは第三の道を選びました。「誰がこのデータの持ち主か」をコンパイル時に追跡し、持ち主がいなくなった瞬間に自動で片付ける。GCのような実行時の係はいないのに、手書きの地雷もない。その代わり、持ち主のルールをあなたが守っているか、コンパイラが厳しくチェックします。

これが所有権・借用・ライフタイムの正体です。順に、貸し借りの言葉で見ていきます。

所有権:データの持ち主は一人だけ

所有権のルールは、たった一行です。「ある値の持ち主(owner)は、つねに一人だけ」

レンタルした傘で考えると早いです。傘はあなたが借りた。それを友達に「はい」と渡したら、もうあなたの手元にはない。返してもらうまで、あなたは使えません。Rustの String もこれと同じ動きをします。

fn main() {
    let a = String::from("傘");
    let b = a; // 持ち主が a から b へ移った(move)
    // println!("{a}"); // これはコンパイルエラー:a はもう傘を持っていない
    println!("{b}"); // b が持ち主なのでOK
}

JavaScriptの感覚なら「ab も同じ文字列を指すだけ」ですが、Rustは違います。let b = a持ち主が引っ越したa を使おうとすると、コンパイラが「move された後だよ」と止めます。

ここで初心者がやりがちなのが、僕がやった .clone() 連打です。let b = a.clone() と書けば、傘をもう一本コピーするので両方使えます。でも、データが大きければコピーのコストもメモリも倍。「とりあえず clone」は、ほぼ毎回もっと良いやり方があるサインです。その良いやり方が、次の借用です。

借用:持ち主を変えずに、ちょっと貸す

.clone() でコピーする代わりに、& を付けて「貸す」だけにできます。これが借用です。

図書館の本だと思ってください。本を読みたい人全員に新品を渡していたら破産します。ふつうは「貸出」しますよね。持ち主(図書館)は変わらず、読みたい人は一時的に借りて、読み終わったら返す。Rustの & はこの貸出です。

fn main() {
    let title = String::from("Rust入門");

    let length = count_chars(&title); // title を「貸す」だけ。持ち主は main のまま
    println!("{title} は {length} 文字"); // 貸しただけなので title はまだ使える
}

// &str を借りる:持ち主にならない。読むだけ
fn count_chars(text: &str) -> usize {
    text.chars().count()
}

count_chars&str(文字列の貸出)を受け取ります。持ち主にならないので、関数が終わっても titlemain の手元に残る。.clone() でコピーする必要は、どこにもありません。

借用には2種類あります。読むだけの借用(&T)は何人でも同時にOK。でも書き換える借用(&mut T)は、その瞬間に一人だけ。これも図書館で考えると自然です。同じ本を5人が同時に読むのは平気。でも一人が本に書き込んでいる最中に、他の人が読んだら内容が食い違う。だからRustは「書き換え中は独占」を強制します。このルールのおかげで、複数スレッドが同じデータを壊し合う事故(データ競合)がコンパイル時点で消えます。

書き方意味同時に何個までたとえ
value(そのまま渡す)所有権を渡す(move)傘をあげる
&value読むだけ借りる何個でもOK本を一緒に眺める
&mut value書き換えに借りる1個だけ本に書き込む
value.clone()丸ごとコピー新品をもう一冊買う

迷ったら、まず & で借りる。借用で書けない事情が出てきて初めて所有や clone を考える。この順番にするだけで、コードはぐっと素直になります。

ライフタイム:その貸出はいつまで有効か

ライフタイムは、所有権・借用に比べて身構えられがちですが、考え方はシンプルです。「借りたモノは、持ち主より長生きできない」。それだけです。

返却期限のない貸出を想像してください。図書館が閉館して本を処分したのに、あなたの手元に「その本への参照」だけ残っていたら? 中身は消えているのに参照だけある、宙ぶらりんの状態です。これがいわゆるダングリング参照で、c++ではクラッシュの定番。Rustはこれをコンパイル時に禁止します。

fn main() {
    let result;
    {
        let temp = String::from("一時的な値");
        result = &temp; // temp を借りた
    } // ここで temp は片付けられる(持ち主が消えた)
    // println!("{result}"); // コンパイルエラー:借りた先がもう存在しない
}

多くの場合、ライフタイムはコンパイラが自動で推論するので、明示的に 'a のような注釈を書く場面は最初は多くありません。「借りた値が、貸し主より長く生きようとしたら止まる」——この感覚さえあれば、エラーメッセージ borrowed value does not live long enough(借りた値の寿命が足りない)が読めるようになります。

ここまでが、Rustの背骨です。所有権・借用・ライフタイム。全部「貸し借り」の言葉で説明できる、と分かれば、もう半分は越えています。

エラー処理:Result と Option で「失敗するかも」を型に書く

Rustの二つ目の特徴が、エラーの扱い方です。例外も null も投げません。代わりに、戻り値の型に「失敗するかも」を書きます。

  • Option<T>:値が「あるかも、ないかも」。Some(値)None。他言語の null を、型で安全にしたもの。
  • Result<T, E>:処理が「成功するかも、失敗するかも」。Ok(値)Err(エラー)

何が嬉しいか。null を返す言語では、呼び出し側が null チェックを忘れても、コンパイルは通ってしまう。そして本番で NullPointerException が出る。Rustでは Option<T> をそのまま値として使えません。SomeNone かを必ず分岐させないとコンパイルが通らない。つまり「チェック忘れ」が構造的に起きません。

fn main() {
    let numbers = vec![10, 20, 30];

    // get は「あるかも、ないかも」なので Option を返す
    match numbers.get(1) {
        Some(value) => println!("見つかった: {value}"),
        None => println!("その位置に値はない"),
    }

    // 範囲外でも panic せず None が返る(numbers[99] だと panic する)
    match numbers.get(99) {
        Some(value) => println!("見つかった: {value}"),
        None => println!("99番目はない"), // こっちが実行される
    }
}

Result も同じ発想です。「失敗するかも」を呼び出し側に必ず意識させる。次の実例で、ファイル読み込みに Result を使います。

コピペで動く:タスク集計CLIを作る

理屈は実物で固めましょう。マークダウン風の [ ] やること / [x] 終わったこと を読んで、完了数と未完了数を数える小さなCLIを作ります。cargo さえ入っていれば動きます。

まずプロジェクトを最小構成で作ります。CargoはRustのビルド・依存管理・テスト・実行をまとめて担う標準ツールです。

cargo new tasknote --bin
cd tasknote

依存は2つだけ。引数処理の clap と、エラーに文脈を足す anyhow。最初から盛りすぎないのがコツです。edition は今の安定版である 2024 を使います(古い記事の 2018 をそのまま持ってこないよう、生成された Cargo.toml の現物を確認してください)。

[package]
name = "tasknote"
version = "0.1.0"
edition = "2024"

[dependencies]
anyhow = "1"
clap = { version = "4", features = ["derive"] }

ロジックは src/lib.rs に置きます。main.rs に全部書くより、テストしやすく、後で直すときも差分が小さくなります。ここまでの所有権・借用・Result が全部出てくるので、コメントを追いながら読んでみてください。

// src/lib.rs
use anyhow::{Context, Result};
use std::{fs, path::Path};

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Task {
    pub title: String, // この構造体が title を所有する
    pub done: bool,
}

// &str を借りるだけ。文字列の持ち主は呼び出し側のまま
pub fn parse_tasks(input: &str) -> Vec<Task> {
    input.lines().filter_map(parse_task_line).collect()
}

// 1行を見て Task かどうか判定。該当しなければ None(Option で「ないかも」を表す)
fn parse_task_line(line: &str) -> Option<Task> {
    let trimmed = line.trim();

    if let Some(title) = trimmed
        .strip_prefix("[x] ")
        .or_else(|| trimmed.strip_prefix("[X] "))
    {
        return Some(Task {
            title: title.trim().to_string(), // ここで初めて所有する文字列にする
            done: true,
        });
    }

    if let Some(title) = trimmed.strip_prefix("[ ] ") {
        return Some(Task {
            title: title.trim().to_string(),
            done: false,
        });
    }

    None
}

// tasks を読むだけなので &[Task] を借用。所有は奪わない
pub fn summarize(tasks: &[Task]) -> String {
    let total = tasks.len();
    let done = tasks.iter().filter(|task| task.done).count();
    let open = total.saturating_sub(done); // 引き算がマイナスにならない安全版

    format!("{total} tasks: {done} done, {open} open")
}

// 失敗しうる処理なので Result を返す。? で失敗を呼び出し側に伝える
pub fn read_tasks(path: impl AsRef<Path>) -> Result<Vec<Task>> {
    let path = path.as_ref();
    let content = fs::read_to_string(path)
        .with_context(|| format!("failed to read {}", path.display()))?; // 文脈を足す

    Ok(parse_tasks(&content))
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn parses_markdown_style_tasks() {
        let tasks = parse_tasks("[ ] write parser\n[x] add tests\n[X] run clippy\n");
        assert_eq!(tasks.len(), 3);
        assert_eq!(tasks[0].done, false);
        assert_eq!(tasks[1].done, true);
    }

    #[test]
    fn ignores_unrecognized_lines() {
        let tasks = parse_tasks("# Sprint notes\n- plain bullet\n[ ] keep this\n");
        assert_eq!(tasks.len(), 1);
        assert_eq!(tasks[0].title, "keep this");
    }

    #[test]
    fn summarizes_counts() {
        let tasks = parse_tasks("[ ] one\n[x] two\n[ ] three\n");
        assert_eq!(summarize(&tasks), "3 tasks: 1 done, 2 open");
    }
}

次に入口の src/main.rsmain は引数を受け取ってライブラリ関数を呼ぶだけにします。

// src/main.rs
use anyhow::Result;
use clap::Parser;
use std::path::PathBuf;
use tasknote::{read_tasks, summarize};

#[derive(Parser, Debug)]
#[command(name = "tasknote", about = "Summarize simple task files")]
struct Cli {
    #[arg(short, long, default_value = "tasks.txt")]
    file: PathBuf,

    #[arg(long)]
    only_open: bool,
}

// main も Result を返せる。read_tasks の失敗が ? で素直に伝わる
fn main() -> Result<()> {
    let args = Cli::parse();
    let tasks = read_tasks(&args.file)?;

    if args.only_open {
        for task in tasks.iter().filter(|task| !task.done) {
            println!("- {}", task.title);
        }
    } else {
        println!("{}", summarize(&tasks));
    }

    Ok(())
}

試すファイル tasks.txt はこんな中身にします。

[ ] write parser
[x] add unit tests
[ ] run clippy

実行はこの順番で十分です。

cargo fmt
cargo test
cargo clippy --all-targets -- -D warnings
cargo run -- --file tasks.txt
cargo run -- --file tasks.txt --only-open

mainResult<()> を返しているので、ファイルが見つからないときは anyhowContext が「どのファイルを読めなかったか」まで表示します。unwrap() で落とすより、読んだ人が次に何を直せばいいか分かる。これがRust流のエラー処理の効きどころです。

僕がRustでやらかした失敗3つ

正直に書きます。最初の数週間は、毎日コンパイラに叱られていました。

ひとつ目は、冒頭の .clone() 連打。エラーを消すために何も考えず付けていたら、本来は借用で済む関数まで全部コピーになっていました。データの持ち主が誰なのか、自分でも追えなくなる。今は「この clone& で済まない?」と一度立ち止まる癖をつけてから、無駄なコピーが激減しました。

ふたつ目は、サンプルの unwrap() を本番に残したことunwrap() は「失敗したら問答無用でクラッシュ」です。練習では楽でいい。でもファイル読み込み、設定パース、ネットワークみたいに普通に失敗する場所に残すと、ユーザーには訳の分からないパニックメッセージだけが出ます。ResultContext に置き換えてから、エラーが「読めるメッセージ」になりました。

みっつ目は、テストなしで大きく書き換えたこと。AIにリファクタを頼んだら、確かに動く差分が返ってきた。でもレビューしきれない量で、後から仕様がズレているのに気づきました。今は先に cargo test でふるまいを固定し、cargo clippy で警告をエラー扱いにしてから手を入れます。Rustは型で多くを守ってくれますが、仕様のズレはテストでしか止められません

いつRustを選ぶか(そして選ばないか)

ここが一番、検索しても答えが曖昧なところだと思います。僕の中の判断軸を正直に出します。

Rustが効く場面

  • 速度と省メモリが効く:CLIツール、画像/動画処理、ゲームエンジン、データベース、ネットワークサーバ。GCの一瞬の停止すら嫌な領域。
  • 長く動かす・壊れたら困る:常駐サービス、組み込み、決済まわり。「コンパイルが通れば、まず一クラスの事故は起きない」安心が効く。
  • 並行処理を安全に書きたい:複数スレッドが同じデータを壊す事故を、コンパイラが先に止めてくれる。
  • WebAssembly:ブラウザで重い計算を速く回したいとき。これは別記事で実際にビルドまでやっています → RustからWasmをビルドしてJavaScriptと連携する

Rustが重い場面

  • 使い捨てスクリプト:ログを一回集計するだけ、みたいな作業は、所有権を考える前にPythonで書いたほうが速い。
  • 要件が毎日ひっくり返るプロトタイプ:型を固めた直後に仕様が変わると、Rustの厳しさが足かせになる。
  • チームにRust経験者がゼロ:学習コストは正直ある。借用で詰まる時間を見込めないなら、別言語のほうが現実的なことも。

同じ「速い系」でも、もっと手軽さ寄りなのがGoです。Goは所有権の概念がなく、軽いgoroutineで並行処理を書けます。代わりにGCがある。**「ガッチリ安全 vs 手軽な並行」**で住み分けると分かりやすい。Go側の落とし穴と任せ方は Claude CodeでGoを書くと共有mapが壊れる にまとめました。読み比べると、なぜRustが所有権をあそこまで厳しくするのかが逆に見えてきます。

Claude Codeと一緒にRustを学ぶときのコツ

Rustは「難しいからAIに丸投げ」が一番もったいない言語です。コンパイラの叱責こそ最高の教材なので、エラーを消すだけでなく、なぜ直すのかを言語化させるのが効きます。

所有権エラーで詰まったら、こう聞きます。

このコードで `cannot borrow as mutable` が出ました。
エラー全文と該当関数を貼ります。

「修正コードだけ」ではなく、まず次を説明してください:
- なぜこの借用がコンパイラに止められたのか
- 借用(&)で直せるのか、所有(move)が必要なのか
- clone() を増やさずに済む書き方はどれか

その上で、最小の差分で直してください。

そしてコード生成を頼むときは、検証コマンドを同じ依頼に必ず入れる。Claude Codeはコードベースを読み、編集し、コマンドを実行できるツールなので、生成と検証を一つの作業単位にできます。

src/lib.rs と src/main.rs を実装してください。
完了後に cargo fmt、cargo test、cargo clippy --all-targets -- -D warnings を実行。
失敗したらエラー全文を要約してから最小差分で修正。
unwrap() はテスト以外で使わないでください。
触ってよいファイルは src/lib.rs, src/main.rs, テストだけ。

ただしAIの報告だけで安心せず、最後は自分で git diff とコマンド結果を見ます。テストの進め方は Claude CodeでTDD、変更の確認手順は レビュー用チェックリスト が地続きで役立ちます。権限を絞りたいチームは Claude Code権限設定ガイド も合わせて。公式の一次情報は Rust Bookの所有権の章 が決定版です。迷ったらここに戻ってください。

よくある質問

Q. 所有権と借用、結局どっちを使えばいいですか? 迷ったら、まず借用(&)から試してください。関数に値を渡すとき、その関数が「読むだけ」なら &T、「書き換える」なら &mut T、「保管して持ち続ける」必要があって初めて所有を渡します。.clone() は最後の手段です。

Q. .clone() は使ったら負けですか? いいえ。小さな値のコピーや、所有権の事情でどうしても必要な場面では .clone() が正解です。問題は「エラーを消すためだけの理由なき clone」。意図して付けた clone はOK、波線を消すための clone は要注意、と覚えてください。

Q. Result? 演算子は何をしていますか? ? は「Ok なら中身を取り出し、Err なら即座にこの関数から Err を返す」省略記法です。read_tasksfs::read_to_string(path)...? は、読めなければそこで関数を抜けて、呼び出し側にエラーを渡します。try/catch のネストを書かずに、失敗を上へ伝播できます。

Q. OptionResult の使い分けは? 「値があるか・ないか」だけなら Option(例:配列の getHashMap の検索)。「処理が成功したか・失敗したか、失敗なら理由も」なら Result(例:ファイル読み込み、パース、ネットワーク)。理由を伝えたいかどうかが分かれ目です。

Q. ライフタイム注釈 'a はいつ書くんですか? 最初のうちはほとんど書きません。コンパイラが自動推論できる場面が多いからです。構造体が参照を保持するときなど、推論しきれないケースで初めて要求されます。エラーで 'a を求められてから学んでも遅くないので、入門段階では深追い不要です。

まとめ

Rustの所有権・借用・ライフタイムは、全部「モノの貸し借り」で説明がつきます。持ち主は一人(所有)、読みたいなら借りる(&)、借りたモノは持ち主より長生きできない(ライフタイム)。エラーは投げずに型に乗せる(Result / Option)。この4点が腹落ちすれば、コンパイラの赤い波線は「敵の妨害」から「本番事故の前倒し通知」に変わります。

まずは上のタスク集計CLIを cargo run まで動かして、summarize&[Task] をわざと Vec<Task> に変えてみてください。コンパイラが何を言うか、何を守ろうとしているかが見えてきます。そこからが、Rustが速くて壊れにくい理由を、頭ではなく手で理解するスタートです。

さらに手を動かす題材や、CLAUDE.md・権限設定・レビュー手順を整理した教材は 教材一覧 にまとめています。実務リポジトリに合わせてRust開発フローを組みたい場合は 研修・導入相談 へどうぞ。

#Rust #Rust入門 #所有権 #借用 #エラー処理 #Claude Code
無料

無料PDF: Claude Code はじめてのチートシート

まずは無料PDFで基本コマンドと最初の使い方をまとめて確認してください。登録後はそのままテンプレート集や導入相談にも進めます。

スパムは送りません。登録情報は厳重に管理します。

Claude Codeを仕事で使える形にしませんか?

まず無料PDFで基本を固め、繰り返し使う作業はGumroad教材へ、チーム導入や権限設計は導入相談へ進めます。

Masa

この記事を書いた人

Masa

Claude Codeの実務活用、導入設計、収益導線改善を検証しているエンジニア。10言語の技術メディアを運営中。

PR

関連書籍・参考図書

この記事のテーマに関連する書籍を楽天ブックスで探せます。

※ 当サイトは楽天市場のアフィリエイトプログラムに参加しています。上記リンクから商品をご購入いただくと、運営者に紹介料が支払われる場合があります。