やってみる

アウトプットすべく己を導くためのブログ。その試行錯誤すらたれ流す。

Rustのスマートポインタ(RefCell<T>)

 借用規則をコンパイル時でなく実行時に適用する。

成果物

参考

RefCell

 RefCellはスマートポインタの一種。

内部可変性

内部可変性は、そのデータへの不変参照がある時でさえもデータを可変化できる

各スマートポインタの違い

スマートポインタ 所有者 可変性 精査タイミング
Rc<T> 複数 不変借用のみ コンパイル
Box<T> 単一 可不変の借用 コンパイル
RefCell<T> 単一 可不変の借用 実行時

RefCellは実行時に精査される可変借用を許可するので、RefCellが不変でも、 RefCell内の値を可変化できる。

内部可変性パターン

不変な値の中の値を可変化することは、内部可変性パターンです。

不変値への可変借用

 Rustでは借用規則により、不変値xを可変借用&mutできない。不変として宣言したときと矛盾するから。

main.rs

fn main() {
    let x = 5;
    let y = &mut x; // error[E0596]: cannot borrow immutable local variable `x` as mutable
}

内部可変のユースケース

ですが、メソッド内で値が自身を可変化するけれども、他のコードにとっては、 不変に見えることが有用な場面もあります。

 たとえばテスト時のモックオブジェクト。コード実行時は不変だが、テスト時には可変にしたいとき。

例(RefCell未使用)

src/lib.rs

pub trait Messenger {
    fn send(&self, msg: &str);
}
pub struct LimitTracker<'a, T: 'a + Messenger> {
    messenger: &'a T,
    value: usize,
    max: usize,
}
impl<'a, T> LimitTracker<'a, T>
    where T: Messenger {
    pub fn new(messenger: &T, max: usize) -> LimitTracker<T> {
        LimitTracker {
            messenger,
            value: 0,
            max,
        }
    }
    pub fn set_value(&mut self, value: usize) {
        self.value = value;
        let percentage_of_max = self.value as f64 / self.max as f64;
        if percentage_of_max >= 0.75 && percentage_of_max < 0.9 {
            self.messenger.send("警告: 割り当ての75%以上を使用してしまいました!");
        } else if percentage_of_max >= 0.9 && percentage_of_max < 1.0 {
            self.messenger.send("切迫した警告: 割り当ての90%以上を使用してしまいました!");
        } else if percentage_of_max >= 1.0 {
            self.messenger.send("エラー: 割り当てを超えています!");
        }
    }
}

src/lib.rs

#[cfg(test)]
mod tests {
    use super::*;
    struct MockMessenger {
        sent_messages: Vec<String>,
    }
    impl MockMessenger {
        fn new() -> MockMessenger {
            MockMessenger { sent_messages: vec![] }
        }
    }
    impl Messenger for MockMessenger {
        fn send(&self, message: &str) {
            self.sent_messages.push(String::from(message));
        }
    }
    #[test]
    fn it_sends_an_over_75_percent_warning_message() {
        let mock_messenger = MockMessenger::new();
        let mut limit_tracker = LimitTracker::new(&mock_messenger, 100);
        limit_tracker.set_value(80);
        assert_eq!(mock_messenger.sent_messages.len(), 1);
    }
}
$ cargo test
...
error[E0596]: cannot borrow `self.sent_messages` as mutable, as it is behind a `&` reference
  --> src/lib.rs:43:13
   |
42 |         fn send(&self, message: &str) {
   |                 ----- help: consider changing this to be a mutable reference: `&mut self`
43 |             self.sent_messages.push(String::from(message));
   |             ^^^^^^^^^^^^^^^^^^ `self` is a `&` reference, so the data it refers to cannot be borrowed as mutable

 「不変なフィールドsent_messagesを可変で借用できない」と怒られる。

 sendメソッドの第一引数を&mutにもできない。

impl Messenger for MockMessenger {
    fn send(&mut self, message: &str) { // error[E0053]: method `send` has an incompatible type for trait

 「トレイトの定義と一致しない」と怒られる。トレイトの定義を確認するとsendメソッドの第一引数は&selfである。&mut selfではない。よって上記エラー。

pub trait Messenger {
    fn send(&self, msg: &str);

 エラーの詳細を見てみるとヘルプで「トレイトのほうで型を可変にしろ」とある。

error[E0053]: method `send` has an incompatible type for trait
  --> src/lib.rs:42:17
   |
2  |     fn send(&self, msg: &str);
   |             ----- type in trait
...
42 |         fn send(&mut self, message: &str) {
   |                 ^^^^^^^^^ types differ in mutability
   |
   = note: expected type `fn(&tests::MockMessenger, &str)`
              found type `fn(&mut tests::MockMessenger, &str)`
help: consider change the type to match the mutability in trait
   |
42 |         fn send(&self, message: &str) {
   |                 ^^^^^

 これに従ってしまうと「実行時は不変にする」ことができなくなる。テストのためだけに本番のコードを可変に変えてしまう。そんな馬鹿げた事態になる。

 試しにやってみると以下のようになる。

pub trait Messenger {
    fn send(&mut self, msg: &str);
}
pub struct LimitTracker<'a, T: 'a + Messenger> {
    messenger: &'a mut T,
    value: usize,
    max: usize,
}
impl<'a, T> LimitTracker<'a, T>
    where T: Messenger {
    pub fn new(messenger: &mut T, max: usize) -> LimitTracker<T> {
        LimitTracker {
            messenger,
            value: 0,
            max,
        }
    }
    pub fn set_value(&mut self, value: usize) {
        self.value = value;
        let percentage_of_max = self.value as f64 / self.max as f64;
        if percentage_of_max >= 0.75 && percentage_of_max < 0.9 {
            self.messenger.send("警告: 割り当ての75%以上を使用してしまいました!");
        } else if percentage_of_max >= 0.9 && percentage_of_max < 1.0 {
            self.messenger.send("切迫した警告: 割り当ての90%以上を使用してしまいました!");
        } else if percentage_of_max >= 1.0 {
            self.messenger.send("エラー: 割り当てを超えています!");
        }
    }
}
#[cfg(test)]
mod tests {
    use super::*;
    struct MockMessenger {
        sent_messages: Vec<String>,
    }
    impl MockMessenger {
        fn new() -> MockMessenger {
            MockMessenger { sent_messages: vec![] }
        }
    }
    impl Messenger for MockMessenger {
        fn send(&mut self, message: &str) { // error[E0053]: method `send` has an incompatible type for trait
//        fn send(&self, message: &str) {
            self.sent_messages.push(String::from(message));
        }
    }
    #[test]
    fn it_sends_an_over_75_percent_warning_message() {
        let mut mock_messenger = MockMessenger::new();
        let mut limit_tracker = LimitTracker::new(&mut mock_messenger, 100);
        limit_tracker.set_value(80);
        assert_eq!(mock_messenger.sent_messages.len(), 1);
    }
}

 たしかに動く。でも不変にしておき、テストのときだけ可変にしたい。それをRefCell<T>で実装してみる。

例(RefCell

 不変および可変参照を作成するとき、それぞれ&&mut記法を使う。RefCell<T>ではborrowborrow_mutメソッドを使用する。

可変性 記法 RefCell<T>
& borrow()
&mut borrow_mut()

src/lib.rs

pub trait Messenger {
    fn send(&self, msg: &str);
}
pub struct LimitTracker<'a, T: 'a + Messenger> {
    messenger: &'a T,
    value: usize,
    max: usize,
}
impl<'a, T> LimitTracker<'a, T>
    where T: Messenger {
    pub fn new(messenger: &T, max: usize) -> LimitTracker<T> {
        LimitTracker {
            messenger,
            value: 0,
            max,
        }
    }
    pub fn set_value(&mut self, value: usize) {
        self.value = value;
        let percentage_of_max = self.value as f64 / self.max as f64;
        if percentage_of_max >= 0.75 && percentage_of_max < 0.9 {
            self.messenger.send("警告: 割り当ての75%以上を使用してしまいました!");
        } else if percentage_of_max >= 0.9 && percentage_of_max < 1.0 {
            self.messenger.send("切迫した警告: 割り当ての90%以上を使用してしまいました!");
        } else if percentage_of_max >= 1.0 {
            self.messenger.send("エラー: 割り当てを超えています!");
        }
    }
}
#[cfg(test)]
mod tests {
    use super::*;
    use std::cell::RefCell;
    struct MockMessenger {
        sent_messages: RefCell<Vec<String>>,
    }
    impl MockMessenger {
        fn new() -> MockMessenger {
            MockMessenger { sent_messages: RefCell::new(vec![]) }
        }
    }
    impl Messenger for MockMessenger {
        fn send(&self, message: &str) {
            self.sent_messages.borrow_mut().push(String::from(message));
        }
    }
    #[test]
    fn it_sends_an_over_75_percent_warning_message() {
        let mock_messenger = MockMessenger::new();
        let mut limit_tracker = LimitTracker::new(&mock_messenger, 100);
        limit_tracker.set_value(80);
        assert_eq!(mock_messenger.sent_messages.borrow().len(), 1);
    }
}

 注目すべきは以下。borrow_mut()で不変なのに可変借用している。

    impl Messenger for MockMessenger {
        fn send(&self, message: &str) {
            self.sent_messages.borrow_mut().push(String::from(message));

 テストは成功する。

借用規則

 ただし、RefCellも借用規則に従っている。借用規則のうち「可変変数は1つのスコープに1つのみ」である。これに抵触する以下コードは実行時にpanic!する。

impl Messenger for MockMessenger {
    fn send(&self, message: &str) {
        let mut one_borrow = self.sent_messages.borrow_mut();
        let mut two_borrow = self.sent_messages.borrow_mut();

        one_borrow.push(String::from(message));
        two_borrow.push(String::from(message));
    }
}
$ cargo test
...
---- tests::it_sends_an_over_75_percent_warning_message stdout ----
thread 'tests::it_sends_an_over_75_percent_warning_message' panicked at 'already borrowed: BorrowMutError', src/libcore/result.rs:997:5
note: Run with `RUST_BACKTRACE=1` environment variable to display a backtrace.

 「すでに借用済みです」という実行時エラー。

Rc<T> + RefCell<T>で複数の所有者がいる内部可変性データをつくる

 RefCell<T>はふつうRc<T>と組合せる。Rc<T>は不変しか許さない。そこで、Rc<T>RefCell<T>をもたせれば可変性を得られる。

#[derive(Debug)]
enum List {
    Cons(Rc<RefCell<i32>>, Rc<List>),
    Nil,
}
use List::{Cons, Nil};
use std::rc::Rc;
use std::cell::RefCell;
fn main() {
    let value = Rc::new(RefCell::new(5));
    let a = Rc::new(Cons(Rc::clone(&value), Rc::new(Nil)));
    let b = Cons(Rc::new(RefCell::new(6)), Rc::clone(&a));
    let c = Cons(Rc::new(RefCell::new(10)), Rc::clone(&a));
    *value.borrow_mut() += 10;
    println!("a after = {:?}", a);
    println!("b after = {:?}", b);
    println!("c after = {:?}", c);
}
$ rustc main.rs
./main
pi@raspberrypi:/tmp/work/Rust.Smartpointer.RefCell.20190705114547/src/6 $ ./main
a after = Cons(RefCell { value: 15 }, Nil)
b after = Cons(RefCell { value: 6 }, Cons(RefCell { value: 15 }, Nil))
c after = Cons(RefCell { value: 10 }, Cons(RefCell { value: 15 }, Nil))

 valueを参照部分がすべて変更後の値になっている。つまり可変参照であることが確認できた。

所感

 でもRefCellは実行速度を犠牲にする。はたしてテストのためにそれをすべきかどうかは判断に迷う。

 あと、Rc::new(RefCell::new(値))はネスト深すぎ。Rc::clone(), RefCell.borrow_mut()など独自メソッドも覚えねばならないし。

 これは理解し使いこなすまでに時間かかりそう。

対象環境

$ uname -a
Linux raspberrypi 4.19.42-v7+ #1219 SMP Tue May 14 21:20:58 BST 2019 armv7l GNU/Linux

前回まで