Представление

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

список на входе:
[Some(ptr)] -> (A, Some(ptr)) -> (B, None)

вставка X:
[Some(ptr)] -> (X, Some(ptr)) -> (A, Some(ptr)) -> (B, None)

удаление:
[Some(ptr)] -> (A, Some(ptr)) -> (B, None)

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

Чтобы перенести в конец вставку, мы должны пробежаться по списку до None и заменить его на Some с новым элементом.

список на входе:
[Some(ptr)] -> (A, Some(ptr)) -> (B, None)

вставка X в конец:
[Some(ptr)] -> (A, Some(ptr)) -> (B, Some(ptr)) -> (X, None)

Чтобы перенести в конец удаление, мы должны пробежаться по списку до узла перед None и изъять узел.

список на входе:
[Some(ptr)] -> (A, Some(ptr)) -> (B, Some(ptr)) -> (X, None)

удаление с конца:
[Some(ptr)] -> (A, Some(ptr)) -> (B, None)

Мы могли бы закрыть задачу прямо сейчас, но, скажем прямо, это решение ужасно! Обе операции пробегаются по всему списку. Вы возразите, что такая реализация вполне нормальна, поскольку полностью соответствует интерфейсу. Однако, я уверена, что гарантии производительности тоже являются частью интерфейса. Меня не волнуют точные асимптотические границы, просто «быстро» или «медленно». Гарантии очереди в том, что вставка и удаление быстрые, а обход всего списка — определённо не быстрый.

Обратите внимание, что мы делаем тучу работы, повторяя одну и ту же вещь снова и снова. Можем ли мы «кешировать» всю эту работу и переиспользовать результат? Да, разумеется! Мы можем сохранить указатель на конец списка и просто перепрыгивать туда!

Оказывается, что с этой идеей работает только один из вариантов инверсии вставки/удаления. Чтобы инвертировать pop, мы должны передвинуть назад указатель на хвост, но, поскольку наш список односвязный, этого нельзя сделать эффективно. Однако, если инвертируем push, надо будет передвинуть вперёд указатель на голову, а легко.

Давайте попробуем:

use std::mem;

pub struct List<T> {
    head: Link<T>,
    tail: Link<T>, // НОВАЯ СТРОКА!
}

type Link<T> = Option<Box<Node<T>>>;

struct Node<T> {
    elem: T,
    next: Link<T>,
}

impl<T> List<T> {
    pub fn new() -> Self {
        List { head: None, tail: None }
    }

    pub fn push(&mut self, elem: T) {
        let new_tail = Box::new(Node {
            elem: elem,
            // Когда вставляете в конец списка, следующий узел всегда None
            next: None,
        });

        // теперь указатель на хвост указывает на новый хвост
        let old_tail = mem::replace(&mut self.tail, Some(new_tail));

        match old_tail {
            Some(mut old_tail) => {
                // Если старый хвост был, обновляем его, чтобы он указывал на новый
                old_tail.next = Some(new_tail);
            }
            None => {
                // В противном случае, обновляем голову, чтобы указывала на него
                self.head = Some(new_tail);
            }
        }
    }
}

Я собираюсь ускорить повествование, опуская некоторые детали, поскольку к настоящему моменту вы уже должны понимать, как работает Rust. Похоже, что этот код запустится с первого раза. Сейчас я не показываю ошибки, которые показывала раньше, вроде пропусков mut или ;, так как они перестали быть поучительными. Не переживайте, мы столкнёмся с множеством других ошибок!

> cargo build

error[E0382]: use of moved value: `new_tail`
  --> src/fifth.rs:38:38
   |
26 |         let new_tail = Box::new(Node {
   |             -------- move occurs because `new_tail` has type `std::boxed::Box<fifth::Node<T>>`, which does not implement the `Copy` trait
...
33 |         let old_tail = mem::replace(&mut self.tail, Some(new_tail));
   |                                                          -------- value moved here
...
38 |                 old_tail.next = Some(new_tail);
   |                                      ^^^^^^^^ value used here after move

Да что ж такое!

use of moved value: new_tail

Использование перемещённого значения: new_tail.

Box не реализует Copy, так что мы просто не можем присвоить его двум разным переменным. Что важнее, Box владеет значением, на которое указывает, и постарается освободить его в своём деструкторе. Если бы наша реализация push компилировалась, мы бы дважды освобождали хвост нашего списка! На самом деле, наш текущий код будет освобождать old_tail при каждом вызове push. Кошмар!

Ладно, мы знаем, как создать указатель без владения. Это будет просто ссылка!

pub struct List<T> {
    head: Link<T>,
    tail: Option<&mut Node<T>>, // НОВАЯ СТРОКА!
}

type Link<T> = Option<Box<Node<T>>>;

struct Node<T> {
    elem: T,
    next: Link<T>,
}

impl<T> List<T> {
    pub fn new() -> Self {
        List { head: None, tail: None }
    }

    pub fn push(&mut self, elem: T) {
        let new_tail = Box::new(Node {
            elem: elem,
            // Когда вставляете в конец списка, следующий узел всегда None
            next: None,
        });

        // Помещаем бокс в правильное место получем ссылку на его узел
        let new_tail = match self.tail.take() {
            Some(old_tail) => {
                // Если старый хвост был, обновляем его, чтобы он указывал на новый
                old_tail.next = Some(new_tail);
                old_tail.next.as_deref_mut()
            }
            None => {
                // В противном случае, обновляем голову, чтобы указывала на него
                self.head = Some(new_tail);
                self.head.as_deref_mut()
            }
        };

        self.tail = new_tail;
    }
}

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

> cargo build

error[E0106]: missing lifetime specifier
 --> src/fifth.rs:3:18
  |
3 |     tail: Option<&mut Node<T>>, // NEW!
  |                  ^ expected lifetime parameter

Да, правильно, мы ведь должны указывать время жизни ссылок в типах. Хмм... а какое время жизни у этой ссылки? Ну, код похож на IterMut, правда? Давайте попробуем, то же самое, что мы делали в IterMut и просто добавим обобщённый 'a:

pub struct List<'a, T> {
    head: Link<T>,
    tail: Option<&'a mut Node<T>>, // НОВЫЙ КОД!
}

type Link<T> = Option<Box<Node<T>>>;

struct Node<T> {
    elem: T,
    next: Link<T>,
}

impl<'a, T> List<'a, T> {
    pub fn new() -> Self {
        List { head: None, tail: None }
    }

    pub fn push(&mut self, elem: T) {
        let new_tail = Box::new(Node {
            elem: elem,
            // Когда вставляете в конец списка, следующий узел всегда None
            next: None,
        });

        // Помещаем бокс в правильное место и получем ссылку на его узел
        let new_tail = match self.tail.take() {
            Some(old_tail) => {
                // Если старый хвост был, обновляем его, чтобы он указывал на новый
                old_tail.next = Some(new_tail);
                old_tail.next.as_deref_mut()
            }
            None => {
                // В противном случае, обновляем голову, чтобы указывала на него
                self.head = Some(new_tail);
                self.head.as_deref_mut()
            }
        };

        self.tail = new_tail;
    }
}
cargo build

error[E0495]: cannot infer an appropriate lifetime for autoref due to conflicting requirements
  --> src/fifth.rs:35:27
   |
35 |                 self.head.as_deref_mut()
   |                           ^^^^^^^^^^^^
   |
note: first, the lifetime cannot outlive the anonymous lifetime #1 defined on the method body at 18:5...
  --> src/fifth.rs:18:5
   |
18 | /     pub fn push(&mut self, elem: T) {
19 | |         let new_tail = Box::new(Node {
20 | |             elem: elem,
21 | |             // When you push onto the tail, your next is always None
...  |
39 | |         self.tail = new_tail;
40 | |     }
   | |_____^
note: ...so that reference does not outlive borrowed content
  --> src/fifth.rs:35:17
   |
35 |                 self.head.as_deref_mut()
   |                 ^^^^^^^^^
note: but, the lifetime must be valid for the lifetime 'a as defined on the impl at 13:6...
  --> src/fifth.rs:13:6
   |
13 | impl<'a, T> List<'a, T> {
   |      ^^
   = note: ...so that the expression is assignable:
           expected std::option::Option<&'a mut fifth::Node<T>>
              found std::option::Option<&mut fifth::Node<T>>


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

the lifetime must be valid for the lifetime 'a as defined on the impl

Время жизни должно быть действительно в течение времени жизни 'a, заданного в блоке impl.

Мы заимствуем self, но компилятор хочет, чтобы мы существовали столько же, сколько и 'a, а что, если мы скажем ему, что self действительно существует столько же?

    pub fn push(&'a mut self, elem: T) {
cargo build

warning: field is never used: `elem`
 --> src/fifth.rs:9:5
  |
9 |     elem: T,
  |     ^^^^^^^
  |
  = note: #[warn(dead_code)] on by default

О, здорово, так работает! Отлично!

Теперь давайте реализуем pop:

pub fn pop(&'a mut self) -> Option<T> {
    // Извлекаем текущую голову списка
    self.head.take().map(|head| {
        let head = *head;
        self.head = head.next;

        // Если новый список пуст, хвост должен быть установлен в `None`.
        if self.head.is_none() {
            self.tail = None;
        }

        head.elem
    })
}

И напишем небольшой тест для нашего кода.

#[cfg(test)]
mod test {
    use super::List;
    #[test]
    fn basics() {
        let mut list = List::new();

        // Проверяем, что пустой список ведёт себя правильно
        assert_eq!(list.pop(), None);

        // Заполняем список
        list.push(1);
        list.push(2);
        list.push(3);

        // Проверяем обычное удаление
        assert_eq!(list.pop(), Some(1));
        assert_eq!(list.pop(), Some(2));

        // Вставляем новые значения, просто чтобы проверить, что ничего не сломается
        list.push(4);
        list.push(5);

        // Проверяем обычное удаление
        assert_eq!(list.pop(), Some(3));
        assert_eq!(list.pop(), Some(4));

        // Проверяем граничный случай
        assert_eq!(list.pop(), Some(5));
        assert_eq!(list.pop(), None);
    }
}
cargo test

error[E0499]: cannot borrow `list` as mutable more than once at a time
  --> src/fifth.rs:68:9
   |
65 |         assert_eq!(list.pop(), None);
   |                    ---- first mutable borrow occurs here
...
68 |         list.push(1);
   |         ^^^^
   |         |
   |         second mutable borrow occurs here
   |         first borrow later used here

error[E0499]: cannot borrow `list` as mutable more than once at a time
  --> src/fifth.rs:69:9
   |
65 |         assert_eq!(list.pop(), None);
   |                    ---- first mutable borrow occurs here
...
69 |         list.push(2);
   |         ^^^^
   |         |
   |         second mutable borrow occurs here
   |         first borrow later used here

error[E0499]: cannot borrow `list` as mutable more than once at a time
  --> src/fifth.rs:70:9
   |
65 |         assert_eq!(list.pop(), None);
   |                    ---- first mutable borrow occurs here
...
70 |         list.push(3);
   |         ^^^^
   |         |
   |         second mutable borrow occurs here
   |         first borrow later used here


....

** ЕЩЁ МНОГО СТРОК С ОШИБКАМИ **

....

error: aborting due to 11 previous errors

🙀🙀🙀🙀🙀🙀🙀🙀🙀🙀🙀🙀🙀🙀🙀🙀🙀🙀🙀🙀🙀🙀🙀

Да божечки мои.

Компилятор совершенно прав, ругаясь на наш код. Мы только что совершили смертный грех языка Rust: сохранили ссылку на себя внутри себя. Нам как-то удалось убедить компилятор, что это действительно имеет смысл в наших реализациях push и pop (я искренне удивляюсь, что мы смогли).

Причина, по которой это вроде бы работает в том, что в Rust вообще нет такого понятия, как указатель на себя. Каждая часть кода технически корректна сама по себе (мы можем вызывать push и pop один раз), но затем вступает в силу абсурдность нашего решения и всё просто ломается.

Я уверена, что у написанного нами может быть какое-то применение, но, насколько мне известно, это всего лишь синтаксически корректная тарабарщина. Мы говорим, что у нас есть что-то с временем жизни 'a, и что push и pop заимствуют self на это время. Это странно, но Rust смотрит на каждую часть нашего кода по отдельности и ни видит никаких нарушений.

Но как только мы пытаемся действительно использовать список, компилятор нам сообщает: «так, вы заимствовали self изменяемым образом на время 'a, так что вы не можете использовать self до конца 'a», но также: «поскольку вы содержите 'a, он должен быть действителен в течение всего времени существования списка».

Это почти противоречие, но есть одно решение: сразу после вызова push или pop список себя закрепляет и к нему больше нельзя получить доступ. Образно говоря, он проглатывает свой хвост и возносится в мир грёз.

ГОЛОС ЗА КАДРОМ: с тех пор, как была написана эта книга, Rust, фактически, формализовал закрепление и нашёл ему применение! Возможно, это было самое сложное расширение языка со времён появления анализатора заимствований (borrow checker). Но нам, в любом случае, не надо закреплять наш список!

Закрепления нужны и полезны для async/await, футур, сопрограмм, поскольку компилятору надо собирать локальные переменные в некоторое подобие структуры и хранить их, пока футура/сопрограмма не будет готова к возобновлению. Поскольку локальные переменные могут ссылаться на другие локальные переменные, и мы хотим, чтобы всё работало, эти структуры в конечном счёте могут содержать ссылки на самих себя!

Таким образом, для await или yield Rust нуждается в корректном способе описания и манипулирования закреплёнными значениями. К счастью, в основном всё это скрыто глубоко в недрах компилятора и при обычных обстоятельствах никому не приходится думать про Pin (или даже про футуры). Главное исключение из этого правила: разработчики и проектировщики асинхронных библиотек, таких как tokio.

Мы не станем реализовывать асинхронную библиотеку в этой книге. Мои друзья знают все виды «крутых» (и странных) трюков, которые можно провернуть с Pin, но я бы не хотела погружаться в эту тему. Продолжу убеждать себя, что закреплённых типов не существуют и они не могут мне навредить.

Наша реализация pop подсказывает, почему хранение ссылки на себя внутри себя может быть опасным:

// ...
if self.head.is_none() {
    self.tail = None;
}

Что, если мы забудем это сделать? Тогда наш хвост будет указывать на какой-то узел, который уже удалён из списка. Такой узел будет мгновенно освобождён и мы получили бы висящий указатель, от чего Rust должен был нас защитить!

И действительно Rust защищает нас от подобной опасности. Просто очень... окольным путём.

Так что же нам теперь делать? Возвращаться в ад Rc<RefCell>>?

Спасибо. Не надо.

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

pub struct List<T> {
    head: Link<T>,
    tail: *mut Node<T>, // ОПАСНО!
}

type Link<T> = Option<Box<Node<T>>>;

struct Node<T> {
    elem: T,
    next: Link<T>,
}

И это всё. Никакого детского лепета про подсчёт ссылок и динамическую проверку заимствований! Реальные. Сложные. Непроверяемые. Указатели.

ГОЛОС ЗА КАДРОМ: наша реализация фактически была не просто неправильной, а опасно неправильной. Но для этого урока время ещё не пришло. В следующей части усвоим его на собственном горьком опыте, как и всегда.

Да будет C ныне, присно и во веки веков!

Я возвращаюсь домой. Я готова.

Привет, unsafe.

ГОЛОС ЗА КАДРОМ: какая невероятная самонадеянность со стороны автора.