Самый недооцененный, но полезный тип стандартной библиотеки Rust

Перевод | Автор оригинала: James [Undefined]

Стандартная библиотека Rust полна множества полезных типов, свойств и абстракций. Сегодня я расскажу об одном, который, на мой взгляд, недооценен, но весьма полезен. Это Корова.

Что такое корова?

Согласно документации стандартной библиотеки, std::заимствовать::Cow:

интеллектуальный указатель, обеспечивающий функциональность клонирования при записи: он может заключать и обеспечивать неизменный доступ к заимствованным данным, а также лениво клонировать данные, когда требуется изменение или владение. Тип предназначен для работы с общими заимствованными данными через трейт Borrow.

Мне кажется, это описание точное, но оно не дает ему должного.

Cow на самом деле просто перечисление:

pub enum Cow<'a, B: ?Sized + 'a>
where
    B: ToOwned,
{
    /// Borrowed data.
    Borrowed(&'a B),

    /// Owned data.
    Owned(<B as ToOwned>::Owned),
}

Это определение не только позволяет иметь семантику клонирования при записи (где, если данные заимствованы, они превращаются в собственные данные до того, как становятся доступными изменчиво), но также позволяет хранить данные, которые потенциально принадлежат. Абстракция с ToOwned позволяет использовать Cow <'_, str>, сохраняя либо &str, либо String. Без этой абстракции Cow не была бы такой полезной, потому что вы могли бы только удерживать, например, str и Box (что определенно не так полезно, как String).

Конечно, вы могли бы просто написать собственное определение типа PotentialOwned <'a, T>, но если оно уже существует в стандартной библиотеке, вы также можете его использовать! Кроме того, семантика клонирования при записи является полезным дополнением к типу «потенциально принадлежащий».

Почему это полезно?

Семантика «потенциально принадлежащего» Cow делает его полезным в некоторых случаях, когда у вас может быть большая строка, которую вам нужно изменить, но вы не хотите клонировать без необходимости. Это пример, который не упоминается в документации стандартной библиотеки (я утверждаю, что это определенно должно быть).

Вот простой (хотя и несколько надуманный) пример того, как это полезно:

fn foo(s: &str, some_condition: bool) -> &str {
    if some_condition {
        &s.replace("foo", "bar")
    } else {
        s
    }
}

Позвольте мне объяснить этот код. Эта функция может заменить все экземпляры «foo» на «bar» в строке s, если условие some_condition истинно. Но есть проблема!

Функция возвращает &str, но str.replace возвращает String, и вы не можете вернуть ссылку на данные, принадлежащие функции!

Хорошо, давайте заставим функцию возвращать String.

fn foo(s: &str, some_condition: bool) -> String {
    if some_condition {
        s.replace("foo", "bar")
    } else {
        s.to_string()
    }
}

Эта функция работает, но обратите внимание, что она всегда клонирует строку. Обычно это плохо, потому что, если строка длинная, это может быть очень дорого! Итак, что нам делать, если мы не хотим без надобности клонировать?

Что ж, мы можем использовать Cow! Это позволяет нам возвращать данные, которые потенциально принадлежат.

Вот версия функции, которая без необходимости не клонируется:

use std::borrow::Cow;

fn foo<'a>(s: &'a str, some_condition: bool) -> Cow<'a, str> {
    if some_condition {
        Cow::from(s.replace("foo", "bar"))
    } else {
        Cow::from(s)
    }
}

И это работает! Наш код проходит проверку заимствований и не тратит впустую память!

Почему это сбивает с толку?

Примечание: этот раздел в основном является моими предположениями, поэтому не принимайте его как факт.

Я лично считаю, что Cow намного больше сбивает с толку, чем должно быть, благодаря программе проверки заимствований. Не поймите меня неправильно, счетчик займов просто потрясающий. Это определенно предотвращает ошибки безопасности памяти, которые в противном случае существовали бы без него. Но для новых пользователей Rust время жизни - это бумажная вырезка, и я думаю, что это одна из причин, по которой Cow в некотором роде недооценивают - ее довольно сложно использовать.

Все, что производит или использует корову, должно иметь привязку к жизни, и это лишь усложняет использование. Новым пользователям также потенциально сложно рассуждать о типах без размера, поэтому использование Cow <'_, str> может не иметь смысла для нового пользователя, поскольку они могут подумать что-то вроде «но подождите, я думал, вы только использовать &str, а не только str? "

На самом деле это не те проблемы, которые можно решить с помощью «быстрого решения», так что это по-прежнему будет потенциальным вырезом из бумаги.

Другая причина, по которой я нахожу, что Cow сбивает с толку, заключается в том, что название и документация затуманивают его потенциальное использование для потенциально принадлежащих данных. Новый пользователь, который задается вопросом: «Могу ли я позаимствовать данные, возвращаемые этой частью функции, и данные, которыми владеет эта часть?», Вероятно, не сразу подумает: «О, мне нужен интеллектуальный указатель клонирования при записи» (и они могут в конечном итоге попытаться для реализации принадлежащего им типа данных "частично принадлежащего", и могут достичь некоторых сокращений со сроками жизни или отсутствием ToOwned). Я не думаю, что действительно есть способ исправить часть именования без большой обратной несовместимости, но определенно есть способ исправить документацию.

Вывод

Корова - очень полезный тип со скрытыми способностями. Я считаю, что эти возможности следует более подробно описать в документации и книгах по Rust. Насколько я знаю, в Rust Book или Rust By Example нет упоминания о коровах.

Спасибо, что прочитали это, и желаю доброго утра / дня / вечера / ночи / и т.д. -Джеймс