Box、Rc、Arc

このページの目次

これらの型はまず、値を存続させる責任を誰が負うか、そして最終的に誰が解放するかを決定します。Box は単一の所有者を表現し、Rc と Arc は複数の共有所有者を表現します。内部値の修正が必要かどうか、スレッド間で同期する必要があるかどうかは、別の問題のグループです。

型所有権クローンの意味典型的な用途
Box独占T: Clone の場合、内部値を新しい Box にクローン再帰構造、DST の所有、独立した割り当て
Rc共有、非アトミックカウント強い参照を増やすだけで、T はクローンしないスレッド単一の共有構造
Arc共有、アトミックカウント強い参照を増やすだけで、T はクローンしないスレッドの制約を満たす共有所有権
WeakT を存続させない弱い参照を増やす逆方向のリンク、所有権を持たない観察者

Box は再帰的なサイズを間接参照に変える

ノードが直接次の同型のノードを含んでいる場合、そのサイズは自身に再帰的に依存します。Box を使って次のノードを間接的に保持することで、現在のノードのサイズを確定できます。Box や Box<[T]> は、コンパイル時に具体的なサイズが不明な値を所有することもできます。

Sized な T に対して、通常の Box は単一ポインタによる表現を保証しますが、ポインタの幅はターゲットプラットフォームに依存します。DST のポインタは対応するメタデータも携带しており、一律に「8 バイト」とは呼びません。ゼロサイズ型の Box は、T のために非ゼロのヒープ領域を確保する必要はありません。Box

Box::new(large_array) は、ヒープ上の値を最終的に所有することを表現していますが、構築過程で大配列のスタック上の一時オブジェクトが絶対に生成されないことを保証するものではありません。大きなバッファは Vec から構築してから into_boxed_slice すべきです。ソースコードにポインタが 1 つあるからといって、ピーク時のスタック使用量を推論しないでください。

fn main() {
    let bytes: Box<[u8]> = vec![0; 1_000_000].into_boxed_slice();
    assert_eq!(bytes.len(), 1_000_000);
    let moved = bytes; // 所有者を移動、バッファ全体が移動するわけではない
    assert_eq!(moved[0], 0);
}

Box 自体は移動できますが、内部の値が固定されているわけではありません。有効な &mut T を通じて、T を置き換えたり移出したりできます。アドレス固定の契約が必要な場合は Pin に関係します。メモリレイアウト を参照してください。

Box の移動と Box 内の値の移動

fn main() {
    let boxed = Box::new(String::from("owned"));
    let address = (&*boxed) as *const String;
    let moved = boxed;
    assert_eq!((&*moved) as *const String, address);
    let value: String = *moved;
    assert_eq!(value, "owned");
}

Box のハンドルを移動しても、ヒープ上の String オブジェクトは元の位置に残っています。その後 *moved によって String の所有権を取得し、Box を消費します。String が所有するバイトバッファと、ヒープ上の String オブジェクト自体は分けて考える必要があります。ハンドル移動の前後でアドレスが同じだからといって、T が永遠に移動不可能である証明にはならず、Pin の契約に代わるものではありません。

再帰構造では破棄パスも考慮する必要があります。Box は型のサイズの無限再帰を解決しますが、長いチェーンにおける逐層の Drop 呼び出しの深さを自動的に解消するわけではありません。非常に長いリストでは、明示的なイテレーションによる解体が必要になる場合があります。連続した要素のみであれば、Vec はレイアウトと走査の面でより直接的な選択肢となることが多いです。

Rc と Arc が共有するのは同じ値

Rc::clone(&a) と Arc::clone(&a) は新しい所有権ハンドルを作成しますが、通常 T: Clone は要求されません。値とカウントの配置は実装の詳細であり、あるプラットフォーム上のフィールドのバイト数を安定した ABI として描画すべきではありません。

Rc は Send でも Sync でもありません。Arc のカウントはスレッド間で調整できますが、デフォルトのアロケータを持つ Arc において、Send と Sync を実現するには依然として T: Send + Sync が必要です。Arc<RefCell> は、自動的にスレッドセーフな内部借用を得るわけではありません。Arc

Arc のアトミックカウントのコストは、アーキテクチャ、競合、キャッシュの影響を受けます。「Rc より 10 倍遅い」という一般的な結論は出せません。頻繁なクローンが重要かどうかは、実際のホットパスで測定すべきです。借用 &T で十分であれば、毎回所有者を作成する必要はありません。

値の等価性と同一の共有オブジェクト

use std::rc::Rc;
fn main() {
    let first = Rc::new(String::from("same"));
    let shared = Rc::clone(&first);
    let independent = Rc::new(String::from("same"));
    assert_eq!(first, independent); // 内容が等しい
    assert!(Rc::ptr_eq(&first, &shared));
    assert!(!Rc::ptr_eq(&first, &independent));
    assert_eq!(Rc::strong_count(&first), 2);
    drop(shared);
    assert_eq!(Rc::strong_count(&first), 1);
}

Rc::clone は所有権ハンドルを明示的に複製します。(*first).clone() は String の Clone を呼び出します。後者は別の String を生成しますが、一般的な型の Clone が必ずしも到達可能なデータをすべて深コピーするとは限りません。例えば Arc フィールドを持つ構造体は、そのフィールドを依然として共有する可能性があります。したがって、「T をクローンした」からといって「完全に隔離されたオブジェクトグラフが得られる」わけではありません。

関数が呼び出し中のみデータを読み取る場合、&T を受け取るだけで十分です。共有所有権を保持する必要がある場合にのみ、Rc/Arc を受け取るかクローンして、対応する契約を表現します。すべての引数を Arc に変更すると、それを必要としないインターフェースに割り当てと共有戦略が伝播してしまいます。

Weak:値を存続させないが、安全に所有権の取得を試みられる

少なくとも 1 つの強い参照T は存続;Weak は upgrade して強い参照を取得できる1最後の強い参照が解放されるT をデストラクト;既存の Weak の upgrade は None を返す2最後の Weak も解放される参照カウントが使用していた残りの割り当てを回収可能3値のライフサイクルと参照カウント割り当てのライフサイクル通常の Rc/Arc のライフサイクルの例;Weak は T を存続させず、生ポインタにも等しくない。
use std::cell::Cell;
use std::rc::Rc;

struct Tracked(Rc<Cell<usize>>);
impl Drop for Tracked {
    fn drop(&mut self) { self.0.set(self.0.get() + 1); }
}

fn main() {
    let drops = Rc::new(Cell::new(0));
    let first = Rc::new(Tracked(Rc::clone(&drops)));
    let second = Rc::clone(&first);
    let observer = Rc::downgrade(&first);
    drop(first);
    assert!(observer.upgrade().is_some());
    assert_eq!(drops.get(), 0);
    drop(second);
    assert_eq!(drops.get(), 1);
    assert!(observer.upgrade().is_none());
}

upgrade が成功して返された新しい強い参照は、その存続期間中 T を存続させます。失敗した場合は None を返し、解参照可能なダングリングポインタを露出させることはありません。最後の強い参照が解放された時点で T はすでにデストラクトされており、Weak があっても背後の割り当ては保持され続けます。

一般的なツリーモデルでは、親ノードが子ノードを強い参照で持ち、子ノードが親ノードを Weak で参照します。重要なのは、どの方向を必ず弱くする固定ルールがあることではなく、所有権グラフ内で不要なノードが強い参照のループによって互いに存続させられないようにすることです。親から子への強い参照のみではループは形成されません。双方向が強い参照である場合にのみ、ループが形成される可能性があります。

逆方向のリンクでは、対象がすでに消えていることを許容する

use std::cell::RefCell;
use std::rc::{Rc, Weak};
struct Node {
    name: &'static str,
    parent: RefCell<Weak<Node>>,
    children: RefCell<Vec<Rc<Node>>>,
}
impl Node {
    fn new(name: &'static str) -> Rc<Self> {
        Rc::new(Self {
            name, parent: RefCell::new(Weak::new()),
            children: RefCell::new(Vec::new()),
        })
    }
}
fn main() {
    let child = Node::new("child");
    {
        let parent = Node::new("parent");
        *child.parent.borrow_mut() = Rc::downgrade(&parent);
        parent.children.borrow_mut().push(Rc::clone(&child));
        let owner = child.parent.borrow().upgrade().unwrap();
        assert_eq!(owner.name, "parent");
    }
    assert!(child.parent.borrow().upgrade().is_none());
    assert_eq!(child.name, "child");
}

child には外部の強い所有者があるため、parent が破棄されても存続し続けます。しかし、その親へのリンクはもはや upgrade できません。これは Weak が許容する正常な状態であり、呼び出し側は None を処理する必要があります。この例は 1 回の接続のみを示しています。完全なツリー API では、古い親ノードのリストの維持や、祖先が子孫にならないようにするなどの構造不変条件を維持する必要があり、Weak 自体がこれらのビジネスルールを検証することはありません。

Weak は自動クリーンアップされるキャッシュでもありません。HashMap の値がすべて Weak の場合、対象が消えた後もキーと弱いハンドルが表に残り続ける可能性があります。upgrade が失敗した場合は削除するか、定期的に掃除する必要があります。存続期間とキャッシュの容量は異なる問題です。

値の破棄と割り当て解放は別の時点

Weak を作ってから強参照をすべて解放すると、T は消えても Weak 用の割り当ては残ります。この時 upgrade は None になり、最後の Weak の解放で割り当ても回収できます。強参照が残る時の upgrade 成功は、新たな強所有者を増やします。

図を準備しています
値の破棄と割り当て解放は別の時点

最後の強参照で T は破棄され、残る Weak は割り当てを保持しますが T を復活できません。

共有値の修正:他の所有者にも変更を反映させるかどうかを明確にする

要件方法結果
すでに一意に所有しており、直接修正するget_mutクローンしない;他の強い参照や Weak が存在する場合は None を返す
自分のバージョンを修正するmake_mut複数の強い参照がある場合、T をクローンし、古い所有者は依然として古い値を見る
所有者全員が見る状態を修正するRefCell、Mutex などの内部可変性共有オブジェクトを維持し、アクセスを別途制約する
最後の強い所有者を消費して T を取り出すtry_unwrap強い参照数が 1 であればよく、既存の Weak は成功を妨げない

Rc/Arc の make_mut において、強い参照は 1 つだが Weak も存在する場合、これらの Weak は新しい値との関連付けから外れます。T をクローンする必要はありませんが、Weak はその後、修正された値への upgrade を行えません。これは get_mut が直接拒否する動作とは異なります。Rc::make_mut

use std::rc::Rc;

fn main() {
    let mut current = Rc::new(String::from("v1"));
    let snapshot = Rc::clone(&current);
    Rc::make_mut(&mut current).push_str("+edit");
    assert_eq!(&*snapshot, "v1");
    assert_eq!(&*current, "v1+edit");

    let weak = Rc::downgrade(&current);
    assert!(Rc::get_mut(&mut current).is_none());
    Rc::make_mut(&mut current).push('!');
    assert!(weak.upgrade().is_none());

    let another_weak = Rc::downgrade(&current);
    assert_eq!(Rc::try_unwrap(current).unwrap(), "v1+edit!");
    assert!(another_weak.upgrade().is_none());
}
強い参照が 1 つのみかつ Weak が存在しない複数の強い参照が存在他の所有者は旧バージョンを保持強い参照 1 つ + Weak弱い観察者は T を存続させない直接 &mut T を取得T はクローンしないT を複製し、複製側を変更他の所有者は元の値を保持既存の Weak から分離T を移し、旧 Weak を切り離すmake_mut:現在の所有関係に基づいてコピーするかどうかを決定これは make_mut の動作の分岐です;get_mut は呼び出し側に代わってクローンしたり Weak から分離したりしません。

コピーオンライトは必ずしも深いスナップショットではない

use std::cell::Cell;
use std::rc::Rc;
#[derive(Clone)]
struct Version {
    title: String,
    counter: Rc<Cell<u32>>,
}
fn main() {
    let mut current = Rc::new(Version {
        title: String::from("v1"), counter: Rc::new(Cell::new(0)),
    });
    let old = Rc::clone(&current);
    Rc::make_mut(&mut current).title.push_str(" edited");
    current.counter.set(7);
    assert_eq!(old.title, "v1");
    assert_eq!(current.title, "v1 edited");
    assert_eq!(old.counter.get(), 7);
    assert!(Rc::ptr_eq(&old.counter, &current.counter));
    assert!(!Rc::ptr_eq(&old, &current));
}

外側の Version は分離されましたが、派生した Clone は counter に対して Rc をコピーするだけで、2 つのバージョンは依然として同じカウンターを見ます。製品上のセマンティクスで完全な不変スナップショットが要求される場合、フィールド自体が不変であることを保証するか、各層のデータを明示的にクローンする必要があります。外側に Rc を被せて make_mut を呼び出すだけでは、この保証を確立するには不十分です。

T の取得に失敗した場合、元の所有権を保持し続ける

use std::rc::Rc;
fn main() {
    let first = Rc::new(String::from("keep me"));
    let second = Rc::clone(&first);
    let first = Rc::try_unwrap(first).expect_err("まだ別の強い所有者がある");
    assert!(Rc::ptr_eq(&first, &second));
    drop(second);
    assert_eq!(Rc::try_unwrap(first).unwrap(), "keep me");
}

try_unwrap が失敗して Err を返した場合、元のハンドルが呼び出し側に返されるため、共有オブジェクトを捨てなければならないわけではなく、保持し続けることができます。Arc の並列シナリオではさらに注意が必要です。複数のスレッドがそれぞれ try_unwrap(...).ok() を実行しても、誰も T を取得できず、各自が Err を破棄し、最終的にデストラクタが T を解放することがあります。すべての強い所有者が「ちょうど 1 つが値を取り出す」というプロトコルに参加することを希望する場合、Arc::into_inner の対応する保証を調べ、各強いハンドルがプロトコルに従うことを確保してください。Arc::into_inner

Arc::strong_count を先にチェックしてから unsafe で &mut T を作成しないでください。カウントの照会と他のスレッドの操作の間で状態が変化する可能性があり、Weak も関係してきます。get_mut などの用意された契約を使用してください。内部可変性の具体的な選択については、Cell、RefCell と Mutex を参照してください。