RustのWeakとCow
スマートポインタのページでは、Box、Rc、RefCell を紹介しました。このページでは、とても現実的な2つの問題を解決する道具をさらに2つ紹介します。Weak<T> を使うと、値を生かしておかずに指すことができます。Cow<T> を使うと、関数ができるときは借用したデータを返し、どうしても必要なときだけコピーを作れます。
Rc を家の鍵だと考えてみてください。誰かが鍵を持っている限り、家はそこにあり続けます。Weak は、紙に書いた住所のようなものです。住所を見れば家を探せますが、みんなが鍵を返して家がなくなっていたら、その住所はどこにもたどり着きません。忘れてしまった家の家賃を払い続けるはめになる人はいない、というわけです。
問題:Rcの循環は片付けられない
Section titled “問題:Rcの循環は片付けられない”Rc の値は、強い参照のカウント(strong count)が0になるとドロップされます。これはうまく働きますが、2つの値がお互いに Rc を持ち合うと話が変わります。それぞれが相手を生かし続け、いつまでも片付けられません。
これを確かめるために、下の Person 型は Drop を実装していて、片付けられるときにメッセージを表示します。どのメッセージが表示されるかに注目してください。
use std::cell::RefCell;use std::rc::Rc;
struct Person { name: String, friend: RefCell<Option<Rc<Person>>>,}
impl Drop for Person { fn drop(&mut self) { println!("{} をドロップ", self.name); }}
fn main() { let alice = Rc::new(Person { name: String::from("alice"), friend: RefCell::new(None) }); let bob = Rc::new(Person { name: String::from("bob"), friend: RefCell::new(None) });
// aliceはbobを指し、bobはaliceを指し返す *alice.friend.borrow_mut() = Some(Rc::clone(&bob)); *bob.friend.borrow_mut() = Some(Rc::clone(&alice));
println!("aliceの強い参照の数: {}", Rc::strong_count(&alice)); println!("bobの強い参照の数: {}", Rc::strong_count(&bob)); println!("mainの終わり");}実行結果:
aliceの強い参照の数: 2bobの強い参照の数: 2mainの終わり何が足りないかに注目してください。alice をドロップ も bob をドロップ も表示されていません。main が終わると変数 alice と bob はなくなるので、それぞれのカウントは2から1に減ります。でも、0にはなりません。それぞれの人が、まだ相手の friend フィールドに持たれているからです。このメモリはリーク(leak、漏れ)しています。
これに驚いても、落ち込む必要はありません。コンパイラはこれを見つけられませんし、クラッシュするわけでもありません。ただ、メモリが二度と戻ってこないだけです。でも、循環を作り続けながら長く動くプログラムでは、それが積み重なっていきます。
Weak<T>:値を生かしておかないポインタ
Section titled “Weak<T>:値を生かしておかないポインタ”解決策は、リンクの片方か両方を弱い(weak)リンクにすることです。Weak は、Rc から Rc::downgrade で作ります。弱いリンクは所有者として数えられないので、値がドロップされるのを止めません。
値がすでになくなっているかもしれないので、Weak をそのまま使うことはできません。.upgrade() を呼ぶと Option<Rc<T>> が返ってきます。値がまだ生きていれば Some、生きていなければ None です。
use std::cell::RefCell;use std::rc::{Rc, Weak};
struct Person { name: String, friend: RefCell<Weak<Person>>,}
impl Drop for Person { fn drop(&mut self) { println!("{} をドロップ", self.name); }}
fn main() { let alice = Rc::new(Person { name: String::from("alice"), friend: RefCell::new(Weak::new()) }); let bob = Rc::new(Person { name: String::from("bob"), friend: RefCell::new(Weak::new()) });
*alice.friend.borrow_mut() = Rc::downgrade(&bob); *bob.friend.borrow_mut() = Rc::downgrade(&alice);
if let Some(friend) = alice.friend.borrow().upgrade() { println!("aliceの友だちは {}", friend.name); }
println!("alice: 強い参照 = {}、弱い参照 = {}", Rc::strong_count(&alice), Rc::weak_count(&alice)); println!("mainの終わり");}実行結果:
aliceの友だちは bobalice: 強い参照 = 1、弱い参照 = 1mainの終わりbob をドロップalice をドロップ今度は「ドロップ」の行が両方とも表示されました。alice には強い所有者がひとり(変数 alice)と、弱いリンクがひとつ(bob から)あります。main が終わると強い参照のカウントが0になり、Rustが alice を片付けます。bob が先にドロップされるのは、変数は作られたのと逆の順番でドロップされるからです。
覚えておきたいことがいくつかあります。
Weak::new()は、何も指していない空の弱いポインタを作ります。これにupgrade()を呼ぶと、いつもNoneが返ります。初期値として便利です。Rc::strong_countは所有者の数を、Rc::weak_countは弱いリンクの数を数えます。値がいつドロップされるかを決めるのは、強い参照のカウントだけです。upgrade()で返ってくるRcは、本物の一時的な所有者です。それがスコープを抜けると、強い参照のカウントはまた減ります。
フォルダのツリー:子は所有し、親へは弱く
Section titled “フォルダのツリー:子は所有し、親へは弱く”ツリーは Weak の典型的な使いどころです。フォルダはサブフォルダを所有するので、children には Rc を入れます。サブフォルダも自分の親を知りたいのですが、親を所有するべきではありません。両方向に Rc を使うと、また循環ができてしまいます。そこで parent は Weak にします。
use std::cell::RefCell;use std::rc::{Rc, Weak};
struct Folder { name: String, parent: RefCell<Weak<Folder>>, children: RefCell<Vec<Rc<Folder>>>,}
fn new_folder(name: &str) -> Rc<Folder> { Rc::new(Folder { name: name.to_string(), parent: RefCell::new(Weak::new()), children: RefCell::new(Vec::new()), })}
fn add_child(parent: &Rc<Folder>, child: Rc<Folder>) { *child.parent.borrow_mut() = Rc::downgrade(parent); parent.children.borrow_mut().push(child);}
fn full_path(folder: &Folder) -> String { match folder.parent.borrow().upgrade() { Some(parent) => format!("{}/{}", full_path(&parent), folder.name), None => folder.name.clone(), }}
fn main() { let home = new_folder("home"); let photos = new_folder("photos"); let trips = new_folder("trips");
add_child(&home, Rc::clone(&photos)); add_child(&photos, Rc::clone(&trips));
match photos.parent.borrow().upgrade() { Some(parent) => println!("photosの親は {}", parent.name), None => println!("photosには親がない"), }
println!("フルパス: {}", full_path(&trips)); println!("home: 強い参照 = {}、弱い参照 = {}", Rc::strong_count(&home), Rc::weak_count(&home)); println!("photos: 強い参照 = {}、弱い参照 = {}", Rc::strong_count(&photos), Rc::weak_count(&photos));}実行結果:
photosの親は homeフルパス: home/photos/tripshome: 強い参照 = 1、弱い参照 = 1photos: 強い参照 = 2、弱い参照 = 1カウントをゆっくり読んでみましょう。photos には強い所有者が2つあります。変数 photos と、home の中の children リストです。弱いリンクは1つで、trips の parent フィールドからのものです。home の強い所有者は自分の変数ひとつだけです。誰も自分の親を所有していないからです。これこそ望んでいた形です。所有権はツリーを下に流れ、上に向かうリンクは弱いリンクになっています。
RefCell があるのは、フォルダがすでに Rc の中に入ったあとで parent を設定したり children に追加したりするためです。Rc だけだと、共有された読み取り専用のアクセスしかできません。
値を生かしておかない弱いリスト
Section titled “値を生かしておかない弱いリスト”Weak は、「様子は見ておきたいけれど、つかんでおきたくはない」もののリストにもぴったりです。リスナー、オブザーバー、ほかの誰かが所有しているもののキャッシュなどです。次の例では、ゲームのスコアの更新を、まだ聞いている相手に送ります。リスナーがドロップされると、その弱いリンクはただ働かなくなります。
use std::rc::{Rc, Weak};
struct Listener { name: String,}
fn notify(listeners: &[Weak<Listener>], event: &str) { for weak in listeners { match weak.upgrade() { Some(listener) => println!("{} が受信: {event}", listener.name), None => println!("(リスナーはもういないので、スキップ)"), } }}
fn main() { let screen = Rc::new(Listener { name: String::from("screen") }); let logger = Rc::new(Listener { name: String::from("logger") });
let mut listeners: Vec<Weak<Listener>> = Vec::new(); listeners.push(Rc::downgrade(&screen)); listeners.push(Rc::downgrade(&logger));
notify(&listeners, "スコアは10");
drop(logger); // loggerは持ち主によって閉じられる
notify(&listeners, "スコアは20");
// 何も指していないリンクを片付ける listeners.retain(|weak| weak.upgrade().is_some()); println!("残りのリスナー: {}", listeners.len());}実行結果:
screen が受信: スコアは10logger が受信: スコアは10screen が受信: スコアは20(リスナーはもういないので、スキップ)残りのリスナー: 1リストが logger のドロップを止めることはありませんでした。もしリストが Rc を持っていたら、logger はリストの中で生き続け、誰も望んでいないメッセージを受け取り続けていたでしょう。Weak なら、いつなくすかを決めるのは持ち主で、ほかのみんなは upgrade() で確かめるだけです。
Cow:借用できるときは借用し、必要なときだけコピーする
Section titled “Cow:借用できるときは借用し、必要なときだけコピーする”今度は別の問題です。ユーザー名を整える関数を書くとしましょう。ほとんどの名前はもともと問題ありません。余分なスペースや大文字が入っているものが少しあるだけです。関数がいつも新しい String を返すと、変更しなかった名前まで含めて、すべての名前をコピーすることになります。
Cow(「clone on write」、つまり書き込むときにクローンする。std::borrow::Cow にあります)がこれを解決します。Cow<'_, str> は、2つのバリアントを持つ列挙型(enum)です。
Cow::Borrowed(&str):「元のテキストをそのまま返します」。メモリの確保はありません。Cow::Owned(String):「変更が必要だったので、新しいStringを返します」。
use std::borrow::Cow;
fn normalize(name: &str) -> Cow<'_, str> { let trimmed = name.trim(); let has_upper = name.chars().any(|c| c.is_uppercase());
if trimmed.len() == name.len() && !has_upper { Cow::Borrowed(name) // もうきれい: 元のものを返す } else { Cow::Owned(trimmed.to_lowercase()) // 直す必要がある: 新しいStringを作る }}
fn main() { for input in ["alice", " Bob ", "CAROL", "dave"] { match normalize(input) { Cow::Borrowed(name) => println!("{input:?} -> {name:?}(借用、コピーなし)"), Cow::Owned(name) => println!("{input:?} -> {name:?}(所有、新しいString)"), } }}実行結果:
"alice" -> "alice"(借用、コピーなし)" Bob " -> "bob"(所有、新しいString)"CAROL" -> "carol"(所有、新しいString)"dave" -> "dave"(借用、コピーなし)名前は英語のままにしています。この関数は大文字・小文字やバイト単位の長さ(len())を見ているので、日本語の名前では違いがわからないからです。
4つのうち2つの名前には、まったくコストがかかっていません。入力の99%がもともときれいな実際のプログラムなら、作らずに済むコピーはかなりの数になります。Cow<'_, str> の '_ は、「入力から借用するかもしれない」という意味にすぎません。ライフタイムがまだぼんやりしているなら、ライフタイムのページで説明しています。
Cowをふつうの文字列のように使う
Section titled “Cowをふつうの文字列のように使う”たいていの場合、Cow を match する必要はまったくありません。Cow は &str に参照外し(deref)されるので、文字列のメソッドをそのまま呼べますし、&str を受け取る関数に &name を渡せます。自分で所有する String がどうしても必要なときは、.into_owned() を呼びましょう。中に String があればそれをムーブして取り出し、Cow が借用だったときだけコピーします。
use std::borrow::Cow;
fn normalize(name: &str) -> Cow<'_, str> { let trimmed = name.trim(); if trimmed.len() == name.len() && !name.chars().any(|c| c.is_uppercase()) { Cow::Borrowed(name) } else { Cow::Owned(trimmed.to_lowercase()) }}
fn greet(name: &str) { println!("ようこそ、{name}さん!");}
fn main() { let name = normalize(" Eve ");
println!("長さ: {}", name.len()); // strのメソッドがそのまま使える println!("eで始まる: {}", name.starts_with('e')); greet(&name); // &Cow<str>が&strになる
let saved: String = name.into_owned(); // これでふつうのStringになった println!("{saved:?} をユーザー一覧に保存しました");}実行結果:
長さ: 3eで始まる: trueようこそ、eveさん!"eve" をユーザー一覧に保存しましたCow はほかの型にも使えます。たとえば、変更が必要になるかもしれないスライスなら Cow<'_, [i32]> です。とはいえ、テキストの Cow<'_, str> がいちばんよく見かける形です。
どれをいつ使う?
Section titled “どれをいつ使う?”これまでに出てきた、ポインタのような型の早見表です。
| 型 | 何ができるか | こんなときに使う |
|---|---|---|
Box<T> |
所有者はひとり、値はヒープに置かれる | 再帰的な型や、安くムーブしたい大きな値 |
Rc<T> |
所有者は複数、スレッドはひとつ | プログラムのいくつかの部分で同じデータを共有する |
Arc<T> |
所有者は複数、スレッドをまたげる | Rc と同じだが、スレッド間で共有する(共有状態を参照) |
Weak<T> |
所有しないリンク | 親へのリンク、オブザーバーのリスト、キャッシュ、Rc の循環を断ち切る |
RefCell<T> |
共有参照を通した変更。チェックは実行時 | Rc の中にあるものを変更する必要がある |
Cow<'_, T> |
借用か所有かを実行時に決める | たいてい入力をそのまま返す関数 |
迷ったら、ふつうの所有権と借用から始めましょう。これらの型は、コンパイラかメモリリークが「必要だよ」と教えてくれたときに足せば十分です。
よくある間違い
Section titled “よくある間違い”値がなくなったあとでupgradeをunwrapする
Section titled “値がなくなったあとでupgradeをunwrapする”upgrade() が Option を返すのには理由があります。いつもの癖で .unwrap() してしまうと、値がドロップされた瞬間にプログラムがパニックします。
use std::rc::Rc;
fn main() { let session = Rc::new(String::from("user-42")); let link = Rc::downgrade(&session);
drop(session); // セッションが終わる
let current = link.upgrade().unwrap(); println!("{current} としてまだログイン中");}実行すると、次のように表示されます。
thread 'main' (12345) panicked at src/main.rs:9:34:called `Option::unwrap()` on a `None` valuenote: run with `RUST_BACKTRACE=1` environment variable to display a backtraceこれは「None の値に対して Option::unwrap() が呼ばれた」という意味です。Rust自身が出すメッセージなので、英語のままになります。
直し方は、「もうない」をふつうのケースとして扱うことです。match か if let を使いましょう。
match link.upgrade() { Some(current) => println!("{current} としてまだログイン中"), None => println!("セッションは終了しました"),}指し返すリンクにRcを使う
Section titled “指し返すリンクにRcを使う”子が Rc<Parent> を持ち、親が Rc<Child> を持つと、最初の節で見た循環ができあがります。何もクラッシュせず、何の警告も出ません。ただメモリが二度と戻ってこないだけです。良いルールは、誰が誰を所有するのかを決めて、その方向には Rc を使い、逆向きのリンクにはすべて Weak を使うことです。
struct Folder { parent: RefCell<Rc<Folder>>, // 間違い: 子が親を生かし続けてしまう children: RefCell<Vec<Rc<Folder>>>,}parent を RefCell<Weak<Folder>> に変えれば、循環はなくなります。
1. 削除された投稿へのコメント
Section titled “1. 削除された投稿へのコメント”title を持つ Post を作り、Rc に入れてください。text と post: Weak<Post> を持つ Comment を作ります。投稿がまだあれば 「<text>」は「<title>」へのコメント、なければ 「<text>」は削除された投稿へのコメント と表示する関数 describe(comment: &Comment) を書いてください。それを呼び、投稿をドロップして、もう一度呼びます。ヒント: match comment.post.upgrade() を使いましょう。
解答を見る
use std::rc::{Rc, Weak};
struct Post { title: String,}
struct Comment { text: String, post: Weak<Post>,}
fn describe(comment: &Comment) { match comment.post.upgrade() { Some(post) => println!("「{}」は「{}」へのコメント", comment.text, post.title), None => println!("「{}」は削除された投稿へのコメント", comment.text), }}
fn main() { let post = Rc::new(Post { title: String::from("Rustを学ぶ") }); let comment = Comment { text: String::from("すばらしい記事!"), post: Rc::downgrade(&post) };
describe(&comment); drop(post); describe(&comment);}実行結果:
「すばらしい記事!」は「Rustを学ぶ」へのコメント「すばらしい記事!」は削除された投稿へのコメントコメントが投稿を生かし続けることはありません。たったひとつの Rc がドロップされると、upgrade() は None を返し、コメントはパニックせずに落ち着いて対処します。
2. 必要なときだけテキストをエスケープする
Section titled “2. 必要なときだけテキストをエスケープする”すべての < を < に置き換える fn escape(text: &str) -> Cow<'_, str> を書いてください。テキストに < がなければ、借用のまま返します。"こんにちは、世界" と "1 < 2" で試し、どちらのバリアントが返ってきたかを表示してください。ヒント: まず text.contains('<') で確認し、所有する場合には text.replace('<', "<") を使います。
解答を見る
use std::borrow::Cow;
fn escape(text: &str) -> Cow<'_, str> { if text.contains('<') { Cow::Owned(text.replace('<', "<")) } else { Cow::Borrowed(text) }}
fn main() { for text in ["こんにちは、世界", "1 < 2"] { match escape(text) { Cow::Borrowed(s) => println!("{s}(借用)"), Cow::Owned(s) => println!("{s}(所有)"), } }}実行結果:
こんにちは、世界(借用)1 < 2(所有)新しい String のコストを払うのは、実際に < を含んでいるテキストだけです。それ以外は、手を加えられずにそのまま通り抜けます。
- お互いを指し合う2つの
Rcの値は、強い参照のカウントが0にならないので、リークし、Dropも実行されません。 Rc::downgradeはWeak<T>を作ります。Weak<T>は値を生かしておきません。upgrade()はOption<Rc<T>>を返します。- ツリーでは、子は
Rcで所有し、親へのリンクはWeakにします。所有権は下へ流れ、弱いリンクは上を指します。 - オブザーバーのリストやキャッシュは
Weakを持つようにすれば、うっかり値を生かし続けることがありません。 Cow<'_, str>は、何も変わらなかったときは借用したテキストを、変わったときだけ所有するStringを返します。&strに参照外しされ、.into_owned()でStringが手に入ります。
理解度チェック
Section titled “理解度チェック”理解度チェック
1. 2つの値が、それぞれ相手への Rc を持っています。main が終わると何が起きますか?
2. weak.upgrade() は何を返しますか?
3. フォルダのツリーで、Weak にするべきなのはどのリンクですか?
4. Cow<'_, str> を返す関数が、新しい String を作らずに済むのはどんなときですか?
スコア: 0 / 4
さらに詳しく
Section titled “さらに詳しく”- 循環参照は、メモリをリークすることもある — The Rust Programming Language 日本語版:
Rcの循環と、Weakで親へのリンクを持つツリーを、公式に一歩ずつ解説しています。 std::rc::Weak— 標準ライブラリのドキュメント(英語):upgrade、strong_count、Weak::newを含む、Weakのすべてのメソッドが載っています。std::rc— 標準ライブラリのドキュメント(英語):強い参照と弱い参照のカウントがどう連携するかを、循環を断ち切る長めの例とともに説明しています。std::borrow::Cow— 標準ライブラリのドキュメント(英語):Cow列挙型、to_mut、into_owned、そして文字列以外の例が載っています。