Продолжаем работать с 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 и так далее.
PS: Далее: Работа с кучей
PsyHaSTe
Спасибо за статью! И за то что обратили внимание на радикальную опечатку в моей изначальной статье.
Честно говоря, статья вышла кмк довольно суховатой: вроде все профессионально, по делу, но не очень понятна цель. То есть получается, взяли параграф из учебник и начинаем по-разному крутить код который его затрагивает, получая разные ошибки компиляции. Но читателю, возможно, не очень интересно получать ошибки) Ему интереснее что-нибудь новое узнать, выполнить какую-то цель. А в таком виде больше похоже на справочник, но его можно найти и просто в перечне ошибок rustc.
То есть технически к статье вопросов нет, а вот с точки зрения "продать" - кажется, что тяжело заинтересовать читателя ворохом технических подробностей, но без особой видимой цели. Лично мне по крайней мере было довольно тяжело продраться сквозь текст, а я вроде не первый день с языком знаком. По отдельности все просто и понятно, а вместе - уже хуже, и не отпускает вопрос "зачем?" - т.е. зачем мы это пишем, чего хотим добиться?
Всё это одна большая имха, возможно в комментариях будут люди которые объяснят, чего я недопонял. Но, комментарий есть комментарий, надеюсь мой будет полезен кому-то.
maxim_ge Автор
Параграф-то ошибочный, и тема валидации ссылок в целом в учебнике (The Rust Programming Language) не раскрыта. Надо читать что-то другое, например, The Rustonomicon — как это сделали, например, Вы, и как это сделал я. Много чего еще пришлось прочитать и осмыслить — ни с одним языком такого не припомню.
В прошлом тысячелетии читал Страуструпа, "Язык программирования С++". Не думаю, что там сильно проще материал, но не возникало мысли бегать по куче других источников чтобы понять, что же автор имел ввиду.