Святая Корова! (Holy std: borrow: Cow!)

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

На днях я участвовал в обсуждении на reddit того, насколько трудны на самом деле времена жизни. При работе над clippy мне не приходилось до этого времени иметь с ними дела, потому что всем, чем мы пользовались, владел компилятор, особого мнения на этот счёт у меня не было, и тут The_Doculope заявил:

Есть места, в которых опускание времён жизни может укусить нас за задницу.

(/u/The_Doculope на /r/rust)

Честно говоря, я был шокирован!

И вот я, благодарный опусканию времени жизни за то, что освободило меня от лишних переживаний, только что узнал, что оно может укусить меня за задницу. Я начал параноить. ; -P

(я на /r/rust)

Сразу после написания этого, мне попалась строка в исходном коде clippy, которой я был не очень доволен, потому что в ней происходило клонирование строки (с помощью .to_string()! богохульство!) просто для использования её в &format(…). Итак, происходило это в Result<String, …>::unpack_or(…), поэтому сначала я попытался дать обладаемой строке такое же время жизни, как и у полученной, без особого смысла.

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

Я избавлю вас от этих покрытых моей кровью сообщений об ошибках, потому что убеждён — если вы писали код на Rust, вы их знаете наизусть, и, даже наоборот, на самом деле все было не так уж и плохо, потому что, к счастью, я вспомнил такой удобный std::borrow::Cow, а уж переписать сниппет с ним (и std::convert::From) было раз плюнуть. Я даже создал fn для его повторного использования, таким образом, так я официально написал свой первый метод, аннотированный временем жизни. [ДОСТИЖЕНИЕ ОТКРЫТО]

Итак что делает Cow (кроме того, что говорит «Муу»)? Cow это аббревиатура Clone- on-write (обычно Copy on Write, но copy имеет другой смысл в Rust), прикольная идея, означающая, что мы можем пользоваться как обладаемым экземпляром чего- либо, так и заимствованным, используя один и тот же код.

Интересующимся, функция выглядит так:

pub fn snippet<'a>(cx: &Context, span: Span, default: &'a str) -> Cow<'a, str> {
    cx.sess().codemap().span_to_snippet(span).map(From::from).unwrap_or(Cow::Borrowed(default))
}

Бонус: Наверное, надо объяснить, что делает эта функция: cx.sess().codemap() извлекает CodeMap из нашей сессии статического анализа. У неё есть функция получения источника Span (который есть почти у всех элементов AST). Заметьте, функция span_to_snippet берет span и возвращает Result<String, SpanSnippetError> (ошибка возвращается для span’ов неправильного формата).

Итак, у нас есть два случая:

  1. Наш Result .is_ok(). Можем использовать типаж From для преобразования нашей String в Cow<'a, str> с любым временем жизни. Приведение типов сделает всю грязную работу за нас, все, что нужно это map(From::from), и мы в шоколаде.

  2. У нас SpanSnippetError. В этом случае мы хотим вернуть значение default, обёрнутое в Cow<'a, str>, где 'a — это время жизни аргумента default. Вот именно поэтому я написал функцию, обобщённую по времени жизни аргумента default: можем использовать его время жизни для нашей Cow. Для обработки этого случая, получая наш Cow из результата, используем unwrap_or.

Тип Result даёт нам удобные методы по работе с ним в читаемом виде. Так как Cow<'a, str> может выполнить Deref в str, мы можем напрямую использовать её в format!. Если мы захотим изменить строку, можем использовать функцию into_owned() у Cow.

Возможная сложная обработка времён жизни упрощена одним обобщённым временем жизни в функции, плюс две аннотации для аргумента и результата. К тому же я могу использовать функцию везде в моем коде, не волнуясь о временах жизни.

Итак, возможно опускание времени жизни и укусит меня когда-то. Но сегодня был явно не этот день.

Дальше читайте Продолжение