参照と借用

9 分で読了
このページの目次

借用により、関数は値の所有権を引き継ぐことなく、一時的に値にアクセスできます。&T は共有アクセスを提供し、&mut T は排他的アクセスを提供します。参照は、使用中は有効で、アライメントが正しく、型の要件を満たすデータを指していなければなりません。アクセス権限、生存期間、アドレスの条件はすべて不可欠です。

「複数の読者または単一の書き込み者」という直感的なモデルは、通常のデータに適用されます。内部可変性により、共有参照の下でも別のプロトコルに従って変更が可能であり、排他的参照も一時的にリボロー(再借用)できるため、ルールを「構文的なすべての参照変数が共存できない」と誤解してはいけません。

Vec の要素借用が push を阻止する理由

push は Vec への排他的アクセスを必要とし、容量が不足すると再割り当てが行われ、既存の要素のアドレスが無効になる可能性があります。事前に容量を確保していても、通常の push インターフェースは依然として &mut Vec を要求するため、その後に要素の参照を使用する必要がある場合でも、借用ルールを回避することはできません。

要素 first = &values[0] を借用first を後で使用する場合、要素はアクセス可能でなければならない1first の最後の読み取り借用の制約はその後終了できる2その後、values を変更この時点で競合しない排他的借用を取得可能3借用制約は後の使用から推論されるfirst の使用を変更の後に移動すると、借用範囲も変化し、プログラムは拒否される可能性がある。
fn main() {
    let mut values = vec![1, 2, 3];
    let first = &values[0];
    assert_eq!(*first, 1);
    values.push(4);
    assert_eq!(values, [1, 2, 3, 4]);
}

assert_eq!(*first, 1) を push の後に移動すると、first の借用が変更を跨ぐことになり、コンパイルエラーが発生します。その後使用されない参照束縛を単に記述しただけでは、その後の変更が必ずしも不正であるとは限りません。

NLL(非構文論理生存期間)は、制御フローと使用制約に基づいて借用が必要な範囲を推論するため、構文的なブロックの末尾まで保守的に延長する必要はありません。それは単なるテキストスキャンの「最終行」ではなく、分岐、参照の返却、デストラクションの制約などが結果に影響を与える可能性があります。借用チェックの概要

使用位置を変更箇所の前後に移す

データと push の API は固定し、first の使用位置だけを変えます。橙色は変更の独占アクセス、青緑は後続の使用に必要な借用です。重なる場合は容量だけでなく、使用順序やインターフェースを見直します。

図を準備しています
使用位置を変更箇所の前後に移す

この直線的な例では、容量に余裕があっても push 後の要素参照の使用は競合します。

リボロー:既存の権限からより短い権限を派生させる

fn append(values: &mut Vec<i32>) { values.push(2); }
fn main() {
    let mut values = vec![1];
    let parent = &mut values;
    {
        let child = &mut *parent;
        append(child);
    }
    parent.push(3);
    assert_eq!(parent, &[1, 2, 3]);
}

parent は既存の &mut Vec であり、child がその対象へのリボローです。child が使用されている間、parent を通じて競合するアクセスを行うことはできません。child の借用が終了した後、parent を引き続き使用できます。元の Vec の所有権は渡されていないため、「関数から返った後に所有権が回復する」というわけではありません。

引数の型が &mut T である呼び出しは、暗黙的にリボローされることが多く、毎回明示的に &mut *parent と書く必要はありません。通常の代入や、値による消費を行うジェネリック関数は、すべてがリボローを行うとは限りません。&mut T 自体は Copy ではありません。

明示的なリボローとムーブされた参照変数

fn consume<T>(_: T) {}
fn main() {
    let mut number = 1;
    let parent = &mut number;
    consume(&mut *parent); // 渡されるのは新しく作成された短い借用
    *parent += 1;
    assert_eq!(*parent, 2);
}

consume(&mut *parent) を consume(parent) に変更すると、ジェネリック引数 T は &mut i32 となり、渡される参照値自体がムーブされ、その後の parent の使用はできなくなります。どちらの書き方も number の所有権をムーブするものではなく、違いは「どの参照値が消費されるか」です。修正する際は、インターフェースが実際に必要な &mut T を受け取るようにするか、明示的に短い借用を作成し、排他的参照に対して Clone を探すのではなくください。

排他的借用は互いに素な部分に分割できる

fn main() {
    let mut values = [1, 2, 3, 4];
    let (left, right) = values.split_at_mut(2);
    left[0] = 10;
    right[0] = 30;
    assert_eq!(values, [10, 2, 30, 4]);
}

2つの &mut スライスが同時に存在するのは安全です。なぜなら、要素の範囲が重複していないからです。コンパイラは構造体フィールドの独立した借用をいくつか認識できますが、一般的な動的インデックスの場合、任意の算術的不等式を推論するのではなく、split_at_mut などの証明済みのインターフェースを使用する必要があります。

動的に2つの要素を選択し、互いに素な関係をスライス境界に変換する

fn pair_mut<T>(values: &mut [T], i: usize, j: usize) -> Option<(&mut T, &mut T)> {
    if i == j || i >= values.len() || j >= values.len() { return None; }
    if i < j {
        let (left, right) = values.split_at_mut(j);
        Some((&mut left[i], &mut right[0]))
    } else {
        let (left, right) = values.split_at_mut(i);
        Some((&mut right[0], &mut left[j]))
    }
}
fn main() {
    let mut values = [10, 20, 30];
    let (a, b) = pair_mut(&mut values, 2, 0).unwrap();
    *a += 1;
    *b += 2;
    assert_eq!(values, [12, 20, 31]);
    assert!(pair_mut(&mut values, 1, 1).is_none());
    assert!(pair_mut(&mut values, 0, 3).is_none());
    assert!(pair_mut::<u8>(&mut [], 0, 0).is_none());
}

i < j の場合、j で分割し、i は必ず左半分、j は右半分の第0項に対応します。逆の場合は返却順序を交換し、呼び出し側が常に i を先に、次に j を得られるようにします。これにより、入力順序が保持され、安全なスライスAPIを通じて排他範囲が表現されます。ビジネスロジックで単に2つの位置を交換したいだけであれば、slice::swap を直接使用する方が簡単であり、2つの参照を取得する必要はありません。借用の分割

メソッドシグネチャが広すぎると不要な競合を引き起こす

struct Report { title: String, rows: Vec<usize> }
fn append_row(rows: &mut Vec<usize>, title: &str) {
    rows.push(title.len());
}
fn main() {
    let mut report = Report { title: String::from("demo"), rows: Vec::new() };
    let title = &report.title;
    append_row(&mut report.rows, title);
    assert_eq!(report.rows, [4]);
}

ここで title と rows は異なるフィールドであり、1つは共有読み取り、もう1つは排他的書き込みを同時に実行できます。もし &mut Report を受け取るメソッドに変更すると、呼び出し側の境界はオブジェクト全体を排他的に使用する可能性があることを宣言することになり、呼び出し側はメソッド内部で現在 rows のみを修正しているという事実だけで、この要件を狭めることはできません。補助関数の引数を狭める優先順位を高くすることは、RefCell を増やしたり title をコピーしたりするよりも、責任をより正確に表現するのに役立ちます。

参照変数に mut が不要でも、対象が変更される可能性がある

let reference = &mut value; において、reference バインディングに mut がなくても、それを通じて対象を変更できます。let mut reference とすると、さらにその変数を別の参照に再束縛できるようになります。「ポインタ変数を変更可能であること」と「排他的な対象アクセス権を持っていること」を区別することは、シグネチャを理解するのに役立ちます。

借用が終了したからといって、ガードがデストラクトされたわけではありません。RefCell の Ref や MutexGuard などは実行時状態を保持しており、通常、適切なスコープを退出するか明示的に drop して初めて対応するプロトコル状態が解放されます。通常の参照の NLL の直感をそのまま適用することはできません。

参照の返却と省略ルール

fn first_word(text: &str) -> &str {
    &text[..text.find(' ').unwrap_or(text.len())]
}
fn main() {
    let text = String::from("中文 rust");
    assert_eq!(first_word(&text), "中文");
}

この関数は、ローカルに新しく作成された String の借用を返すのではなく、入力からのサブスライスを返します。find はスペースのバイト位置を正確に見つけ出し、それは UTF-8 の境界上にあります。文字番号を任意にバイトオフセットとして扱うことはできません。

ここでは入力参照の生存期間が1つだけあり、出力はルールに従ってそれに関連付けられるため、明示的な注釈は不要です。入力が複数あり、曖昧さがある場合にのみ、より明確な関係を表現する必要があり、生存期間を参照してください。

String: "中文 rust"テキストとバイトバッファを所有first_word(&text)同じバッファからの &str を返すword を使用するソースの有効性を保つ借用されたビューの返却:ビューの消費が終了してからでないと、ソースを排他的に修正できないその後に word を使用する必要がある場合、それ以前に text.push でソースを変更することはできない実際に独立したテキストが必要な場合は、明示的に to_owned を使用して String を作成する。これにより所有関係が変化し、割り当てが行われる可能性がある。

借用された返却値は制約を呼び出し元に持ち帰る

fn first_word(text: &str) -> &str {
    &text[..text.find(' ').unwrap_or(text.len())]
}
fn main() {
    let mut text = String::from("中文 rust");
    let word = first_word(&text);
    assert_eq!(word, "中文");
    text.push('!'); // word はもはや使用されていない

    let owned = first_word(&text).to_owned();
    text.clear();
    assert_eq!(owned, "中文");
}

&str バージョンは結果をソースに関連付けるため、短期的な読み取りに適しています。String バージョンはストレージを明確に分離するため、ソースの変更や解放を跨ぐ使用に適しています。後者は「参照の生存期間を延長する」ものではなく、別の所有者が責任を負うテキストを作成するものです。

借用エラーが発生した際に順次確認する

問題対応処理
参照が変更後も使用されているか読み取り専用の消費を変更前に移動するか、結果の表現を変更する
2つの操作が実際には異なるフィールド/範囲を対象としているか関数引数を狭め、分割インターフェースを使用する
&mut 参照自体がムーブされたか明示的な引数型または明示的なリボローを使用する
長期的に独立したデータを保存する必要があるか所有値を返却し、コピーまたは共有のコストを明確にする
実際にはガードが実行時ロックを保持しているかガードのスコープを制限するか、明示的に drop する

これらの処理は、それぞれアクセスのタイミング、アクセス範囲、または所有関係を変更します。unsafe を使用してエラーを回避しても、関係が自動的に成立するわけではなく、clone を追加してもリソースの独立性が保証されるわけではありません。真に必要な契約を最初に特定する必要があります。

コンパイル時チェックの保証範囲

通常の借用チェックは、実行時に参照カウントを追跡せず、任意のリソースに対するビジネスロジックのロックも提供しません。&[T] と &dyn Trait はそれぞれメタデータを含んでいるため、すべての参照が固定の8バイトであるとは一概に言えません。unsafe、FFI、または内部可変性のラップに入った場合、追加の契約は実装側が維持します。安全な呼び出し側は、これらのラップが健全(sound)であることを前提に依存します。