Rcに内包されたデータを安全に返す方法
この記事では、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_ref
がdrop
されるまで、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