🍡

Rcに内包されたデータを安全に返す方法

2023/04/17に公開

この記事では、Rc<RefCell<T>>に内包されたデータを保持する構造体から、データを参照する方法を簡単にまとめます。

※話しをわかりやすくするために、Rc限定で書いていますが、Arcの場合も同様の考え方を適用可能です。

以下のコード例では、Cart構造体とCartItem構造体を使用しています。CartItemはコレクションに格納される要素です。

struct CartInner {
  cart_items: Vec<CartItem>,
}

#[derive(Debug, Clone)]
pub struct Cart {
  inner: Rc<RefCell<CartInner>>, // 読み込みのみならRc<CartInner>でも可
}

impl Cart {
  pub fn new() -> Self {
    Self {
      inner: Rc::new(RefCell::new(CartInner { cart_items: Vec::new() })),
    }
  }
}

Cart構造体にpub fn cart_items(&self) -> CartItemのコレクションのようなメソッドの実装を考えてみましょう。戻り値の型としては、実体を返す場合と参照を返す場合があります。

&Vec<CartItem>を返す

まずは参照を返す場合です。

慣れないうちは以下のような実装を書いてしまいがちですが、コンパイルは通りません。

impl Cart {
  // ...
  pub fn cart_items(&self) -> &Vec<CartItem> {
    let borrow = self.inner.borrow();
    &borrow.cart_items
  }
  // ...
}
error[E0515]: cannot return value referencing local variable `borrow`
  --> src/cart_rc_imm_1.rs:32:5
   |
32 |     &borrow.cart_items
   |     ^------^^^^^^^^^^^
   |     ||
   |     |`borrow` is borrowed here
   |     returns a value referencing data owned by the current function

Rcを使わない場合は参照を返すことができたのですが、Rcを使う場合は、borrowがメソッドの最後でdropされるため、コンパイルエラーになります。

Vec<CartItem>を返す

次は実体を返す場合です。

impl Cart {
  // ...
  pub fn cart_items(&self) -> Vec<CartItem> {
    let lock = self.inner.borrow();
    lock.cart_items.clone()
  }
  // ...
}

この実装はコンパイルが通ります。しかし、cart_itemsメソッドを呼び出すたびに、Vec<CartItem>の複製が作られます。この実装が必ずしも悪いということはありませんが、データ量が大きい場合は要注意です。

Rc<Ref<'a, CartInner>>を内部に持ち、&'a Vec<CartItem>を返す

次は、Rc<Ref<'a, CartInner>>を内部に持ち、&'a Vec<CartItem>を返す方法です。

#[derive(Debug, Clone)]
pub struct Cart<'a> {
  inner: Rc<RefCell<CartInner>>,
  car_inner_ref: Option<Rc<Ref<'a, CartInner>>>,
}

impl<'a> Cart<'a> {
  pub fn new() -> Self {
    Self {
      inner: Rc::new(RefCell::new(CartInner { cart_items: Vec::new() })),
      guard: None,
    }
  }

  // よさそうにみえるが、self.guardがdropされるまで借用し続ける
  pub fn cart_items(&'a mut self) -> &'a Vec<CartItem> {
    let borrow = self.inner.borrow();
    self.car_inner_ref = Some(Rc::new(borrow));
    let result = self.car_inner_ref.as_ref().unwrap();
    &result.cart_items
  }
}

これは普通は避けるべき実装です。borrowしたRefを一時的にメンバーのself.car_inner_refに格納しています。そのself.car_inner_refから参照を返しています。self.car_inner_refdropされるまで、borrowしたRefが借用され続けるためパニックの温床になりやすいです。借用は必要な範囲でのみ行うようにしましょう。

追記:

テストコードを書いてみたのですが、コンパイルできませんでした。

#[test]
fn test_cart() {
  let mut cart = Cart::new();
  let cart_items = cart.cart_items();
  assert_eq!(cart_items.len(), 0);
}
error[E0597]: `cart` does not live long enough
  --> src/cart_rc_imm_3.rs:48:24
   |
48 |       let cart_items = cart.cart_items();
   |                        ^^^^^^^^^^^^^^^^^ borrowed value does not live long enough
...
52 |   }
   |   -
   |   |
   |   `cart` dropped here while still borrowed
   |   borrow might be used here, when `cart` is dropped and runs the destructor for type `cart_rc_imm_3::Cart<'_>`

self.guardよりselfのほうがライフタイムが短いというのが原因のようです。構造的な問題で解消するのは難しいので、この案は没にします…。

要素への参照をクロージャーの引数に渡す

最後に、要素への参照をクロージャーの引数に渡す方法です。

  pub fn cart_items<F>(&self, f: F)
  where
    F: Fn(&CartItem), {
    let borrow = self.inner.borrow();
    borrow.cart_items.iter().for_each(f);
  }

この実装では、cart_itemsメソッドの引数にクロージャーを渡しています。cart_itemsメソッドの中で、borrow.cart_itemsの要素をイテレートして、クロージャーを呼び出しています。この実装は、cart_itemsメソッドの呼び出し元で、cart_itemsメソッドの中でイテレートされた要素に対して、クロージャーを呼び出すことができます。

あと、可変参照を扱いたい場合も同様にクロージャに渡すことができます。

  pub fn cart_items_mut<F>(&mut self, f: F)
  where
    F: FnMut(&mut CartItem), {
    let mut borrow = self.inner.borrow_mut();
    borrow.cart_items.iter_mut().for_each(f)
  }

この実装では、cart_items_mutメソッドの引数にFnMutトレイトを実装したクロージャーを渡しています。cart_items_mutメソッドの中で、borrow.cart_itemsの要素をイテレートして、クロージャーを呼び出しています。この実装は、cart_items_mutメソッドの呼び出し元で、cart_items_mutメソッドの中でイテレートされた要素に対して、クロージャーを呼び出すことができます。

今回のように、Rcで内包されたデータ型を扱う場合、データ型の参照をクロージャーの引数に渡すことで、コレクションの要素に対して外部からの操作を制限し、不変性を維持することができます。また、クロージャーを使うことで、コレクション全体をクローンする必要がなくなり、パフォーマンスの向上にもつながります。さらに、クロージャーに引き渡す要素の条件を指定することでコールバックする要素を絞ることもできるでしょう。

Discussion