Конспектируем Книгу Rust:: Времена и структуры

от автора

Продолжаем работать с 10.3.

КМБ.- Двойная жизнь.- Восстание мертвецов.- Ошибка в документации.- Ужасающие подробности из The Rustonomicon.- Архитектурные озарения.- Развязка.

КМБ по структурам

Простой вариант:

#[derive(Debug)] struct Point {     x: i32,     y: i32, }  #[derive(Debug)] struct Circle {     center: Point,     radius: i32, }  fn main(){     let p1 = Point{x:10, y:20};     println!("p: {:?}", p1);      let c = Circle{ center: Point{x: 1, y:2}, radius: 3,};     println!("c: {:?}", c); }

  • #[derive(Debug)] обеспечивает генерацию кода для красивой печати через {:?}
  • Все поля должны быть явно проинициализированы

Вариант с обобщенными типами:

#[derive(Debug)] struct Point<T> {     x: T,     y: T, }  #[derive(Debug)] struct Circle<T> {     center: Point<T>,     radius: T, }  fn main(){     let p_i32 = Point::<i32>{x:10, y:20};     println!("p_i32: {:?}", p_i32);      let p_str = Point::<&str>{x: "I'm x", y: "I'm y"};     println!("p_str: {:?}", p_str);      let c_f64 = Circle::<f64>{ center: Point{x: 1.1, y:2.2}, radius: 3.3,};     println!("c_f64: {:?}", c_f64); }

Структуры со ссылочными полями

До структур процесс засахаривания еще не дошел, поэтому необходимо явно аннотировать абсолютно все, что относится к временам жизни. Пример:

#[derive(Debug)] struct Point<'a, T> {     x: &'a T,     y: &'a T, }  #[derive(Debug)] struct Circle<'a, T> {     center: Point<'a, T>,     radius: &'a T, }  fn main(){     let p_i32 = Point::<i32>{x: &10, y: &20};     println!("p_i32: {:?}", p_i32);      let c_f64 = Circle::<f64>{ center: Point{x: &1.1, y: &2.2}, radius: &3.3,};     println!("c_f64: {:?}", c_f64); }

  • &10 — занятно, Go так не может (cannot take the address of 10)
  • Примеры неразрывно связаны с иррациональным ритуалом проставления 'a в параметрах структур и далее везде
  • Как показано ранее, можно сделать Point<T>...Point::<&str>, что в итоге дает структуру с двумя ссылочными полями, явно времена нигде не указаны и ничего — пол не провалился
  • В чем тут сила смысл, брат?

Может быть, смысл появится, если параметров времени жизни будет несколько?

Двойная жизнь

В текущей документации по Rust я не нашел внятного примера на этот счет, но зато он есть в одной из старых версий, 19.2 Advanced Lifetimes. В примере используются конструкции, которые мы еще «не проходили», так что я решил его немного переделать, заодно подправил «архитектуру» по своему вкусу.

Целью было заставить Rust выдавать ошибку компиляции, если используется только одно время жизни. Должен сказать, это удалось не сразу — обычно строгий, до садизма, компилятор в этот раз благодушно прощал мне попытки манипулировать временами. Интересно, что пример из старой версии теперь также прекрасно компилируется. Видимо, технические писатели помыкались, помыкались… да и не стали вообще рассказывать про это в новой версии.

Что ж, мы не привыкли отступать. Общий замысел примера таков — есть короткоживущий ('s) Parser и долгоживущий ('l) Context. Parser берет данные из Context и возвращает результат в рамках жизни Context (т.е. результат работы Parser тоже долгоживущий).

Context:

struct Context<'l> {     data: &'l str, }

  • Здесь все живет долго ('l)

Parser:

struct Parser<'s, 'l: 's> {     ctx: &'s Context<'l>,     internal_data: &'s str, }

  • struct Parser<'s, 'l: 's>: означает, что у структуры два параметра времени жизни, время жизни 'l включает в себя 's ('l outlives 's)
  • ctx: &'s Context<'l> означает, что ctx является короткоживущей ссылкой, которая ссылается на экземпляр Context с долгоживущими ссылками внутри
  • На всякий случай — вместо 's и 'l можно использовать другие имена, от этого ничего не изменится, т.е. это не какие-то магические спецслова

Метод Parser.parse():

impl<'s, 'l> Parser<'s, 'l> {     fn parse(&self) -> &'l str {         if self.ctx.data.len() > 1{             &self.ctx.data[1..]         } else {             self.ctx.data         }     } }

  • Смысл метода — вернуть подстроку ctx.data начиная со второго байта, если этот второй байт есть

Все вместе:

#![allow(unused)]  struct Context<'l> {     data: &'l str, }  struct Parser<'s, 'l: 's> {     ctx: &'s Context<'l>,     internal_data: &'s str, }  impl<'s, 'l> Parser<'s, 'l> {     fn parse(&self) -> &'l str {         if self.ctx.data.len() > 0{             &self.ctx.data[1..]         } else {             self.ctx.data         }     } }  fn main(){     let working_ctx = Context{data: "0123456"};     let parsing_res: &str;     {         let dummy = &String::from("dummy");         let p = Parser{ctx: &working_ctx, internal_data: dummy};         parsing_res = p.parse();     }     println!("parsing_res: {}", parsing_res) }

Сломать это можно таким образом:

... impl<'a> Parser<'a, 'a> {     fn parse(&self) -> &'a str { ...

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

26 |         let dummy = &String::from("dummy");    |                      ^^^^^^^^^^^^^^^^^^^^^ creates a temporary which is freed while still in use ... 29 |     }    |     - temporary value is freed at the end of this statement 30 |     println!("parsing_res: {}", parsing_res)    |                                 ----------- borrow later used here ...    = note: consider using a `let` binding to create a longer lived value

А вот и не угадал, dummy вообще не используется! Кроме того, должен признать, что совет про let мне непонятен. Конечно, приятно загнать в угол компилятор, но действует он в правильном направлении, и даже, возможно, по-своему прав.

Строка let p = Parser{ctx: &working_ctx, internal_data: dummy}; связывает с p два времени жизни, короткое от dummy и длинное от working_ctx, но по сигнатуре parse() теперь не понять, с каким из них связан результат, так что компилятор выбирает из двух зол времен наименьшее, т.е. dummy. C коротким временем жизни dummy связывать долгоживущий parsing_res, естественно, нельзя. Сообщение компилятора, конечно, оставляет желать лучшего, с lifetime такая проблема есть, этим даже ребята из Zürich озабочены.

Ладно, посмотрим, как можно дополнительно сломать уже сломанное, чтобы минус на минус дал плюс.

Первый вариант, инициализировать dummy таким образом:

... // let dummy = &String::from("dummy"); let dummy = "dummy"; ...

Все заработало, несмотря на то, что у нас все еще «закорочены» времена (impl<'a> Parser<'a, 'a>). В чем тут дело? Видать, компилятор использует не только сигнатуры, но и data-flow analysis.

Значение переменной dummy теперь связано со временем жизни 'static (возможно, компилятор вообще эту переменную «оптимизирует», в любом случае — ее значение не «тухнет») и переменная p, получается, связана либо со 'static, либо с working_ctx. Соответственно, возвращаемое p.parse() значение можно смело «поднимать» на уровень working_ctx, и код потенциально «сертифицируется» без всякой дополнительной разметки времен.

Тут, кстати, можно начать подозревать, что понятие «время жизни» относится не столько к ссылкам, сколько к значениям, на которые они указывают…

Второй вариант, убрать internal_data из Parser:

... struct Parser<'s, 'l: 's> {     ctx: &'s Context<'l>, } ...

Теперь p у нас инициализируется так: let p = Parser{ctx: &working_ctx};, т.е. она связана только с долгоживущим working_ctx и возвращаемое любыми методами Parser значения можно использовать там же, где и working_ctx.

Все? Нет, не все.

Структуры со ссылками в параметрах функций

Приготовьтесь, сейчас будет грустно.

Обратимся к документации по поводу неявного выведения времен жизни (lifetime elision):

Первое правило говорит, что каждый параметр являющийся ссылкой, получает свой собственный параметр времени жизни. Другими словами, функция с одним параметром получит один параметр времени жизни: fn foo<‘a>(x: &’a i32); функция с двумя аргументами получит два различных параметра времени жизни: fn foo<‘a, ‘b>(x: &’a i32, y: &’b i32) — и так далее.

Второе правило говорит, что если существует точно один входной параметр времени жизни, то его время жизни назначается всем выходным параметрам: fn foo<'a>(x: &'a i32) -> &'a i32.

Рассмотрим такой код:

... struct Circle<'a, T> {     center: Point<'a, T>,     radius: &'a T, }  fn get_radius<T>(c: &Circle<T>) -> &T {     c.radius }

Теперь следите за руками. Входной параметр один? Да. Ergo — будет один входной параметр времени жизни. Согласно второму правилу, время жизни этого параметра назначается всем выходным параметрам. Так почему же…:

15 | fn get_radius<T>(c: &Circle<T>) -> &T {    |                     ----------     ^ expected named lifetime parameter

А потому, что в документации написано неправильно. Может быть, это проблема перевода? Нет, это не проблема перевода, вот оригинал:

The first rule is that each parameter that is a reference gets its own lifetime parameter. In other words, a function with one parameter gets one lifetime parameter: fn foo<‘a>(x: &’a i32); a function with two parameters gets two separate lifetime parameters: fn foo<‘a, ‘b>(x: &’a i32, y: &’b i32); and so on.

The second rule is if there is exactly one input lifetime parameter, that lifetime is assigned to all output lifetime parameters: fn foo<‘a>(x: &’a i32) -> &’a i32.

Эта ошибочная формулировка пошла гулять по другим руководствам, немного мутируя по пути, см., например, тут: «…only one input parameter passes by reference…».

Где правда? Вестимо, в «старом учебнике» получше, тогда деревья были большие, и все такое — но все равно туманно, без примеров надлежащего качества. Относительно неплохо также изложено в страшном и ужасном The Rustonomicon, но уже несколько иными словами. Эти слова цитирует ув. PsyHaSTe тут, правда, при их интерпретации использует ошибочную формулировку:

… в случае статических функций время жизни всех аргументов полагаются равными…

Может быть это и опечатка, одну букву всего-то заменить… но ведь кто-то должен, наконец, сформулировать нормально на русском языке, да и примеров подходящих все равно нигде нет… Итак:

Правило 1. Неуказанные времена жизни во входных параметрах функции автоматически проставляются и полагаются разными.
Правило 2. Если во входных параметрах функции можно указать максимум одно время жизни, то оно используется для всех неуказанных времен в выходных параметрах.
Правило 3. Если во входных параметрах есть &self или &mut self, то время жизни этого параметра используется для всех неуказанных времен в выходных параметрах.

Теперь опять рассмотрим непонятный пример. Сколько времен жизни можно максимально указать во входящих параметрах? Два:

fn get_radius<'a, 'b, T>(c: &'a Circle<'b, T>) -> &T {     c.radius }

Собственно, как-то так они проставляются компилятором «для себя» по Правилу 1, соответственно, Правило 2 не работает. Как починить?

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

Первый вариант:

fn get_radius<'a, 'b, T>(c: &'a Circle<'b, T>) -> &'a T {

или:

fn get_radius<'c, T>(c: &'c Circle<T>) -> &'c T {

Второй вариант:

fn get_radius<'a, 'b, T>(c: &'a Circle<'b, T>) -> &'b T {

или

fn get_radius<'circle, T>(c: &Circle<'circle, T>) -> &'circle T {

И последний пример::

... fn get_radius<T>(c: Circle<T>) -> &T {     c.radius }

Во входящих параметрах нет ни ссылок ни времен, почему работает? А потому, что можно указать только одно время жизни, соответственно, срабатывает Правило 2:

fn get_radius<'a, T>(c: Circle<'a, T>) -> &T {     c.radius }

Размышления про архитектуру приложения

Сложность написания и, что немаловажно, сложность чтения кода с большим количеством параметров времени жизни в структурах с увеличением числа этих самых параметров растет экспоненциально (или даже круче!), так что, КМК, лучше избегать сложных случаев.

  • Структуры должны быть простыми, с одним параметром времени жизни (S — SOLID)
  • «Агрегировать» структуры следует на уровне функций/методов
  • Есть даже такое мнение: You’re not allowed to use references in structs until you think Rust is easy…Use Box or Arc to store things in structs «by reference» (про Box и Arc будет в следующих частях)

В свете сказанного Parser можно переписать так:

struct Context<'a> {     data: &'a str, }  struct Parser<'a>  {     internal_data: &'a str, }  impl<'s, 'l: 's> Parser<'s> {     fn parse(&self, ctx: &'l Context) -> &'l str { ...

Или, используя anonymous lifetime, так (но пропадает соотношение времен):

... impl Parser<'_> {     fn parse<'l>(&self, ctx: &'l Context) -> &'l str { ...

  • Мы больше не храним ссылку на Context внутри Parser, а передаем прямо в метод parse()
  • В реализации методов для Parser мы указываем компилятору, что возвращаемое значение parse() связано параметром ctx
  • В принципе, соотносить времена не требуется, достаточно того, что они разные. Parser живет дольше Context? — Ну ок, почему нет. Главное, не использовать результат в более «широкой» области, чем Context, но тут-то компилятор за всем проследит!

Заключение

Конспект получился длиннее лекции, но это ничего — зато теперь можно смело идти получать «зачет»по 10.3.

Провидению препоручаю вас, дети мои, и заклинаю: остерегайтесь использовать ссылки в структурах, особенно в сложных структурах. Это, конечно, была шутка. Тем не менее — praemonitus praemunitus, forewarned is forearmed и так далее.

Stay tuned.

PS: Далее: Работа с кучей


ссылка на оригинал статьи https://habr.com/ru/articles/569496/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *