RustのSmart pointersってなんなん
概要
最近自分の勉強が全然進んでない。。。
という弱音を吐きながら隙間時間でTHE BOOKの続きを読み進める
文字が多いからか、Ownership, Lifetimeに続いていまいちピンと来なかった
Smart pointersを後で振り返るようにメモ残す
Smart pointersとは・・・
Pointerは&
で示したみたいな参照とかのこと
Smartがつくのでただの参照だけじゃなくてちょっとした機能とかついてるみたい
Box<T>
再帰的な構造に役立つうまくいかないケース
Listの中にListがあり、その中にもListがあり再起的に続いていくケース
コードで表現するとこんな感じ
use List::{Cons, Nil};
#[derive(Debug)]
enum List {
Cons(i32, List),
Nil,
}
fn main() {
let list = Cons(1, Cons(2, Cons(3, Nil)));
println!("list = {:?}", list);
}
これをコンパイルすると、こんなエラーが出てくる。。。
Compiling playground v0.0.1 (/playground)
error[E0072]: recursive type `List` has infinite size
--> src/main.rs:3:1
Listのenum型メモリ領域を確保しようとしたときに
Consの中のListの中のConsの中のメモリ領域を・・・
としているうちに無限に追いかけ続けて上記エラーが出ちゃう
Box<T>
でコンパイルできるようにする
上記再起的な構造を実現するのに使えるのが
Smaprt Pointerの人るBox<T>
use List::{Cons, Nil};
#[derive(Debug)]
enum List {
Cons(i32, Box<List>),
Nil,
}
fn main() {
let list = Cons(1, Box::new(Cons(2, Box::new(Cons(3, Box::new(Nil))))));
println!("list = {:?}", list);
}
Box<T>
はpointerなので実際ヒープ領域に格納されたデータ量ではなく
データの格納された場所を指すpointerのためのデータ量だけわかればいい
そのため、再起的にデータ量を調べず、一つ目のBox<T>
に必要なデータ量を計算してくれて
前項でのコンパイルエラーを突破できる
RC<T>
複数の所有権を持つうまくいかないケース
せっかくBox<T>
で再帰的な構造を表現できるようになったが
まだ、コンパイルエラーにぶつかってしまうケースがある
こんな感じで複数の変数のOwnershipを保持するようなケースである
コードにするとこんな感じ
use List::{Cons, Nil};
#[derive(Debug)]
enum List {
Cons(i32, Box<List>),
Nil,
}
fn main() {
let a = Cons(5, Box::new(Cons(10, Box::new(Nil))));
let b = Cons(3, Box::new(a));
let c = Cons(4, Box::new(a));
println!("a = {:?}", a);
println!("b = {:?}", b);
println!("c = {:?}", c);
}
こんなコンパイルエラーになっちゃう
Compiling playground v0.0.1 (/playground)
error[E0382]: use of moved value: `a`
--> src/main.rs:12:30
let b = Cons(3, Box::new(a));
でaのownershipがb移り、aはmoveされてるので
let c = Cons(4, Box::new(a));
ここで怒られちゃう😭
RC<T>
で解決する
RC<T>
は複数のOwnershipを許可するので、この状況で使える!
Ownershipを奪うのではなく共有するイメージ
use std::rc::Rc;
use List::{Cons, Nil};
#[derive(Debug)]
enum List {
Cons(i32, Rc<List>),
Nil,
}
fn main() {
let a = Rc::new(Cons(5, Rc::new(Cons(10, Rc::new(Nil)))));
let b = Cons(3, Rc::clone(&a));
let c = Cons(4, Rc::clone(&a));
println!("a = {:?}", a);
println!("b = {:?}", b);
println!("c = {:?}", c);
}
RC<T>
が複数Ownership保持してるのを確認する
Rc::strong_count(&a)
を使うと
どれくらいOwnershipが共有される(参照カウントがある)のか確認できる
use std::rc::Rc;
use List::{Cons, Nil};
#[derive(Debug)]
enum List {
Cons(i32, Rc<List>),
Nil,
}
fn main() {
let a = Rc::new(Cons(5, Rc::new(Cons(10, Rc::new(Nil)))));
println!("count after creating a = {}", Rc::strong_count(&a));
// 最初は参照カウント1
let _b = Cons(3, Rc::clone(&a));
println!("count after creating b = {}", Rc::strong_count(&a));
// cloneしたので参照カウントが2になる
{
let _c = Cons(4, Rc::clone(&a));
println!("count after creating c = {}", Rc::strong_count(&a));
// cloneしたので参照カウントが3になる
}
println!("count after c goes out of scope = {}", Rc::strong_count(&a));
// ここでDropされるので参照カウントが1減って2になる
} // 参照カウントが0になるのでaは完全に破棄される
RefCell<T>
モックに役立つ内部可変性のうまくいかないケース
こんな感じの実装があったとして
#![allow(unused)]
fn main() {
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 {
// 警告: 割り当ての75%以上を使用してしまいました
self.messenger
.send("Warning: You've used up over 75% of your quota!");
} else if percentage_of_max >= 0.9 && percentage_of_max < 1.0 {
// 切迫した警告: 割り当ての90%以上を使用してしまいました
self.messenger
.send("Urgent warning: You've used up over 90% of your quota!");
} else if percentage_of_max >= 1.0 {
// エラー: 割り当てを超えています
self.messenger.send("Error: You are over your quota!");
}
}
}
}
send
メソッドをテストコードでMockにするとこんな感じ
#![allow(unused)]
fn main() {
#[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);
}
}
}
ただこれ実行するとコンパイルエラーになっちゃう
error[E0596]: cannot borrow immutable field `self.sent_messages` as mutable
(エラー: 不変なフィールド`self.sent_messages`を可変で借用できません)
--> src/lib.rs:52:13
fn send(&self, message: &str) {
の通り、selfが不変参照
そいじゃ&mut self
にしようかしら。
ってしちゃうとMessageのTraitとシグニチャ異なるのでできない。。。
そんな時、使えるのが内部可変性のRefCell<T>
RefCell<T>
で解決
#![allow(unused)]
fn main() {
#[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() {
// --snip--
let mock_messenger = MockMessenger::new();
let mut limit_tracker = LimitTracker::new(&mock_messenger, 100);
limit_tracker.set_value(75);
assert_eq!(mock_messenger.sent_messages.borrow().len(), 1);
}
}
}
RefCell<Vec<String>>
で定義すると
&self
のままでこの不変参照を可変できる
具体的にはborrow_mut()
を呼び出して変更することができる!
このようなMockケースでは有効そう🤩
循環参照に気をつけよう
循環参照になっちゃうケース
チュートリアルまま貼り付けだけどこんなケース
use std::cell::RefCell;
use std::rc::Rc;
use List::{Cons, Nil};
#[derive(Debug)]
enum List {
Cons(i32, RefCell<Rc<List>>),
Nil,
}
impl List {
fn tail(&self) -> Option<&RefCell<Rc<List>>> {
match *self {
Cons(_, ref item) => Some(item),
Nil => None,
}
}
}
fn main() {
let a = Rc::new(Cons(5, RefCell::new(Rc::new(Nil))));
println!("a initial rc count = {}", Rc::strong_count(&a));
// aの最初の参照カウント = 1
println!("a next item = {:?}", a.tail());
// aの次の要素は = Some(RefCell { value: Nil })
let b = Rc::new(Cons(10, RefCell::new(Rc::clone(&a))));
println!("a rc count after b creation = {}", Rc::strong_count(&a));
// b作成後のaの参照カウント = 2
println!("b initial rc count = {}", Rc::strong_count(&b));
// bの最初の参照カウント = 1
println!("b next item = {:?}", b.tail());
// bの次の要素 = Some(RefCell { value: Cons(5, RefCell { value: Nil }) })
if let Some(link) = a.tail() {
*link.borrow_mut() = Rc::clone(&b);
}
// aのtailにbをセットする
println!("b rc count after changing a = {}", Rc::strong_count(&b));
// aを変更後のbの参照カウント = 2
println!("a rc count after changing a = {}", Rc::strong_count(&a));
// aを変更後のaの参照カウント = 2
// Uncomment the next line to see that we have a cycle;
// it will overflow the stack
// 次の行のコメントを外して循環していると確認してください; スタックオーバーフローします
// println!("a next item = {:?}", a.tail()); // aの次の要素 = {:?}
}
内部可変性のRefCell<T>
と複数所有権を許可するRC<T>
を組み合わせると
循環参照を生み出せてしまう
上記のケースで最後のコメントを外すと循環参照して無限に出力しちゃう
コメントを外さなければコンパイル自体はできるものの
mainの最後で、b, aがドロップされ参照カウントが2->1になる
参照カウントは0にならないので、データは破棄されずメモリにずっとで残ってしまう。。。
ということで循環参照よくない。というお話
Weak<T>
で回避する
Weak<T>
使うと文字通り弱い参照なるものができるので
循環参照を作っても前項のような事態にならない。
前の章でいうListをNodeに置き換えて
親とは弱い参照(Weak<T>
)、子とは強い参照(Rc<T>
)にして
branchのchildはleafを参照
leafのparentはbranchを参照という循環した参照を作るが
Weakの参照は0にならなくても、強い参照数が0になるとデータも消してくれる
という特性を利用して、上記の循環参照問題参照が起きなくなる!!
まぁこういう階層構造とか循環が起きるケースで使ってみよう!って感じかな
use std::cell::RefCell;
use std::rc::{Rc, Weak};
#[derive(Debug)]
struct Node {
value: i32,
parent: RefCell<Weak<Node>>,
child: RefCell<Option<Rc<Node>>>,
}
fn main() {
let leaf = Rc::new(Node {
value: 3,
parent: RefCell::new(Weak::new()),
child: RefCell::new(None),
});
// leafの親 = {:?}
println!("leaf parent = {:?}", leaf.parent.borrow().upgrade());
let branch = Rc::new(Node {
value: 5,
parent: RefCell::new(Weak::new()),
child: RefCell::new(Some(Rc::clone(&leaf))),
});
*leaf.parent.borrow_mut() = Rc::downgrade(&branch);
println!("leaf parent = {:?}", leaf.parent.borrow().upgrade());
}
まとめ
1回読むだけではどうもピンと来なかったが
何度も読み直すと、ふむふむとなった。
使わざる負えない状況になった時にまた振り返りに見にこよう
Discussion