История: Когда стандартная библиотека Rust ввела Cow (Clone-on-Write), цель заключалась в том, чтобы абстрагировать данные, которые могут быть заимствованы или владены, без принуждения к немедленной аллокации. Трейд Clone изначально рассматривался, но он только позволяет создавать идентичные копии того же типа. Для заимствованных данных, таких как &str, клонирование создает другую ссылку, а не владеющую String, необходимую для модификации. Трейд ToOwned был разработан специально для выражения отношений между заимствованными и владеющими формами через связанный тип Owned.
Проблема: Если бы Cow полагался на Clone, то преобразование Cow::Borrowed(&str) в владеющее представление для модификации потребовало бы внешней логики преобразования. Clone не имеет механизма на уровне типов для преобразования &str в String, что заставляет либо выполнять преждевременную аллокацию во время создания, либо использовать сложное ручное управление состоянием. Это нарушило бы принцип нулевой стоимости абстракции Cow, поскольку сделало бы невозможным отложить аллокацию в куче до момента, когда модификация действительно необходима.
Решение: ToOwned определяет type Owned и fn to_owned(&self) -> Self::Owned, позволяя &str указывать Owned = String. Это позволяет Cow::to_mut() лениво аллоцировать только тогда, когда запрашивается изменение. Если Cow уже Owned, он возвращает изменяемую ссылку на существующие данные без аллокации. Следующий пример демонстрирует эту эффективность:
use std::borrow::Cow; fn normalize_whitespace(input: &str) -> Cow<'_, str> { if input.contains(" ") { let cleaned = input.replace(" ", " "); Cow::Owned(cleaned) // Аллокация происходит только здесь } else { Cow::Borrowed(input) // Заимствование без затрат } }
Служба обработки логов с высокой производительностью нуждалась в нормализации временных меток в записях, полученных из файлов с отображением в память. Входные данные приходили в виде срезов &str, указывающих на отображаемую область, но примерно 10% записей требовали корректировки часовых поясов, что требовало аллокации String. Первоначальная реализация использовала пользовательский перечисление с вариантами String и &str, требуя исчерпывающего сопоставления шаблонов на каждой точке доступа и ручной логики клонирования, что было подвержено ошибкам и было многословным.
Альтернатива 1: Жадное преобразование в String. Команда рассматривала возможность преобразования всех входных данных в String немедленно по их получению. Этот подход упростил модель данных и устранил проблемы с продолжительностью, но привел к серьезному превышению использования памяти. В условиях пиковых нагрузок это удвоило использование памяти для 90% логов, которые никогда не нуждались в модификации, вызывая ошибки OOM при обработке файлов размером 10 ГБ.
Альтернатива 2: Использование Arc<str> с copy-on-write. Другой вариант включал Arc<str> для неизменяемого совместного использования, дополненный Arc::make_mut для модификаций. Хотя это обеспечивало семантику совместного владения, это вводило накладные расходы на атомарное подсчет ссылок для каждого доступа. Кроме того, это по-прежнему требовало явной логики для обработки перехода от совместного к изменяемому, что усложняло модель заимствования, не предоставляя желаемой эргономики.
Альтернатива 3: Принятие Cow<'_, str>. Команда выбрала Cow, чтобы абстрагироваться от двух состояний. Варианты Borrowed указывали непосредственно на область памяти без алокации, тогда как варианты Owned хранили модифицированные строки. Это решение было выбрано, поскольку to_mut() откладывало аллокацию до первой модификации, сохраняя нулевую стоимость для путей только для чтения и предлагая единый API.
Результат: Парсер поддерживал высокую производительность, обрабатывая 10 ГБ логов с всего 200 МБ фактических аллокаций в куче. Используя Cow, система устранила ручное отслеживание состояния, сохранила свойства Send и Sync для параллельной обработки и уменьшила сложность кода на 60% по сравнению с подходом с пользовательским перечислением.
into_owned возвращает ToOwned::Owned по значению, что требует известного на этапе компиляции размера для выделения пространства стека. Хотя Cow может обертывать неразмерные типы, такие как str, через Cow<'_, str>, тип Owned (String) имеет фиксированный размер. Кандидаты часто путают Cow<'_, T> и Cow<'_, &T>, пытаясь реализовать трейды для ссылки, а не для заимствованного типа. Без ограничения Sized на ToOwned::Owned компилятор не мог бы создать возвращаемое значение для into_owned, так как он пытался бы вернуть неразмерное str напрямую, а не фиксированный контейнер String.
Cow реализует Borrow<Borrowed>, где Borrowed: ToOwned, позволяя Cow<String> быть найденным с помощью &str. Однако Borrow накладывает строгий контракт: если два значения равны через Eq, они должны давать идентичные хеш-значения. Кандидаты часто реализуют пользовательский PartialEq для Cow (например, регистрационное сравнение) при сохранении стандартной реализации Hash. Это нарушает контракт, поскольку два значения Cow могут быть равны по пользовательской логике, но хешироваться по-разному, если реализация Hash видит оригинальные байты. Это приводит к сбоям поиска в HashMap, где ключ кажется существующим, но не может быть найден.
Для создания варианта Borrowed Cow требует ссылку &'a B с продолжительностью 'a. Универсальная реализация Default потребуется производить ссылку, действительную для 'static (например, &'static str для ""), но &str сам по себе не реализует Default, потому что нет универсального значения ссылки, которое можно было бы вернуть. Кандидаты часто предлагают использовать Cow::Borrowed("") по умолчанию, но это требует либо ограничения времени жизни 'static на B, либо специализации, недоступной в стабильном Rust. Следовательно, стандартная библиотека требует ToOwned::Owned: Default, что вынуждает использовать Cow::Owned(String::new()) (аллокация) даже для пустых значений по умолчанию. Кандидаты упускают это различие, путая доступность строковых литералов в определенных областях с общей реализацией Default для ссылок.