Rustの所有権と借用:完全ガイド

Rustの所有権と借用システムをマスターします。所有権ルール、参照、ライフタイム、高度なメモリ管理パターンを解説します。

Rustの所有権と借用 - 完全ガイド

所有権システムは、Rustを他のあらゆるプログラミング言語と区別する特徴です。この独自のアプローチは、ガベージコレクタなしでメモリ安全性を保証し、実行時ではなくコンパイル時にバグを検出します。この詳細なガイドでは、所有権と借用のメカニズムを基礎から本番環境向けの高度なパターンまで解説します。

Rustの哲学

Rustコンパイラは厳格なプログラミングアシスタントとして機能します。コンパイル時にブロックされる所有権エラーは、本番環境で防がれた潜在的なバグを意味します。

所有権の3つの基本ルール

所有権システムは、シンプルでありながら厳格な3つのルールに基づいています。これらを身につければ、Rustのメンタルモデルは自然で予測可能なものになります。

ownership_rules.rsrust
// Demonstration of the three fundamental rules

fn main() {
    // Rule 1: Each value has exactly ONE owner
    let s1 = String::from("hello");  // s1 is the sole owner

    // Rule 2: There can only be one owner at a time
    let s2 = s1;  // Ownership transferred (moved) from s1 to s2
    // println!("{}", s1);  // Compile ERROR: s1 no longer exists
    println!("s2 = {}", s2);  // Only s2 is valid now

    // Rule 3: When the owner goes out of scope, the value is dropped
    {
        let s3 = String::from("temporary");
        println!("s3 inside block = {}", s3);
    }  // s3 is automatically freed here (drop is called)
    // println!("{}", s3);  // ERROR: s3 no longer exists
}

これら3つのルールは、use-after-free、double-free、メモリリークといったバグのカテゴリ全体を排除します。コンパイラはこれらのルールが守られていることを静的に検証します。

MoveとCopy:転送セマンティクスの理解

代入の振る舞いはデータ型に依存します。Copyトレイトを実装する型は複製され、それ以外の型はmoveによって転送されます。

move_vs_copy.rsrust
// Distinction between Copy types and Move types

fn main() {
    // Copy types: values stored on the stack, known size
    let x: i32 = 42;
    let y = x;  // x is COPIED, not moved
    println!("x = {}, y = {}", x, y);  // Both are valid

    // Other Copy types: f64, bool, char, tuples of Copy types
    let point = (3.0, 4.0);
    let point_copy = point;  // Tuple copy
    println!("Original: {:?}, Copy: {:?}", point, point_copy);

    // Move types: values on the heap, dynamic size
    let s1 = String::from("owned");
    let s2 = s1;  // s1 is MOVED to s2
    // println!("{}", s1);  // ERROR: value moved
    println!("s2 = {}", s2);

    // Vec, HashMap, Box are also Move types
    let vec1 = vec![1, 2, 3];
    let vec2 = vec1;  // Move, not copy
    // println!("{:?}", vec1);  // ERROR
    println!("vec2 = {:?}", vec2);
}

// Explicit clone to duplicate Move types
fn explicit_clone() {
    let original = String::from("important data");
    let clone = original.clone();  // Explicit duplication (memory cost)

    println!("Original: {}", original);  // Still valid
    println!("Clone: {}", clone);  // Independent copy
}

Move/Copyの区別は基本的なものです。代入が所有権を転送するのか、独立したコピーを作成するのかを決定します。

Cloneを使うタイミング

.clone()の呼び出しは意図的であるべきです。cloneだらけのコードは設計上の問題を示しているかもしれません。借用がより良い解決策となることが多いです。

借用:不変参照と可変参照

借用により、所有権を取得することなく値にアクセスできます。このメカニズムにより、Rustコードは安全かつ高性能になります。

borrowing_basics.rsrust
// Immutable and mutable references

fn main() {
    let s = String::from("hello");

    // Immutable reference: read-only, multiple allowed
    let len = calculate_length(&s);  // Immutable borrow
    println!("'{}' has {} characters", s, len);  // s still valid

    // Multiple simultaneous immutable references: OK
    let r1 = &s;
    let r2 = &s;
    let r3 = &s;
    println!("r1={}, r2={}, r3={}", r1, r2, r3);
}

fn calculate_length(s: &String) -> usize {
    // s is a reference, not the owner
    s.len()
}  // s goes out of scope but doesn't drop anything (not owner)

// Mutable references: modification allowed
fn mutable_borrowing() {
    let mut s = String::from("hello");

    change(&mut s);  // Mutable borrow
    println!("After modification: {}", s);
}

fn change(s: &mut String) {
    s.push_str(", world!");  // Modification via mutable reference
}

借用の黄金ルール:複数の不変参照、または1つの可変参照のいずれかであり、両者が同時に存在することは決してありません。

ボローチェッカーのルール

ボローチェッカーは借用ルールを検証するコンパイラのコンポーネントです。そのエラーを理解することで、問題を素早く解決できます。

borrow_checker_rules.rsrust
// Strict borrow checker rules

fn main() {
    // RULE 1: No mutable reference with immutable references
    let mut s = String::from("hello");

    let r1 = &s;      // Immutable reference: OK
    let r2 = &s;      // Another immutable reference: OK
    // let r3 = &mut s;  // ERROR: cannot borrow as mutable
    println!("{} and {}", r1, r2);

    // AFTER using r1 and r2, they are "dead"
    let r3 = &mut s;  // Now OK: r1 and r2 no longer used
    r3.push_str(" world");
    println!("{}", r3);

    // RULE 2: Only one mutable reference at a time
    let mut data = String::from("exclusive");
    let ref1 = &mut data;
    // let ref2 = &mut data;  // ERROR: already borrowed mutably
    ref1.push_str("!");
    println!("{}", ref1);
}

// RULE 3: References cannot outlive the data
fn dangling_reference_prevented() {
    let reference;
    {
        let s = String::from("short-lived");
        // reference = &s;  // ERROR: s doesn't live long enough
    }
    // s is dropped here, reference would be invalid

    // Solution: move the value out of the scope
    let owned_outside;
    {
        let s = String::from("moved out");
        owned_outside = s;  // Move, not reference
    }
    println!("{}", owned_outside);  // OK: owned_outside is the owner
}

ボローチェッカーはNon-Lexical Lifetimes (NLL)を使用します。参照は最後に使用された時点までアクティブとみなされ、スコープの終わりまでではありません。

Rustの面接対策はできていますか?

インタラクティブなシミュレーター、flashcards、技術テストで練習しましょう。

ライフタイム:参照の有効期間を注釈する

ライフタイムは、参照が有効であり続けることをコンパイラが検証するのを助ける注釈です。ほとんどの場合、自動的に推論されます。

lifetimes_explained.rsrust
// Explicit lifetime annotations

// Without annotation: compiler infers lifetimes
fn first_word(s: &str) -> &str {
    match s.find(' ') {
        Some(i) => &s[..i],
        None => s,
    }
}

// With explicit annotation: same function
fn first_word_explicit<'a>(s: &'a str) -> &'a str {
    // 'a means: returned reference lives as long as the input
    match s.find(' ') {
        Some(i) => &s[..i],
        None => s,
    }
}

// When annotations are necessary: multiple references
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
    // Compiler cannot guess which reference is returned
    if x.len() > y.len() {
        x
    } else {
        y
    }
}

fn main() {
    let string1 = String::from("long string");
    let result;
    {
        let string2 = String::from("xyz");
        result = longest(&string1, &string2);
        println!("Longest: {}", result);  // OK here
    }
    // println!("{}", result);  // ERROR if uncommented: string2 dropped
}

ライフタイムはデータが生きる期間を変更するものではなく、異なる参照のライフタイム間の関係を記述します。

構造体におけるライフタイム

構造体が参照を含む場合、構造体が参照先のデータより長く生存しないことを保証するため、ライフタイムを注釈する必要があります。

struct_lifetimes.rsrust
// Structs containing references

// Struct with reference: lifetime required
struct ImportantExcerpt<'a> {
    part: &'a str,  // This reference must live at least as long as the struct
}

impl<'a> ImportantExcerpt<'a> {
    // Method returning a reference with the same lifetime
    fn level(&self) -> i32 {
        3
    }

    // Elision rule: &self implies the output lifetime
    fn announce_and_return_part(&self, announcement: &str) -> &str {
        println!("Attention: {}", announcement);
        self.part  // Returns with 'a lifetime from self
    }
}

fn main() {
    let novel = String::from("Call me Ishmael. Some years ago...");
    let first_sentence = novel.split('.').next().unwrap();

    let excerpt = ImportantExcerpt {
        part: first_sentence,  // OK: novel outlives excerpt
    };

    println!("Excerpt: {}", excerpt.part);
    println!("Level: {}", excerpt.level());
}

// Static lifetime: reference valid for the entire program duration
fn static_lifetime_example() {
    let s: &'static str = "This string is in the binary";
    // String literals always have 'static lifetime
    println!("{}", s);
}

ライフタイム省略のルールにより、一般的なケースでは注釈を省略でき、コードが読みやすくなります。

高度なパターン:内部可変性

可変性をコンパイル時ではなく実行時に検証する必要がある場合があります。Rustはこのパターンに対応する型を提供しています:RefCellとCellです。

interior_mutability.rsrust
// Interior mutability with RefCell and Cell

use std::cell::{Cell, RefCell};

// Cell: for Copy types, replaces the entire value
struct Counter {
    count: Cell<u32>,  // Mutable despite &self
}

impl Counter {
    fn new() -> Counter {
        Counter { count: Cell::new(0) }
    }

    fn increment(&self) {
        // Modification via immutable reference!
        self.count.set(self.count.get() + 1);
    }

    fn get(&self) -> u32 {
        self.count.get()
    }
}

// RefCell: for non-Copy types, checks at runtime
struct CachedValue {
    value: RefCell<Option<String>>,
}

impl CachedValue {
    fn new() -> CachedValue {
        CachedValue { value: RefCell::new(None) }
    }

    fn get_or_compute(&self, compute: impl FnOnce() -> String) -> String {
        // borrow() for reading, borrow_mut() for writing
        if self.value.borrow().is_none() {
            *self.value.borrow_mut() = Some(compute());
        }
        self.value.borrow().as_ref().unwrap().clone()
    }
}

fn main() {
    let counter = Counter::new();
    counter.increment();
    counter.increment();
    println!("Counter: {}", counter.get());  // 2

    let cache = CachedValue::new();
    let result = cache.get_or_compute(|| {
        println!("Expensive computation...");
        String::from("result")
    });
    println!("Value: {}", result);

    // Second call: no recomputation
    let result2 = cache.get_or_compute(|| String::from("never executed"));
    println!("Cache hit: {}", result2);
}

RefCellとCellは借用検証を実行時に移します。ルール違反はコンパイルエラーではなくpanicを引き起こします。

Panicに注意

RefCell::borrow_mut()は、値がすでに借用されている場合panicします。明示的なエラー処理にはtry_borrow_mut()を使うとよいでしょう。

スマートポインタと所有権

Box、Rc、Arcなどのスマートポインタは、特定のユースケースに対して異なる所有権戦略を提供します。

smart_pointers.rsrust
// Box, Rc, and Arc for different ownership patterns

use std::rc::Rc;
use std::sync::Arc;
use std::thread;

// Box: single owner, data on the heap
fn box_example() {
    let boxed = Box::new(vec![1, 2, 3, 4, 5]);
    println!("Boxed vec: {:?}", boxed);
    // Useful for: recursive types, large objects, trait objects
}

// Rc: reference counting, multiple owners (single-thread)
fn rc_example() {
    let data = Rc::new(String::from("shared data"));

    let clone1 = Rc::clone(&data);  // Increments the counter
    let clone2 = Rc::clone(&data);

    println!("Count: {}", Rc::strong_count(&data));  // 3
    println!("All share: {}, {}, {}", data, clone1, clone2);
}  // Freed when counter reaches 0

// Arc: thread-safe Rc (Atomic Reference Counting)
fn arc_example() {
    let data = Arc::new(vec![1, 2, 3]);

    let handles: Vec<_> = (0..3).map(|i| {
        let data_clone = Arc::clone(&data);
        thread::spawn(move || {
            println!("Thread {}: {:?}", i, data_clone);
        })
    }).collect();

    for handle in handles {
        handle.join().unwrap();
    }
}

fn main() {
    box_example();
    rc_example();
    arc_example();
}

スマートポインタの選択は所有権パターンに依存します:単一(Box)、シングルスレッドでの共有(Rc)、マルチスレッドでの共有(Arc)です。

実用的な所有権パターン

所有権システムを中心にコードを構造化するための一般的なパターンを示します。

ownership_patterns.rsrust
// Practical patterns for ownership management

// Pattern 1: Builder pattern with chained ownership
struct RequestBuilder {
    url: String,
    headers: Vec<(String, String)>,
    timeout: Option<u64>,
}

impl RequestBuilder {
    fn new(url: &str) -> Self {
        RequestBuilder {
            url: url.to_string(),
            headers: Vec::new(),
            timeout: None,
        }
    }

    // Consumes self and returns the new self
    fn header(mut self, key: &str, value: &str) -> Self {
        self.headers.push((key.to_string(), value.to_string()));
        self  // Returns ownership
    }

    fn timeout(mut self, seconds: u64) -> Self {
        self.timeout = Some(seconds);
        self
    }

    fn build(self) -> Request {
        Request {
            url: self.url,
            headers: self.headers,
            timeout: self.timeout.unwrap_or(30),
        }
    }
}

struct Request {
    url: String,
    headers: Vec<(String, String)>,
    timeout: u64,
}

// Pattern 2: Cow (Copy-on-Write) to avoid allocations
use std::borrow::Cow;

fn process_text(input: &str) -> Cow<str> {
    if input.contains("REPLACE") {
        // Allocation only if modification needed
        Cow::Owned(input.replace("REPLACE", "NEW"))
    } else {
        // No allocation, returns a reference
        Cow::Borrowed(input)
    }
}

// Pattern 3: Take to extract from an Option
fn extract_value(data: &mut Option<String>) -> String {
    data.take().unwrap_or_else(|| String::from("default"))
    // take() replaces with None and returns ownership of the value
}

fn main() {
    // Builder pattern
    let request = RequestBuilder::new("https://api.example.com")
        .header("Authorization", "Bearer token")
        .header("Content-Type", "application/json")
        .timeout(60)
        .build();

    println!("URL: {}, Timeout: {}s", request.url, request.timeout);

    // Cow pattern
    let text1 = process_text("hello world");  // No allocation
    let text2 = process_text("hello REPLACE");  // Allocation
    println!("{} | {}", text1, text2);

    // Take pattern
    let mut optional = Some(String::from("extracted"));
    let value = extract_value(&mut optional);
    println!("Value: {}, Option: {:?}", value, optional);  // None
}

これらのパターンは所有権システムを活用して、人間工学的で高性能なAPIを作成します。

今すぐ練習を始めましょう!

面接シミュレーターと技術テストで知識をテストしましょう。

まとめ

Rustの所有権と借用システムは、メモリ管理におけるパラダイムシフトを表しています。一度マスターすれば、高性能で安全なコードを書くための強力な味方となります。

重要なポイント:

✅ 所有権の3つのルール:単一の所有者、所有権の転送、自動drop

✅ 借用:複数の不変参照、または1つの排他的可変参照

✅ ライフタイム:参照のライフタイム間の関係を注釈する

✅ 内部可変性:実行時に検証される可変性のためのRefCellとCell

✅ スマートポインタ:Box(単一)、Rc(共有)、Arc(thread-safe)

✅ 実用的なパターン:イディオマティックなAPIのためのBuilder、Cow、Take

ボローチェッカーは最初は厳しく感じるかもしれませんが、報告される各エラーは回避された潜在的なバグを表します。実践を通じて、所有権の観点で考えることが自然になり、すべての言語におけるコード品質が向上します。

今すぐ練習を始めましょう!

面接シミュレーターと技術テストで知識をテストしましょう。

今日のチャレンジ

Rust のバグを見つけられますか

実際のコード、隠れたバグ、1日1回。アカウントなしで試せます。

Anthony Fillion-Maillet

執筆

Anthony Fillion-Maillet

SharpSkill 創業者

10 年以上フルスタック開発に携わっています。SharpSkill を運営し、ここで公開される内容に責任を負っています。

2026年4月29日 更新

タグ

#rust
#ownership
#borrowing
#memory management
#systems programming

共有

関連記事