Что такое Cow (Clone on Write) в Rust и когда стоит его использовать?
В Rust Cow (сокращение от Clone on Write) — это умный указатель, позволяющий эффективно работать с данными, которые обычно не требуют клонирования, но при необходимости могут быть склонированы. Это гибкая структура, которая может прозрачно оборачивать как заимствованные (borrowed), так и владельческие (owned) данные, переключаясь между ними только при необходимости.
Тип Cow определён как:
enum Cow<'a, B: ?Sized + ToOwned> {
Borrowed(&'a B),
Owned(<B as ToOwned>::Owned),
}
Он доступен в стандартной библиотеке в модуле std::borrow.
Основная идея Cow
Когда у вас есть данные, которыми можно пользоваться по ссылке (например, &str, &[u8], &[T]), но в каких-то редких случаях может понадобиться их изменить, Cow позволяет:
-
по умолчанию работать с данными как с заимствованными (без аллокаций);
-
клонировать данные только в момент, когда они действительно модифицируются.
Таким образом, Cow помогает экономить память и ускорять выполнение в случаях, где копирование может быть дорогим и редко необходимым.
Как работает Cow
Рассмотрим простой пример:
use std::borrow::Cow;
fn modify(input: &str) -> Cow<str> {
if input.contains("!") {
let replaced = input.replace("!", ".");
Cow::Owned(replaced)
} else {
Cow::Borrowed(input)
}
}
В этом примере:
-
Если строка не содержит восклицательных знаков, Cow::Borrowed(input) возвращает ссылку — без копирования.
-
Если символ ! найден — создаётся новая строка, и возвращается Cow::Owned.
Снаружи Cow<str> можно использовать как обычную строку, например:
let result = modify("Hello, world!");
println!("{}", result); // если был '!', строка уже склонирована
Где применяется Cow
1. API, которые принимают как &T, так и T
Иногда вы хотите, чтобы функция принимала или ссылку, или владельца, в зависимости от ситуации:
fn process<'a>(input: Cow<'a, str>) {
println!("Processed: {}", input);
}
Теперь process(Cow::Borrowed("hello")) и process(Cow::Owned("hello".to_string())) оба допустимы.
2. Оптимизация чтения с условной мутацией
Например, если вы загружаете данные, обрабатываете и лишь изредка модифицируете — Cow помогает избежать копий:
fn sanitize<'a>(data: &'a str) -> Cow<'a, str> {
if data.trim() == data {
Cow::Borrowed(data)
} else {
Cow::Owned(data.trim().to_owned())
}
}
3. Хранение данных с экономией памяти
Если у вас есть структура, хранящая строки, и вы хотите избежать лишнего String, пока данные не модифицированы:
struct Person<'a> {
name: Cow<'a, str>,
}
Если имя не изменяется — вы храните &'a str. Если нужно обновить — Cow становится владельцем String.
Преимущества Cow
-
Гибкость: можно работать и со ссылками, и с владельческими данными.
-
Экономия ресурсов: копирование происходит только при необходимости.
-
Безопасность: остаётся в рамках системы владения и заимствования Rust.
-
Простота API: функция с Cow<'a, str> позволяет вызывать её как с &'static str, так и с String.
Когда стоит использовать Cow
Использование Cow оправдано в следующих случаях:
-
Данные чаще читаются, чем модифицируются.
-
Нужно избежать лишних аллокаций и копирования.
-
Хотите создать API, совместимый и с &T, и с T без лишних аллок.
-
В структуре данных нужно хранить заимствование, которое может стать владельческим.
Когда не стоит использовать Cow
-
Если вы точно знаете, что всегда будете владеть данными — проще сразу использовать String, Vec<T> и т.п.
-
Если данные почти всегда модифицируются — копия всё равно произойдёт, и Cow не даст выигрыша.
-
Если важна производительность в критических местах — Cow добавляет небольшую обёртку (enum), и хотя она zero-cost, в hot path лучше использовать фиксированные типы.