Перейти к содержимому

Weak и Cow в Rust

На странице Умные указатели ты познакомился с Box, Rc и RefCell. Здесь мы добавим ещё два инструмента, которые решают две вполне реальные проблемы. Weak<T> позволяет указывать на значение, не продлевая ему жизнь. Cow<T> позволяет функции вернуть заимствованные данные, когда это возможно, и делать копию, только когда без неё никак.

Представь, что Rc — это ключ от дома: пока хоть у кого-то есть ключ, дом стоит. Weak больше похож на адрес, записанный на бумажке. По нему можно найти дом, но если все вернули ключи и дома больше нет, адрес просто никуда не ведёт. И никто не платит за дом, о котором все забыли.

Значение в Rc удаляется, когда его счётчик сильных ссылок (strong count) доходит до 0. Это отлично работает, пока два значения не начинают держать 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("Алиса"), friend: RefCell::new(None) });
let bob = Rc::new(Person { name: String::from("Боб"), friend: RefCell::new(None) });
// Алиса указывает на Боба, а Боб — обратно на Алису
*alice.friend.borrow_mut() = Some(Rc::clone(&bob));
*bob.friend.borrow_mut() = Some(Rc::clone(&alice));
println!("сильных ссылок на Алису: {}", Rc::strong_count(&alice));
println!("сильных ссылок на Боба: {}", Rc::strong_count(&bob));
println!("конец main");
}

Вывод:

сильных ссылок на Алису: 2
сильных ссылок на Боба: 2
конец main
▶ Попробуй сам

Обрати внимание, чего здесь нет: ни удаляем Алиса, ни удаляем Боб. Когда main заканчивается, переменные alice и bob исчезают, и каждый счётчик падает с 2 до 1. Но до 0 он так и не доходит: каждого человека всё ещё держит поле friend другого. Эта память утекла (memory leak).

Не расстраивайся, если это тебя удивило. Компилятор такое не ловит, и программа не падает. Просто память, которая уже никогда не вернётся. В программе, которая работает долго и всё время строит такие циклы, это накапливается.

Weak<T>: указатель, который не продлевает жизнь

Заголовок раздела «Weak<T>: указатель, который не продлевает жизнь»

Решение — сделать одну или обе связи слабыми. 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("Алиса"), friend: RefCell::new(Weak::new()) });
let bob = Rc::new(Person { name: String::from("Боб"), 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!("друг Алисы — {}", friend.name);
}
println!("Алиса: strong = {}, weak = {}", Rc::strong_count(&alice), Rc::weak_count(&alice));
println!("конец main");
}

Вывод:

друг Алисы — Боб
Алиса: strong = 1, weak = 1
конец main
удаляем Боб
удаляем Алиса
▶ Попробуй сам

На этот раз появились обе строки удаляем. У Алисы один сильный владелец (переменная alice) и одна слабая ссылка (от Боба). Когда main заканчивается, счётчик сильных ссылок доходит до 0, и Rust её убирает. Боб удаляется первым, потому что переменные удаляются в порядке, обратном созданию.

Что стоит запомнить:

  • Weak::new() создаёт пустой слабый указатель, который ни на что не указывает. upgrade() на нём всегда возвращает None. Удобное начальное значение.
  • Rc::strong_count считает владельцев. Rc::weak_count считает слабые ссылки. Когда удалять значение, решает только счётчик сильных ссылок.
  • Rc, который вернул upgrade(), — настоящий временный владелец. Когда он выходит из области видимости, счётчик сильных ссылок снова уменьшается.

Дерево папок: дети во владении, родители — слабые

Заголовок раздела «Дерево папок: дети во владении, родители — слабые»

Деревья — классическое место для 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: strong = {}, weak = {}", Rc::strong_count(&home), Rc::weak_count(&home));
println!("photos: strong = {}, weak = {}", Rc::strong_count(&photos), Rc::weak_count(&photos));
}

Вывод:

photos лежит в home
полный путь: home/photos/trips
home: strong = 1, weak = 1
photos: strong = 2, weak = 1
▶ Попробуй сам

Прочитай счётчики не спеша. У photos два сильных владельца: переменная photos и список children внутри home. И одна слабая ссылка: поле parent у trips. У home только один сильный владелец — её собственная переменная, потому что никто не владеет своим родителем. Это ровно та форма, которая нужна: владение идёт вниз по дереву, а ссылки вверх — слабые.

RefCell здесь нужен потому, что parent задаётся, а в children добавляются элементы уже после того, как папки оказались внутри Rc. А сам по себе Rc даёт только общий доступ на чтение.

Слабые списки, которые ничего не удерживают

Заголовок раздела «Слабые списки, которые ничего не удерживают»

Weak отлично подходит и для списков вида «присматривай за ними, но не держи»: слушатели, наблюдатели (observers) или кеш вещей, которыми владеет кто-то другой. Здесь счёт в игре рассылает обновления всем, кто ещё слушает. Когда слушателя удаляют, его слабая ссылка просто перестаёт работать:

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("экран") });
let logger = Rc::new(Listener { name: String::from("логгер") });
let mut listeners: Vec<Weak<Listener>> = Vec::new();
listeners.push(Rc::downgrade(&screen));
listeners.push(Rc::downgrade(&logger));
notify(&listeners, "счёт 10");
drop(logger); // владелец закрыл логгер
notify(&listeners, "счёт 20");
// убираем ссылки, которые ни на что не указывают
listeners.retain(|weak| weak.upgrade().is_some());
println!("осталось слушателей: {}", listeners.len());
}

Вывод:

экран получил: счёт 10
логгер получил: счёт 10
экран получил: счёт 20
(слушателя больше нет, пропускаем)
осталось слушателей: 1
▶ Попробуй сам

Список ни разу не помешал удалить логгер. Если бы в нём лежали Rc, логгер продолжал бы жить внутри списка и получать сообщения, которые никому не нужны. С Weak когда что-то исчезает, решает владелец, а все остальные просто проверяют это через upgrade().

Cow: заимствуй, когда можно, копируй, когда нужно

Заголовок раздела «Cow: заимствуй, когда можно, копируй, когда нужно»

Теперь другая проблема. Допустим, ты пишешь функцию, которая приводит имена пользователей в порядок. Большинство имён уже нормальные. В некоторых есть лишние пробелы или заглавные буквы. Если функция всегда возвращает новый String, ты копируешь каждое имя — даже те, что не менял.

Эту задачу решает Cow («clone on write», «клонировать при записи», из std::borrow::Cow). Cow<'_, str> — это перечисление с двумя вариантами:

  • 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" (заимствовано, без копии)
▶ Попробуй сам

Два имени из четырёх не стоили вообще ничего. В реальной программе, где 99% входных данных уже чистые, это очень много копий, которые ты так и не сделал. '_ в Cow<'_, str> просто говорит: «это может заимствовать из входных данных». Если времена жизни пока кажутся туманными, загляни на страницу Времена жизни.

Чаще всего делать match по Cow вообще не нужно. Он разыменовывается (deref) в &str, так что на нём можно вызывать методы строк и передавать &name в любую функцию, которая принимает &str. Когда тебе действительно нужен собственный 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> Заимствованное или собственное, решается во время выполнения Функция, которая обычно возвращает входные данные без изменений

Если сомневаешься, начни с обычного владения и заимствования. Добавляй эти типы, только когда компилятор или утечка памяти подскажут, что они нужны.

unwrap() у upgrade() после того, как значение исчезло

Заголовок раздела «unwrap() у upgrade() после того, как значение исчезло»

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
▶ Попробуй сам

Сообщение паники Rust печатает по-английски: на строке 9 был вызван unwrap() на значении None. Исправление — считать «значения больше нет» обычным случаем и обрабатывать его через 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>>, и цикла больше нет.

Создай Post с полем title и храни его в Rc. Создай Comment с полями text и post: Weak<Post>. Напиши функцию describe(comment: &Comment), которая печатает "<text>" — комментарий к «<title>», если пост ещё существует, или "<text>" — комментарий к удалённому посту, если нет. Вызови её, удали пост и вызови ещё раз. Подсказка: используй 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, и комментарий спокойно это обрабатывает, вместо того чтобы паниковать.

Напиши fn escape(text: &str) -> Cow<'_, str>, которая заменяет каждый < на &lt;. Если в тексте нет <, верни его заимствованным. Проверь функцию на "привет, мир" и "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 платит только тот текст, в котором действительно есть <. Всё остальное проходит насквозь, нетронутым.

  • Два значения Rc, указывающие друг на друга, никогда не доводят счётчик сильных ссылок до 0, поэтому утекают, и их Drop не запускается.
  • Rc::downgrade создаёт Weak<T>, который не продлевает жизнь значению. upgrade() возвращает Option<Rc<T>>.
  • В деревьях дети хранятся через Rc, а ссылки на родителя — через Weak. Владение идёт вниз, слабые ссылки смотрят вверх.
  • Списки наблюдателей и кеши могут хранить Weak, чтобы случайно ничего не удерживать.
  • Cow<'_, str> возвращает заимствованный текст, если ничего не изменилось, и собственный String, только если изменилось. Он разыменовывается в &str, а .into_owned() даёт String.

Проверь себя

1. Два значения держат Rc друг на друга. Что произойдёт, когда закончится main?

2. Что возвращает weak.upgrade()?

3. Какая связь в дереве папок должна быть Weak?

4. Когда функция, возвращающая Cow<'_, str>, обходится без нового String?

Результат: 0 / 4