Как стать автором
Обновить

Распространённые заблуждения о временах жизни в Rust

Время на прочтение 27 мин
Количество просмотров 20K
Автор оригинала: kirill aka @pretzelhammer

(прим. переводчика: времена жизни (lifetimes) — это одна из самых запутанных вещей в Rust, которая часто вызывает затруднение у новичков, даже несмотря на официальную документацию. Разъяснения по отдельным аспектам времён жизни есть, но они все разбросаны по разным источникам и ответам на Stack Overflow. Автор статьи собрал в одном месте и разъяснил множество связанных с временами жизни вопросов, что и делает эту статью столь ценной (я и сам почерпнул новое для себя отсюда). Я решил перевести её, чтобы дать возможность прочитать её тем, кто не владеет английским в достаточной степени, чтобы свободно читать оригинал, а также для того, чтобы повысить известность этой статьи среди русскоязычного Rust-сообщества)


19 мая 2020 г. · 37 минут · #rust · # lifetimes


Оглавление



Вступление


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


Термин Значение
T 1) множество всех возможных типов или
2) некоторый тип из этого множества
владеющий тип некоторый тип, не являющийся ссылкой, например, i32, String, Vec и т. д.
1) заимствованный тип или
2) ссылочный тип
некоторый ссылочный тип, вне зависимости от его изменяемости, например &i32, &mut i32 и т. д.
1) mut-ссылка или
2) эксклюзивная ссылка
эксклюзивная изменяемая ссылка, т.е. &mut T
1) immut-ссылка или
2) разделяемая ссылка
разделяемая неизменяемая ссылка, т.е. &T

Заблуждения


В двух словах: время жизни переменной — это статически проверяемый компилятором промежуток времени, в течение которого валидны обращения к данным, лежащим по адресу, сохранённому в переменной. Следующие ~6500 слов я потрачу на более подробный рассказ о том, где обычно возникает путаница.


1) T содержит только владеющие типы


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


Когда я впервые начал изучать Rust, я понял, что i32, &i32 и &mut i32 — это разные типы. Я также понял, что некоторая переменная типа T представляет собой множество, которое содержит все возможные типы. Однако, несмотря на то, что я понимал обе эти вещи по отдельности, я не мог понять их в совокупности. Я наивно полагал, что в Rust обобщённые типы работают так:


Переменная типа T &T &mut T
Примеры i32 &i32 &mut i32

T содержит все владеющие типы. &T содержит все неизменяемо заимствованные типы. &mut T содержит все изменяемо заимствованные типы. T, &T и &mut T — непересекающиеся конечные множества. Красиво, просто, кратко, легко, интуитивно понятно и совершенно неправильно. Вот как в Rust обобщённые типы работают по-настоящему:


Переменная типа T &T &mut T
Примеры i32, &i32, &mut i32, &&i32, &mut &mut i32, ... &i32, &&i32, &&mut i32, ... &mut i32, &mut &mut i32, &mut &i32, ...

T, &T и &mut T — бесконечные множества, так как тип можно заимствовать до бесконечности. T является надмножеством обоих &T и &mut T, а &T и &mut T — непересекающиеся множества. Вот пара примеров, подтверждающих эти представления:


trait Trait {}

impl<T> Trait for T {}

impl<T> Trait for &T {} // ошибка компиляции

impl<T> Trait for &mut T {} // ошибка компиляции

Программа выше не компилируется, как и ожидалось:


error[E0119]: conflicting implementations of trait `Trait` for type `&_`:
 --> src/lib.rs:5:1
  |
3 | impl<T> Trait for T {}
  | ------------------- first implementation here
4 |
5 | impl<T> Trait for &T {}
  | ^^^^^^^^^^^^^^^^^^^^ conflicting implementation for `&_`

error[E0119]: conflicting implementations of trait `Trait` for type `&mut _`:
 --> src/lib.rs:7:1
  |
3 | impl<T> Trait for T {}
  | ------------------- first implementation here
...
7 | impl<T> Trait for &mut T {}
  | ^^^^^^^^^^^^^^^^^^^^^^^^ conflicting implementation for `&mut _`

Компилятор не позволяет нам определять реализацию Trait для &T и &mut T, потому что она будет конфликтовать с реализацией Trait для T, которая уже включает в себя все &T и &mut T. Программа ниже компилируется, как и ожидалось, так как &T и &mut T не пересекаются:


trait Trait {}

impl<T> Trait for &T {} // компилируется

impl<T> Trait for &mut T {} // компилируется

Ключевые выводы


  • T является надмножеством как &T, так и &mut T
  • &T и &mut T являются непересекающимися множествами

2) если T: 'static, то T должно жить на протяжении всего времени работы программы


Ошибочные выводы


  • T: 'static должно читаться как «T имеет время жизни 'static»
  • &'static T и T: 'static — это одно и то же
  • если T: 'static, то T должен быть неизменяемым
  • если T: 'static, то T может быть создан только на этапе компиляции

Большинство новичков в Rust впервые знакомятся со временем жизни 'static, используя код, выглядящий примерно так:


fn main() {
    let str_literal: &'static str = "str literal";
}

Им говорят, что "str literal" хранится в сегменте данных в скомпилированном двоичном файле и загружается в доступную только для чтения память во время исполнения, поэтому он неизменяем и действителен на протяжении всего времени работы программы, что и делает его 'static. Эти понятия дополнительно подкрепляются правилами определения static переменных с использованием ключевого слова static .


static BYTES: [u8; 3] = [1, 2, 3];
static mut MUT_BYTES: [u8; 3] = [1, 2, 3];

fn main() {
   MUT_BYTES[0] = 99; // ошибка компиляции, изменение статической переменной является небезопасной операцией

    unsafe {
        MUT_BYTES[0] = 99;
        assert_eq!(99, MUT_BYTES[0]);
    }
}

Что справедливо по отношению к static переменным:


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

'static время жизни, вероятно, было названо так из-за времён жизни по умолчанию static переменных, ведь так? Так что логично, что время жизни 'static должно следовать тем же правилам, не так ли?


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


Важно различать &'static T и T: 'static.


&'static T — это неизменяемая ссылка на некоторый T, которую можно хранить безопасно неопределенно долгое время, в том числе вплоть до конца работы программы. Это возможно только в том случае, если сам T является неизменяемым и не перемещается после создания ссылки. T не обязательно должен быть создан на этапе компиляции. Вполне возможно генерировать произвольные динамически выделяемые значения во время исполнения и возвращать 'static ссылки на них ценой утечки памяти, например:


use rand;

// генерирует во время исполнения случайные 'static str ссылки
fn rand_str_generator() -> &'static str {
    let rand_string = rand::random::<u64>().to_string();
    Box::leak(rand_string.into_boxed_str())
}

T: 'static — это некий T, который можно хранить безопасно бесконечно долго, в том числе вплоть до конца работы программы. T: 'static включает в себя все &'static T, однако оно также включает в себя все владеющие типы, такие как String, Vec и т. д. Владелец некоторых данных является гарантом того, что данные никогда не будут инвалидированы, пока владелец их удерживает, следовательно, владелец может безопасно хранить данные сколь угодно долго, в том числе вплоть до конца работы программы. T: 'static должно читаться, как «T ограничен временем жизни 'static», а не «T имеет время жизни 'static». Вот программа для иллюстрации этих концепций:


use rand;

fn drop_static<T: 'static>(t: T) {
    std::mem::drop(t);
}

fn main() {
    let mut strings: Vec<String> = Vec::new();
    for _ in 0..10 {
        if rand::random() {
            // все строки сгенерированны случайным образом
            // и динамически созданы в куче во время исполнения
            let string = rand::random::<u64>().to_string();
            strings.push(string);
        }
    }

    // строки являются владеющими типами,
    // поэтому они удовлетворяют ограничению 'static
    for mut string in strings {
        // все строки изменяемы
        string.push_str("a mutation");
        // все строки динамически уничтожаемы
        drop_static(string); // компилируется
    }

    // все строки были инвалидированы до окончания работы программы
    println!("i am the end of the program");
}

Ключевые выводы


  • T: 'static должно читаться, как «T ограничен временем жизни 'static»
  • если T: 'static, то T может быть заимствованным типом с временем жизни 'static или владеющим типом.
  • поскольку T: 'static включает в себя владеющие типы, это означает, что T
    • может быть динамически создан в рантайме
    • не обязан существовать на протяжении всего времени работы программы
    • может быть безопасно и беспрепятственно изменён
    • может быть динамически уничтожен во время исполнения
    • может иметь время жизни произвольной продолжительности

3) &'a T и T: 'a — это одно и то же


Это заблуждение является обобщением заблуждения выше.


&'a T требует и подразумевает T: 'a, поскольку ссылка на T, имеющая время жизни 'a, не может быть действительной для 'a, если сам T недействителен для 'a. Например, компилятор Rust никогда не разрешит конструкцию типа &'static Ref<'a, T> потому что если Ref действителен только для 'a, то мы не можем сделать 'static ссылку на него.


T: 'a включает в себя все &'a T, но обратное неверно.


// принимает только ссылки, ограниченные 'a
fn t_ref<'a, T: 'a>(t: &'a T) {}

// принимает любые типы, ограниченные 'a
fn t_bound<'a, T: 'a>(t: T) {}

// владеющий тип, содержащий ссылку
struct Ref<'a, T: 'a>(&'a T);

fn main() {
    let string = String::from("string");

    t_bound(&string); // компилируется
    t_bound(Ref(&string)); // компилируется
    t_bound(&Ref(&string)); // компилируется

    t_ref(&string); // компилируется
    t_ref(Ref(&string)); // ошибка компиляции, ожидалась ссылка, обнаружена структура
    t_ref(&Ref(&string)); // компилируется

    // строковая переменная ограничена 'static, которое, в свою очередь, ограничено 'a
    t_bound(string); // компилируется
}

Ключевые выводы


  • T: 'a является более общим и более гибким типом, чем &'a T
  • T: 'a принимает владеющие типы, владеющие типы, которые содержат ссылки, и ссылки
  • &'a T принимает только ссылки
  • если T: 'static, то T: 'a, так как 'static >= 'a для всех 'a

4) мой код не является обобщённым и не имеет времён жизни


Ошибочные выводы


  • можно избежать использования обобщённых типов и времён жизни

Это удобное заблуждение сохраняется из-за правил вывода времён жизни (lifetime elision), позволяющих опустить аннотации времён жизни в функциях, поскольку компилятор Rust выведет их согласно следующим правилам:


  • каждая ссылка-аргумент функции получает отдельное время жизни
  • если есть ровно одно входное время жизни, то оно приписывается всем возвращаемым ссылкам (прим. переводчика: не только ссылкам, но и вообще всем обобщённым типам, параметризованным временем жизни)
  • если есть несколько входных времён жизни, но одно из них — это время жизни &self или &mut self, то время жизни self приписывается всем возвращаемым ссылкам
  • в противном случае времена жизни возвращаемых значений должны быть указаны явно

Это довольно много, поэтому давайте рассмотрим несколько примеров:


// неявно
fn print(s: &str);

// выведено
fn print<'a>(s: &'a str);

// неявно
fn trim(s: &str) -> &str;

// выведено
fn trim<'a>(s: &'a str) -> &'a str;

// правила вывода неприменимы, нельзя вывести время жизни возвращаемого значения,
// т. к. нет входных времён жизни
fn get_str() -> &str;

// возможные варианты явных аннотаций
fn get_str<'a>() -> &'a str; // обобщённая версия
fn get_str() -> &'static str; // 'static версия

// правила вывода неприменимы, нельзя вывести время жизни возвращаемого значения,
// т. к. есть несколько входных времён жизни
fn overlap(s: &str, t: &str) -> &str;

// возможные варианты с явным указанием времён жизни
// (но всё равно с частичным выводом)
fn overlap<'a>(s: &'a str, t: &str) -> &'a str; // возвращаемое значение не может прожить дольше s
fn overlap<'a>(s: &str, t: &'a str) -> &'a str; // возвращаемое значение не может прожить дольше t
fn overlap<'a>(s: &'a str, t: &'a str) -> &'a str; // возвращаемое значение не может прожить дольше s и t
fn overlap(s: &str, t: &str) -> &'static str; // возвращаемое значение может прожить дольше s и t
fn overlap<'a>(s: &str, t: &str) -> &'a str; // никакой связи между входными и выходными временами жизни

// выведено
fn overlap<'a, 'b>(s: &'a str, t: &'b str) -> &'a str;
fn overlap<'a, 'b>(s: &'a str, t: &'b str) -> &'b str;
fn overlap<'a>(s: &'a str, t: &'a str) -> &'a str;
fn overlap<'a, 'b>(s: &'a str, t: &'b str) -> &'static str;
fn overlap<'a, 'b, 'c>(s: &'a str, t: &'b str) -> &'c str;

// неявно
fn compare(&self, s: &str) -> &str;

// выведено
fn compare<'a, 'b>(&'a self, &'b str) -> &'a str;

Если вы когда-нибудь писали:


  • метод структуры
  • функцию, принимающую ссылки
  • функцию, возвращающую ссылки
  • обобщённую функцию
  • трейт-объект (подробнее об этом позже)
  • замыкание (подробнее об этом позже)

то тогда повсюду в вашем коде есть выведенные аннотации времён жизни.


Ключевые выводы


  • почти весь код Rust является обобщённым кодом, и везде есть выведенные аннотации времён жизни

5) если мой код компилируется, то мои аннотации времён жизни верны


Ошибочные выводы


  • Правила вывода времён жизни для функций всегда верны
  • Анализатор заимствований (borrow checker) в Rust всегда прав, и технически, и семантически
  • Rust знает о семантике моей программы больше меня

Программа на Rust может быть технически компилируемой, но в то же время семантически неверной. Рассмотрим пример:


struct ByteIter<'a> {
    remainder: &'a [u8]
}

impl<'a> ByteIter<'a> {
    fn next(&mut self) -> Option<&u8> {
        if self.remainder.is_empty() {
            None
        } else {
            let byte = &self.remainder[0];
            self.remainder = &self.remainder[1..];
            Some(byte)
        }
    }
}

fn main() {
    let mut bytes = ByteIter { remainder: b"1" };
    assert_eq!(Some(&b'1'), bytes.next());
    assert_eq!(None, bytes.next());
}

ByteIter — это итератор, который перебирает срез байтов. Для краткости мы пропустили реализацию трейта Iterator. Кажется, всё работает нормально, но что, если мы хотим проверить пару байтов за раз?


fn main() {
    let mut bytes = ByteIter { remainder: b"1123" };
    let byte_1 = bytes.next();
    let byte_2 = bytes.next();
    if byte_1 == byte_2 {
        // что-то делаем
    }
}

Ой! Ошибка компиляции:


error[E0499]: cannot borrow `bytes` as mutable more than once at a time
  --> src/main.rs:20:18
   |
19 |     let byte_1 = bytes.next();
   |                  ----- first mutable borrow occurs here
20 |     let byte_2 = bytes.next();
   |                  ^^^^^ second mutable borrow occurs here
21 |     if byte_1 == byte_2 {
   |        ------ first borrow later used here

Думаю, мы можем позволить себе скопировать каждый байт. Копирование — это нормально, когда мы работаем с байтами, но если мы превратим ByteIter в обобщённый итератор среза, который может перебирать любой &'a [T], то мы могли бы захотеть использовать его в будущем с типами, которые дорого или вовсе невозможно копировать/клонировать. Что ж, я думаю, мы ничего не можем с этим поделать, ведь код компилируется, а значит, аннотации времён жизни должны быть правильными, ведь так?


Отнюдь, текущие аннотации времён жизни на самом деле являются источником ошибки! Это особенно трудно заметить, потому что ошибочные аннотации жизни опущены. Давайте выпишем развёрнутые аннотации времён жизни, чтобы получить более чёткое представление о проблеме:


struct ByteIter<'a> {
    remainder: &'a [u8]
}

impl<'a> ByteIter<'a> {
    fn next<'b>(&'b mut self) -> Option<&'b u8> {
        if self.remainder.is_empty() {
            None
        } else {
            let byte = &self.remainder[0];
            self.remainder = &self.remainder[1..];
            Some(byte)
        }
    }
}

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


struct ByteIter<'remainder> {
    remainder: &'remainder [u8]
}

impl<'remainder> ByteIter<'remainder> {
    fn next<'mut_self>(&'mut_self mut self) -> Option<&'mut_self u8> {
        if self.remainder.is_empty() {
            None
        } else {
            let byte = &self.remainder[0];
            self.remainder = &self.remainder[1..];
            Some(byte)
        }
    }
}

Каждый возвращаемый байт аннотирован 'mut_self, но байты явно поступают из 'remainder! Давайте исправим это.


struct ByteIter<'remainder> {
    remainder: &'remainder [u8]
}

impl<'remainder> ByteIter<'remainder> {
    fn next(&mut self) -> Option<&'remainder u8> {
        if self.remainder.is_empty() {
            None
        } else {
            let byte = &self.remainder[0];
            self.remainder = &self.remainder[1..];
            Some(byte)
        }
    }
}

fn main() {
    let mut bytes = ByteIter { remainder: b"1123" };
    let byte_1 = bytes.next();
    let byte_2 = bytes.next();
    std::mem::drop(bytes); // теперь мы даже можем удалить итератор!
    if byte_1 == byte_2 { // компилируется
        // что-то делаем
    }
}

Теперь, рассматривая предыдущую версию нашей программы, мы видим, что она явно была ошибочной. Так почему же Rust скомпилировал ее? Ответ прост: это было безопасным использованием памяти (memory safe).


Анализатор заимствований (borrow checker) Rust-а заботится о аннотациях времён жизни в программе только до той степени, до которой он может использовать их для статической проверки безопасности работы с памятью. Rust с радостью скомпилирует программы, даже если аннотации времён жизни содержат семантические ошибки и из-за этого программа становится излишне строгой.


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


#[derive(Debug)]
struct NumRef<'a>(&'a i32);

impl<'a> NumRef<'a> {
    // моя структура параметризована 'a, так что мне нужно
    // также аннотировать self 'a, верно? (ответ: нет, не верно)
    fn some_method(&'a mut self) {}
}

fn main() {
    let mut num_ref = NumRef(&5);
    num_ref.some_method(); // изменяемо заимствует num_ref до конца её времени жизни
    num_ref.some_method(); // ошибка компиляции
    println!("{:?}", num_ref); // также ошибка компиляции
}

Если у нас есть какая-то обобщённая структура, параметризованная 'a, то мы почти никогда не хотим писать метод с аргументом &'a mut self. Мы таким образом сообщаем Rust, что «этот метод будет изменяемо заимствовать структуру на всё её время жизни». На практике это означает, что анализатор заимствований Rust разрешит не более одного вызова some_method, после которого структура станет перманентно изменяемо заимствованной и, таким образом, непригодной к использованию. Случаи, в которых это требуется, крайне редки, но приведенный выше код новички очень легко могут написать по ошибке, и он компилируется. Исправление ошибки состоит в том, чтобы не добавлять ненужные явные аннотации времён жизни и положиться на правила вывода времён жизни Rust:


#[derive(Debug)]
struct NumRef<'a>(&'a i32);

impl<'a> NumRef<'a> {
    // на mut self больше нет 'a
    fn some_method(&mut self) {}

    // строчка выше рассахаривается таким образом
    fn some_method_desugared<'b>(&'b mut self){}
}

fn main() {
    let mut num_ref = NumRef(&5);
    num_ref.some_method();
    num_ref.some_method(); // компилируется
    println!("{:?}", num_ref); // компилируется
}

Ключевые выводы


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

6) трейт-объекты за владеющими указателями не имеют времён жизни


Ранее мы обсуждали правила Rust для вывода времён жизни применительно к функциям. У Rust также есть подобные правила для трейт-объектов:


  • если трейт-объект используется в качестве ти́пового параметра обобщённого типа, то время жизни трейт-объекта выводится из этого внешнего обобщённого типа
    • если есть только одно ограничение, следующее из внешнего типа, то оно и используется
    • если ограничений, следующих из внешнего типа, больше одного, то ограничение должно быть указано явно
  • если правила выше не применимы, то
    • если трейт определён с единственным ограничением времени жизни, то оно и используется
    • если для любого ограничения времени жизни используется 'static, то используется 'static
    • если трейт не имеет ограничений времён жизни, то в выражениях его время жизни выводится, а вне выражений имеет ограничение 'static

Всё это звучит очень сложно, но это можно выразить коротко как «ограничения времени жизни трейт-объекта выводятся из контекста». Изучив несколько примеров, мы увидим, что выведенные времена жизни довольно интуитивны, поэтому нам не нужно запоминать формальные правила:


use std::cell::Ref;

trait Trait {}

// неявно
type T1 = Box<dyn Trait>;
// выведено, Box<T> не налагает ограничений времён жизни на T,
// поэтому выводится 'static
type T2 = Box<dyn Trait + 'static>;

// неявно
impl dyn Trait {}
// выведено
impl dyn Trait + 'static {}

// неявно
type T3<'a> = &'a dyn Trait;
// выведено, &'a T требует T: 'a, поэтому выводится 'a
type T4<'a> = &'a (dyn Trait + 'a);

// неявно
type T5<'a> = Ref<'a, dyn Trait>;
// выведено, Ref<'a, T> требует T: 'a, поэтому выводится 'a
type T6<'a> = Ref<'a, dyn Trait + 'a>;

trait GenericTrait<'a>: 'a {}

// неявно
type T7<'a> = Box<dyn GenericTrait<'a>>;
// выведено
type T8<'a> = Box<dyn GenericTrait<'a> + 'a>;

// неявно
impl<'a> dyn GenericTrait<'a> {}
// выведено
impl<'a> dyn GenericTrait<'a> + 'a {}

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


trait Trait {}

struct Struct {}
struct Ref<'a, T>(&'a T);

impl Trait for Struct {}
impl Trait for &Struct {} // реализация трейта напрямую на ссылке
impl<'a, T> Trait for Ref<'a, T> {} // реализация трейта для типа, содержащего ссылку

В любом случае, это стоит повторить, поскольку это часто сбивает с толку новичков, когда они преобразуют функцию из использующей трейт-объекты в обобщённую или наоборот. Рассмотрим пример:


use std::fmt::Display;

fn dynamic_thread_print(t: Box<dyn Display + Send>) {
    std::thread::spawn(move || {
        println!("{}", t);
    }).join();
}

fn static_thread_print<T: Display + Send>(t: T) {
    std::thread::spawn(move || {
        println!("{}", t);
    }).join();
}

Оно выдаёт ошибку компиляции:


error[E0310]: the parameter type `T` may not live long enough
  --> src/lib.rs:10:5
   |
9  | fn static_thread_print<T: Display + Send>(t: T) {
   |                        -- help: consider adding an explicit lifetime bound...: `T: 'static +`
10 |     std::thread::spawn(move || {
   |     ^^^^^^^^^^^^^^^^^^
   |
note: ...so that the type `[closure@src/lib.rs:10:24: 12:6 t:T]` will meet its required lifetime bounds
  --> src/lib.rs:10:5
   |
10 |     std::thread::spawn(move || {
   |     ^^^^^^^^^^^^^^^^^^

Отлично, компилятор говорит нам, как решить проблему. Давайте исправим её.


use std::fmt::Display;

fn dynamic_thread_print(t: Box<dyn Display + Send>) {
    std::thread::spawn(move || {
        println!("{}", t);
    }).join();
}

fn static_thread_print<T: Display + Send + 'static>(t: T) {
    std::thread::spawn(move || {
        println!("{}", t);
    }).join();
}

Теперь код компилируется, но эти две функции в сравнении выглядят несколько странно. Почему вторая функция требует ограничение 'static на T, а первая функция — нет? Это вопрос с подвохом. Используя правила вывода времён жизни, Rust автоматически выводит ограничение 'static в первой функции, поэтому у них обеих фактически есть ограничение 'static. Вот что видит компилятор Rust:


use std::fmt::Display;

fn dynamic_thread_print(t: Box<dyn Display + Send + 'static>) {
    std::thread::spawn(move || {
        println!("{}", t);
    }).join();
}

fn static_thread_print<T: Display + Send + 'static>(t: T) {
    std::thread::spawn(move || {
        println!("{}", t);
    }).join();
}

Ключевые выводы


  • все трейт-объекты имеют определенные выведенные ограничения времён жизни по умолчанию

7) сообщения об ошибках компиляции скажут мне, как исправить мою программу


Ошибочные выводы


  • правила вывода времён жизни для трейт-объектов всегда верны
  • Rust знает о семантике моей программы больше меня

Данное заблуждение — это предыдущие два, объединённые в один пример:


use std::fmt::Display;

fn box_displayable<T: Display>(t: T) -> Box<dyn Display> {
    Box::new(t)
}

Эта программа выдаёт следующую ошибку:


error[E0310]: the parameter type `T` may not live long enough
 --> src/lib.rs:4:5
  |
3 | fn box_displayable<T: Display>(t: T) -> Box<dyn Display> {
  |                    -- help: consider adding an explicit lifetime bound...: `T: 'static +`
4 |     Box::new(t)
  |     ^^^^^^^^^^^
  |
note: ...so that the type `T` will meet its required lifetime bounds
 --> src/lib.rs:4:5
  |
4 |     Box::new(t)
  |     ^^^^^^^^^^^

Хорошо, давайте исправим проблему так, как говорит нам компилятор. Не будем обращать внимание на тот факт, что он, не говоря нам, автоматически выводит для нашего трейт-объекта ограничение времени жизни 'static и что его рекомендуемое исправление основано на этом предположении:


use std::fmt::Display;

fn box_displayable<T: Display + 'static>(t: T) -> Box<dyn Display> {
    Box::new(t)
}

Итак, теперь программа компилируется… Но действительно ли это то, чего мы хотим? Возможно, да, но возможно, что и нет. Компилятор не предложил никаких других исправлений, но этот вариант также применим:


use std::fmt::Display;

fn box_displayable<'a, T: Display + 'a>(t: T) -> Box<dyn Display + 'a> {
    Box::new(t)
}

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


fn return_first(a: &str, b: &str) -> &str {
    a
}

Выдаёт ошибку:


error[E0106]: missing lifetime specifier
 --> src/lib.rs:1:38
  |
1 | fn return_first(a: &str, b: &str) -> &str {
  |                    ----     ----     ^ 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 `a` or `b`
help: consider introducing a named lifetime parameter
  |
1 | fn return_first<'a>(a: &'a str, b: &'a str) -> &'a str {
  |                ^^^^    ^^^^^^^     ^^^^^^^     ^^^

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


fn return_first<'a>(a: &'a str, b: &str) -> &'a str {
    a
}

Ключевые выводы


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

8) время жизни может расти и сокращаться во время исполнения


Ошибочные выводы


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

Этот код не компилируется:


struct Has<'lifetime> {
    lifetime: &'lifetime str,
}

fn main() {
    let long = String::from("long");
    let mut has = Has { lifetime: &long };
    assert_eq!(has.lifetime, "long");

    {
        let short = String::from("short");
        // "переключиться" на более короткое время жизни
        has.lifetime = &short;
        assert_eq!(has.lifetime, "short");

        // "переключиться обратно" на более длинное время жизни (на самом деле нет)
        has.lifetime = &long;
        assert_eq!(has.lifetime, "long");
        // здесь `short` удаляется
    }

    // ошибка компиляции, `short` все ещё "заимствовано" после удаления
    assert_eq!(has.lifetime, "long");
}

Он выдаёт ошибку:


error[E0597]: `short` does not live long enough
  --> src/main.rs:11:24
   |
11 |         has.lifetime = &short;
   |                        ^^^^^^ borrowed value does not live long enough
...
15 |     }
   |     - `short` dropped here while still borrowed
16 |     assert_eq!(has.lifetime, "long");
   |     --------------------------------- borrow later used here

Этот код также не компилируется и выдает ту же ошибку, что и пример выше:


struct Has<'lifetime> {
    lifetime: &'lifetime str,
}

fn main() {
    let long = String::from("long");
    let mut has = Has { lifetime: &long };
    assert_eq!(has.lifetime, "long");

    // этот блок никогда не будет выполнен
    if false {
        let short = String::from("short");
        // "переключиться" на более короткое время жизни
        has.lifetime = &short;
        assert_eq!(has.lifetime, "short");

        // "переключится обратно" на более длинное время жизни (на самом деле нет)
        has.lifetime = &long;
        assert_eq!(has.lifetime, "long");
        // здесь `short` удаляется
    }

    // все ещё ошибка компиляции, `short` все ещё "заимствовано" после удаления
    assert_eq!(has.lifetime, "long");
}

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


Ключевые выводы


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

9) ослабление mut-ссылок до разделяемых безопасно


Ошибочные выводы


  • перезаимствование (re-borrowing) ссылки заканчивает её время жизни и начинает новое

Вы можете передать mut-ссылку в функцию, ожидающую разделяемую ссылку, потому что Rust неявно перезаимствует mut-ссылку как неизменяемую:


fn takes_shared_ref(n: &i32) {}

fn main() {
    let mut a = 10;
    takes_shared_ref(&mut a); // компилируется
    takes_shared_ref(&*(&mut a)); // рассахаренная строка выше
}

Интуитивно это понятно, поскольку в повторном заимствовании mut-ссылки как иммутабельной нет ничего страшного, ведь так? На удивление — нет, так как программа ниже не компилируется:


fn main() {
    let mut a = 10;
    let b: &i32 = &*(&mut a); // перезаимствована как иммутабельная
    let c: &i32 = &a;
    dbg!(b, c); // ошибка компиляции
}

Выдаёт следующую ошибку:


error[E0502]: cannot borrow `a` as immutable because it is also borrowed as mutable
 --> src/main.rs:4:19
  |
3 |     let b: &i32 = &*(&mut a);
  |                     -------- mutable borrow occurs here
4 |     let c: &i32 = &a;
  |                   ^^ immutable borrow occurs here
5 |     dbg!(b, c);
  |          - mutable borrow later used here

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


use std::sync::Mutex;

struct Struct {
    mutex: Mutex<String>
}

impl Struct {
    // ослабляет мутабельную ссылку на self до разделяемой
    fn get_string(&mut self) -> &str {
        self.mutex.get_mut().unwrap()
    }
    fn mutate_string(&self) {
        // если бы Rust разрешал ослабление мутабельных ссылок до разделяемых,
        // то следующая строка делала бы недействительной все
        // разделяемые ссылки, возвращённые get_string
        *self.mutex.lock().unwrap() = "surprise!".to_owned();
    }
}

fn main() {
    let mut s = Struct {
        mutex: Mutex::new("string".to_owned())
    };
    let str_ref = s.get_string(); // мутабельная ссылка ослаблена до разделяемой
    s.mutate_string(); // str_ref инвалидирована, теперь это висячий указатель
    dbg!(str_ref); // ошибка компиляции, как и ожидалось
}

Дело в том, что когда вы повторно заимствуете mut-ссылку как разделяемую, вы получаете ссылку, но с некоторым подвохом: она продлевает время жизни изначальной ссылки на время перезаимствования, даже если та уже не используется. Использовать перезаимствованную разделяемую ссылку очень сложно, потому что она неизменяемая, но при этом не может пересекаться с другими разделяемыми ссылками. Перезаимствованная разделяемая ссылка имеет все минусы и разделяемой ссылки, и изменяемой, и не имеет плюсов ни той, ни другой. Я считаю, что повторное заимствование мутабельной ссылки как разделяемой должно считаться в Rust анти-паттерном. Знать этот анти-паттерн важно, чтобы вы могли легко заметить его в коде, подобном этому:


// ослабляет изменяемый T до разделяемого T
fn some_function<T>(some_arg: &mut T) -> &T;

struct Struct;

impl Struct {
    // ослабляет мутабельную ссылку на self до разделяемой
    fn some_method(&mut self) -> &Self;

    // ослабляет мутабельную ссылку на self до разделяемой ссылки на T
    fn other_method(&mut self) -> &T;
}

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


use std::collections::HashMap;

type PlayerID = i32;

#[derive(Debug, Default)]
struct Player {
    score: i32,
}

fn start_game(player_a: PlayerID, player_b: PlayerID, server: &mut HashMap<PlayerID, Player>) {
    // получить от сервера игроков или создать и сохранить новых, если их ещё нет
    let player_a: &Player = server.entry(player_a).or_default();
    let player_b: &Player = server.entry(player_b).or_default();

    // делаем что-то с игроками
    dbg!(player_a, player_b); // ошибка компиляции
}

Пример выше не удаётся скомпилировать. or_default() возвращает &mut Player, который мы неявно повторно заимствуем как &Player из-за наших явных аннотаций типов. Чтобы сделать то, что мы хотим, нам приходится делать следующее:


use std::collections::HashMap;

type PlayerID = i32;

#[derive(Debug, Default)]
struct Player {
    score: i32,
}

fn start_game(player_a: PlayerID, player_b: PlayerID, server: &mut HashMap<PlayerID, Player>) {
    // избавляемся от изменяемых ссылок на Player, так как мы всё равно не можем использовать их вместе
    server.entry(player_a).or_default();
    server.entry(player_b).or_default();

    // снова получаем доступ к игрокам, на этот раз по неизменяемым ссылкам, без неявных перезаимствований
    let player_a = server.get(&player_a);
    let player_b = server.get(&player_b);

    // что-то делаем с игроками
    dbg!(player_a, player_b); // компилируется
}

Да, это несколько неловко и неуклюже, но это — жертва, которую мы приносим на Алтарь Безопасной Работы С Памятью.


(прим. переводчика: на самом деле у этого неудобного поведения есть причина. Допустим, что таблица server заполнена практически полностью: там есть свободное место для одного Player, но нету для двух. В этом случае первый экземпляр Player будет сохранён в имеющейся памяти, а второй спровоцирует реаллокацию словаря. Если при этом увеличить количество выделенной памяти по месту не получится, то будет выделен новый кусок памяти по новому адресу, в который копируется содержимое старого словаря, при этом ссылки на старую память становятся недействительными. Если бы Rust разрешал паттерн кода выше, то в некоторых редких случаях у нас бы после вставки второго Player ссылка на первый становилась бы висячей)


Ключевые выводы


  • старайтесь не перезаимствовать mut-ссылки как разделяемые, иначе вы об этом пожалеете
  • повторное заимствование mut-ссылки не заканчивает её время жизни, даже если сама ссылка отброшена

10) замыкания следуют тем же правилам вывода времён жизни, что и функции


Это скорее подвох, нежели заблуждение.


Замыкания, несмотря на то, что они являются функциями, не следуют тем же правилам вывода времён жизни, что и функции.


fn function(x: &i32) -> &i32 {
    x
}

fn main() {
    let closure = |x: &i32| x;
}

Выдаёт ошибку:


error: lifetime may not live long enough
 --> src/main.rs:6:29
  |
6 |     let closure = |x: &i32| x;
  |                       -   - ^ returning this value requires that `'1` must outlive `'2`
  |                       |   |
  |                       |   return type of closure is &'2 i32
  |                       let's call the lifetime of this reference `'1`

После рассахаривания мы получаем:


// время жизни аргумента применяется к возвращаемому значению
fn function<'a>(x: &'a i32) -> &'a i32 {
    x
}

fn main() {
    // аргумент и возвращаемое значение получают разные времена жизни
    let closure = for<'a, 'b> |x: &'a i32| -> &'b i32 { x };
    // внимание, строка выше не является корректным синтаксисом Rust, но она требуется для пояснения
}

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


fn main() {
    // кастуем к трейт-объекту, значение становится безразмерным, упс, ошибка компиляции
    let identity: dyn Fn(&i32) -> &i32 = |x: &i32| x;

    // можем выделить в куче, но это как-то неуклюже
    let identity: Box<dyn Fn(&i32) -> &i32> = Box::new(|x: &i32| x);

    // можем не аллоцировать вовсе, а создать статическую ссылку
    let identity: &dyn Fn(&i32) -> &i32 = &|x: &i32| x;

    // рассахаренная строка выше :)
    let identity: &'static (dyn for<'a> Fn(&'a i32) -> &'a i32 + 'static) = &|x: &i32| -> &i32 { x };

    // в идеале нам хотелось бы написать так, но это некорректный синтаксис
    let identity: impl Fn(&i32) -> &i32 = |x: &i32| x;

    // это тоже было бы хорошо, но это также некорректный синтаксис
    let identity = for<'a> |x: &'a i32| -> &'a i32 { x };

    // так как "impl trait" работает в позиции возвращаемого значения
    fn return_identity() -> impl Fn(&i32) -> &i32 {
        |x| x
    }
    let identity = return_identity();

    // более общая версия предыдущего решения
    fn annotate<T, F>(f: F) -> F where F: Fn(&T) -> &T {
        f
    }
    let identity = annotate(|x: &i32| x);
}

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


Тут нет какого-то урока или озарения, оно просто есть.


Ключевые выводы


  • у каждого языка есть свои проблемы

11) 'static-ссылки всегда можно привести к 'a-ссылкам


Ранее я показал следующий пример:


fn get_str<'a>() -> &'a str; // обобщённая версия
fn get_str() -> &'static str; // версия со 'static

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


Обычно, когда мы работаем со значениями, мы можем использовать 'static-ссылки там, где ожидаются 'a-ссылки, потому что Rust автоматически приводит 'static-ссылки к 'a-ссылкам за нас. Это соответствует нашей интуиции: мы можем использовать долгоживущую ссылку там, где ожидается ссылка с более коротким временем жизни, и это не приведёт ни к каким проблемам. Программа ниже, как и ожидалось, компилируется.


use rand;

fn generic_str_fn<'a>() -> &'a str {
    "str"
}

fn static_str_fn() -> &'static str {
    "str"
}

fn a_or_b<T>(a: T, b: T) -> T {
    if rand::random() {
        a
    } else {
        b
    }
}

fn main() {
    let some_string = "string".to_owned();
    let some_str = &some_string[..];
    let str_ref = a_or_b(some_str, generic_str_fn()); // компилируется
    let str_ref = a_or_b(some_str, static_str_fn()); // компилируется
}

Однако это приведение не работает, когда ссылки являются частью сигнатуры функции, поэтому этот код не компилируется:


use rand;

fn generic_str_fn<'a>() -> &'a str {
    "str"
}

fn static_str_fn() -> &'static str {
    "str"
}

fn a_or_b_fn<T, F>(a: T, b_fn: F) -> T
    where F: Fn() -> T
{
    if rand::random() {
        a
    } else {
        b_fn()
    }
}

fn main() {
    let some_string = "string".to_owned();
    let some_str = &some_string[..];
    let str_ref = a_or_b_fn(some_str, generic_str_fn); // компилируется
    let str_ref = a_or_b_fn(some_str, static_str_fn); // ошибка компиляции
}

И выдаёт эту ошибку компиляции:


error[E0597]: `some_string` does not live long enough
  --> src/main.rs:23:21
   |
23 |     let some_str = &some_string[..];
   |                     ^^^^^^^^^^^ borrowed value does not live long enough
...
25 |     let str_ref = a_or_b_fn(some_str, static_str_fn);
   |                   ---------------------------------- argument requires that `some_string` is borrowed for `'static`
26 | }
   | - `some_string` dropped here while still borrowed

Является ли это подвохом или нет — вопрос спорный, так как это не простой прямолинейный случай приведения &'static str к &'a str, а приведение for<T> Fn() -> &'static T к for<'a, T> Fn() -> &'a T. Первое — это приведение между значениями, а второе — это приведение между типами.


Ключевые выводы


  • функции с сигнатурами вида for<'a, T> fn() -> &'a T являются более гибкими и работают в большем числе случаев, чем функции с сигнатурами вида for<T> fn() -> &'static T

Заключение


  • T является надмножеством как &T, так и &mut T
  • &T и &mut T являются непересекающимися множествами
  • T: 'static должно читаться, как « T ограничен временем жизни 'static»
  • если T: 'static, то T может быть заимствованным типом с временем жизни 'static или владеющим типом.
  • поскольку T: 'static включает в себя владеющие типы, это означает, что T
    • может быть динамически создан во время исполнения
    • не обязан существовать на протяжении всего времени работы программы
    • может быть безопасно изменён без каких-либо ограничений
    • может быть динамически уничтожен во время исполнения
    • может иметь время жизни произвольной продолжительности
  • T: 'a является более общим и более гибким типом, чем &'a T
  • T: 'a принимает владеющие типы, владеющие типы, которые содержат ссылки, и ссылки
  • &'a T принимает только ссылки
  • если T: 'static, то T: 'a, так как 'static >= 'a для всех 'a
  • почти весь код на Rust является обобщённым кодом, и повсюду есть выведенные аннотации времён жизни
  • правила вывода времён жизни Rust не всегда подходят к конкретной ситуации
  • Rust не знает о семантике вашей программы больше, чем вы
  • давайте вашим аннотациям времён жизни осмысленные имена
  • старайтесь понимать, где вы размещаете явные аннотации времён жизни и почему
  • все трейт-объекты имеют определенные выведенные ограничения времён жизни по умолчанию
  • сообщения об ошибках компиляции Rust предлагают исправления, которые приведут к успешной компиляции вашей программы, но это не то же самое, что и исправления, которые заставят вашу программу успешно компилироваться и одновременно наилучшим образом соответствуют требованиям вашей программы.
  • времена жизни проверяется статически на этапе компиляции
  • времена жизни не могут расти, уменьшаться или изменяться каким-либо образом в рантайме
  • анализатор заимствований Rust всегда выбирает наименьшее возможное время жизни для переменной, предполагая, что все пути исполнения кода возможны
  • старайтесь не перезаимствовать изменяемые ссылки как разделяемые, иначе вы об этом пожалеете
  • повторное заимствование mut-ссылки не заканчивает её время жизни, даже если сама ссылка уничтожена
  • у каждого языка есть свои проблемы
  • функции с сигнатурами вида for<'a, T> fn() -> &'a T являются более гибкими и работают в большем числе случаев, чем функции с сигнатурами вида for<T> fn() -> &'static T

Обсуждение


Обсудите эту статью на



Контакты


Получайте уведомления о будущих публикациях в блоге



Дальнейшее чтение



Переводчик выражает признательность за помощь в переводе статьи русскоязычному сообщество Rust, в особенности nlinker.

Теги:
Хабы:
+49
Комментарии 5
Комментарии Комментарии 5

Публикации

Истории

Работа

Ближайшие события

Московский туристический хакатон
Дата 23 марта – 7 апреля
Место
Москва Онлайн
Геймтон «DatsEdenSpace» от DatsTeam
Дата 5 – 6 апреля
Время 17:00 – 20:00
Место
Онлайн