Проверка ссылок с помощью Lifetimes

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

Lifetimes являются уникальной парадигмой языка Rust. Эта тема несколько обширна, что в этой главе мы сможем изложить только синтаксис. В Главе 19 вы узнаете больше и подробнее о возможностях этой парадигмы программирования на Rust.

Lifetimes защищают программу от недействительных ссылок

Самое важное свойство lifetimes - это предотвращение недействительных ссылок. Такую ошибку весьма трудно заметить. Это весьма коварная ошибка. Для примера, давайте рассмотрим код (10-18). Здесь демонстрируется поведение переменных в различных областях видимости. Во внешней области видимости мы декларируем переменную r без её инициализации. Во внутренней области видимости мы декларируем переменную x и инициализируем её значением 5. Внутри области видимости переменой x мы присваиваем, точнее, пытаемся присвоить значение переменной r. Присваиваем её ссылочное значение переменной x. Затем, мы хотим напечатать содержимое переменной r во внешней области видимости:

fn main(){
    let r;

    {
        let x = 5;
        r = &x;
    }

    println!("r: {}", r);
}

Listing 10-18: Попытка использования ссылки, которая стала недействительной после выхода переменной из внутренней области видимости

Неинициализированные переменные не использоваться (в выражениях, параметрах

вызова функций или методов и пр.)

Следующие примеры демонстрируют объявление переменных без их инициализации. Имя переменной существует во внешней области видимости.

Сообщение об ошибке:

error: `x` does not live long enough
   |
6  |         r = &x;
   |              - borrow occurs here
7  |     }
   |     ^ `x` dropped here while still borrowed
...
10 | }
   | - borrowed value needs to live until here

Переменная x больше не существует, т.к. её области видимости закончилась сразу же после выхода за пределы области её объявления. В тоже время переменная r продолжает находится в зоне свого объявления. Как же в таком случае компилятор Rust понимает, что содержание переменной уже недействительно?

Проверка заимствования

У в состав компилятора Rust входит функционал называющий ся провека заимствования. Демонстрационный код (10-19) иллюстрирует всё тот же пример (10-18), графически изображая область действия имеющихся переменных:

{
    let r;         // -------+-- 'a
                   //        |
    {              //        |
        let x = 5; // -+-----+-- 'b
        r = &x;    //  |     |
    }              // -+     |
                   //        |
    println!("r: {}", r); // |
                   //        |
                   // -------+
}

Пример кода 10-19: Описание времени жизни переменных r и x, с помощью идентификаторов 'a и 'b

Мы описываем время жизни переменной r с помощью 'a и время жизни переменной x с помощью описательной переменной 'b. Обратите внимание, что блок 'b находится внутри блока 'a и значительно меньше. Во время компиляции, компилятор Rust сравнивает два идентификатора времени жизни и получается так, что описание времени жизни переменной r находится в идентификаторе времени жизни 'a, но в тоже время хранит в себе ссылку на объект с идентификатором времени жизни 'b. Такая программа не компилируется, т.к. время жизни 'b короче, чем время жизни 'a, время жизни переменой больше, чем её содержание. Это неприемлемо.

Рассмотрим другой пример (10-20), в котором нет проблем с недействительными ссылками. В нём просто закомментировали наличие вложенного блока кода. Данный код пройдет тест на ссылочную целостность и будет принят компиляторов, как действительный и готовый к исполнению:


# #![allow(unused_variables)]
#fn main() {
{
    let x = 5;            // -----+-- 'b
    //{                   //      |
    let r = &x;           // --+--+-- 'a
    //}                   //   |  |
    println!("r: {}", r); //   |  |
                          // --+  |
}                         // -----+
#}

Блок кода 10-20: Все ссылки действительные, т.к. и данные и ссылка имеют одинаковое время жизни

В данном примере переменная x имеет время жизни 'b, что большое чем время жизни 'a. Это означает, что переменная r может ссылаться на переменную x и её значение будет действительно при введении значения на консоль и до конца блока, в котором переменная x была объявлена.

В этом пример мы рассмотрели и проанализировали работу анализатора времени жизни переменных. Далее, рассмотрим время жизни обобщенных переменных, а также возвращаемых функциями значений.

Идентификаторы времени жизни обобщенных типов данных в функциях

Напишем функцию, которая возвращает наибольшую по длине строку. Эта функция должна получать две переменные в качестве параметров функции и возвращать результат. В строке вывода должно быть напечатано The longest string is abcd. Пример кода (10-21):

Filename: src/main.rs

fn main() {
    let string1 = String::from("abcd");
    let string2 = "xyz";

    let result = longest(string1.as_str(), string2);
    println!("The longest string is {}", result);
}

Пример кода 10-21: Функция main вызывает функцию longest для поиска наибольшей строки

Обратите внимание, что мы хотим иметь в качестве параметров срезы строк (которые являются ссылками (об этом мы говорили в Главе 4)). Это делается для того, чтобы не передавать владение в функцию передаваемых аргументов. Мы хотим, чтобы функция принимала в качестве аргументов строковые срезы.

Освежим в памяти материал Главы 4 (секцию "Срезы строк в качестве аргументов") для того, чтобы хорошо представлять особенности а тонкости рассматриваемого вопроса в данном примере.

Если просто реализовать функции так, как это показано в примере кода (10-22), то то программа не будет скомпилирована:

Filename: src/main.rs

fn longest(x: &str, y: &str) -> &str {
    if x.len() > y.len() {
        x
    } else {
        y
    }
}

Код программы 10-22: Пример реализации функции longest, которая возвращает наибольший срез строки, но пока ещё не компилируется (содержит в себе ошибку)

В описании ошибки компилятор сообщает о проблемах в определении времени жизни:

error[E0106]: missing lifetime specifier
   |
1  | fn longest(x: &str, y: &str) -> &str {
   |                                 ^ expected lifetime parameter
   |
   = help: this function's return type contains a borrowed value, but the
   signature does not say whether it is borrowed from `x` or `y`

Описание ошибки давольно-таки информативно, но всё-таки не очень понятно. Ясно одно - анализатору времени жизни переменных не хватает данных для анализа корректности ссылочной целостности. Необходимо видоизменить код функции так, чтобы было устранить неопределенность и дать возможность компилятору провести все необходимые проверки.

Синтаксис описания времени жизни переменных

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

Описание времени жизни имеет необычный синтаксис: имена параметров времени жизни обязаны начинаться с символа '. Имена их обычно пишутся в нижнем регистре и также, как обобщенные типы данных, их имена очень короткие. Обычно, по умолчанию используется 'a. Параметры описания времени жизни следуют после символа & ссылочного типа данных и разделяются пробелом от названия тип данных.

Пример ссылки на переменную типа данных i32:

&i32        // a reference

Пример ссылки на переменную типа данных i32 с описанием времени жизни 'a:

&'a i32     // a reference with an explicit lifetime

Пример изменяемой переменной типа данных i32 с описанием времени жизни 'a:

&'a mut i32 // a mutable reference with an explicit lifetime

Само по себе описание времени жизни не имеет значения: оно сообщает компилятору Rust, как обобщенные параметры времени жизни изменяемой переменных связаны между собой. Если у нас есть функция с параметром first, которая имеет ссылочный тип данных i32 и имеет описание времени жизни 'a и эта функция имеет другой параметр с именем second, который в свою очередь является ссылкой на переменную типа i32 и имеет описание времени жизни 'a, то, следовательно у них одно и тоже описание времени жизни. Следовательно эти ссылки существовать столько же, сколько существует обобщенное описание времени жизни.

Описания времени жизни в синтаксисе функций

Теперь когда мы познакомились с аннотациями времени жизни переменных, применим эти знания. В функцию longest внесём описания времени жизни переменных. Обратите внимание, что аннотации в функциях располагаются в том же мести и квадратных скобках, также как и обобщенные типы данных. Ограничение которое мы хотим внести в данную функцию следующее: время жизни параметров и возвращаемого значения одно и тоже. Пример (10-23):

Filename: src/main.rs


# #![allow(unused_variables)]
#fn main() {
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
    if x.len() > y.len() {
        x
    } else {
        y
    }
}
#}

Listing 10-23: В функции longest определено время жизни ссылочных переменных 'a, благодаря чему компилятор может провести соответствующие проверки корректности кода

Теперь код (10-21) использующий эту функцию может быть скомпилирован.

Компилятор теперь знает достаточно, о функции longest, чтобы убедиться в ссылочной целостности.

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

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

Рассмотрим пример ограничений времени жизни работает на следующем примере (10-24):

Filename: src/main.rs

# fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
#     if x.len() > y.len() {
#         x
#     } else {
#         y
#     }
# }
#
fn main() {
    let string1 = String::from("long string is long");

    {
        let string2 = String::from("xyz");
        let result = longest(string1.as_str(), string2.as_str());
        println!("The longest string is {}", result);
    }
}

Код программы 10-24: Использование функции longest и ссылок на строковые данные String, которые имеют разное время жизни

Далее, рассмотрим пример, которые покажет, что время жизни результата работы функции минимальное из имеющихся. Мы переместим определение переменной result из внутренней области видимости, но присвоим значение внутри внутренней области видимости. При таких условиях, пример кода 10-25 не будет скомпилирован:

Filename: src/main.rs

fn main() {
    let string1 = String::from("long string is long");
    let result;
    {
        let string2 = String::from("xyz");
        result = longest(string1.as_str(), string2.as_str());
    }
    println!("The longest string is {}", result);
}

Listing 10-25: Попытка использование переменной result после выхода переменной string2 за пределы области видимости

Описание ошибки:

error: `string2` does not live long enough
   |
6  |         result = longest(string1.as_str(), string2.as_str());
   |                                            ------- borrow occurs here
7  |     }
   |     ^ `string2` dropped here while still borrowed
8  |     println!("The longest string is {}", result);
9  | }
   | - borrowed value needs to live until here

Эта ошибка возникает из-за минимального значения времени жизни одного из значений входных данных функции.

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

Думай категориями времени жизни

Для того, чтобы точно определить время жизни параметров необходимо точно знать, что будет делать функция. Например, если мы изменили реализацию функции longest в новой редакции функция будет всегда возвращать первый аргумент, то в этом случае нет необходимости устанавливать время жизни параметра y:

Filename: src/main.rs


# #![allow(unused_variables)]
#fn main() {
fn longest<'a>(x: &'a str, y: &str) -> &'a str {
    x
}
#}

Для того, чтобы не было сообщений компилятора о неиспользуемом параметре, сделайте, как рекомендуется:

Filename: src/main.rs


# #![allow(unused_variables)]
#fn main() {
fn longest<'a>(x: &'a str, _y: &str) -> &'a str {
    x
}
#}

В этом примере мы сообщили параметр времени жизни 'a для параметра x и возвращаемого значения, но не для параметра y.

Если же в коде метода возвращаемая ссылка не будет ссылаться на какой-либо из аргументов, тогда эта ссылка при выходе из области видимости станет недействительной, что приведёт к ошибке компиляции и другим тревожным сообщениям:

Filename: src/main.rs

fn longest<'a>(x: &str, y: &str) -> &'a str {
    let result = String::from("really long string");
    result.as_str()
}

Даже если будет установлен параметр время жизни у возвращаемого значения, этот код также не будет скомпилирован, т.к. возвращаемое значение не будет связано с временем жизни какого-либо из параметров.

Сообщение об ошибке:

error: `result` does not live long enough
  |
3 |     result.as_str()
  |     ^^^^^^ does not live long enough
4 | }
  | - borrowed value only lives until here
  |
note: borrowed value must be valid for the lifetime 'a as defined on the block
at 1:44...
  |
1 | fn longest<'a>(x: &str, y: &str) -> &'a str {
  |                                             ^

Т.к. время жизни переменной истекает после выхода её из области видимости функции, такой код не будет скомпилирован.

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

Определение времени жизни при объявлении структур

До сих пор мы объявляли структуры, которые содержали нессылочные типы данных. Также возможно в структурах использовать ссылочные типы данных, но при этом необходимо добавить описание времени жизни каждой ссылки в определение структуры. Пример кода 10-26 описывает структуру ImportantExcerpt содержащую срезы строковых данных:

Filename: src/main.rs

struct ImportantExcerpt<'a> {
    part: &'a str,
}

fn main() {
    let novel = String::from("Call me Ishmael. Some years ago...");
    let first_sentence = novel.split('.')
        .next()
        .expect("Could not find a '.'");
    let i = ImportantExcerpt { part: first_sentence };

    println!("{}",i.part);
}

Пример кода 10-26: Структура, которая содержит ссылку и определение времени жизни

Обратите внимание, что поле структуры ImportantExcerpt содержит ссылку

Синтаксис такой же, как и при работе с обобщенными типами данных.

Функция main создаёт экземпляр структуры ImportantExcerpt, который содержит ссылку на перовое предложение из переменной novel.

Правила неявного определения времени жизни

Подведём промежуточные итоги изучения описания времени жизни. Из этой части главы мы узнали, что каждая ссылка имеет время жизни и нам нужно устанавливать параметры времени жизни для функций или структур, которые используют ссылки.

Также, изучая материал Главы 4, мы создали функции (в секции “String Slices”), которая также использует ссылки, но при этом компилятор для её работы не требует информации о времени жизни (Пример кода 10-27):

Filename: src/lib.rs


# #![allow(unused_variables)]
#fn main() {
fn first_word(s: &str) -> &str {
    let bytes = s.as_bytes();

    for (i, &item) in bytes.iter().enumerate() {
        if item == b' ' {
            return &s[0..i];
        }
    }

    &s[..]
}
#}

Код программы 10-27: Обратите внимание, что функция, которою мы определили в Главе 4 компилируется без описания времени жизни ссылок, несмотря на то, что и входной параметр и выходной - ссылки

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

fn first_word<'a>(s: &'a str) -> &'a str {

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

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

Шаблоны, по которым компилятор Rust анализирует ссылки называется правилами неявного поредения времени жизни. Эти правила существую для компилятора, а не для пишущего программы на Rust программиста. Знание этих правил позволит не описывать время жизни ссылок там, где это делать необязательно.

Неявные правила не имеет четких ограничений. Эти правила выводятся на основе имеющихся данных в коде программы. Если этих данных не будет достаточно - компилятор выведет соответствующие сообщения и не скомпилирует код.

Сейчас мы рассмотрим правила анализа переменных времени жизни. Первое правило применяется к входным переменным. Последующие два правила применяются к выходным переменным. Если компилятор применил эти правила и в результате этого он не смог определить связывание данных переменных компилятор остановит свою работу и сообщит об ошибке. Вот эти правила:

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

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

  3. Если есть множество входных параметров времени жизни и один из них является ссылкой &self или &mut self (т.к. это ссылка на метод структуры или перечисления то в этом случае, параметр времени жизни self будет связан со всеми выходными параметрами времени жизни.

Вооружившись этими знаниями вернёмся к функции first_word и рассмотрим подробнее анализ компилятора её заголовка. В нём нет описания времени жизни:

fn first_word(s: &str) -> &str {

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

fn first_word<'a>(s: &'a str) -> &str {

Второе правило таки применяется, т.к. здесь только один входной параметр времени жизни. Следовательно, данная переменная времени жизни свяжется с выходной переменной времени жизни. Перепишем заголовок функции с учётом этого правила:

fn first_word<'a>(s: &'a str) -> &'a str {

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

Давайте рассмотрим ещё один пример - заголовок функции longest, в котором нет параметров времени жизни из примера 10-22:

fn longest(x: &str, y: &str) -> &str {

Применим правила времени жизни:

fn longest<'a, 'b>(x: &'a str, y: &'b str) -> &str {

Второе правило нельзя применить, т.к. входных параметров несколько. Смотрим третье правило. Оно также не применимо, т.к. это функция, а не метод. Поэтому компилятор в данном случае сообщит об ошибке (Пример кода 10-22)

Т.к. третье правило применяется только к методам, давайте рассмотрим работу этого правила подробнее.

Описание времени жизни в определении методов

Когда мы реализуем методы в структурах с описанием времени жизни, синтаксис описаний схож с аннотациями обобщенного программирования (Пример кода 10-11). Место где описания времени жизни определяется и используется зависит от того с чем он связывается - с полем структуры либо с аргументами методов и возвращаемыми значениями.

Имена переменных времени жизни для полей структур всегда должны описывается после ключевого слова impl и затем помещаться после имени структуры, т.к. это имя - неотъемлемая часть типа данных структуры.

В описании методов внутри блока impl, ссылки могут быть связаны с ссылками полей или могут быть независимыми. Дополнительно, правила неявного использования времени жизни делают использование переменных времени жизни необязательными. Рассмотрим пример кода. Используем структуру ImportantExcerpt из примера 10-26.

Здесь метод в методе level входной параметр - ссылка на self и возвращаемое значение типа i32 (не ссылка):


# #![allow(unused_variables)]
#fn main() {
# struct ImportantExcerpt<'a> {
#     part: &'a str,
# }
#
impl<'a> ImportantExcerpt<'a> {
    fn level(&self) -> i32 {
        3
    }
}
#}

Описание параметра времени жизни находится после impl и используется после имени. Нам не надо добавлять информацию к входному параметру (правило 1).

Пример применения третьего правила:


# #![allow(unused_variables)]
#fn main() {
# struct ImportantExcerpt<'a> {
#     part: &'a str,
# }
#
impl<'a> ImportantExcerpt<'a> {
    fn announce_and_return_part(&self, announcement: &str) -> &str {
        println!("Attention please: {}", announcement);
        self.part
    }
}
#}

Тут два входных параметра. Применяем первое правило. Т.к. один из параметров &self возвращаемое значения будет иметь время жизни переменой &self

Статическая переменная времени жизни

Существует ещё одно особенное врем жизни - 'static`. Оно описывает всё врем жизни программы. Все строковые литералы имеют этот тип времени жизни, которое мы можем указать явным образом:


# #![allow(unused_variables)]
#fn main() {
let s: &'static str = "I have a static lifetime.";
#}

Содержание этой строки сохраняется внутри бинарного кода вашей программы и всегда доступно для использования. Поэтому время жизни всех строковых литералов 'static.

В сообщениях компилятора могут быть сообщении использовать 'static. Прежде чем использовать данный тип переменной времени жизни, пожалуйста, подумайте, должна ли данная переменна быть доступна во время работы программы. Очень часто такие ссылки являются источником скрытых ошибок.

Обобщенные типы, связывание с типажом и переменные времени жизни

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


# #![allow(unused_variables)]
#fn main() {
use std::fmt::Display;

fn longest_with_an_announcement<'a, T>(x: &'a str, y: &'a str, ann: T) -> &'a str
    where T: Display
{
    println!("Announcement! {}", ann);
    if x.len() > y.len() {
        x
    } else {
        y
    }
}
#}

Это видоизмененный пример функции longest (код прогрммы 10-23). Программа возвращает наибольшую строку. Обратите внимание на дополнительный аргумент обобщенного типа ann: T! Он может быть любым, реализующим типаж Display. Содержание данной переменной будет напечатано прежде, чем будет произведено сравнение строк. Т.к. переменные времени жизни - это разновидность обобщенного типа параметров, они располагаются вместе.

Итоги

В этой главе мы рассмотрели много важного материала для понимания работы переменных времени жизни. Вы уже знаете достаточно, чтобы писать код программы и не дублировать создаваемый вами код. Обобщенные параметры помогаю использовать код для различных типов данных. Типажи и связывании с типажами помогает соблюсти конвенции и контракты чтобы иметь предсказуемое поведение. Также у нас есть эффективных способ борьбы с недействительными ссылками. Вся эта подготовительная работа проводиться в момент компиляции. Есть ещё тему которые дополнят эту картину. В Главе 17 вы изучите типажные объекты (это ещё один способ использовать типажи). В Главе 19 будет говориться о сложных сценариях использования переменных времени жизни. В Главе 20 будет рассмотрена система опций. В следующей главе будут рассказано, как написать тесты в Rust.