Святая Корова! — Продолжение (Holy std: borrow: Cow! — Redux)

оригинал: llogic • перевод: Сергей Ефремов • обучение • поддержите на Patreon

Последний раз я использовал очень полезный Cow, чтобы разобраться с тем, нужно ли клонировать str или просто заимствовать её. Это было моё первое применение аннотаций времён жизни, и это было явно похоже на достижение. : -)

На /r/rust, пользователь Artemciy спросил меня очень хороший вопрос

[…] Как это работает? Похоже на какую-то магию. Имею ввиду, как String становится str?

А также предложил свой вариант ответа:

P. S. Похоже, что в String есть реализация IntoCow’, которая преобразует её в str, но если посмотреть на эту реализацию — опять какая-то магия. Может String в тайне является str?

(выделено мной)

Хороший вопрос. На самом деле, наверное, вы бы хотели прочитать про обмен догадками, озарение, исследования, неправильные решения и драму в интернете. Но увы! У кого есть на это время? Поэтому попытаюсь донести всю суть от начала и до конца.

Первой подсказкой является сам Cow. Если посмотреть на сырой код, можно найти следующее (документация модуля и некоторые дополнения):

pub enum Cow<'a, B: ?Sized + 'a> where B: ToOwned {
    Borrowed(&'a B),
    Owned(<B as ToOwned>::Owned)
}

Я не буду обсуждать различные реализации Into и From, которые объединяют всех Cow, String и другие типы; можете посмотреть их в документации к библиотеке. Вещи по-настоящему гениальны, а тот, кто их добавил, заслуживает Премии Тьюринга.

Как мы помним с недавнего времени, у нас был тип Cow<'a, str> с временем жизни 'a, который мы получали из аргумента default. Но это вообще ничего не говорит о str или String! Где же тут реализация from?

Подсказкой была граница типажа ToOwned, который реализован для str. Глядя на неё, можно выделить следующее определение (укорочено здесь):

impl ToOwned for str {
    type Owned = String;
    fn to_owned(&self) -> String { ... }
}

Итак, большая часть мозаики сложилась. Определение содержит ассоциированный тип, называемый Owned. В случае str, тип Owned это…

String. Та-дамс!

Он определяет также, как получить String из str, к этому мы ещё вернёмся попозже. Итак, наш Cow<'a, str> может стать следующим:

именно это мы и хотели. Если мы выполним Deref или Borrow нашего Cow, мы получим из него &'a str (в случае владения, он просто заимствует наш обёрнутый обладаемый экземпляр), а если мы просим .to_owned(), мы всегда получим String (в случае реализации ToOwned, приведённой выше).

Отлично. Но это ещё не конец. Мы не ответили на вопрос, правда ли что наш такой простой String на самом деле ведёт двойную жизнь, и становится str при полной луне, во тьме или когда у него просто подходящее настроение. Или как сказал Artemciy: Может String в тайне является str?

Я верю, что каждый может поступать так, как он хочет, но вмешиваться в действия типов Rust все же довольно грубо. Однако, String действует подозрительно, и нет ничего лучше хорошей детективной истории. Просто не говорите String, откуда мы узнали его маленький секрет, если мы действительно что-то найдём.

Для начала нашего расследования у нас есть подсказки:

&str. Подождите. Eddy B. объяснил, что я ошибаюсь: Любой &String является типом &String. На самом деле неявные приведения типов при разыменовании преобразовывают её в &str, потому что Deref так реализован у String, что ассоциированным целевым типом является str

Итак, что такое str? И что такое String?

В документации сказано следующее про str:

Тип str в Rust является одним из примитивных типов в ядре языка. &str это тип заимствованной строки. Этот тип может быть создан только из другой строки, кроме случаев когда он является &'static str (см ниже). Нельзя переместить значение из заимствованной строки, потому что в другом месте кто-то владеет им.

Посмотрев в исходный код, str.rs убеждаемся, что str это примитивный тип. Это означает, что нет там нет описания struct или enum, а также алиаса type. Компилятор, очевидно, понимает str более ясно, чем с готовностью в этом признается.

Дальше в документации есть раздел представлений, содержащий следующее предложение:

The actual representation of strs have direct mappings to slices: &str is the same as &[u8]. (Настоящее представление str точно совпадает со срезом: &str это тоже самое, что и &[u8]).

Итак, str это просто набор байтов, которые гарантированно являются правильными UTF-8. &str таким образом, это заимствованная последовательность байтов, в то же время String это…здесь у нас пустота. У нас есть один кусочек пазла, а второй поищем в collections: string, где представлено описание String:

pub struct String {
    vec: Vec<u8>,
}

Итак, String это обёртка вокруг Vec из байт, конструктор которой гарантирует, что содержимое всегда будет правильным UTF-8.

Теперь мы видим, что наше подозрение было необоснованным: str и String очень близки друг другу (на самом деле у них такие же отношения, как у содержимого среза по отношению к Vec), но это не один человек. Дело закрыто!

Без мучения нет и развлечения, так ведь? Вы же не расскажете String о нашем маленьком расследовании, да? Пожалуйста, пообещайте мне…

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

Бонус: Главный сыщик diwic поделился результатами расследования шокирующей связи между Borrow и ToOwned в комментарии на /r/rust.