После главы 4 (было здесь) переходим к 10.3. Ну а куда еще… такие нынче времена.


Проблема


Для начала обозначим проблему, ради которой затеваются игры со временем. Простой пример висячей ссылки (dangling reference):


fn main() {
  let r;

  {
    let s = String::from("Hello");
    r = &s;
  } 
  // <- s will be drop()-ed here

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

Значение s будет создано в куче, затем уничтожено, а r — останется и будет ссылаться на то, чего нет. Rust отклонит такую заявку на выстрел в ногу с подробным пояснением, что s не живет достаточно долго:


   Compiling playground v0.0.1 (/playground) error[E0597]: `s` does not live long enough
 --> src/main.rs:6:9
  |
6 |     r = &s;
  |         ^^ borrowed value does not live long enough
7 |   }
  |   - `s` dropped here while still borrowed
8 | 
9 |   println!("r: {}", r);
  |                     - borrow later used here

Но такие простые штуки давно ловятся и в других языках статическими анализаторами кода. Вот вариант посложнее, функция получает две ссылки на строки и возвращает ту, референт которой (то, на что указывает ссылка) длиннее:


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

Вызывать будем так:


fn main() {
    let short_string = String::from("Short string");    
    let r_longest;
    {
        let long_string = String::from("Long string................");
        r_longest = longest(&short_string, &long_string);
    }
    println!("The longest string: {}", r_longest);
}

r_longest получит, очевидно, ссылку на long_string и будет жить непозволительно долго (дольше своего референта) — такое в production попасть не должно.


В принципе, статический анализатор кода для C++ может и и такую ситуацию выловить — если "зайдет" внутрь вызываемой функции. Интересно, PVS-Studio справится с такой задачей, что скажет Andrey2008?


Rust, однако, для проверки вызывающего кода смотрит только на сигнатуру того, что вызывается, внутрь не заглядывая. Посмотрим, как он это делает. Запускаем на компиляцию… Компилятор указывает нам, что сигнатура недостаточно информативна:


   Compiling playground v0.0.1 (/playground)
error[E0106]: missing lifetime specifier
  --> src/main.rs:12:39
   |
12 | fn longest(x: &String, y: &String) -> &String {
   |               -------     -------     ^ expected named 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`
help: consider introducing a named lifetime parameter
   |
12 | fn longest<'a>(x: &'a String, y: &'a String) -> &'a String {
   |           ^^^^    ^^^^^^^^^^     ^^^^^^^^^^     ^^^

Указание подробное и очень полезное. Нам говорят, что функция возвращает одолженное значение (т.е. ссылку), но непонятно, с каким параметром это одалживание связано — с x или y. Более того, компилятор даже предлагает способ починить объявление функции, введя параметр времени жизни (lifetime parameter).


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


Итак, нам нужно разметить сигнатуру функции специальным образом, чтобы компилятор Rust "понял", с чем связано возвращаемое значение (у нас связано со всеми параметрами). Это делается при помощи generic lifetime parameters:


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

Такое описание означает, что результат может быть связан как с x, так и с y, и после изменения примера компилятор ожидаемо в ожидаемом месте сообщает, что:


6 |         r_longest = longest(&short_string, &long_string);
  |                                            ^^^^^^^^^^^^ borrowed value does not live long enough

Враг пойман, пилоты молчат, смотрят на звезды, задание выполнено успешно. Все улыбаются. Но возникают два вопроса. Первый — а как сделать так, чтобы время жизни результата было связано, например, только со вторым параметром? Вот так:


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

Но при этом функция перестанет компилироваться, так как мы в одной из веток if пытаемся возвратить x со временем жизни, не совпадающим с таковым для возвращаемого значения — как видим, машинерия Rust исправно работает.


Второй вопрос (риторический) — а нельзя было "засахарить" синтаксис, вводить <'a> неявно и применять его по умолчанию ко всем параметрам и результатам, а у кого есть специальная нужда, тот пусть и указывает другие времена? С учетом уникальности имен параметров можно было бы вообще сделать как-то так (но это не точно):


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

Сразу было бы ясно, что результат связан с параметром y и не имеет права этот параметр пережить ("outlive")...


Впрочем, процесс засахаривания Rust идет, применительно к теме его результаты называются Lifetime Elision.


Правила неявного выведения времен жизни (Lifetime Elision)


Первое правило бойцовского клуба: Для каждого параметра-ссылки с неуказанным временем жизни заводится свой неявный параметр:


Вместо (возвращаемое значение связано со вторым параметром):


fn myfunc<'a, 'b>(x: &'a String, y: &'b String) -> &'b String {

Пишем:


fn myfunc<'a>(x: &String, y: &'a String) -> &'a String {

Второе правило. Если в параметрах только одна ссылка, то ее время жизни переносится на все возвращаемые значения:


Вместо:


fn myfunc<'a>(p1: &'a str, p2: int, p3: int) -> (out1: &'a str, out2: &'a str) { 

Пишем:


fn myfunc(p1: &str, p2: int, p3: int) -> (out1: &str, out2: &str) {

Вот это хорошо, конечно.


Третье правило. Если в параметрах много ссылок, но одна из них &self или &mut self, то ее время жизни переносится на все возвращаемые значения


Для методов все шикарно. Видимо, нас как бы подталкивают в сторону ООП (которого в Rust "по-настоящему" и нет, хе-хе).


Под занавес надо разрешить загадку из первой части.


Время жизни 'static


Напомню, вот такой пример не компилируется:


fn dangling_reference() -> &str {
    let s = String::from("hello");
    return &s
}

Результат:


error[E0106]: missing lifetime specifier
 --> src/lib.rs:1:28
  |
1 | fn dangling_reference() -> &str {
  |                            ^ expected named lifetime parameter
  |
  = help: this function's return type contains a borrowed value, but there is no value for it to be borrowed from
help: consider using the `'static` lifetime
  |
1 | fn dangling_reference() -> &'static str {
  |                            ^^^^^^^^                      ^^^^^^^^

Компилятор закономерно жалуется, мол, неоткуда занять возвращаемое значение, все умерло. И опять предлагает решение! — обозначить время жизни возвращаемого значения как 'static.


Делается так:


static STATIC_STR: &str = "I am STATIC_STR";

fn static_reference() -> &'static str {
    return STATIC_STR
}

fn main(){
    println!("static_reference: {}", static_reference());
}

  • Статическую переменную типа String, по-видимому, сделать нельзя.