コンテンツにスキップ

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の強い参照の数: 2
bobの強い参照の数: 2
mainの終わり
▶ 実際に試してみよう

何が足りないかに注目してください。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の友だちは bob
alice: 強い参照 = 1、弱い参照 = 1
mainの終わり
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/trips
home: 強い参照 = 1、弱い参照 = 1
photos: 強い参照 = 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 が受信: スコアは10
logger が受信: スコアは10
screen が受信: スコアは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:?} をユーザー一覧に保存しました");
}

実行結果:

長さ: 3
eで始まる: true
ようこそ、eveさん!
"eve" をユーザー一覧に保存しました
▶ 実際に試してみよう

Cow はほかの型にも使えます。たとえば、変更が必要になるかもしれないスライスなら Cow<'_, [i32]> です。とはいえ、テキストの Cow<'_, str> がいちばんよく見かける形です。

これまでに出てきた、ポインタのような型の早見表です。

型 何ができるか こんなときに使う
Box<T> 所有者はひとり、値はヒープに置かれる 再帰的な型や、安くムーブしたい大きな値
Rc<T> 所有者は複数、スレッドはひとつ プログラムのいくつかの部分で同じデータを共有する
Arc<T> 所有者は複数、スレッドをまたげる Rc と同じだが、スレッド間で共有する(共有状態を参照)
Weak<T> 所有しないリンク 親へのリンク、オブザーバーのリスト、キャッシュ、Rc の循環を断ち切る
RefCell<T> 共有参照を通した変更。チェックは実行時 Rc の中にあるものを変更する必要がある
Cow<'_, T> 借用か所有かを実行時に決める たいてい入力をそのまま返す関数

迷ったら、ふつうの所有権と借用から始めましょう。これらの型は、コンパイラかメモリリークが「必要だよ」と教えてくれたときに足せば十分です。

値がなくなったあとで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` value
note: 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<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. 必要なときだけテキストをエスケープする”

すべての < を &lt; に置き換える fn escape(text: &str) -> Cow<'_, str> を書いてください。テキストに < がなければ、借用のまま返します。"こんにちは、世界" と "1 < 2" で試し、どちらのバリアントが返ってきたかを表示してください。ヒント: まず text.contains('<') で確認し、所有する場合には text.replace('<', "&lt;") を使います。

解答を見る
use std::borrow::Cow;
fn escape(text: &str) -> Cow<'_, str> {
if text.contains('<') {
Cow::Owned(text.replace('<', "&lt;"))
} 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 &lt; 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 が手に入ります。

理解度チェック

1. 2つの値が、それぞれ相手への Rc を持っています。main が終わると何が起きますか?

2. weak.upgrade() は何を返しますか?

3. フォルダのツリーで、Weak にするべきなのはどのリンクですか?

4. Cow<'_, str> を返す関数が、新しい String を作らずに済むのはどんなときですか?

スコア: 0 / 4