Предисловие переводчика

Оригинальный текст написан в разговорной манере с обилием слов-эвфемизмов — heck, dang и подобных.

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

Divergent function получила перевод расходящаяся функция по аналогии с расходящимися и сходящимися вычислениями. В конечном итоге, если вычисления не сходятся, функция выполняется бесконечно. Это и есть определение divergent function — функция, которая не возвращает управления. Одна из интересных особенностей расходящейся функции заключается в том, что тип её результата (обозначается !) совместим с любыми типами программы: целыми числами, строками, с чем угодно! Результат можно присвоить любой переменной и компилятор это «проглотит», потому что реального присвоения никогда не случится.

#![allow(unused)]
fn main() {
let a: i32 = panic!("Компилируется!");
}

Моя попытка найти русский аналог для iterator invalidation закончилась провалом, поэтому в переводе вы встретите слово инвалидация. Мне это не нравится, но, кажется, инвалидация — устоявшийся термин в профессиональной русской литературе. Я бы предпочёл поломку итератора или что-то в этом роде, но не стану спорить с традицией.

Структуру данных persistent stack, описанную в третьей главе, я перевёл как устойчивый стек. Ссылаюсь на перевод книги Криса Окасаки «Чисто функциональные структуры данных», где persistent data structures превратились в устойчивые структуры данных. На мой взгляд, удачный термин. Хорошо передаёт смысл.

Аббревиатура UB не переводится — в соответствии с традицией. Хотя Undefined Behaviour — тоже в соответствии с традицией — становится Неопределённым Поведением.

Если вы нашли ошибку, форкайте репозиторий проекта, вносите правки и отправляйте мне — я приму.

И — приятного вам знакомства с лучшим введением в продвинутый Rust!

P.S. По совету друзей добавлю, что эта книга переведена вручную. Конечно, я использовал ИИ — вместе с другими инструментами переводчика — urban dictionary, reverso context и google translate (последний больше как словарь).

ИИ помогал мне справиться со сложными местами, но сложные места я находил самостоятельно, пытаясь ухватить мысль автора. Время от времени google translate подсказывал мне формулировки, гораздо более удачные, чем мои.

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

Марк Шевченко

Целая прорва связных списков, чтобы выучить Rust

Возникли какие-то сложности или хотите сразу получить весь финальный код? Всё на Github!

ПРИМЕЧАНИЕ: Текущая редакция этой книги написана для Rust 2018, выпущенного совместно с rustc 1.31 (8 декабря 2018). Если у вас достаточно новый инструментарий Rust, файл Carto.toml, созданный при запуске cargo new, будет содержать строку edition = "2018" (или, если вы из далёкого будущего, большее число!). Можно использовать старый инструментарий, но тогда вы разблокируете секретный режим повышенной сложности: больше ошибок от компилятора, совершенно не упомянутых в этой книге. Как по мне, звучит весело!

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

В этом цикле я научу вас и базовому, и продвинутому программированию на Rust, с помощью реализации 6 связных списков. За время изучения вы освоите:

  • Следующие типы указателей: &, &mut, Box, Rc, Arc, *const, *mut, NonNull(?)
  • Владение, заимствование, наследуемую изменчивость, внутреннюю изменчивость, типаж Copy
  • Все ключевые слова: struct, enum, fn, pub, impl, use, ...
  • Сопоставление с образцом, обобщения, деструкторы
  • Тестирование, установку новых инструментов, использование miri
  • Небезопасный Rust: сырые указатели, псевдонимы, стековые заимствования, вариантность

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

Полное содержание книги есть на боковой панели (может быть свёрнута на смартфоне), но, чтобы вы получили представление, вот что мы собираемся сделать:

  1. Неправильный односвязный стек
  2. Правильный односвязный стек
  3. Устойчивый односвязный стек
  4. Неправильный, но безопасный двусвязный дек
  5. Правильная небезопасная односвязная очередь
  6. Небезопасный двусвязный дек для прода
  7. Куча странных списков

Чтобы мы понимали друг друга, я буду писать здесь все команды, которые ввожу в терминал. Также, для разработки проекта, я использую стандартный растовский пакетный менеджер Cargo. Писать программы на Rust можно и без Cargo, но с ним намного удобнее, чем с rustc. Если вам захочется поэкспериментировать, простые программы можно запускать в браузере через play.rust-lang.org.

В следующих разделах мы будем использовать rustup для установки дополнительных инструментов Rust. Я настоятельно рекомендую устанавливать весь инструментарий Rust, используя rustup.

Давайте начнём и создадим наш проект:

cargo new --lib lists
cd lists

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

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

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

Мы будем двигаться достаточно медленно, и я на 100% не собираюсь всё это время быть серьёзной. Я, блин, думаю, что программирование должно быть весёлым! Если вы — читатель, которому нужен максимально информативный, серьёзный и формальный текст, эта книга не для вас. Вообще всё, что я когда-либо напишу — не для вас. Читайте другие книги.

Обязательное Общественное Обращение

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

  • Вам надо много раз сливать и разделять большие списки. Много раз.
  • Вы пишите какую-то удивительную неблокирующую конкурентную штуку.
  • Вы пишите системную/низкоуровневую штуку и вам нужны интрузивные списки.
  • Вы пишите на чистом функциональном языке, где ограниченная семантика вкупе с неизменяемостью делают связные списки простейшим решением.
  • ...ну и так далее!

Но у всех, кто пишет на Rust, эти задачи встречаются супер редко. В 99% случаев вы должны просто использовать Vec (массив-стек), а в 99% из оставшегося 1% — VecDeque (массив-дек). Это безусловно лучшие структуры данных для большинства рабочих задач из-за нечастых выделений, небольшой служебной памяти, честного произвольного доступа и локальности кэша.

Связные списки — такая же нишевая и расплывчатая структура данных, как и префиксное дерево. Мало кто станет возражать против моего утверждения, что префиксное дерево — нишевая структура, с которой средний программист, к счастью, никогда в своей жизни не столкнётся. В то же время, у связных списков какая-то необъяснимая популярность. Каждого студента мы учим, как писать связные списки. Это единственная нишевая коллекция, которую я не смогла удалить из std::collections. И в стандартной библиотеке C++ связный список тоже есть!

Мы, как сообщество, должны сказать нет связным спискам как «стандартной» структуре данных. Это — прекрасная структура данных, для которой есть несколько прекрасных сценариев использования, но эти сценарии — исключения, а не правила.

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

Я собрала здесь несколько контр-аргументов и свои ответы на них, просто чтобы у меня под рукой была ссылка на подробную дискуссию. Можете смело переходить к первой главе, если просто хотите изучить немного Rust!

Производительность не всегда имеет значение

Да! Скажем, ваше приложение занимается в основном вводом и выводом. Или его запускают пару раз в год и время его работы действительно не имеет значения. Но это не аргумент в пользу связного списка. Это аргумент в пользу чего угодно. Зачем ограничиваться связным списком? Используйте связный ассоциативный массив!

Если производительность не имеет значения, то в качестве структуры данных безусловно подойдёт и массив.

У списков разделение-добавление-вставка-удаление выполняются за O(1), если есть указатель на нужный узел

Точняк! Хотя, как заметил Бьёрн Страуструп, на самом деле это неважно, поскольку получение указателя на нужный узел занимает гигантское время по сравнению с простым копированием элементов массива (что на практике довольно быстро).

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

Но — да, если профилирование показало, что приложение тратит всё время на слияние и разделение, вы получите выгоду от связных списков.

Я не могу позволить себе амортизированную сложность

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

В таком сценарии push и pop гарантированно будут выполняться за O(1). И они окажутся значительно быстрее, чем push и pop на связных списках. Вы смещаете указатель, пишите байты и увеличиваете целое число. И на надо выделять память.

Как вам такая скорость?

Но — да, если вы не можете предсказать количество элементов, связные списки работают быстро даже в худшем случае!

Связные списки занимают меньше места

Ну, это сложно. «Стандартная» стратегия при изменении размера массива — следить, чтобы при увеличении или уменьшении, по меньшей мере половина массива оставалась пустой. При этом действительно уходит много места. Особенно в Rust, где мы не уплотняем коллекции автоматически (что оправданно, если вы собираетесь заполнять их снова), так что потери могут стремиться к бесконечности!

Но это худший случай. В лучшем случае массив-стек имеет всего три дополнительных указателя. Считайте, никаких накладных расходов.

С другой стороны, связные списки гарантированно требуют места для каждого элемент. Служебные данные в односвязном списке — это один указатель, а в двусвязном — два. В отличие от массивов, относительные расходы у списков обратно пропорциональны размеру элементов. Если элементы огромные, накладные расходы близки к 0. Если элементы крошечные (скажем, байты), служебные данные в 16 раз превышают размер самих элементов (в 8 — на 32-хбитных машинах)!

А, если учесть выравнивание по размеру указателя  — в 23 раза (в 11 за 32-хбитные машинах).

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

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

Я всё время использую связные списки в «функциональных языках»

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

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

В Rust подобные вещи делаются с помощью [итераторов][]. Они могут быть бесконечными. Над ними можно выполнять операции map, filter, reverse и concatenate, как и над обычными функциональными списками. Помимо прочего, они ещё и ленивые!

Наконец, Rust облегчает работу с под-массивами с помощью [срезов][]. В функцинальных языках списки обычно делят на голову и хвост, точно также, как это делает slice.split_at_mut(1). На протяжении долгого времени в Rust существовала крутая экспериментальная система сопоставления с образцом для срезов. Во время стабилизации её всё-таки упростили, но даже сейчас образцы для срезов весьма изящны! Кстати, срезы легко превратить в итераторы!

Но — да, если вы ограничены иммутабельной семантикой, связные списки очень удобны.

Заметьте, я не утверждаю, что функциональное программирование — какое-то плохое и недоделаное. Однако, речь идёт о фундаментальном семантическом ограничении: вы можете рассуждать о том, что вещи из себя представляют, но не можете — о том, что они могут делать. На самом деле это такая фича, позволяющая компилятору выполнить тонны экзотических преобразований и может быть даже найти лучший способ решения, снимая с вас необходимость об этом думать. Цена, однако — потеря самой возможности об этом думать. Как правило, есть обходные пути, но время от времени вам всё равно приходиться писать обычный процедурный код.

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

Связные списки отлично подходят для реализации потокобезопасных структур данных

Да! Хотя создание потокобезопасных структур данных совершенно из другой оперы, и к нему нельзя относиться легкомысленно. Однозначно, в теме разбираются не так много людей. И как только они разработают для вас готовую структуру, вы больше не будете выбирать связный список. Вы будете выбирать MPSC-очередь или что-то такое. А для этого очереди стратегия реализации очень сильно отличается!

Но — да, связные списки — де факто герои тёмного мира неблокирующей конкуррентности.

Тык-пык ядро, встраиваемые системы, туда-сюда интрузивные

Это — ниши. Вы говорите о ситуациях, в которых нельзя использовать даже стандартную библиотеку. Разве это не красный флаг, что вы делаете что-то очень необычное?

Плюс ко всему, всё это крайне небезопасно.

Но — ладно. Пишите свои потрясающие списки без выделения памяти в куче.

Итераторы могут работать после вставки/удаления элементов

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

Но — да, с помощью курсоров можно делать действительно крутые и безумные штуки.

Они простые и хорошо подходят для обучения!

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

Сделайте вдох

Что ж. С этим разобрались. Давайте теперь напишем хреналлиард связных списков.

Вперёд, к первой главе!

Плохой односвязный стек

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

Наш первый список мы поместим в файл src/first.rs. Надо сказать компилятору Rust, что first.rs входит в состав нашей библиотеки. Для этого в начале файла src/lib.rus (который создала для нас утилита Cargo) достаточно написать :

// в lib.rs
pub mod first;

Базовое представление данных

Ладно, что такое связный список? Ну, в целом, это набор кусочков данных в куче, которые последовательно указывают друг на друга. (Да, я знаю, что разработчикам ядра куча недоступна, мы сейчас не об этом!) Связные списки — это то, что процедурные программисты не станут трогать 10-метровой палкой, а функциональные программисты используют вообще для всего. Кажется справедливым, что определение связного списка в этом случае что мы должны спросить у функциональных программистов. И они дадут вам что-то такое:

List a = Empty | Elem a (List a)

Что приблизительно читается как «Список является либо Пустым, либо Элементом за которым следует Список». Это рекурсивное определение, выраженное через тип-сумму или, если подробнее, через тип, который «может принимать одно из нескольких значений, у каждого из которых может быть свой тип». В языке Rust типы-суммы называются перечислениями (enum)! Если вы пришли из C-подобного языка, то речь идёт о знакомых и любимых нами enum, но на максималках. Ладно, давайте перепишем это функциональное определение на Rust!

Для простоты первое время мы не будем использовать обобщения (generics). Значениями узлов у нас будут обычные 32-битные числа.:

// в first.rs

// pub говорит, что мы хотим, чтобы авторы других модулей могли использовать List
pub enum List {
    Empty,
    Elem(i32, List),
}

Уф, как сложно. Попробуем скомпилировать:

> cargo build

error[E0072]: recursive type `first::List` has infinite size
 --> src/first.rs:4:1
  |
4 | pub enum List {
  | ^^^^^^^^^^^^^ recursive type has infinite size
5 |     Empty,
6 |     Elem(i32, List),
  |               ---- recursive without indirection
  |
  = help: insert indirection (e.g., a `Box`, `Rc`, or `&`) at some point to make `first::List` representable

Текст ошибки: рекурсивный тип first::List имеет бесконечный размер.

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

Подумав над сообщением об ошибке (после того, как переживём всю эту историю с предательством), мы обнаружим, что rustc на самом деле подсказывает нам, как решить нашу проблему:

insert indirection (e.g., a Box, Rc, or &) at some point to make first::List representable

Перевод: чтобы сделать first::List представимым, добавьте в нужном месте косвенный доступ, т. е. Box, Rc или &

Так, box. Что это такое? Давайте погуглим rust box...

std::boxed::Box - Rust

Посмотрим, что пишут...

pub struct Box<T>(_);

Тип-указатель для размещения в куче. Дополнительную информацию см. в документации к модулю boxed.

щёлкает по ссылке

Box<T>, обычно называемый «боксом» — простейшая форма размещения объекта в куче в языке Rust. Боксы являются владельцами этого объекта и уничтожают его, когда выходят из области видимости.

Примеры

Создание бокса

let x = Box::new(5);

Создание рекурсивной структуры данных:

#![allow(unused)]
fn main() {
#[derive(Debug)]
enum List<T> {
    Cons(T, Box<List<T>>),
    Nil,
}
}
fn main() {
    let list: List<i32> = List::Cons(1, Box::new(List::Cons(2, Box::new(List::Nil))));
    println!("{:?}", list);
}

Этот код напечатает Cons(1, Box(Cons(2, Box(Nil)))).

Рекурсивные структуры должны быть помещены в бокс, потому что определение:

Cons(T, List<T>),

не работает. В данном случае размер List зависит от количества элементов, так что мы не знаем, сколько памяти выделить для Cons. Box, который по сути является указателем, имеет фиксированный размер, поэтому компилятор может вычислить размер Cons.

Ого! Хм. Кажется, это самая релевантная и полезная документация, которую я когда-либо видела. Первые абзацы — буквально о том, что мы пытаемся написать, почему оно не работает и как всё исправить.

Блин, доки рулят.

Ладно, давайте напишем так:

pub enum List {
    Empty,
    Elem(i32, Box<List>),
}
> cargo build

   Finished dev [unoptimized + debuginfo] target(s) in 0.22s

Ага, проект собрался!

...но у нас получилось достаточно дурацкое определение списка. Сразу по нескольким причинам.

Представим список из двух элементов.

[] = Стек
() = Куча

[Elem A, ptr] -> (Elem B, ptr) -> (Empty, *junk*)

Есть два ключевых момента:

  • Мы выделили память под узел, который как бы говорит «На самом деле я не Узел»
  • Один из наших узлов вообще не размещён в куче

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

[ptr] -> (Elem A, ptr) -> (Elem B, *null*)

Здесь мы размещаем в куче все наши узлы без исключения. Ключевое отличие в отсутствии мусора (junk) из нашего первого сценария. Что это за мусор? Что в этом разобраться, рассмотрим, как наш enum размещается в памяти.

Пусть у нас будет такой enum:

enum Foo {
    D1(T1),
    D2(T2),
    ...
    Dn(Tn),
}

В Foo хранится целое число, которое обозначает, какой из вариантов перечисления (D1, D2, .. Dn) представлен. Это — метка (tag) перечисления. Ему также требуется достаточно памяти, чтобы сохранить наибольший из типов T1, T2, .. Tn (плюс немного дополнительного пространства, чтобы удовлетворить требования выравнивания).

Это значит, что хотя Empty хранит один бит информации, он в любом случае резервирует место для указателя и элемента, поскольку в любой момент может превратиться в Elem. Поэтому в первом сценарии в куче хранится дополнительный элемент, где полезным является один бит, а всё остальное — мусор. Во втором сценарии последний узел содержит нулевой указатель, который означает конец списка, то есть, не является мусором.

Один из узлов вообще не размещается в куче и это, как ни странно, не очень хорошо. Причина — в неунифицированном размещении узлов (часть узлов хранится одним способом, а часть — другим). Для вставки и удаления узлов это не важно, но разделение и слияние списков становятся сложнее.

Сравним разделение списка в обоих сценариях.

сценарий 1:

[Elem A, ptr] -> (Elem B, ptr) -> (Elem C, ptr) -> (Empty *junk*)

разбиваем список на элементе C:

[Elem A, ptr] -> (Elem B, ptr) -> (Empty *junk*)
[Elem C, ptr] -> (Empty *junk*)
сценарий 2:

[ptr] -> (Elem A, ptr) -> (Elem B, ptr) -> (Elem C, *null*)

разбиваем список на элементе C:

[ptr] -> (Elem A, ptr) -> (Elem B, *null*)
[ptr] -> (Elem C, *null*)

Во втором сценарии разделение сводится к копированию указателя на B в стек и обнулению указателя на C. В первом сценарии в целом происходит то же самое, но, помимо прочего, приходиться копировать C из кучи в стек. Со слияние ситуация похожая, только действия выполняются в обратном порядке.

Одно из немногих преимуществ связных списков в том, что элемент хранится непосредственно в узле. Его можно свободно перемещать по списку, в то же время оставляя на одном и том же месте в памяти. Вы играете с указателями — и элементы «перемещаются». Первый сценарий ломает это свойство.

Хорошо, я почти уверена, что первый сценарий плох. Как нам переписать список теперь? Для начала, мы могли бы сделать что-то вроде этого:

pub enum List {
    Empty,
    ElemThenEmpty(i32),
    ElemThenNotEmpty(i32, Box<List>),
}

Надеюсь, вы заметили, что эта идея ещё хуже. Во-первых, теперь нам придётся писать дополнительные проверки, потому что появился совершенно недопустимый вариант ElemThenNotEmpty(0, Box(Empty)). А во-вторых, мы всё ещё не избавились от неунифицированного размещения элементов.

Впрочем, у этого варианта есть одно интересное свойство: он позволяет полностью избавиться от размещения узла Empty, сокращая общее число размещений в куче на 1. К сожалению, при этом расходуется ещё больше места! Связано это с тем, что в прошлом сценарии применялась оптимизация указателей на null.

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

enum Foo {
    A,
    B(ContainsANonNullPtr), // ContainsANonNullPtr — ненулевой указатель
}

срабатывает оптимизация указателя на null, которая избавляется от места, выделяемого под метку. Для варианта A все байты перечисления равны 0. Если байты не равны 0, у нас неизбежно вариант B. Всё работает, поскольку B не может состоять из одних 0, так как содержит ненулевой указатель. Ловко!

Не приходят ли вам в голову другие перечисления и типы, где могла бы работать такого рода оптимизация? На самом деле их очень много! По этой причине Rust не регламентирует способ хранения перечислений. Существует несколько сложных оптимизаций, которые для нас делает Rust, но указатель на null безусловно один из самых важных! Это значит, что &, &mut, Box, Rc, Arc, Vec и некоторые другие важные типы Rust не требуют дополнительной памяти, если завернуть их в тип Option! (Большинство из них мы обсудим позже.)

Итак, как избавиться от мусора, унифицировано хранить узлы и добиться заветной оптимизации указателя на null? Попробуем разграничить идеи наличия элемента и размещения последующего списка. Чтобы это сделать, будем думать в стиле C. Конечно, структуры!

В то время, как перечисления позволяют нам объявлять типы, которые содержат одно из нескольких значений, структуры позволяют нам объявлять типы, которые одновременно содержат много значений. Разделим наш List на два типа: List и Node.

Как и раньше, List может быть либо пустым, либо содержать элемент, за которым следует другой список. Представив вариант «содержать элемент, за которым следует другой список" в виде структуры, мы приводим Box в оптимальное положение:

struct Node {
    elem: i32,
    next: List,
}

pub enum List {
    Empty,
    More(Box<Node>),
}

Проверим, всех ли целей мы добились?

  • Хвост списка никогда не выделяет дополнительное пространство: есть!
  • enum хранится в заветной форме оптимизированного указателя на null: есть!
  • Все элементы хранятся унифицировано: есть!

Прекрасно! Мы сконструировали тот самый способ представления, который использовали, чтобы продемонстрировать, что наш первый способ был неверным (о чём нам поведала документация Rust).

> cargo build

warning: private type `first::Node` in public interface (error E0446)
 --> src/first.rs:8:10
  |
8 |     More(Box<Node>),
  |          ^^^^^^^^^
  |
  = note: #[warn(private_in_public)] on by default
  = warning: this was previously accepted by the compiler but
    is being phased out; it will become a hard error in a future release!

Текст ошибки: приватный тип first::Node в публичном интерфейсе.

:(

Rust снова сердится — и всё по нашей вине. Мы сделали List публичным (чтобы люди могли его использовать), а Node — нет. Проблема в том, что внутренности enum публичны и там, по идее, не должно быть приватных типов. Можно было бы сделать Node публичным, однако мы в Rust предпочитаем скрывать детали реализации. Сделаем List структурой:

pub struct List {
    head: Link,
}

enum Link {
    Empty,
    More(Box<Node>),
}

struct Node {
    elem: i32,
    next: Link,
}

Поскольку List — это структура с единственным полем, её размер будет совпадать с размером поля. Сила абстракций с нулевой стоимостью!

> cargo build

warning: field is never used: `head`
 --> src/first.rs:2:5
  |
2 |     head: Link,
  |     ^^^^^^^^^^
  |
  = note: #[warn(dead_code)] on by default

warning: variant is never constructed: `Empty`
 --> src/first.rs:6:5
  |
6 |     Empty,
  |     ^^^^^

warning: variant is never constructed: `More`
 --> src/first.rs:7:5
  |
7 |     More(Box<Node>),
  |     ^^^^^^^^^^^^^^^

warning: field is never used: `elem`
  --> src/first.rs:11:5
   |
11 |     elem: i32,
   |     ^^^^^^^^^

warning: field is never used: `next`
  --> src/first.rs:12:5
   |
12 |     next: Link,
   |     ^^^^^^^^^^

Rust выводит множество предупреждений, потому что, насколько он может судить, мы написали совершенно бесполезный код. Мы не используем head и никто из пользователей нашей библиотекой не может этого сделать, потому что это приватное поле. Транзитивно это значит, что List и Node тоже никто не использует. Давайте с этим разберёмся! Напишем немного кода для нашего списка!

Метод new

Чтобы связать написанный код с типом, используем блоки impl:

impl List {
    // воплотить код в жизнь
}

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

fn foo(arg1: Type1, arg2: Type2) -> ReturnType {
    // тело
}

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

impl List {
    pub fn new() -> Self {
        List { head: Link::Empty }
    }
}

Несколько замечаний по поводу кода:

  • Self — это псевдоним для «того тип, который я написал в заголовке impl». Отличный способ не повторяться!
  • Мы создаём экземпляр структуры практически также, как объявляем её, за исключением того, чтобы вместо указания типов полей, мы инициализируем их значениями.
  • Мы ссылаемся на варианты перечисления, используя ::, то есть оператор указания пространства имён.
  • Последнее выражение функции считается её результатом. Такой подход позволяет делает простые функции лаконичными. Но всё ещё можно использовать return для досрочного возврата из функции, как и в других C-подобных языках.

Основы владения

Теперь, когда мы можем сконструировать список, было бы неплохо что-нибудь с ним сделать. Делать будем при помощи «обычных» (не статических) методов. Метод — это особый вид функции в Rust. У него есть аргумент self без типа:

fn foo(self, arg2: Type2) -> ReturnType {
    // тело
}

Есть три основные формы, которые он может принимать: self, &must self и &self. Эти три формы представляют три основные формы владения в Rust:

  • self — Значение
  • &mut self — изменяемая ссылка
  • &self — разделяемая ссылка

Значение — это, по сути, истинное владение. Вы можете делать со значением всё, что хотите: отдать, уничтожить, изменить или передавать по ссылке. Если вы передаёте что-то по значению, оно отдаётся в новое место. Новое место теперь владеет значением, а старое место больше не имеет к нему доступа. По этой причине большинство методов не используют self — было бы довольно глупо работать со списком, теряя к нему доступ!

Изменяемая ссылка означает временный эксклюзивный доступ к значению, которым вы не владеете. Вы можете делать со значением абсолютно всё, что хотите, если в конце вы оставляете его в рабочем состоянии (иное было бы невежливым по отношению к владельцу!). Это значит, что на самом деле значение можно полностью перезаписать. Очень полезный частный случай — обмен двух значений, который нам пригодится, и не единожды. Единственное, что нельзя сделать с &mut — уничтожить, не заменив другим значением. &mut self отлично подходит для методов, которым надо изменить self.

Разделяемое владение означает временный разделяемый доступ к значению, которым вы не владеете. Имея разделяемый доступ, вы в общем случае ничего не можете изменить. Как в музее: смотреть можно, руками трогать нельзя! & отлично подходит для методов, которым self нужен только для чтения.

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

Метод push

Пришло время написать функцию вставки числа в начало списка. Вставка (push) изменяет список, поэтому нам нужен параметр &mut self. Кроме того, нам потребуется значение i32, которое мы будем вставлять.

impl List {
    pub fn push(&mut self, elem: i32) {
        // написать
    }
}

Прежде всего нам надо создать узел и сохранить в нём наш элемент:

    pub fn push(&mut self, elem: i32) {
        let new_node = Node {
            elem: elem,
            next: ?????
        };
    }

Что должно быть по ссылке next? Ну, весь наш старый список! Можем ли мы... просто так и написать?

impl List {
    pub fn push(&mut self, elem: i32) {
        let new_node = Node {
            elem: elem,
            next: self.head,
        };
    }
}
> cargo build
error[E0507]: cannot move out of borrowed content
  --> src/first.rs:19:19
   |
19 |             next: self.head,
   |                   ^^^^^^^^^ cannot move out of borrowed content

Неееееет. Наверное, Rust написал всё правильно, но непонятно, что это значит и что с этим делать:

cannot move out of borrowed content

Нельзя переместить заимствованное содержимое.

Мы пытаемся переместить поле self.head в next, но Rust не разрешает нам это сделать. Проблема в том, что когда мы завершим заимствование и «вернём назад» законному владельцу, self останется частично инициализированным. Как мы уже говорили, это единственное, что нельзя сделать с помощью &mut. Rust очень вежлив, а это — невежливо (кстати, и опасно тоже, но вряд ли компилятор беспокоит какая-то опасность).

Что если вместо старого значения мы разместим узел, который мы создаём:

pub fn push(&mut self, elem: i32) {
    let new_node = Box::new(Node {
        elem: elem,
        next: self.head,
    });

    self.head = Link::More(new_node);
}
> cargo build
error[E0507]: cannot move out of borrowed content
  --> src/first.rs:19:19
   |
19 |             next: self.head,
   |                   ^^^^^^^^^ cannot move out of borrowed content

Ничего не вышло. В принципе, это одна из тех штук, которую Rust мог бы принять, но не принимает (по многим причинам, одна из которых — это [безопасность исключений][]). Нам нужен способ забрать значение head так, чтобы Rust не заметил пропажи. Обратимся за советом к печально известному взломщику ржавых замков Индиане Джонсу:

Инди готовится к вызову mem::replace

Хм, Инди советует прибегнуть к маневру mem::replace. Эта невероятно полезная функция позволяет нам забрать значение заимствованного объекта, заменив его другим значением. Добавим std::mem в начало файла, чтобы модуль mem оказался в локальной области видимости:

use std::mem;

и используем надлежащим образом:

pub fn push(&mut self, elem: i32) {
    let new_node = Box::new(Node {
        elem: elem,
        next: mem::replace(&mut self.head, Link::Empty),
    });

    self.head = Link::More(new_node);
}

Здесь мы временно заменяем (replace) self.head значением Link::Empty перед тем, как заменить его новой головой списка. Не стану врать: так делать не надо (по меньшей мере, это решение не простое, и не очевидное), но в учебных целях мы должны проверить все грабли. Позже мы познакомимся с простым и очевидным способом.

Но, подождите, метод push уже готов! Кажется. Честно говоря, это стоит протестировать. Как это сделать проще всего? Написать метод pop и убедиться, что он возвращает правильные результаты.

Метод pop

Как и push, метод pop меняет список. Но, в отличие от push, метод pop возвращает какое-то значение. При этом он должен учитывать каверзный граничный случай: что, если список пуст? Чтобы представить этот случай, воспользуемся проверенным типом Option:

pub fn pop(&mut self) -> Option<i32> {
    // дописать
}

Option<T> — это перечисление, которое представляет опциональное значение, которое, возможно, существует. Вариант Some(T) соответствует существующему значению, а None — отсутствию значения. Как и в случае с Link, можно было бы придумать собственный тип, но мы хотим, чтобы пользователи понимали, что именно представляет из себя возвращаемое значение, а Option настолько вездесущ, что все про него все знают. Фактически, он настолько фундаментален, что неявно импортируется в область видимости каждого модуля вместе с вариантами Some и None (поэтому мы и не пишем Option::None).

Угловые скобки в Option<T> означают, что Option, на самом деле — обобщённый тип над T. То есть, опциональным можно сделать значение любого типа!

Хорошо, у нас есть ссылка Link, как нам узнать, содержит ли она Empty или More? Конечно же с помощью сопоставления с образцом и оператора match!

pub fn pop(&mut self) -> Option<i32> {
    match self.head {
        Link::Empty => {
            // дописать
        }
        Link::More(node) => {
            // дописать
        }
    };
}
> cargo build

error[E0308]: mismatched types
  --> src/first.rs:27:30
   |
27 |     pub fn pop(&mut self) -> Option<i32> {
   |            ---               ^^^^^^^^^^^ expected enum `std::option::Option`, found ()
   |            |
   |            this function's body doesn't return
   |
   = note: expected type `std::option::Option<i32>`
              found type `()`

Ой, pop должна вернуть значение, но этого мы пока не написали. Мы могли бы вернуть None, но, возможно в данном случае лучше вернуть unimplemented!(), чтобы показать, что мы ещё не завершили реализацию функции. unimplemented!() — это макрос (! указывает на макрос), который вызывает панику, когда к нему обращаются. Происходит так называемый контролируемый сбой.

pub fn pop(&mut self) -> Option<i32> {
    match self.head {
        Link::Empty => {
            // дописать
        }
        Link::More(node) => {
            // дописать
        }
    };
    unimplemented!()
}

Безусловная паника — это пример расходящейся функции. Расходящиеся функции не возвращают управления вызывающей функции, поэтому их можно использовать в любом месте, где ожидается значение любого типа. В нашем случае unimplemented!() используется вместо значения типа Option<T>.

Обратите внимание, что в методе pop мы не используем оператор return. Значением функции становится её последнее выражение (обычно, последняя строка). Благодаря этому, действительно простые функции можно сделать намного короче. Однако, вы всегда можете завершить функцию досрочно с помощью оператора return, как и в любом другом C-подобном языке.

> cargo build

error[E0507]: cannot move out of borrowed content
  --> src/first.rs:28:15
   |
28 |         match self.head {
   |               ^^^^^^^^^
   |               |
   |               cannot move out of borrowed content
   |               help: consider borrowing here: `&self.head`
...
32 |             Link::More(node) => {
   |                        ---- data moved here
   |
note: move occurs because `node` has type `std::boxed::Box<first::Node>`, which does not implement the `Copy` trait
  --> src/first.rs:32:24
   |
32 |             Link::More(node) => {
   |                        ^^^^

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

help: consider borrowing here: `&self.head`

Rust утверждает, что мы должны добавить ссылку в оператор match, чтобы исправить ошибку. 🤷‍♀️ Попробуем:

pub fn pop(&mut self) -> Option<i32> {
    match &self.head {
        Link::Empty => {
            // дописать
        }
        Link::More(node) => {
            // дописать
        }
    };
    unimplemented!()
}
> cargo build

warning: unused variable: `node`
  --> src/first.rs:32:24
   |
32 |             Link::More(node) => {
   |                        ^^^^ help: consider prefixing with an underscore: `_node`
   |
   = note: #[warn(unused_variables)] on by default

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

warning: field is never used: `next`
  --> src/first.rs:14:5
   |
14 |     next: Link,
   |     ^^^^^^^^^^

Ура, снова компилируется! Теперь давайте обсудим логику работы нашего метода. Мы хотим создать Option, так что пусть у нас будет переменная для опционального значения. В случае пустого списка мы присвоим ей None. В случае непустого списка, мы присвоим ей Some(i32) и заменим голову списка. Что ж, попробуем?

pub fn pop(&mut self) -> Option<i32> {
    let result;
    match &self.head {
        Link::Empty => {
            result = None;
        }
        Link::More(node) => {
            result = Some(node.elem);
            self.head = node.next;
        }
    };
    result
}
> cargo build
   Compiling lists v0.1.0 (/Users/ADesires/dev/temp/lists)
error[E0507]: cannot move out of borrowed content
  --> src/first.rs:35:29
   |
35 |                 self.head = node.next;
   |                             ^^^^^^^^^ cannot move out of borrowed content

голова

стол

Мы пытаемся забрать владение node в то время как узел доступен нам только по разделяемой ссылке.

Возможно, нам следует вернуться на шаг назад и подумать, что мы пытаемся сделать. Мы хотим:

  • Проверить, не пустой ли список.
  • Если пустой, просто вернуть None.
  • Если не пустой:
    • удалить голову списка
    • удалить elem
    • заменить голову списка следующим узлом
    • вернуть Some(elem)

Ключевая мысль заключается в том, что мы хотим что-то удалить, а это значит, что доступ к голове списка нам нужен по значению. Мы определённо не можем ничего менять через разделяемую ссылку, которую получаем с помощью &self.head. Кроме того, имея на руках только изменяемую ссылку на self, мы можем переместить что-либо единственным способом — через замену. Выглядит так, словно нам опять предстоит танец вокруг Empty!

Попробуем:

pub fn pop(&mut self) -> Option<i32> {
    let result;
    match mem::replace(&mut self.head, Link::Empty) {
        Link::Empty => {
            result = None;
        }
        Link::More(node) => {
            result = Some(node.elem);
            self.head = node.next;
        }
    };
    result
}
cargo build

   Finished dev [unoptimized + debuginfo] target(s) in 0.22s

Боже мой!

Всё скомпилировалось без единого предупреждения!!!!!

Я собираюсь выступить в роли линтера для нашего кода. Мы сделали result возвращаемым значением, но на самом деле эта переменная нам вообще не нужна! Также, как и функция получает значение своего последнего выражения, каждый блок получает значение своего последнего выражения. Обычно мы ставим в конце операторов точку с запятой, а это значит, что блок возвращает пустой кортеж, (). На самом деле, пустой кортеж — то самое значение, которое возвращают функции без возвращаемого значения, такие, как push.

Так что мы можем переписать pop так:

pub fn pop(&mut self) -> Option<i32> {
    match mem::replace(&mut self.head, Link::Empty) {
        Link::Empty => None,
        Link::More(node) => {
            self.head = node.next;
            Some(node.elem)
        }
    }
}

Так чуть лаконичнее и идиоматичнее. Обратите внимание, что ветка Link::Empty полностью избавилась от фигурных скобок, потому что состоит теперь только из одного выражения. Удобная короткая запись для простых случаев.

cargo build

   Finished dev [unoptimized + debuginfo] target(s) in 0.22s

Прекрасно, всё ещё работает!

Тестирование

Прекрасно, теперь, когда у нас есть push и pop, мы можем протестировать наш стек! Для Rust и cargo тестирование — это гражданин первого класса, так что всё должно быть очень просто. Достаточно написать функцию и пометить её аннотацией #[test].

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

Ключевое слово mod можно использовать не только для объявления внешнего модуля (как мы сделали это с first.rs и lib.rs), но и для создания встроенного модуля:

// в first.rs

mod test {
    #[test]
    fn basics() {
        // написать
    }
}

Мы запускаем тесты командой cargo test.

> cargo test
   Compiling lists v0.1.0 (/Users/ADesires/dev/temp/lists)
    Finished dev [unoptimized + debuginfo] target(s) in 1.00s
     Running /Users/ADesires/dev/lists/target/debug/deps/lists-86544f1d97438f1f

running 1 test
test first::test::basics ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
; 0 filtered out

Ура, наш тест, который ничего не проверяет, успешно пройден! Теперь давайте напишем какую-нибудь проверку. В этом нам поможет макрос assert_eq!. В нём нет никакой особой магии тестирования. Он всего навсего сравнивает две штуки, которые ему передали, и паникует, если они не совпадают. Да, мы сигнализируем о проблеме при помощи паники!

mod test {
    #[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(3));
        assert_eq!(list.pop(), Some(2));

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

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

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

error[E0433]: failed to resolve: use of undeclared type or module `List`
  --> src/first.rs:43:24
   |
43 |         let mut list = List::new();
   |                        ^^^^ use of undeclared type or module `List`


Ой! Поскольку мы создали новый модуль, в него надо явным образом импортировать List.

mod test {
    use super::List;
    // всё остальное точно также
}
> cargo test

warning: unused import: `super::List`
  --> src/first.rs:45:9
   |
45 |     use super::List;
   |         ^^^^^^^^^^^
   |
   = note: #[warn(unused_imports)] on by default

    Finished dev [unoptimized + debuginfo] target(s) in 0.43s
     Running /Users/ADesires/dev/lists/target/debug/deps/lists-86544f1d97438f1f

running 1 test
test first::test::basics ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
; 0 filtered out

Да!

Хотя, что это за предупреждение?.. В нашем тесте мы используем List явным образом!

...но только при запуске тестов! Чтобы угодить компилятору (и быть дружелюбным к другим программистам) мы должны явно указать, что весь модуль test должен быть скомпилирован только когда мы запускаем тесты.

#[cfg(test)]
mod test {
    use super::List;
    // всё остальное точно также
}

На этом тестирование завершено!

Метод drop

Мы можем создать стек, вставить в него элемент, извлечь его обратно. Мы даже протестировали, что всё это правильно работает!

Надо ли нам беспокоиться об освобождении памяти? Технически — нет, вообще не надо! Как и C++, Rust использует деструкторы, чтобы автоматически освобождать ресурсы, после их использования. Тип имеет деструктор, если он реализует типаж, который называется Drop. Типажи — это научное название интерфейсов в языке Rust. Типаж Drop имеет следующий вид:

pub trait Drop {
    fn drop(&mut self);
}

Что-то вроде: «когда вы выйдете из области видимости, я дам вам секунду, чтобы прибрать за собой».

Вы не должны реализовывать Drop, если у вас есть типы, реализующие Drop и всё, что вы хотите — это вызвать их деструкторы. В случае List, всё что ему нужно — это освободить голову, что, по идее приведёт к освобождению Box<Node>. Всё это будет сделано автоматически... с одним «но».

Автоматическая уборка может пойти не по плану.

Посмотрим на простой список:

list -> A -> B -> C

Когда удаляется list, он попытается удалить A, который попытается удалить B, который попытается удалить C. Возможно, кое-кто из вас занервничал. Это рекурсивный код, а рекурсивный код может привести к переполнению стека!

Некоторые из вас, возможно, подумали: «здесь точно хвостовая рекурсия, а любой приличный язык программирования гарантирует, что при хвостовой рекурсии переполнения не будет». На самом деле это неверно! Чтобы понять, почему, давайте вручную реализуем Drop так, как это сделал бы компилятор:

impl Drop for List {
    fn drop(&mut self) {
        // ПРИМЕЧАНИЕ: в реальном коде нельзя вызвать `drop` явно;
        // так что мы притворимся компилятором!
        self.head.drop(); // хвостовая рекурсия - хорошо!
    }
}

impl Drop for Link {
    fn drop(&mut self) {
        match *self {
            Link::Empty => {} // Готово!
            Link::More(ref mut boxed_node) => {
                boxed_node.drop(); // хвостовая рекурсия - хорошо!
            }
        }
    }
}

impl Drop for Box<Node> {
    fn drop(&mut self) {
        self.ptr.drop(); // ой, подождите, не хвостовая рекурсия!
        deallocate(self.ptr);
    }
}

impl Drop for Node {
    fn drop(&mut self) {
        self.next.drop();
    }
}

Мы не можем освободить содержимое Box после вызова deallocate, так что здесь у нас хвостовая рекурсия отменяется! Вместо этого, напишем итеративное освобождение List, которое выведет узлы из их боксов.

impl Drop for List {
    fn drop(&mut self) {
        let mut cur_link = mem::replace(&mut self.head, Link::Empty);
        // `while let` == "повторяй, пока образец совпадает"
        while let Link::More(mut boxed_node) = cur_link {
            cur_link = mem::replace(&mut boxed_node.next, Link::Empty);
            // здесь boxed_node выходит из области видимости и уничтожается;
            // но поле `next` заменяется на `Link::Empty`,
            // поэтому бесконечная рекурсия не возникает
        }
    }
}
> cargo test

     Running target/debug/lists-5c71138492ad4b4a

running 1 test
test first::test::basics ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured

Великолепно!


Бонус

Бонусный раздел для преждевременной оптимизации!

Наша реализация освобождения очень напоминает код while let Some(_) = self.pop() { }, который, безусловно, проще. Чем они отличаются, и какие проблемы с производительностью могут возникнуть, если мы обобщим наш список для хранения объектов любого типа, а не только целых чисел?

Раскрыть ответ

pop возвращает Option<i32> в то время, как наша реализация манипулирует только ссылками (Box<Node>). Поэтому наша реализация всего лишь переносит указатели на узлы, в то время как pop переносит значения, которые хранятся в узлах. Может быть очень дорого обобщать наш список, ведь кто-то может использовать его для экземпляров типа VBTWADI (VeryBigThingWithADropImpl — очень большая штука с реализацией Drop). Боксы способны запускать реализацию drop для своего содержимого непосредственно, так что у них нет такой проблемы. Ну, а поскольку VBTWADI — именно та штука, для которой связные списки гораздо предпочтительнее массивов, такое поведение немного разочарует.

Если вы хотите взять лучшее от обеих реализаций, вы можете добавить новый метод fn pop_node(&mut sefl) -> Link который корректно вызывается как из pop, так и из drop.

Финальный код

Итак, 6000 слов спустя, мы написали такой код:

#![allow(unused)]
fn main() {
use std::mem;

pub struct List {
    head: Link,
}

enum Link {
    Empty,
    More(Box<Node>),
}

struct Node {
    elem: i32,
    next: Link,
}

impl List {
    pub fn new() -> Self {
        List { head: Link::Empty }
    }

    pub fn push(&mut self, elem: i32) {
        let new_node = Box::new(Node {
            elem: elem,
            next: mem::replace(&mut self.head, Link::Empty),
        });

        self.head = Link::More(new_node);
    }

    pub fn pop(&mut self) -> Option<i32> {
        match mem::replace(&mut self.head, Link::Empty) {
            Link::Empty => None,
            Link::More(node) => {
                self.head = node.next;
                Some(node.elem)
            }
        }
    }
}

impl Drop for List {
    fn drop(&mut self) {
        let mut cur_link = mem::replace(&mut self.head, Link::Empty);

        while let Link::More(mut boxed_node) = cur_link {
            cur_link = mem::replace(&mut boxed_node.next, Link::Empty);
        }
    }
}

#[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(3));
        assert_eq!(list.pop(), Some(2));

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

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

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

Только посмотрите! 80 строк, из них половина — тесты! Ну, я предупреждала, что первая глава потребует времени!

Хороший односвязный стек

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

  • Переизобрести велосипед
  • Сделать наш список способным содержать элементы любого типа
  • Добавить метод peek
  • Реализовать итератор

В процессе мы узнаем о:

  • Продвинутом использовании Option
  • Обобщениях
  • Времени жизни
  • Итераторах

Для начала создадим новый файл second.rs:

// в lib.rs

pub mod first;
pub mod second;

И скопируем в него содержимое first.rs.

Использование Option

Внимательные читатели могли заметить, что мы, фактически, переизобрели не самую удачную версию Option:

enum Link {
    Empty,
    More(Box<Node>),
}

Link — это просто Option<Box<Node>>. Приятно, что нам не надо было везде писать Option<Box<Node>> и, в отличие от pop, не надо было делать реализацию, доступную всему внешнему миру, что, кажется, неплохо. Однако, в Option есть несколько поистине приятных методов, которые нам пришлось написать самим. Давайте не будем больше так делать и заменим всё на вызовы методов Option. Для начала давайте без изысков просто переименуем все элементы в Some и None:

use std::mem;

pub struct List {
    head: Link,
}

// да, псевдонимы типов!
type Link = Option<Box<Node>>;

struct Node {
    elem: i32,
    next: Link,
}

impl List {
    pub fn new() -> Self {
        List { head: None }
    }

    pub fn push(&mut self, elem: i32) {
        let new_node = Box::new(Node {
            elem: elem,
            next: mem::replace(&mut self.head, None),
        });

        self.head = Some(new_node);
    }

    pub fn pop(&mut self) -> Option<i32> {
        match mem::replace(&mut self.head, None) {
            None => None,
            Some(node) => {
                self.head = node.next;
                Some(node.elem)
            }
        }
    }
}

impl Drop for List {
    fn drop(&mut self) {
        let mut cur_link = mem::replace(&mut self.head, None);
        while let Some(mut boxed_node) = cur_link {
            cur_link = mem::replace(&mut boxed_node.next, None);
        }
    }
}

Код не стал кардинально лучше, но основную выгоду нам принесут методы Option.

Во-первых, mem::replace(&mut option, None) — настолько распространённая идиома, что Option просто взял и превратил её в метод take.

pub struct List {
    head: Link,
}

type Link = Option<Box<Node>>;

struct Node {
    elem: i32,
    next: Link,
}

impl List {
    pub fn new() -> Self {
        List { head: None }
    }

    pub fn push(&mut self, elem: i32) {
        let new_node = Box::new(Node {
            elem: elem,
            next: self.head.take(),
        });

        self.head = Some(new_node);
    }

    pub fn pop(&mut self) -> Option<i32> {
        match self.head.take() {
            None => None,
            Some(node) => {
                self.head = node.next;
                Some(node.elem)
            }
        }
    }
}

impl Drop for List {
    fn drop(&mut self) {
        let mut cur_link = self.head.take();
        while let Some(mut boxed_node) = cur_link {
            cur_link = boxed_node.next.take();
        }
    }
}

Во-вторых, match option { None => None, Some(x) => Some(y) } — настолько распространённая идиома, что её назвали map (отображение). Метод map принимает функцию, которая берёт x, отдаёт y, а затем с её помощью превращает Some(x) в Some(y). Мы могли бы написать подходящую функции и передать её в map, но вместо этого мы встроим её в место вызова.

Для этого воспользуемся замыканиями. Замыкания — это анонимные функции (то есть функции без имени) с дополнительной супер-способностью: им доступны локальные переменные вне замыкания! Благодаря этому они супер-удобны для условной логики любого рода. В нашем коде match встречается в единственном месте — методе pop, так что давайте просто его перепишем:

pub fn pop(&mut self) -> Option<i32> {
    self.head.take().map(|node| {
        self.head = node.next;
        node.elem
    })
}

Так намного лучше. Давайте убедимся, что мы ничего не сломали:

> cargo test

     Running target/debug/lists-5c71138492ad4b4a

running 2 tests
test first::test::basics ... ok
test second::test::basics ... ok

test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured

Великолепно! Теперь от внешнего вида перейдём к улучшению поведения.

Всё обобщаем

Мы уже немного затронули тему обобщений при обсуждении Option и Box. Однако, пока мы избегали объявления нового типа, который бы обобщал произвольные элементы.

На самом деле это очень просто. Давайте прямо сейчас сделаем наши типы обобщёнными:

pub struct List<T> {
    head: Link<T>,
}

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

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

Вы добавили несколько угловых скобок — и внезапно ваш код стал универсальным. Естественно, не всё так просто. Компилятор начал Очень Сильно Ругался.

> cargo test

error[E0107]: wrong number of type arguments: expected 1, found 0
  --> src/second.rs:14:6
   |
14 | impl List {
   |      ^^^^ expected 1 type argument

error[E0107]: wrong number of type arguments: expected 1, found 0
  --> src/second.rs:36:15
   |
36 | impl Drop for List {
   |               ^^^^ expected 1 type argument

Проблема довольно прозрачна: мы везде пишем List, но это неправильно. Как и в случае с Option, и Box, теперь мы должны писать List<Something>, то есть «Список чего-то».

Но что такое это «что-то», которое мы используем во всех реализациях? Как и в случае с List, мы хотим, чтобы реализация работала со всеми T. Поэтому, как и в случае с List, давайте добавим угловые скобки к impl:

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

    pub fn push(&mut self, elem: T) {
        let new_node = Box::new(Node {
            elem: elem,
            next: self.head.take(),
        });

        self.head = Some(new_node);
    }

    pub fn pop(&mut self) -> Option<T> {
        self.head.take().map(|node| {
            self.head = node.next;
            node.elem
        })
    }
}

impl<T> Drop for List<T> {
    fn drop(&mut self) {
        let mut cur_link = self.head.take();
        while let Some(mut boxed_node) = cur_link {
            cur_link = boxed_node.next.take();
        }
    }
}

...и это всё!

> cargo test

     Running target/debug/lists-5c71138492ad4b4a

running 2 tests
test first::test::basics ... ok
test second::test::basics ... ok

test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured

Весь наш код теперь полностью обобщён для произвольных T. Блин, Rust реально простой. Особо хочу отметить метод new, который даже не изменился.

pub fn new() -> Self {
    List { head: None }
}

Купайтесь в лучах Славы, дарованной вам Self, хранителем рефакторинга и кодирования методом копи-пасты. Кстати, интересно, что мы не пишем List<T> при создании экземпляра списка. Тип выводится автоматически, на основании того факта, что мы возвращаем значение из функции, которая имеет тип List<T>.

Хорошо, теперь перейдём к реализации нового поведения!

Метод Peek

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

pub fn peek(&self) -> Option<&T> {
    self.head.map(|node| {
        &node.elem
    })
}
> cargo build

error[E0515]: cannot return reference to local data `node.elem`
  --> src/second.rs:37:13
   |
37 |             &node.elem
   |             ^^^^^^^^^^ returns a reference to data owned by the current function

error[E0507]: cannot move out of borrowed content
  --> src/second.rs:36:9
   |
36 |         self.head.map(|node| {
   |         ^^^^^^^^^ cannot move out of borrowed content


Вздох. Что теперь, Rust?

Функция map получает self по значению, что удаляет Option из self.head. Раньше это было нормально, потому что мы забирали узел себе, но сейчас мы хотим оставить его на месте. Корректный способ обработать такую ситуацию — вызвать у Option метод as_ref. Он имеет определение:

impl<T> Option<T> {
    pub fn as_ref(&self) -> Option<&T>;
}

Метод сужает Option<T> до опциональной ссылки на внутреннее содержимое. То же самое можно сделать с помощью оператора match, но в этом нет смысла, поскольку в стандартной библиотеке есть готовый метод. Теоретически, нам надо выполнить дополнительное разыменование, чтобы убрать один уровень косвенности, но, к счастью оператор . делает это за нас.

pub fn peek(&self) -> Option<&T> {
    self.head.as_ref().map(|node| {
        &node.elem
    })
}
cargo build

    Finished dev [unoptimized + debuginfo] target(s) in 0.32s

Справились.

Мы также можем сделать изменяемую версию этого метода, используя as_mut:

pub fn peek_mut(&mut self) -> Option<&mut T> {
    self.head.as_mut().map(|node| {
        &mut node.elem
    })
}
cargo build

Лег! Ко!

Не забудем протестировать:

#[test]
fn peek() {
    let mut list = List::new();
    assert_eq!(list.peek(), None);
    assert_eq!(list.peek_mut(), None);
    list.push(1); list.push(2); list.push(3);

    assert_eq!(list.peek(), Some(&3));
    assert_eq!(list.peek_mut(), Some(&mut 3));
}
cargo test

     Running target/debug/lists-5c71138492ad4b4a

running 3 tests
test first::test::basics ... ok
test second::test::basics ... ok
test second::test::peek ... ok

test result: ok. 3 passed; 0 failed; 0 ignored; 0 measured

Всё это здорово, но мы так и не проверили, можно ли изменить значение, которое возвращает peek_mut, ведь так? Если ссылка изменяемая, но никто её не изменил, действительно ли мы протестировали изменяемость? Попробуем вызвать map на экземпляре Option<&mut T>, чтобы изменить содержимое:

#[test]
fn peek() {
    let mut list = List::new();
    assert_eq!(list.peek(), None);
    assert_eq!(list.peek_mut(), None);
    list.push(1); list.push(2); list.push(3);

    assert_eq!(list.peek(), Some(&3));
    assert_eq!(list.peek_mut(), Some(&mut 3));
    list.peek_mut().map(|&mut value| {
        value = 42
    });

    assert_eq!(list.peek(), Some(&42));
    assert_eq!(list.pop(), Some(42));
}
> cargo test

error[E0384]: cannot assign twice to immutable variable `value`
   --> src/second.rs:100:13
    |
99  |         list.peek_mut().map(|&mut value| {
    |                                   -----
    |                                   |
    |                                   first assignment to `value`
    |                                   help: make this binding mutable: `mut value`
100 |             value = 42
    |             ^^^^^^^^^^ cannot assign twice to immutable variable          ^~~~~

Компилятор жалуется, что переменная value неизменяемая, но мы довольно ясно написали &mut value. Так в чём же дело? Оказывается, эта запись не означает, что value является неизменяемой ссылкой. Она означает образец, который сопоставляется с аргументом замыкания; |&mut value| — это «аргумент является изменяемой ссылкой, но ты просто скопируй значение, на которое она ссылается, в переменную value, пожалуйста». А вот если мы напишем просто |value|, тип переменной value превратится в &mut i32 и мы действительно сможем изменить голову:

    #[test]
    fn peek() {
        let mut list = List::new();
        assert_eq!(list.peek(), None);
        assert_eq!(list.peek_mut(), None);
        list.push(1); list.push(2); list.push(3);

        assert_eq!(list.peek(), Some(&3));
        assert_eq!(list.peek_mut(), Some(&mut 3));

        list.peek_mut().map(|value| {
            *value = 42
        });

        assert_eq!(list.peek(), Some(&42));
        assert_eq!(list.pop(), Some(42));
    }
cargo test

     Running target/debug/lists-5c71138492ad4b4a

running 3 tests
test first::test::basics ... ok
test second::test::basics ... ok
test second::test::peek ... ok

test result: ok. 3 passed; 0 failed; 0 ignored; 0 measured

Гораздо лучше!

IntoIter

Перебор коллекций в Rust выполняется с помощью типажа Iterator. Он ненамного сложнее, чем Drop:

pub trait Iterator {
    type Item;
    fn next(&mut self) -> Option<Self::Item>;
}

Новая штука здесь — это type Item. Она объявляет, что каждая реализация типажа Iterator имеет связанный с ней тип, называемый Iter. В это тот самый тип, который возвращает функция next.

Причина по которой интерфейс Iterator возвращает значения Option<Self::Item> в том, что он объединяет концепции has_next (есть ли следующий элемент?) и get_next (верни следующий элемент). Когда у вас есть следующее значение, вы возвращаете Some(value), а когда нет — None. Такой подход делает API более эргономичным и безопасным как для реализации, так и для использования, поскольку позволяет убрать проверки и логику между has_next и get_next. Прекрасно!

К сожалению, (пока) в Rust нет ничего похожего на оператор yield, так что нам предстоит самостоятельно реализовывать логику перебора. Есть три различных вида итераторов, которые должна постараться реализовать каждая коллекция:

  • IntoIter (итератор по значениям) — T
  • IterMut (итератор по изменяемым ссылкам) — &mut T
  • Iter (итератор по разделяемым ссылкам) — &T

На самом деле у нас уже есть всё, что нужно, чтобы реализовать IntoIter используя интерфейс List: нам всего лишь надо раз за разом вызывать pop. Поэтому мы просто реализуем IntoInter, как новый тип поверх List:

// Кортежи — альтернативная форма структур,
// полезная для тривиальных обёрток вокруг других типов.
pub struct IntoIter<T>(List<T>);

impl<T> List<T> {
    pub fn into_iter(self) -> IntoIter<T> {
        IntoIter(self)
    }
}

impl<T> Iterator for IntoIter<T> {
    type Item = T;
    fn next(&mut self) -> Option<Self::Item> {
        // получаем доступ к полям кортежа по номеру
        self.0.pop()
    }
}

Ну, а теперь напишем тест:

#[test]
fn into_iter() {
    let mut list = List::new();
    list.push(1); list.push(2); list.push(3);

    let mut iter = list.into_iter();
    assert_eq!(iter.next(), Some(3));
    assert_eq!(iter.next(), Some(2));
    assert_eq!(iter.next(), Some(1));
    assert_eq!(iter.next(), None);
}
> cargo test

     Running target/debug/lists-5c71138492ad4b4a

running 4 tests
test first::test::basics ... ok
test second::test::basics ... ok
test second::test::into_iter ... ok
test second::test::peek ... ok

test result: ok. 4 passed; 0 failed; 0 ignored; 0 measured

Прекрасно!

Iter

Теперь попробуем реализовать Iter. На этот раз мы не можем рассчитывать, что Rust предоставит нам необходимые функции. Их придётся написать самим. Основная логика, которую мы хотим сделать — хранить указатель на текущий узел, который мы вёрнём при следующем вызове next. Такого узла может и не быть (список пуст или мы завершили итерацию), поэтому ссылка будет опциональной. Возвращая элементы, мы должны передвинуться к следующему узлу, то есть ссылке next текущего узла.

Хорошо, давайте попробуем:

pub struct Iter<T> {
    next: Option<&Node<T>>,
}

impl<T> List<T> {
    pub fn iter(&self) -> Iter<T> {
        Iter { next: self.head.map(|node| &node) }
    }
}

impl<T> Iterator for Iter<T> {
    type Item = &T;

    fn next(&mut self) -> Option<Self::Item> {
        self.next.map(|node| {
            self.next = node.next.map(|node| &node);
            &node.elem
        })
    }
}
> cargo build

error[E0106]: missing lifetime specifier
  --> src/second.rs:72:18
   |
72 |     next: Option<&Node<T>>,
   |                  ^ expected lifetime parameter

error[E0106]: missing lifetime specifier
  --> src/second.rs:82:17
   |
82 |     type Item = &T;
   |                 ^ expected lifetime parameter

О, боже. Время жизни. Я слышал про эту штуку. Я слышал, что это кошмар.

Давайте попробуем что-то новое: видите ошибку error[E0106]? Это код ошибки компилятора. Мы можем попросить rustc объяснить её с помощью --explain:

> rustc --explain E0106
This error indicates that a lifetime is missing from a type. If it is an error
inside a function signature, the problem may be with failing to adhere to the
lifetime elision rules (see below).

Here are some simple examples of where you'll run into this error:

struct Foo { x: &bool }        // error
struct Foo<'a> { x: &'a bool } // correct

enum Bar { A(u8), B(&bool), }        // error
enum Bar<'a> { A(u8), B(&'a bool), } // correct

type MyStr = &str;        // error
type MyStr<'a> = &'a str; //correct
...

Так, хм... Это не очень прояснило ситуацию (дока думает, что мы разбираемся в Rust намного лучше, чем есть на самом деле). Но разве всё не звучит так, что мы должны добавить эти 'a к нашей структуре? Давайте попробуем.

pub struct Iter<'a, T> {
    next: Option<&'a Node<T>>,
}
> cargo build

error[E0106]: missing lifetime specifier
  --> src/second.rs:83:22
   |
83 | impl<T> Iterator for Iter<T> {
   |                      ^^^^^^^ expected lifetime parameter

error[E0106]: missing lifetime specifier
  --> src/second.rs:84:17
   |
84 |     type Item = &T;
   |                 ^ expected lifetime parameter

error: aborting due to 2 previous errors

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

pub struct Iter<'a, T> {
    next: Option<&'a Node<T>>,
}

impl<'a, T> List<T> {
    pub fn iter(&'a self) -> Iter<'a, T> {
        Iter { next: self.head.map(|node| &'a node) }
    }
}

impl<'a, T> Iterator for Iter<'a, T> {
    type Item = &'a T;
    fn next(&'a mut self) -> Option<Self::Item> {
        self.next.map(|node| {
            self.next = node.next.map(|node| &'a node);
            &'a node.elem
        })
    }
}
> cargo build

error: expected `:`, found `node`
  --> src/second.rs:77:47
   |
77 |         Iter { next: self.head.map(|node| &'a node) }
   |         ---- while parsing this struct        ^^^^ expected `:`

error: expected `:`, found `node`
  --> src/second.rs:85:50
   |
85 |             self.next = node.next.map(|node| &'a node);
   |                                                  ^^^^ expected `:`

error[E0063]: missing field `next` in initializer of `second::Iter<'_, _>`
  --> src/second.rs:77:9
   |
77 |         Iter { next: self.head.map(|node| &'a node) }
   |         ^^^^ missing `next`

Боже. Мы сломали Rust.

Может быть, нам всё-таки пора разобраться, что за хрень это самое время жизни 'a?

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

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

  • Хранить указатель на что-то, чего больше не существует
  • Хранить указатель на что-то, что было кем-то изменено

Время жизни решает обе эти проблемы и в 99% случаев делает это совершенно прозрачно.

Так что ж такое время жизни?

Говоря простыми словами, время жизни — это название участка кода (блока/области видимости) где-то в программе. Вот и всё. Когда ссылка помечена временем жизни, мы говорим, что она должна быть действительна на всём участке. То, как долго ссылка должна быть и может быть действительна, зависит от множества вещей. Вся система управления временем жизни представляет собой систему решения ограничений, которая пытается минимизировать участок, где каждая ссылка действительна. Если ей удаётся найти множество времён жизни, которые удовлетворяют всем ограничениям, ваша программа компилируется! В противном случае вы получаете ошибку, которая утверждает, что какая-то переменная живёт недостаточно долго.

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

В принципе, времена жизни можно было бы опустить, но тогда проверка заимствований привела бы к чудовищному анализу всей программы и сложным нелокальным ошибкам. (Речь о том, что ошибка в функции A приводила бы к ошибке в вызывающей её функции B и вам предстояла бы большая работа, чтобы найти истинную причину — прим. пер.) С другой стороны, если вы явно указываете время жизни, Rust может независимо проверить каждую функцию, и все ваши ошибки должны быть довольно локальными (или у ваших типов некорректные сигнатуры).

Впрочем, мы и раньше писали ссылки в сигнатурах функций, и всё было нормально! Оказывается, всё работало потому, что есть несколько распространенных случаев, когда Rust может вывести время жизни за нас. Речь идёт о неявном выведении времени жизни (lifetime elision).

В частности:

// Только одна ссылка на входе, так что выход можно вывести из входа
fn foo(&A) -> &B; // сахар для:
fn foo<'a>(&'a A) -> &'a B;

// Много ссылок на входе, предполагаем, что все они независимы
fn foo(&A, &B, &C); сахар для:
fn foo<'a, 'b, 'c>(&'a A, &'b B, &'c C);

// Методы, выводим все выходные времена жизни из `self`.
fn foo(&self, &B, &C) -> &D; // sugar for: сахар для:
fn foo<'a, 'b, 'c>(&'a self, &'b B, &'c C) -> &'a D;

Итак, что значит fn foo<'a>(&'a A) -> &'a B? Всего лишь то, что входная переменная должна жить по крайней мере столько же, сколько и выходная. Следовательно, если мы храним выходные переменные в течение длительного времени, участок, в котором должны быть действительны и входные переменные, становится больше. После того, как вы прекращаете использовать выходную переменную, компилятор понимает, что входная переменная тоже стала недействительной.

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

Хорошо. Так. Iter.

Откатимся к коду без времени жизни:

pub struct Iter<T> {
    next: Option<&Node<T>>,
}

impl<T> List<T> {
    pub fn iter(&self) -> Iter<T> {
        Iter { next: self.head.map(|node| &node) }
    }
}

impl<T> Iterator for Iter<T> {
    type Item = &T;
    fn next(&mut self) -> Option<Self::Item> {
        self.next.map(|node| {
            self.next = node.next.map(|node| &node);
            &node.elem
        })
    }
}

Мы должны добавить времена жизни только в сигнатуры функций и типов:

// Iter это обобщённый тип с каким-то временем, остальное ему не важно
pub struct Iter<'a, T> {
    next: Option<&'a Node<T>>,
}

// Никакого врмени жизни: у List нет никаких связанных времён жизни
impl<T> List<T> {
    // Здесь мы объявляем новое время жизни для заимствования, чтобы
    // создать итератор. Теперь &self должен оставаться действительным до тех
    // пор, пока существует Iter.
    pub fn iter<'a>(&'a self) -> Iter<'a, T> {
        Iter { next: self.head.map(|node| &node) }
    }
}

// Здесь *нужно* время жизни, потому что оно есть у Iter
impl<'a, T> Iterator for Iter<'a, T> {
    // Здесь тоже нужно, поскольку это декларация типа
    type Item = &'a T;

    // Здесь ничего менять не нужно, всё будет работать, как и раньше
    // Self всё ещё невероятно чудесен и хайпов
    fn next(&mut self) -> Option<Self::Item> {
        self.next.map(|node| {
            self.next = node.next.map(|node| &node);
            &node.elem
        })
    }
}

Ладно, надеюсь, на этот раз у нас получилось.

cargo build

error[E0308]: mismatched types
  --> src/second.rs:77:22
   |
77 |         Iter { next: self.head.map(|node| &node) }
   |                      ^^^^^^^^^^^^^^^^^^^^^^^^^^^ expected struct `second::Node`, found struct `std::boxed::Box`
   |
   = note: expected type `std::option::Option<&second::Node<T>>`
              found type `std::option::Option<&std::boxed::Box<second::Node<T>>>`

error[E0308]: mismatched types
  --> src/second.rs:85:25
   |
85 |             self.next = node.next.map(|node| &node);
   |                         ^^^^^^^^^^^^^^^^^^^^^^^^^^^ expected struct `second::Node`, found struct `std::boxed::Box`
   |
   = note: expected type `std::option::Option<&'a second::Node<T>>`
              found type `std::option::Option<&std::boxed::Box<second::Node<T>>>`

(╯°□°)╯︵ ┻━┻

Так. Хорошо. Мы исправили ошибки с временем жизни, но у нас появился новый тип ошибок.

Нам нужны &Node, но у нас есть только &Box<Node>. Ладно, это достаточно просто, мы всего лишь должны разыменовать бокс, чтобы получить нашу ссылку:

impl<T> List<T> {
    pub fn iter<'a>(&'a self) -> Iter<'a, T> {
        Iter { next: self.head.map(|node| &*node) }
    }
}

impl<'a, T> Iterator for Iter<'a, T> {
    type Item = &'a T;
    fn next(&mut self) -> Option<Self::Item> {
        self.next.map(|node| {
            self.next = node.next.map(|node| &*node);
            &node.elem
        })
    }
}
cargo build
   Compiling lists v0.1.0 (/Users/ADesires/dev/temp/lists)
error[E0515]: cannot return reference to local data `*node`
  --> src/second.rs:77:43
   |
77 |         Iter { next: self.head.map(|node| &*node) }
   |                                           ^^^^^^ returns a reference to data owned by the current function

error[E0507]: cannot move out of borrowed content
  --> src/second.rs:77:22
   |
77 |         Iter { next: self.head.map(|node| &*node) }
   |                      ^^^^^^^^^ cannot move out of borrowed content

error[E0515]: cannot return reference to local data `*node`
  --> src/second.rs:85:46
   |
85 |             self.next = node.next.map(|node| &*node);
   |                                              ^^^^^^ returns a reference to data owned by the current function

error[E0507]: cannot move out of borrowed content
  --> src/second.rs:85:25
   |
85 |             self.next = node.next.map(|node| &*node);
   |                         ^^^^^^^^^ cannot move out of borrowed content

(ノಥ益ಥ)ノ ┻━┻

Мы забыли вызвать as_ref, так что мы передаём бокс в map, что означает, что он будет удалён, что означает, что наши ссылки станут висячими (перестанут ссылаться на реальный объект).

pub struct Iter<'a, T> {
    next: Option<&'a Node<T>>,
}

impl<T> List<T> {
    pub fn iter<'a>(&'a self) -> Iter<'a, T> {
        Iter { next: self.head.as_ref().map(|node| &*node) }
    }
}

impl<'a, T> Iterator for Iter<'a, T> {
    type Item = &'a T;

    fn next(&mut self) -> Option<Self::Item> {
        self.next.map(|node| {
            self.next = node.next.as_ref().map(|node| &*node);
            &node.elem
        })
    }
}
cargo build
   Compiling lists v0.1.0 (/Users/ADesires/dev/temp/lists)
error[E0308]: mismatched types
  --> src/second.rs:77:22
   |
77 |         Iter { next: self.head.as_ref().map(|node| &*node) }
   |                      ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ expected struct `second::Node`, found struct `std::boxed::Box`
   |
   = note: expected type `std::option::Option<&second::Node<T>>`
              found type `std::option::Option<&std::boxed::Box<second::Node<T>>>`

error[E0308]: mismatched types
  --> src/second.rs:85:25
   |
85 |             self.next = node.next.as_ref().map(|node| &*node);
   |                         ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ expected struct `second::Node`, found struct `std::boxed::Box`
   |
   = note: expected type `std::option::Option<&'a second::Node<T>>`
              found type `std::option::Option<&std::boxed::Box<second::Node<T>>>`

😭

as_ref добавил ещё один уровень косвенности и его надо убрать:

pub struct Iter<'a, T> {
    next: Option<&'a Node<T>>,
}

impl<T> List<T> {
    pub fn iter<'a>(&'a self) -> Iter<'a, T> {
        Iter { next: self.head.as_deref() }
    }
}

impl<'a, T> Iterator for Iter<'a, T> {
    type Item = &'a T;

    fn next(&mut self) -> Option<Self::Item> {
        self.next.map(|node| {
            self.next = node.next.as_deref();
            &node.elem
        })
    }
}
cargo build

🎉 🎉 🎉

Функции as_deref и as_deref_mut стабильны, начиная с Rust 1.40. До этого вам надо было писать map(|node| &**node) и map(|node| &mut**node). Вам может показаться, что эта штука с &** на самом деле корявая, и в этом вы не ошибаетесь. Но, как хорошее вино, Rust со временем становится только лучше и нам больше не нужно так писать. Обычно Rust справляется с такими преобразованиями неявно, посредством процесса, называемого автоматическим разыменованием (deref coercion), во время которого он может вставлять * по всему коду, чтобы код прошёл проверку типов. Такой подход работает благодаря проверке заимствований, которая гарантирует, что мы никогда не сломаем указатели!

Но в нашем случае замыкание в сочетании с тем фактом, что нам нужен Option<&T> вместо &T, оказалось слишком сложным. В результате пришлось явно указывать время жизни. К счастью, по моему опыту, такое случается довольно редко.

Для полноты картины: мы могли бы дать другую подсказку с помощью оператора ::<> который на сленге называют турборыбой (turbofish):

    self.next = node.next.as_ref().map::<&Node<T>, _>(|node| &node);

Смотрите, map — это обобщённая функция:

pub fn map<U, F>(self, f: F) -> Option<U>

Турборыба ::<> позволяет нам сообщить компилятору, какими, как нам кажется, должны быть обобщённые типы. В данном случае ::<&Node<T>, _> говорит «функция должна вернуть &Node<T>, а второй тип меня не волнует».

Это, в свою очередь сообщает компилятору, что к &node следует применить автоматическое разыменование, так что нам не придётся писать все эти * вручную!

Но в данном случае я не думаю, что так следует писать. Это был всего лишь плохо завуалированный предлог, чтобы рассказать про автоматическое разыменование и оператор ::<>, весьма полезный время от времени. 😅

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

#[test]
fn iter() {
    let mut list = List::new();
    list.push(1); list.push(2); list.push(3);

    let mut iter = list.iter();
    assert_eq!(iter.next(), Some(&3));
    assert_eq!(iter.next(), Some(&2));
    assert_eq!(iter.next(), Some(&1));
}
> cargo test

     Running target/debug/lists-5c71138492ad4b4a

running 5 tests
test first::test::basics ... ok
test second::test::basics ... ok
test second::test::into_iter ... ok
test second::test::iter ... ok
test second::test::peek ... ok

test result: ok. 4 passed; 0 failed; 0 ignored; 0 measured

Блин, да.

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

impl<T> List<T> {
    pub fn iter<'a>(&'a self) -> Iter<'a, T> {
        Iter { next: self.head.as_deref() }
    }
}

эквивалентно:

impl<T> List<T> {
    pub fn iter(&self) -> Iter<T> {
        Iter { next: self.head.as_deref() }
    }
}

Ура, меньше времён жизни!

Или, если вам некомфортно «скрывать», что у структуры есть время жизни, мы можете использовать синтаксис «явно пропущенного времени жизни» '_, который появился в Rust 2018.

impl<T> List<T> {
    pub fn iter(&self) -> Iter<'_, T> {
        Iter { next: self.head.as_deref() }
    }
}

IterMut

Я хочу быть честной: IterMut это БЕЗУМИЕ. Что само по себе звучит безумно: он ведь должен быть похож на Iter!

Семантически да, но природа разделяемых и изменяемых ссылок такова, что Iter можно считать «тривиальным», в то время как IterMut — это Настоящее Колдовство.

Возьмём ключевую идею из нашей реализации Iterator для Iter:

impl<'a, T> Iterator for Iter<'a, T> {
    type Item = &'a T;

    fn next(&mut self) -> Option<Self::Item> { /* что-то */ }
}

Которую можно преобразовать в:

impl<'a, T> Iterator for Iter<'a, T> {
    type Item = &'a T;

    fn next<'b>(&'b mut self) -> Option<&'a T> { /* что-то */ }
}

Сигнатура next не устанавливает ограничений между временем жизни входной и выходной переменных! Почему для нас это важно? Потому, что мы можем вызывать next снова и снова без всяких условий!

let mut list = List::new();
list.push(1); list.push(2); list.push(3);

let mut iter = list.iter();
let x = iter.next().unwrap();
let y = iter.next().unwrap();
let z = iter.next().unwrap();

Круто!

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

В результате, написать безопасный код для IterMut гораздо сложнее (и мы, кстати, пока не выяснили, что это значит...). Удивительно, но IterMut можно совершенно безопасно реализовать для многих структур данных!

Начнём с того, что просто возьмём код Iter и перепишем всё так, чтобы ссылки стали изменяемыми:

pub struct IterMut<'a, T> {
    next: Option<&'a mut Node<T>>,
}

impl<T> List<T> {
    pub fn iter_mut(&self) -> IterMut<'_, T> {
        IterMut { next: self.head.as_deref_mut() }
    }
}

impl<'a, T> Iterator for IterMut<'a, T> {
    type Item = &'a mut T;

    fn next(&mut self) -> Option<Self::Item> {
        self.next.map(|node| {
            self.next = node.next.as_deref_mut();
            &mut node.elem
        })
    }
}
> cargo build
error[E0596]: cannot borrow `self.head` as mutable, as it is behind a `&` reference
  --> src/second.rs:95:25
   |
94 |     pub fn iter_mut(&self) -> IterMut<'_, T> {
   |                     ----- help: consider changing this to be a mutable reference: `&mut self`
95 |         IterMut { next: self.head.as_deref_mut() }
   |                         ^^^^^^^^^ `self` is a `&` reference, so the data it refers to cannot be borrowed as mutable

error[E0507]: cannot move out of borrowed content
   --> src/second.rs:103:9
    |
103 |         self.next.map(|node| {
    |         ^^^^^^^^^ cannot move out of borrowed content

Хорошо, выглядит так, словно у нас тут две разные ошибки. Первая очень понятная и в сообщении даже написано, как её исправить! Нельзя преобразовать разделяемую ссылку в изменяемую, так что iter_mut должен принимать &mut self. Обычная глупая ошибка копи-пасты.

pub fn iter_mut(&mut self) -> IterMut<'_, T> {
    IterMut { next: self.head.as_deref_mut() }
}

А что насчёт второй?

Ой! На самом дела я случайно ошиблась при написании iter в прошлом разделе, и нам просто повезло, что всё заработало!

Мы только что столкнулись с типажом Copy. Когда мы вводили понятие владения, то сказали, что передав владение, вы больше не можете использовать переменную. Для многих типов это правда. Наш добрый друг Box управляет размещением памяти в куче и мы конечно же не хотим, чтобы два куска кода думали, что они должны освободить эту память.

В то же время для других типов всё это мусор. Целые числа не обладают семантикой владения, они всего лишь бессмысленные числа! Именно по этой причине, целые числа реализуют типаж Copy. Известно, что типы, помеченные как Copy, идеально копируется побитовым копированием. Поэтому они обладают сверхспособностью: при перемещении старое значение всё ещё можно использовать. Как следствие, вы можете даже забрать копируемый тип из ссылки без замены!

Копируемыми являются все численные примитивы Rust (i32, u64, bool, f32, char и т. д.). Вы также можете объявить свой тип копируемым, если все его компоненты — также копируемые.

Ключевым моментом, объясняющим, почему этот код работал, является то, что разделяемые ссылки также копируемые! А, поскольку & можно копировать, Option<&> также можно копировать. Так что когда мы вызывали self.next.map, это работало потому что что Option просто копировался. Но теперь мы не можем так сделать, потому что &mut не копируется (если вы скопируете &mut, у вас будет две изменяемые ссылки на одно и то же место в памяти, что запрещено). Вместо копирования, мы должны забрать владение, вызвав метод take.

fn next(&mut self) -> Option<Self::Item> {
    self.next.take().map(|node| {
        self.next = node.next.as_deref_mut();
        &mut node.elem
    })
}
cargo build

Так... подождите. Охренеть! IterMut просто работает!

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

#[test]
fn iter_mut() {
    let mut list = List::new();
    list.push(1); list.push(2); list.push(3);

    let mut iter = list.iter_mut();
    assert_eq!(iter.next(), Some(&mut 3));
    assert_eq!(iter.next(), Some(&mut 2));
    assert_eq!(iter.next(), Some(&mut 1));
}
> cargo test

     Running target/debug/lists-5c71138492ad4b4a

running 6 tests
test first::test::basics ... ok
test second::test::basics ... ok
test second::test::iter_mut ... ok
test second::test::into_iter ... ok
test second::test::iter ... ok
test second::test::peek ... ok

test result: ok. 7 passed; 0 failed; 0 ignored; 0 measured

Да. Он работает.

Охренеть.

Хотя.

Конечно, IterMut должен работать, но обычно этому мешает какая-нибудь глупость! Проясню:

Мы только что реализовали код, который принимает односвязный список и возвращает изменяемые ссылки на каждый отдельный элемент списка максимум один раз. И он статически проверен. А также полностью безопасен. И нам не пришлось писать ничего безумного.

Как по мне, это большое достижение. Есть пара причин, поэтому всё работает.

  • Мы вызываем take у Option<&mut>, так что получаем эксклюзивный доступ к изменяемой ссылке. Не надо беспокоиться, что кто-то ещё сможет получить к ней доступ.
  • Rust понимает, что это нормально — разбить изменяемую ссылку на отдельные поля указываемой структуры, потому что нет способа «собрать её обратно» и поля определённо не пересекаются.

Та же самая базовая логика применима при реализации безопасного IterMut для массива или дерева! Можно даже сделать итератор DoubleEnded, чтобы перебирать элементы одновременно от начала к концу, и от конца к началу! Вот как!

Финальный код

Итак, мы дописали второй список и вот финальный код!

#![allow(unused)]
fn main() {
pub struct List<T> {
    head: 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 }
    }

    pub fn push(&mut self, elem: T) {
        let new_node = Box::new(Node {
            elem: elem,
            next: self.head.take(),
        });

        self.head = Some(new_node);
    }

    pub fn pop(&mut self) -> Option<T> {
        self.head.take().map(|node| {
            self.head = node.next;
            node.elem
        })
    }

    pub fn peek(&self) -> Option<&T> {
        self.head.as_ref().map(|node| {
            &node.elem
        })
    }

    pub fn peek_mut(&mut self) -> Option<&mut T> {
        self.head.as_mut().map(|node| {
            &mut node.elem
        })
    }

    pub fn into_iter(self) -> IntoIter<T> {
        IntoIter(self)
    }

    pub fn iter(&self) -> Iter<'_, T> {
        Iter { next: self.head.as_deref() }
    }

    pub fn iter_mut(&mut self) -> IterMut<'_, T> {
        IterMut { next: self.head.as_deref_mut() }
    }
}

impl<T> Drop for List<T> {
    fn drop(&mut self) {
        let mut cur_link = self.head.take();
        while let Some(mut boxed_node) = cur_link {
            cur_link = boxed_node.next.take();
        }
    }
}

pub struct IntoIter<T>(List<T>);

impl<T> Iterator for IntoIter<T> {
    type Item = T;
    fn next(&mut self) -> Option<Self::Item> {
        // получаем доступ к полям кортежа по номеру
        self.0.pop()
    }
}

pub struct Iter<'a, T> {
    next: Option<&'a Node<T>>,
}

impl<'a, T> Iterator for Iter<'a, T> {
    type Item = &'a T;
    fn next(&mut self) -> Option<Self::Item> {
        self.next.map(|node| {
            self.next = node.next.as_deref();
            &node.elem
        })
    }
}

pub struct IterMut<'a, T> {
    next: Option<&'a mut Node<T>>,
}

impl<'a, T> Iterator for IterMut<'a, T> {
    type Item = &'a mut T;

    fn next(&mut self) -> Option<Self::Item> {
        self.next.take().map(|node| {
            self.next = node.next.as_deref_mut();
            &mut node.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(3));
        assert_eq!(list.pop(), Some(2));

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

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

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

    #[test]
    fn peek() {
        let mut list = List::new();
        assert_eq!(list.peek(), None);
        assert_eq!(list.peek_mut(), None);
        list.push(1); list.push(2); list.push(3);

        assert_eq!(list.peek(), Some(&3));
        assert_eq!(list.peek_mut(), Some(&mut 3));

        list.peek_mut().map(|value| {
            *value = 42
        });

        assert_eq!(list.peek(), Some(&42));
        assert_eq!(list.pop(), Some(42));
    }

    #[test]
    fn into_iter() {
        let mut list = List::new();
        list.push(1); list.push(2); list.push(3);

        let mut iter = list.into_iter();
        assert_eq!(iter.next(), Some(3));
        assert_eq!(iter.next(), Some(2));
        assert_eq!(iter.next(), Some(1));
        assert_eq!(iter.next(), None);
    }

    #[test]
    fn iter() {
        let mut list = List::new();
        list.push(1); list.push(2); list.push(3);

        let mut iter = list.iter();
        assert_eq!(iter.next(), Some(&3));
        assert_eq!(iter.next(), Some(&2));
        assert_eq!(iter.next(), Some(&1));
    }

    #[test]
    fn iter_mut() {
        let mut list = List::new();
        list.push(1); list.push(2); list.push(3);

        let mut iter = list.iter_mut();
        assert_eq!(iter.next(), Some(&mut 3));
        assert_eq!(iter.next(), Some(&mut 2));
        assert_eq!(iter.next(), Some(&mut 1));
    }
}

}

Обрастаем функционалом!

Устойчивый односвязный стек

Прекрасно, мы овладели искусством создания изменяемых односвязных стеков.

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

В процессе мы, в основном, познакомимся с Rc и Arc, и это подготовит нас к следующему, кардинально отличному списку.

Создадим новый файл и назовём его third.rs:

// в lib.rs

pub mod first;
pub mod second;
pub mod third;

Пока никакой копи-пасты. Начнём с чистого листа.

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

Что ж, назад — к чертёжной доске! Будем рисовать представление.

Главный факт об устойчивых списках заключается в том, что вы свободно можете манипулировать их хвостами. Скажем, такое поведение не является чем то необычным при работе с устойчивым списком:

list1 = A -> B -> C -> D
list2 = tail(list1) = B -> C -> D
list3 = push(list2, X) = X -> B -> C -> D

Но в итоге мы хотим, чтобы память выглядела так:

list1 -> A ---+
              |
              v
list2 ------> B -> C -> D
              ^
              |
list3 -> X ---+

С боксами такая штука работать не будет, поскольку элементом B на рисунке одновременно владеют (разделяют владение) три переменные. Какая из них будет его освобождать? Если удалить list2, приведёт ли это к освобождению B? В случае боксов мы определённо должны были бы этого ожидать!

Функциональным языкам — да, по сути, всем языкам — это сходит с рук за счёт сборки мусора. Благодаря этой магии, B освободится только после того, как исчезнет последняя ссылка на него. Ура!

В Rust нет ничего похожего на сборщик мусора, которые перелопачивает всю память в поиске объектов, которые можно убрать. Такой сборщик называют отслеживающим. Всё, что есть в Rust сегодня — это подсчёт ссылок. О нём можно думать, как об очень простом сборщике мусора. Во многих сценариях его производительность значительно ниже, чем у отслеживающих сборщиков и он полностью выходит из строя, если вам удастся создать циклические ссылки. Но — эй, это всё, что у нас есть! К счастью, в наших сценариях мы никогда не создадим цикла (можете попробовать это доказать — я точно не буду).

Как же работает сборка мусора на основе подсчёта ссылок? Rc! Rc — это то же самое, что и Box, который можно дублировать. Его память освободится только тогда, когда будут уничтожены все дубли Rc. К сожалению, эта гибкость ведёт к серьёзному ограничению: можно получить только разделяемую ссылку на внутренний объект. Это означает, что мы не можем даже ни забрать данные из наших списков, ни изменить их.

Так что на что должно быть похоже наше представление данных? Ну, раньше у нас было:

pub struct List<T> {
    head: Link<T>,
}

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

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

Можем ли мы просто заменить Box на Rc?

// в third.rs

pub struct List<T> {
    head: Link<T>,
}

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

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

error[E0412]: cannot find type `Rc` in this scope
 --> src/third.rs:5:23
  |
5 | type Link<T> = Option<Rc<Node<T>>>;
  |                       ^^ not found in this scope
help: possible candidate is found in another module, you can import it into scope
  |
1 | use std::rc::Rc;
  |

Блин, какая едкая насмешка. В отличие от всего, что мы использовали при разработке изменяемых списков, Rc настолько отстоен, что даже не импортируется автоматически. Что за неудачник.

use std::rc::Rc;
cargo build

warning: field is never used: `head`
 --> src/third.rs:4:5
  |
4 |     head: Link<T>,
  |     ^^^^^^^^^^^^^
  |
  = note: #[warn(dead_code)] on by default

warning: field is never used: `elem`
  --> src/third.rs:10:5
   |
10 |     elem: T,
   |     ^^^^^^^

warning: field is never used: `next`
  --> src/third.rs:11:5
   |
11 |     next: Link<T>,
   |     ^^^^^^^^^^^^^

Выглядит неплохо. Rust — всё ещё полностью тривиален для написания. Держу пари, мы просто заменим Box на Rc, и на этом закончим!

...

Нет. У нас не выйдет.

Основы

Сейчас мы уже знаем основы языка Rust, так что можем делать множество простых вещей.

Конструктор можно просто скопировать из предыдущей главы:

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

Методы push и pop теперь не имеют смысла. Вместо них мы напишем prepend (вставить в начало) и tail (получить хвост), которые делают приблизительно то же самое.

Начнём с prepend. Метод получает элемент и список. и возвращает список. Как и в случае с изменяемым списком, мы создадим новый узел, у которого старый список помещается в поле next. Единственное нововведение заключается в том, как получить значение для next, поскольку мы не можем ничего менять.

Ответом на наши молитвы является типаж Clone. Его реализуют практически все типы. Он предоставляет универсальный способ получить «похожую штуку» и при этом использует только разделяемую ссылку. Логически клон отличается от оригинала. Метод похож на копирующий конструктор в C++, но всегда вызывается явно.

Конкретно Rc использует Clone, чтобы увеличить счётчик ссылок на единицу. Поэтому вместо того, чтобы перемещать Box в подсписок, мы просто клонируем голову старого списка. Нам даже не придётся писать оператор match, поскольку Option предоставляет реализацию Clone, которая делает именно то, что нам нужно.

Ладно, давайте попробуем:

pub fn prepend(&self, elem: T) -> List<T> {
    List { head: Some(Rc::new(Node {
        elem: elem,
        next: self.head.clone(),
    }))}
}
> cargo build

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

warning: field is never used: `next`
  --> src/third.rs:11:5
   |
11 |     next: Link<T>,
   |     ^^^^^^^^^^^^^

Да уж, Rust поистине непреклонен по поводу неиспользуемых полей. Фактически, он предупреждает, что может не компилировать этот код, поскольку никто пользователей библиотеки даже не заметит разницы! В любом случае, пока всё выглядит неплохо.

tail — это логическая инверсия предыдущей операции. Она получает список и возвращает список без первого элемента. Всё, что нужно сделать — это клонировать второй элемент (если он существует) и сделать его головой нового списка. Давайте попробуем:

pub fn tail(&self) -> List<T> {
    List { head: self.head.as_ref().map(|node| node.next.clone()) }
}
cargo build

error[E0308]: mismatched types
  --> src/third.rs:27:22
   |
27 |         List { head: self.head.as_ref().map(|node| node.next.clone()) }
   |                      ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ expected struct `std::rc::Rc`, found enum `std::option::Option`
   |
   = note: expected type `std::option::Option<std::rc::Rc<_>>`
              found type `std::option::Option<std::option::Option<std::rc::Rc<_>>>`

Грм, здесь мы напортачили. map ожидает, что мы вернём Y, но мы возвращаем Option<Y>. К счастью, есть ещё один известный паттерн для Option, так что мы можем просто вызвать and_then вместо map.

pub fn tail(&self) -> List<T> {
    List { head: self.head.as_ref().and_then(|node| node.next.clone()) }
}
cargo build

Великолепно.

Теперь, когда у нас есть tail, мы, возможно, должны написать head, чтобы возвращать ссылку на первый элемент. По сути, это метод peek, который мы реализовали для изменяемого списка:

pub fn head(&self) -> Option<&T> {
    self.head.as_ref().map(|node| &node.elem)
}
cargo build

Здорово.

Теперь у нас достаточно функциональности, чтобы её протестировать:

#[cfg(test)]
mod test {
    use super::List;

    #[test]
    fn basics() {
        let list = List::new();
        assert_eq!(list.head(), None);

        let list = list.prepend(1).prepend(2).prepend(3);
        assert_eq!(list.head(), Some(&3));

        let list = list.tail();
        assert_eq!(list.head(), Some(&2));

        let list = list.tail();
        assert_eq!(list.head(), Some(&1));

        let list = list.tail();
        assert_eq!(list.head(), None);

        // Убеждаемся, что tail работает с пустым списком
        let list = list.tail();
        assert_eq!(list.head(), None);

    }
}
> cargo test

     Running target/debug/lists-5c71138492ad4b4a

running 5 tests
test first::test::basics ... ok
test second::test::into_iter ... ok
test second::test::basics ... ok
test second::test::iter ... ok
test third::test::basics ... ok

test result: ok. 5 passed; 0 failed; 0 ignored; 0 measured

Превосходно!

Iter тоже идентичен реализации из нашего изменяемого списка:

pub struct Iter<'a, T> {
    next: Option<&'a Node<T>>,
}

impl<T> List<T> {
    pub fn iter(&self) -> Iter<'_, T> {
        Iter { next: self.head.as_deref() }
    }
}

impl<'a, T> Iterator for Iter<'a, T> {
    type Item = &'a T;

    fn next(&mut self) -> Option<Self::Item> {
        self.next.map(|node| {
            self.next = node.next.as_deref();
            &node.elem
        })
    }
}
#[test]
fn iter() {
    let list = List::new().prepend(1).prepend(2).prepend(3);

    let mut iter = list.iter();
    assert_eq!(iter.next(), Some(&3));
    assert_eq!(iter.next(), Some(&2));
    assert_eq!(iter.next(), Some(&1));
}
cargo test

     Running target/debug/lists-5c71138492ad4b4a

running 7 tests
test first::test::basics ... ok
test second::test::basics ... ok
test second::test::iter ... ok
test second::test::into_iter ... ok
test second::test::peek ... ok
test third::test::basics ... ok
test third::test::iter ... ok

test result: ok. 6 passed; 0 failed; 0 ignored; 0 measured

Кто вообще сказал, что динамическая типизация проще?

(наверняка, какие-то болваны)

Обратите внимание, что мы не можем реализовать IntoIter или IterMut для этого типа. К элементам может быть только разделяемый доступ.

Метод drop

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

impl<T> Drop for List<T> {
    fn drop(&mut self) {
        let mut cur_link = self.head.take();
        while let Some(mut boxed_node) = cur_link {
            cur_link = boxed_node.next.take();
        }
    }
}

Но теперь проблема возникает в теле цикла:

cur_link = boxed_node.next.take();

Здесь меняется узел внутри Box, но мы не может сделать то же самое с Rc; он предоставляет нам только разделяемый доступ, так как на него может указывать любое количество Rc-указателей.

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

Оказывается, в Rc есть метод, который делает именно то, что нам нужно — try_unwrap:

impl<T> Drop for List<T> {
    fn drop(&mut self) {
        let mut head = self.head.take();
        while let Some(node) = head {
            if let Ok(mut node) = Rc::try_unwrap(node) {
                head = node.next.take();
            } else {
                break;
            }
        }
    }
}
cargo test
   Compiling lists v0.1.0 (/Users/ADesires/dev/too-many-lists/lists)
    Finished dev [unoptimized + debuginfo] target(s) in 1.10s
     Running /Users/ADesires/dev/too-many-lists/lists/target/debug/deps/lists-86544f1d97438f1f

running 8 tests
test first::test::basics ... ok
test second::test::basics ... ok
test second::test::into_iter ... ok
test second::test::iter ... ok
test second::test::iter_mut ... ok
test second::test::peek ... ok
test third::test::basics ... ok
test third::test::iter ... ok

test result: ok. 8 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out

Великолепно! Здорово.

Arc

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

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

Чтобы получить потокобезопасность, мы должны использовать Arc. Arc полностью идентичен Rc за тем исключением, что счётчик ссылок модифицируется атомарно. Это влечёт небольшие накладные расходы, и если оно вам не надо, вы всегда можете использовать Rc. Всё, что нам надо с делать с нашим списком, это заменить каждое упоминание Rc на std::sync::Arc. И это всё. Мы потокобезопасны. Сделано!

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

Нет! В Rust потокобезопасность сломать нельзя!

Причина в том, что Rust моделирует потокобезопасность с помощью двух типажей: Send и Sync.

Тип относится к Send (отправляемый), если его можно безопасно передать в другой поток. Тип относится к Sync (синхронный), если его можно безопасно разделить между потоками. Есть правило: если T реализует Sync, то &T реализует Send. Безопасность в данном случае означает, что мы не можем устроить гонку данных (data races). Не путайте её с более общий случаем — состоянием гонки (race conditions).

Это типажи-маркеры или, говоря простым языком, они не предоставляют ни одного метода. Тип просто либо является Send, либо не является. Можно сказать, что это свойство типа, которое могут требовать другие API. Если тип не помечен как Send, компилятор проверяет, что его нельзя передавать в другой поток! Идеально!

Send и Sync автоматически реализуются для типов, состоящих только из полей Send и Sync. Похожим образом, типаж Copy может быть реализован только для типов, состоящих из Copy-типов. Но для Send и Sync компилятор идёт ещё дальше и реализует всё за вас.

Почти все типы — одновременно и Send, и Sync. Большинство типов относятся к Send, потому что они полностью владеют своими данными. Большинство типов относятся к Sync, потому что единственный способ разделить данные между потоками — воспользоваться разделяемой ссылкой, которая, как мы знаем, неизменяемая!

Однако существуют особые типы, которые нарушают эти свойства. Они обладают внутренней изменчивостью (interior mutability). До сих пор мы сталкивались только с унаследованной изменчивостью (или внешней изменчивостью): изменчивость значения наследуется у контейнера. Вы не можете просто взять и поменять какое-то поле неизменяемого объекта, просто потому, что вам так захотелось.

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

Какое отношение всё это имеет к Rc и Arc, спросите вы? Ну, они оба внутренне изменчивы из-за счётчика ссылок. Хуже того, этот счётчик разделяется между всеми экземплярами! Rc просто использует ячейку и это значит, что он не является потокобезопасным. Arc использует атомарный тип и это значит, что он является потокобезопасным. Естественно, вы не можете магически превратить тип в потокобезопасный, просто поместив его в Arc. Arc может только наследовать потокобезопасность, как и любой другой тип.

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

Финальный код

Это всё, что я хотела рассказать о неизменяемом стеке. Мы тем временем уже неплохо освоили работу со списками!

#![allow(unused)]
fn main() {
use std::rc::Rc;

pub struct List<T> {
    head: Link<T>,
}

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

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

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

    pub fn prepend(&self, elem: T) -> List<T> {
        List { head: Some(Rc::new(Node {
            elem: elem,
            next: self.head.clone(),
        }))}
    }

    pub fn tail(&self) -> List<T> {
        List { head: self.head.as_ref().and_then(|node| node.next.clone()) }
    }

    pub fn head(&self) -> Option<&T> {
        self.head.as_ref().map(|node| &node.elem)
    }

    pub fn iter(&self) -> Iter<'_, T> {
        Iter { next: self.head.as_deref() }
    }
}

impl<T> Drop for List<T> {
    fn drop(&mut self) {
        let mut head = self.head.take();
        while let Some(node) = head {
            if let Ok(mut node) = Rc::try_unwrap(node) {
                head = node.next.take();
            } else {
                break;
            }
        }
    }
}

pub struct Iter<'a, T> {
    next: Option<&'a Node<T>>,
}

impl<'a, T> Iterator for Iter<'a, T> {
    type Item = &'a T;

    fn next(&mut self) -> Option<Self::Item> {
        self.next.map(|node| {
            self.next = node.next.as_deref();
            &node.elem
        })
    }
}

#[cfg(test)]
mod test {
    use super::List;

    #[test]
    fn basics() {
        let list = List::new();
        assert_eq!(list.head(), None);

        let list = list.prepend(1).prepend(2).prepend(3);
        assert_eq!(list.head(), Some(&3));

        let list = list.tail();
        assert_eq!(list.head(), Some(&2));

        let list = list.tail();
        assert_eq!(list.head(), Some(&1));

        let list = list.tail();
        assert_eq!(list.head(), None);

        // Убеждаемся, что tail работает с пустым списком
        let list = list.tail();
        assert_eq!(list.head(), None);
    }

    #[test]
    fn iter() {
        let list = List::new().prepend(1).prepend(2).prepend(3);

        let mut iter = list.iter();
        assert_eq!(iter.next(), Some(&3));
        assert_eq!(iter.next(), Some(&2));
        assert_eq!(iter.next(), Some(&1));
    }
}
}

Плохой, но безопасный двусвязный дек

Теперь, когда мы познакомились с Rc и внутренней изменчивостью, возникает интересный вопрос... а нельзя ли изменять объекты с помощью Rc? И, если это так, может быть двусвязный список можно сделать совершенно безопасным!

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

Создадим новый файл fourth.rs:

// и lib.rs

pub mod first;
pub mod second;
pub mod third;
pub mod fourth;

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

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

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

Ключевой элемент нашей архитектуры — это тип RefCell. А сердцем RefCell являются пара методов:

fn borrow(&self) -> Ref<'_, T>;
fn borrow_mut(&self) -> RefMut<'_, T>;

Правила для borrow и borrow_mut точно такие же, как и для & и &mut: вы можете вызвать borrow столько раз, сколько захотите, но borrow_mut требует эксклюзивности.

RefCell проверяет возможность заимствования не статически (во время компиляции), а динамически (во время выполнения). Если вы нарушили правила, RefCell вызовет panic! и завершит программу. Почему методы возвращают объекты Ref и RefMut? Они, по сути, ведут себя как Rc, но только по отношению к заимствованию. Они также держат объект RefCell заимствованным, пока не выйдут из области видимости. Позже мы вернёмся к этому моменту.

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

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

Так что, кажется, нам нужно что-то такое:

use std::rc::Rc;
use std::cell::RefCell;

pub struct List<T> {
    head: Link<T>,
    tail: Link<T>,
}

type Link<T> = Option<Rc<RefCell<Node<T>>>>;

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

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

warning: field is never used: `tail`
 --> src/fourth.rs:6:5
  |
6 |     tail: Link<T>,
  |     ^^^^^^^^^^^^^

warning: field is never used: `elem`
  --> src/fourth.rs:12:5
   |
12 |     elem: T,
   |     ^^^^^^^

warning: field is never used: `next`
  --> src/fourth.rs:13:5
   |
13 |     next: Link<T>,
   |     ^^^^^^^^^^^^^

warning: field is never used: `prev`
  --> src/fourth.rs:14:5
   |
14 |     prev: Link<T>,
   |     ^^^^^^^^^^^^^

Ого, код собрался! Много предупреждений о неиспользуемых переменных, но код собрался! Давайте попробуем его использовать.

Сборка

Как водится, начнём со сборки списка. С нашей новой системой это довольно просто. Метод new всё ещё тривиален — он просто заполняет все поля значением None. Кроме того, поскольку создание узла стало достаточно громоздким, вынесем его в отдельный метод.

impl<T> Node<T> {
    fn new(elem: T) -> Rc<RefCell<Self>> {
        Rc::new(RefCell::new(Node {
            elem: elem,
            prev: None,
            next: None,
        }))
    }
}

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

**МНОГО ПРЕДУПРЕЖДЕНИЙ О НЕИСПОЛЬЗУЕМЫХ ПЕРЕМЕННЫХ, НО КОД СОБИРАЕТСЯ**

Ура!

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

В частности, теперь мы должны обрабатывать новые граничные условия, связанные с пустыми списками. Большинство операций затрагивают либо указатель head, либо указатель tail. Однако при переходе к пустому списку и обратно, нам придётся редактировать оба указателя.

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

Хорошо, приступим:

pub fn push_front(&mut self, elem: T) {
    // новому узлы нужны +2 ссылки, любому другому +0
    let new_head = Node::new(elem);
    match self.head.take() {
        Some(old_head) => {
            // не-пустой список, надо связать со старой головой
            old_head.prev = Some(new_head.clone()); // +1 new_head
            new_head.next = Some(old_head);         // +1 old_head
            self.head = Some(new_head);             // +1 new_head, -1 old_head
            // всего: +2 new_head, +0 old_head -- правильно!
        }
        None => {
            // пустой список, надо установить значение tail
            self.tail = Some(new_head.clone());     // +1 new_head
            self.head = Some(new_head);             // +1 new_head
            // всего: +2 new_head -- правильно!
        }
    }
}
cargo build

error[E0609]: no field `prev` on type `std::rc::Rc<std::cell::RefCell<fourth::Node<T>>>`
  --> src/fourth.rs:39:26
   |
39 |                 old_head.prev = Some(new_head.clone()); // +1 new_head
   |                          ^^^^ unknown field

error[E0609]: no field `next` on type `std::rc::Rc<std::cell::RefCell<fourth::Node<T>>>`
  --> src/fourth.rs:40:26
   |
40 |                 new_head.next = Some(old_head);         // +1 old_head
   |                          ^^^^ unknown field

Что ж. Ошибка компилятора. Хорошее начало. Хорошее начало.

Почему нам недоступны поля prev и next в наших узлах? Раньше это работало, потому что у нас был Rc<Node>. Похоже, проблемы возникают из-за RefCell.

Кажется, пора прочитать документацию.

Гуглим "rust refcell"

щёлкает по первой ссылке

Работа с изменяемой памятью с динамической проверкой правил заимствования

См. документацию на модуль for more.

щёлкает по ссылке

Разделяемые изменяемые контейнеры

Значения типов Cell<T> и RefCell<T> можно изменять через разделяемые ссылки (т. е., в целом, через тип &T), в то время как большинство типов Rust можно изменять только через уникальные ссылки (&mut T). Говорят, что Cell<T> и RefCell<T> обеспечивают «внутреннюю изменяемость» (interior mutability), в то время как обычные типы Rust демонстрируют «унаследованную изменяемость» (inherited mutability).

Типы-ячейки бывают двух видов: Cell<T> и RefCell<T>. Cell<T> предоставляет методы get и set, которые изменяют внутреннее значение за один вызов метода. Однако, Cell<T> совместим только с типами, реализующими Copy. Для других типов необходимо использовать тип RefCell<T>, получая блокировку на запись перед изменением.

RefCell<T> использует время жизни Rust, чтобы реализовать «динамическое заимствование» — процесс, посредством которого можно претендовать на временный, эксклюзивный изменяемый доступ к внутреннему значению. Заимствование для RefCell<T> отслеживается «во время выполнения», в отличие от обычных ссылочных типов Rust, которые отслеживаются полностью статически, во время компиляции. Поскольку заимствования RefCell<T> динамичны, возможна попытка заимствовать значение, которое уже заимствовано на изменение; когда это происходит, в потоке возникает паника.

Когда следует выбирать внутреннюю изменчивость

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

  • Внедрение корней унаследованной изменчивости в разделяемые типы.
  • Детали реализации логически чистых методов.
  • Изменяющие реализации Clone.

Внедрение корней унаследованной изменчивости в разделяемые типы

Разделяемые умные типы-указатели, включая Rc<T> и Arc<T> предоставляют контейнеры, которые могут быть клонированы и разделены между несколькими частями приложения. Поскольку может существовать несколько ссылок на хранящиеся в них значения, их можно заимствовать только посредством разделяемых, а не изменяемых ссылок. Без ячеек было бы вообще невозможно менять данные внутри разделяемых боксов!

Общей практикой является помещение RefCell<T> в разделяемый тип-указатель, чтобы обеспечить возможность изменения:

use std::collections::HashMap;
use std::cell::RefCell;
use std::rc::Rc;

fn main() {
    let shared_map: Rc<RefCell<_>> = Rc::new(RefCell::new(HashMap::new()));
    shared_map.borrow_mut().insert("africa", 92388);
    shared_map.borrow_mut().insert("kyoto", 11837);
    shared_map.borrow_mut().insert("piccadilly", 11826);
    shared_map.borrow_mut().insert("marbles", 38);
}

Обратите внимание, что в этом примере используется Rc<T>, а не Arc<T>. RefCell<T> можно использовать только в однопоточных сценариях. Используйте Mutex<T>, если вам нужна разделяемая изменчивость в многопоточном окружении.

Да, дока по Rust всё ещё невероятно крута.

Больше всего в приведённом коде нас интересует вот эта строка:

shared_map.borrow_mut().insert("africa", 92388);

А конкретно — вызов borrow_mut. Похоже, мы должны явно заимствовать RefCell. Оператор . за нас этого не делает. Странно. Попробуем:

pub fn push_front(&mut self, elem: T) {
    let new_head = Node::new(elem);
    match self.head.take() {
        Some(old_head) => {
            old_head.borrow_mut().prev = Some(new_head.clone());
            new_head.borrow_mut().next = Some(old_head);
            self.head = Some(new_head);
        }
        None => {
            self.tail = Some(new_head.clone());
            self.head = Some(new_head);
        }
    }
}
> cargo build

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

Ага, получилось! Снова победили доки.

Разборка

У pop_front должна быть та же самая базовая логика, что и у push_front, но обращённая. Попробуем:

pub fn pop_front(&mut self) -> Option<T> {
    // надо получить старую голову, убедимся, что количество ссылок -2
    self.head.take().map(|old_head| {                         // -1 old_head
        match old_head.borrow_mut().next.take() {
            Some(new_head) => {                               // -1 new_head
                // не пустой список
                new_head.borrow_mut().prev.take();            // -1 old_head
                self.head = Some(new_head);                   // +1 new_head
                // всего: -2 старых, +0 новых
            }
            None => {
                // пустой список
                self.tail.take();                             // -1 old_head
                // всего: -2 старых, (новых нет)
            }
        }
        old_head.elem
    })
}
> cargo build

error[E0609]: no field `elem` on type `std::rc::Rc<std::cell::RefCell<fourth::Node<T>>>`
  --> src/fourth.rs:64:22
   |
64 |             old_head.elem
   |                      ^^^^ unknown field

ТАК. RefCells. Чувствую, нам снова нужен borrow_mut...

pub fn pop_front(&mut self) -> Option<T> {
    self.head.take().map(|old_head| {
        match old_head.borrow_mut().next.take() {
            Some(new_head) => {
                new_head.borrow_mut().prev.take();
                self.head = Some(new_head);
            }
            None => {
                self.tail.take();
            }
        }
        old_head.borrow_mut().elem
    })
}
cargo build

error[E0507]: cannot move out of borrowed content
  --> src/fourth.rs:64:13
   |
64 |             old_head.borrow_mut().elem
   |             ^^^^^^^^^^^^^^^^^^^^^^^^^^ cannot move out of borrowed content

вздох

cannot move out of borrowed content

Не могу извлечь заимствованное содержимое.

Грм... Похоже, на самом деле Box нас баловал. borrow_num возвращает нам &mut Nude<T>, но мы не можем извлечь оттуда ничего!

Нам нужно что-то, что возьмёт у нас RefCell<T> и вернёт нам T. Давайте проверим доки на предмет наличия чего-то похожего:

fn into_inner(self) -> T

Получает RefCell, возвращает завёрнутое значение.

Выглядит многообещающе!

old_head.into_inner().elem
> cargo build

error[E0507]: cannot move out of an `Rc`
  --> src/fourth.rs:64:13
   |
64 |             old_head.into_inner().elem
   |             ^^^^^^^^ cannot move out of an `Rc`

Вот блин. into_inner хочет извлечь RefCell, но мы не можем её предоставить, потому что она находится в Rc. В предыдущей главе мы видели, что Rc<T> разрешает доступ к своему значению только по разделяемой ссылке. И это правильно, потому что главный смысл указателей с подсчётом ссылок в том, что они разделяемые!

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

Rc::try_unwrap(old_head).unwrap().into_inner().elem

Rc::try_unwrap возвращает Result<T, Rc<T>>. Значения типа Result — расширенный тип Option в котором у варианта None появляются связанные с ним данные. В нашем случае — Rc, из которого мы пытаемся достать значение. Поскольку нас не беспокоят возможные ошибки (если мы правильно написали программу, она обязана завершиться успешно), мы просто вызываем unwrap.

В любом случае, давайте посмотрим, когда ошибка компиляции возникнет на этот раз (будем честными — она обязательно возникнет).

> cargo build

error[E0599]: no method named `unwrap` found for type `std::result::Result<std::cell::RefCell<fourth::Node<T>>, std::rc::Rc<std::cell::RefCell<fourth::Node<T>>>>` in the current scope
  --> src/fourth.rs:64:38
   |
64 |             Rc::try_unwrap(old_head).unwrap().into_inner().elem
   |                                      ^^^^^^
   |
   = note: the method `unwrap` exists but the following trait bounds were not satisfied:
           `std::rc::Rc<std::cell::RefCell<fourth::Node<T>>> : std::fmt::Debug`

Да что ты будешь делать? unwrap у Result требует, чтобы тип ошибки мог печатать себя при отладке. А RefCell<T> реализует Debug только тогда, когда его реализует T. А Node не реализует Debug.

Вместо того, чтобы добавлять реализацию, выполним обходной маневр, преобразовав Result в Options с помощью вызова ok:

Rc::try_unwrap(old_head).ok().unwrap().into_inner().elem

НУ, ПОЖАЛУЙСТА.

cargo build

ДА.

уф

Получилось.

Мы реализовали push и pop.

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

#[cfg(test)]
mod test {
    use super::List;

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

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

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

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

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

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

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

     Running target/debug/lists-5c71138492ad4b4a

running 9 tests
test first::test::basics ... ok
test fourth::test::basics ... ok
test second::test::iter_mut ... ok
test second::test::basics ... ok
test fifth::test::iter_mut ... ok
test third::test::basics ... ok
test second::test::iter ... ok
test third::test::iter ... ok
test second::test::into_iter ... ok

test result: ok. 9 passed; 0 failed; 0 ignored; 0 measured

В яблочко.

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

Rc не умеет работать с циклическими ссылками. Если есть цикл, объекты поддерживают существование друг друга. А двусвязный список, как выяснилось — одна большая цепочка из крошечных циклов! Так что когда мы уничтожаем наш список, два конечных узла уменьшают свои счётчики до 1... а затем ничего не происходит. Ладно, когда наш список содержит ровно один узел, счётчик ссылок сбрасывается но нуля. Но в идеале список должен корректно работать даже когда нём больше одного элемента. Возможно, это только моё мнение.

Как мы видели, удаление элементов оказалось довольно болезненным. Поэтому простейший способ для нас — это просто вызывать pop, пока мы не получим None:

impl<T> Drop for List<T> {
    fn drop(&mut self) {
        while self.pop_front().is_some() {}
    }
}
cargo build

(На самом деле мы могли бы сделать это с помощью наших изменяемых стеков, но короткий путь только для тех, кто знает, куда идти!)

Можно было заняться реализацией версий _back для методов push и pop, но это всего лишь копи-паста. К ней мы вернёмся позже в этой главе. А сейчас давайте посмотрим на более интересные вещи!

Метод peek

Хорошо, мы справились с методами push и pop. Не буду врать, я немного волнуюсь. Проверка правильности на этапе компиляции — охрененное чувство.

Надо немного успокоиться и сделать что-нибудь простое: скажем, реализовать peek_front. Этот метод всегда был простым. Должен и остаться простым, правда?

Правда?

Фактически, я уверена, что могу просто скопировать старое решение!

pub fn peek_front(&self) -> Option<&T> {
    self.head.as_ref().map(|node| {
        &node.elem
    })
}

Но, погодите. Не в этот раз.

pub fn peek_front(&self) -> Option<&T> {
    self.head.as_ref().map(|node| {
        // ЗАИМСТВОВАНИЕ!!!!
        &node.borrow().elem
    })
}

ХА.

cargo build

error[E0515]: cannot return value referencing temporary value
  --> src/fourth.rs:66:13
   |
66 |             &node.borrow().elem
   |             ^   ----------^^^^^
   |             |   |
   |             |   temporary value created here
   |             |
   |             returns a value referencing data owned by the current function

Ладно, просто сожгу свой компьютер.

Здесь та же самая логика, что и в односвязном стеке. Почему всё по другому? ПОЧЕМУ?

Ответ на самом деле отражает мораль всей этой главы: RefCell всё усложняет. До текущего момента RefCell просто доставлял неудобства. Теперь он превратился в ночной кошмар.

Так что же случилось? Чтобы в этом разобраться, надо вернуться к определению borrow:

fn borrow<'a>(&'a self) -> Ref<'a, T>;
fn borrow_mut<'a>(&'a self) -> RefMut<'a, T>;

В разделе о представлении я писала:

RefCell проверяет возможность заимствования не статически (во время компиляции), а > динамически (во время выполнения). Если вы нарушили правила, RefCell вызовет panic! и завершит программу. Почему методы возвращают объекты Ref и RefMut? Они, по сути, ведут себя как Rc, но только по отношению к заимствованию. Они также держат объект RefCell заимствованным, пока не выйдут из области видимости. Позже мы вернёмся к этому моменту.

Это «позже» наступило.

Ref и RefMut реализуют Deref и DerefMut соответственно. В большинстве случаев они ведут себя точно так же как &T и &mut T. Однако из-за особенностей работы этих типажей, возвращаемая ссылка связана с временем жизни Ref, а не с самой ячейкой RefCell. Это значит, что Ref должна оставаться доступна до тех пор, пока мы пользуемся полученной ссылкой.

Это нужно, чтобы сохранить корректность. Когда Ref уничтожается, он сообщает RefCell, что больше не заимствуется. Таким образом, если бы нам удалось сохранить нашу ссылку дольше, чем существует Ref, мы бы могли создать RefMut на ту же самую область памяти. Тогда бы у нас одновременны были изменяемая и разделяемая ссылки на одно и то же значение, что полностью ломает систему типов Rust.

Итак, что нам остаётся? Мы хотим всего лишь вернуть ссылку, но для этого нам надо сохранить Ref. Но как только мы возвращаем ссылку из peek, функция завершается и Ref выходит из области видимости.

😖

Насколько я понимаю, сейчас мы оказались в абсолютном тупике. Нельзя полностью инкапсулировать использование RefCell тем способом, который мы придумали.

Но... что если мы откажемся от идеи полностью скрывать детали реализации? Что если мы будем возвращать Ref?

pub fn peek_front(&self) -> Option<Ref<T>> {
    self.head.as_ref().map(|node| {
        node.borrow()
    })
}
> cargo build

error[E0412]: cannot find type `Ref` in this scope
  --> src/fourth.rs:63:40
   |
63 |     pub fn peek_front(&self) -> Option<Ref<T>> {
   |                                        ^^^ not found in this scope
help: possible candidates are found in other modules, you can import them into scope
   |
1  | use core::cell::Ref;
   |
1  | use std::cell::Ref;
   |

Бла, бла, бла. Надо кое-что импортировать.

use std::cell::{Ref, RefCell};
> cargo build

error[E0308]: mismatched types
  --> src/fourth.rs:64:9
   |
64 | /         self.head.as_ref().map(|node| {
65 | |             node.borrow()
66 | |         })
   | |__________^ expected type parameter, found struct `fourth::Node`
   |
   = note: expected type `std::option::Option<std::cell::Ref<'_, T>>`
              found type `std::option::Option<std::cell::Ref<'_, fourth::Node<T>>>`

Хмм... всё правильно. У нас есть Ref<Node<T>>, но нам нужен Ref<T>. Мы могли бы отказаться от всякой надежды на инкапсуляцию и просто возвращать то, что есть. Мы могли бы ещё больше усложнить код и завернуть Ref<Node<T>> в новый тип, чтобы предоставлять доступ только к &T.

Оба варианта так себе.

Вместо этого, углубимся в тему. Давайте немного повеселимся. Источник нашего веселья — вот это чудовище:

fn map<U, F>(orig: Ref<'b, T>, f: F) -> Ref<'b, U>
    where F: FnOnce(&T) -> &U,
          U: ?Sized

Создаёт новую Ref для компонента заимствованных данных.

Да: точно также, как вы можете вызывать map для Option, вы можете вызывать map для Ref.

Я уверена, что кто-то где-то сейчас возбудился из-за монад или чего-то подобного, но мне они совершенно не интересны. К тому же я не уверена, что речь идёт о настоящей монаде, поскольку нет варианта, похожего на None. Впрочем, я отвлеклась.

Мне нравится, что я нашла крутое решение. Люблю такие штуки.

pub fn peek_front(&self) -> Option<Ref<T>> {
    self.head.as_ref().map(|node| {
        Ref::map(node.borrow(), |node| &node.elem)
    })
}
> cargo build

Да-да-да-да.

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

#[test]
fn peek() {
    let mut list = List::new();
    assert!(list.peek_front().is_none());
    list.push_front(1); list.push_front(2); list.push_front(3);

    assert_eq!(&*list.peek_front().unwrap(), &3);
}
> cargo test

     Running target/debug/lists-5c71138492ad4b4a

running 10 tests
test first::test::basics ... ok
test fourth::test::basics ... ok
test second::test::basics ... ok
test fourth::test::peek ... ok
test second::test::iter_mut ... ok
test second::test::into_iter ... ok
test third::test::basics ... ok
test second::test::peek ... ok
test second::test::iter ... ok
test third::test::iter ... ok

test result: ok. 10 passed; 0 failed; 0 ignored; 0 measured

Великолепно!

Симметричный мусор

Что ж, давайте разберёмся со всей этой комбинаторной симметрией.

Всё, что нам нужно, это выполнить простую замену текста:

tail <-> head
next <-> prev
front -> back

Да, нам также надо написать варианты _mut для методов peek.

use std::cell::{Ref, RefCell, RefMut};

//..

pub fn push_back(&mut self, elem: T) {
    let new_tail = Node::new(elem);
    match self.tail.take() {
        Some(old_tail) => {
            old_tail.borrow_mut().next = Some(new_tail.clone());
            new_tail.borrow_mut().prev = Some(old_tail);
            self.tail = Some(new_tail);
        }
        None => {
            self.head = Some(new_tail.clone());
            self.tail = Some(new_tail);
        }
    }
}

pub fn pop_back(&mut self) -> Option<T> {
    self.tail.take().map(|old_tail| {
        match old_tail.borrow_mut().prev.take() {
            Some(new_tail) => {
                new_tail.borrow_mut().next.take();
                self.tail = Some(new_tail);
            }
            None => {
                self.head.take();
            }
        }
        Rc::try_unwrap(old_tail).ok().unwrap().into_inner().elem
    })
}

pub fn peek_back(&self) -> Option<Ref<T>> {
    self.tail.as_ref().map(|node| {
        Ref::map(node.borrow(), |node| &node.elem)
    })
}

pub fn peek_back_mut(&mut self) -> Option<RefMut<T>> {
    self.tail.as_ref().map(|node| {
        RefMut::map(node.borrow_mut(), |node| &mut node.elem)
    })
}

pub fn peek_front_mut(&mut self) -> Option<RefMut<T>> {
    self.head.as_ref().map(|node| {
        RefMut::map(node.borrow_mut(), |node| &mut node.elem)
    })
}

И значительно расширить наши тесты:

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

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

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

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

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

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

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

    // ---- back -----

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

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

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

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

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

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

#[test]
fn peek() {
    let mut list = List::new();
    assert!(list.peek_front().is_none());
    assert!(list.peek_back().is_none());
    assert!(list.peek_front_mut().is_none());
    assert!(list.peek_back_mut().is_none());

    list.push_front(1); list.push_front(2); list.push_front(3);

    assert_eq!(&*list.peek_front().unwrap(), &3);
    assert_eq!(&mut *list.peek_front_mut().unwrap(), &mut 3);
    assert_eq!(&*list.peek_back().unwrap(), &1);
    assert_eq!(&mut *list.peek_back_mut().unwrap(), &mut 1);
}

Есть ли сценарии, которые мы не протестировали? Возможно. Количество комбинаций методов действительно стало очень большим. Но, по крайней мере, наш код не явно ошибочный.

> cargo test

     Running target/debug/lists-5c71138492ad4b4a

running 10 tests
test first::test::basics ... ok
test fourth::test::basics ... ok
test second::test::basics ... ok
test fourth::test::peek ... ok
test second::test::iter ... ok
test third::test::iter ... ok
test second::test::into_iter ... ok
test second::test::iter_mut ... ok
test second::test::peek ... ok
test third::test::basics ... ok

test result: ok. 10 passed; 0 failed; 0 ignored; 0 measured

Прекрасно. Копи-паста — лучший способ программирования.

Итерация

Попробуем написать итераторы для нашего неудачливого списка.

IntoIter

IntoInter, как всегда — самый простой. Просто завернём стек в новый тип и вызовем pop:

pub struct IntoIter<T>(List<T>);

impl<T> List<T> {
    pub fn into_iter(self) -> IntoIter<T> {
        IntoIter(self)
    }
}

impl<T> Iterator for IntoIter<T> {
    type Item = T;
    fn next(&mut self) -> Option<Self::Item> {
        self.0.pop_front()
    }
}

Но у нас появилось новое интересное усовершенствование. Раньше у нас был только один «натуральный» порядок перебора элементов, а дек по своей природе двунаправленный. Что особенного в переборе от начала к концу? Что если кому-то понадобится итерация в другом направлении?

На самом деле в Rust есть ответ на этот вопрос: DoubleEndedIterator. DoubleEndedIterator наследует Iterator (это значит, что все объекты, реализующие DoubleEndedIterator реализуют так же и Iterator) и добавляет один новый метод: next_back. У него такая же сигнатура, как и у next, но он перебирает элементы с другого конца. Семантика DoubleEndedIterator очень удобна для нашего случая: итератор становится деком. Мы можем брать элементы как с начала, так и с конца, пока два конца не встретятся и в этой точке итератор станет пустым.

Нам нечасто приходится вызывать next вручную. То же самое и с next_back — этот метод не так важен пользователям DoubleEndedIterator. Лучшая часть интерфейса — это метод rev, который заворачивает итератор, чтобы создать другой итератор, который возвращает элементы в обратном порядке. Семантика здесь довольно проста: вызов next у обращённого итератора — это на самом деле вызов next_back.

В любом случае, поскольку у нас уже есть дек, предоставить новый API довольно просто:

impl<T> DoubleEndedIterator for IntoIter<T> {
    fn next_back(&mut self) -> Option<T> {
        self.0.pop_back()
    }
}

И давайте протестируем:

#[test]
fn into_iter() {
    let mut list = List::new();
    list.push_front(1); list.push_front(2); list.push_front(3);

    let mut iter = list.into_iter();
    assert_eq!(iter.next(), Some(3));
    assert_eq!(iter.next_back(), Some(1));
    assert_eq!(iter.next(), Some(2));
    assert_eq!(iter.next_back(), None);
    assert_eq!(iter.next(), None);
}
cargo test

     Running target/debug/lists-5c71138492ad4b4a

running 11 tests
test fourth::test::basics ... ok
test fourth::test::peek ... ok
test fourth::test::into_iter ... ok
test first::test::basics ... ok
test second::test::basics ... ok
test second::test::iter ... ok
test second::test::iter_mut ... ok
test third::test::iter ... ok
test third::test::basics ... ok
test second::test::into_iter ... ok
test second::test::peek ... ok

test result: ok. 11 passed; 0 failed; 0 ignored; 0 measured

Прекрасно.

Iter

Iter не будет таким снисходительным. Нам снова придётся иметь дело с этими ужасными Ref! Из-за Ref мы не сможем хранить &Node, как мы делали раньше. Вместо этого попробуем хранить Ref<Node>.

pub struct Iter<'a, T>(Option<Ref<'a, Node<T>>>);

impl<T> List<T> {
    pub fn iter(&self) -> Iter<T> {
        Iter(self.head.as_ref().map(|head| head.borrow()))
    }
}
> cargo build

Пока всё хорошо. Реализация next окажется чуть сложнее, но я думаю, это будет та же самая базовая логика, как и в старом IterMut для стека, с некоторыми дополнительными приседаниями из-за RefCell:

impl<'a, T> Iterator for Iter<'a, T> {
    type Item = Ref<'a, T>;
    fn next(&mut self) -> Option<Self::Item> {
        self.0.take().map(|node_ref| {
            self.0 = node_ref.next.as_ref().map(|head| head.borrow());
            Ref::map(node_ref, |node| &node.elem)
        })
    }
}
cargo build

error[E0521]: borrowed data escapes outside of closure
   --> src/fourth.rs:155:13
    |
153 |     fn next(&mut self) -> Option<Self::Item> {
    |             --------- `self` is declared here, outside of the closure body
154 |         self.0.take().map(|node_ref| {
155 |             self.0 = node_ref.next.as_ref().map(|head| head.borrow());
    |             ^^^^^^   -------- borrow is only valid in the closure body
    |             |
    |             reference to `node_ref` escapes the closure body here

error[E0505]: cannot move out of `node_ref` because it is borrowed
   --> src/fourth.rs:156:22
    |
153 |     fn next(&mut self) -> Option<Self::Item> {
    |             --------- lifetime `'1` appears in the type of `self`
154 |         self.0.take().map(|node_ref| {
155 |             self.0 = node_ref.next.as_ref().map(|head| head.borrow());
    |             ------   -------- borrow of `node_ref` occurs here
    |             |
    |             assignment requires that `node_ref` is borrowed for `'1`
156 |             Ref::map(node_ref, |node| &node.elem)
    |                      ^^^^^^^^ move out of `node_ref` occurs here

Ну, понеслась!

node_ref живёт недостаточно долго. В отличие от обычных ссылок, Rust не позволяет нам повторно использовать Ref подобным образом. Ref, который мы получили из head.borrow(), существует до тех пор, пока существует node_ref, который уничтожается при вызове Ref::map.

Функция, которая нам нужна, существует, и она называется map_split:

pub fn map_split<U, V, F>(orig: Ref<'b, T>, f: F) -> (Ref<'b, U>, Ref<'b, V>) where
    F: FnOnce(&T) -> (&U, &V),
    U: ?Sized,
    V: ?Sized,

Уф. Давайте попробуем.

fn next(&mut self) -> Option<Self::Item> {
    self.0.take().map(|node_ref| {
        let (next, elem) = Ref::map_split(node_ref, |node| {
            (&node.next, &node.elem)
        });

        self.0 = next.as_ref().map(|head| head.borrow());

        elem
    })
}
cargo build
   Compiling lists v0.1.0 (/Users/ADesires/dev/temp/lists)
error[E0521]: borrowed data escapes outside of closure
   --> src/fourth.rs:159:13
    |
153 |     fn next(&mut self) -> Option<Self::Item> {
    |             --------- `self` is declared here, outside of the closure body
...
159 |             self.0 = next.as_ref().map(|head| head.borrow());
    |             ^^^^^^   ---- borrow is only valid in the closure body
    |             |
    |             reference to `next` escapes the closure body here

Рррр. Нам снова нужен Ref::map чтобы у нас было правильное время жизни. Но Ref::map возвращает Ref, а нам нужен Option<Ref>, но для этого нам надо пройти сквозь Ref, чтобы вызвать map для нашего Option...

долго смотрит вдаль

??????

fn next(&mut self) -> Option<Self::Item> {
    self.0.take().map(|node_ref| {
        let (next, elem) = Ref::map_split(node_ref, |node| {
            (&node.next, &node.elem)
        });

        self.0 = if next.is_some() {
            Some(Ref::map(next, |next| &**next.as_ref().unwrap()))
        } else {
            None
        };

        elem
    })
}
error[E0308]: mismatched types
   --> src/fourth.rs:162:22
    |
162 |                 Some(Ref::map(next, |next| &**next.as_ref().unwrap()))
    |                      ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ expected struct `fourth::Node`, found struct `std::cell::RefCell`
    |
    = note: expected type `std::cell::Ref<'_, fourth::Node<_>>`
               found type `std::cell::Ref<'_, std::cell::RefCell<fourth::Node<_>>>`

Ну да. Правильно. У нас несколько RefCell. Чем дальше мы углубляемся в список, тем более вложенными становятся RefCell. Нам потребуется поддерживать что-то вроде стека Ref для представления всех объектов, на которые мы ссылаемся, потому что если мы перестаём наблюдать за элементом, нам надо снизить на единицу счётчик заимствований у каждого RefCell, который находится перед ним...

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

Что насчёт наших Rc? Кто вообще сказал, что нам нужно хранить ссылки? Почему мы не можем просто вызвать Clone у Rc, чтобы получить удобный указатель в середине списка?

pub struct Iter<T>(Option<Rc<Node<T>>>);

impl<T> List<T> {
    pub fn iter(&self) -> Iter<T> {
        Iter(self.head.as_ref().map(|head| head.clone()))
    }
}

impl<T> Iterator for Iter<T> {
    type Item =

Э-э-э... Подождите, а что мы теперь возвращаем? &T? Ref<T>?

Ни один из них не работает... у нашего Iter в любом случае нет времени жизни! И &T, и Ref<T> требуют объявления какого-то времени жизни перед тем, как мы сможем вызвать next. Но всё, что мы сможем получить из нашего Rc, будет заимствованием Iterator... мозг... кипит... аааааааааааа

Может быть, мы можем... вызвать map... для Rc... чтобы получить Rc<T>? Так вообще можно? В доках Rc, похоже, ничего подобного нет. На самом деле, кто-то сделал крейт, который это умеет.

Но, подождите, даже если у нас это получится, мы получим ещё большую проблему: ужасную угрозу инвалидации итератора. Раньше мы не сталкивались с инвалидацией итератора, потому что Iter заимствовал список, оставляя его, в целом, неизменяемым. Однако, если бы наш итератор возвращал Rc, ему вообще бы не пришлось заимствовать список! Это значит, что люди могут начать вызывать push и pop, пока у них есть указатель на список!

О, боже, во что всё это выльется?

Ну, в принципе, с push всё в порядке. Мы видим какую-то часть списка и список будет расти за пределами поля нашего зрения. Ничего страшного.

Однако, с pop ситуация другая. Если элементы извлекаются за пределами поля нашего зрения, это всё ещё должно быть нормально. Мы не можем видеть эти узлы, так что ничего случиться не может. В то же время если попытаться извлечь узел, на который мы указываем... всё взлетит на воздух! В частности, вызов unwrap у результата, полученного с помощью try_unwrap, на самом деле приводит к панике и аварийному завершению программы.

А это ведь здорово. Мы можем добавлять в список тонны указателей с внутренним владением и одновременно менять их и это будет просто работать, если не удалять узла, на который указывает итератор.

И даже тогда у нас не возникнет висячих указателей или чего-то похожего: программа детерминировано запаникует!

Но необходимость иметь дело с инвалидацией итераторов в дополнение к вызову map у Rc кажется просто... плохой. Rc<RefCell> окончательно нас подвёл. Интересно, мы столкнулись с инверсией случая с устойчивым стеком. В то время как устойчивый стек не мог справиться с владением, но мог днями напролёт предоставлять ссылки, наш список не испытывает проблем с владением, не не может справиться со ссылками.

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

Блин, да мы могли бы сделать несколько конкурентных IterMuts, которые проверяли бы наличие изменяемого доступа к одному и тому же элементу прямо во время работы программы!

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

В любом случае, я отказываюсь от реализации Iter и IterMut. Можно было бы, но... получается некрасиво.

Финальный код

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

Но его можно реализовать.

Кстати, я думаю, что у нас слишком много «ненужных» проверок между Rc и RefCell. В взяла слово ненужных в кавычки, потому что на самом деле они необходимы для гарантий реальной безопасности. У нас есть несколько мест, где они нужны. У двусвязных списков действительно запутанное взаимодействие псевдонимов и владения!

Тем не менее, вот то, что нам удалось сделать. Особенно если нас не страшит раскрытие дателей реализации.

Далее мы попробуем другой подход: вернём себе контроль, сделав реализацию небезопасной.

#![allow(unused)]
fn main() {
use std::rc::Rc;
use std::cell::{Ref, RefMut, RefCell};

pub struct List<T> {
    head: Link<T>,
    tail: Link<T>,
}

type Link<T> = Option<Rc<RefCell<Node<T>>>>;

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


impl<T> Node<T> {
    fn new(elem: T) -> Rc<RefCell<Self>> {
        Rc::new(RefCell::new(Node {
            elem: elem,
            prev: None,
            next: None,
        }))
    }
}

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

    pub fn push_front(&mut self, elem: T) {
        let new_head = Node::new(elem);
        match self.head.take() {
            Some(old_head) => {
                old_head.borrow_mut().prev = Some(new_head.clone());
                new_head.borrow_mut().next = Some(old_head);
                self.head = Some(new_head);
            }
            None => {
                self.tail = Some(new_head.clone());
                self.head = Some(new_head);
            }
        }
    }

    pub fn push_back(&mut self, elem: T) {
        let new_tail = Node::new(elem);
        match self.tail.take() {
            Some(old_tail) => {
                old_tail.borrow_mut().next = Some(new_tail.clone());
                new_tail.borrow_mut().prev = Some(old_tail);
                self.tail = Some(new_tail);
            }
            None => {
                self.head = Some(new_tail.clone());
                self.tail = Some(new_tail);
            }
        }
    }

    pub fn pop_back(&mut self) -> Option<T> {
        self.tail.take().map(|old_tail| {
            match old_tail.borrow_mut().prev.take() {
                Some(new_tail) => {
                    new_tail.borrow_mut().next.take();
                    self.tail = Some(new_tail);
                }
                None => {
                    self.head.take();
                }
            }
            Rc::try_unwrap(old_tail).ok().unwrap().into_inner().elem
        })
    }

    pub fn pop_front(&mut self) -> Option<T> {
        self.head.take().map(|old_head| {
            match old_head.borrow_mut().next.take() {
                Some(new_head) => {
                    new_head.borrow_mut().prev.take();
                    self.head = Some(new_head);
                }
                None => {
                    self.tail.take();
                }
            }
            Rc::try_unwrap(old_head).ok().unwrap().into_inner().elem
        })
    }

    pub fn peek_front(&self) -> Option<Ref<T>> {
        self.head.as_ref().map(|node| {
            Ref::map(node.borrow(), |node| &node.elem)
        })
    }

    pub fn peek_back(&self) -> Option<Ref<T>> {
        self.tail.as_ref().map(|node| {
            Ref::map(node.borrow(), |node| &node.elem)
        })
    }

    pub fn peek_back_mut(&mut self) -> Option<RefMut<T>> {
        self.tail.as_ref().map(|node| {
            RefMut::map(node.borrow_mut(), |node| &mut node.elem)
        })
    }

    pub fn peek_front_mut(&mut self) -> Option<RefMut<T>> {
        self.head.as_ref().map(|node| {
            RefMut::map(node.borrow_mut(), |node| &mut node.elem)
        })
    }

    pub fn into_iter(self) -> IntoIter<T> {
        IntoIter(self)
    }
}

impl<T> Drop for List<T> {
    fn drop(&mut self) {
        while self.pop_front().is_some() {}
    }
}

pub struct IntoIter<T>(List<T>);

impl<T> Iterator for IntoIter<T> {
    type Item = T;

    fn next(&mut self) -> Option<T> {
        self.0.pop_front()
    }
}

impl<T> DoubleEndedIterator for IntoIter<T> {
    fn next_back(&mut self) -> Option<T> {
        self.0.pop_back()
    }
}

#[cfg(test)]
mod test {
    use super::List;

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

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

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

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

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

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

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

        // ---- back -----

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

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

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

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

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

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

    #[test]
    fn peek() {
        let mut list = List::new();
        assert!(list.peek_front().is_none());
        assert!(list.peek_back().is_none());
        assert!(list.peek_front_mut().is_none());
        assert!(list.peek_back_mut().is_none());

        list.push_front(1); list.push_front(2); list.push_front(3);

        assert_eq!(&*list.peek_front().unwrap(), &3);
        assert_eq!(&mut *list.peek_front_mut().unwrap(), &mut 3);
        assert_eq!(&*list.peek_back().unwrap(), &1);
        assert_eq!(&mut *list.peek_back_mut().unwrap(), &mut 1);
    }

    #[test]
    fn into_iter() {
        let mut list = List::new();
        list.push_front(1); list.push_front(2); list.push_front(3);

        let mut iter = list.into_iter();
        assert_eq!(iter.next(), Some(3));
        assert_eq!(iter.next_back(), Some(1));
        assert_eq!(iter.next(), Some(2));
        assert_eq!(iter.next_back(), None);
        assert_eq!(iter.next(), None);
    }
}
}

Хорошая небезопасная односвязная очередь

Что же, история с внутренней изменчивостью счётчика ссылок вышла из под контроля. Возможно Rust не рассчитан на структуры подобного вида? И да, и нет. Rc и RefCell могут прекрасно подойти для обработки простых случаев, но в то же время, они могут быть очень неуклюжими. Особенно, если вы пытаетесь скрыть эту неуклюжесть. Должен быть лучший способ!

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

ГОЛОС ЗА КАДРОМ: А я укажу на ошибки.

Мы не собираем делать любые ошибки.

Давайте добавим новый файл с именем fifth.rs:

// в lib.rs

pub mod first;
pub mod second;
pub mod third;
pub mod fourth;
pub mod fifth;

Мы будем писать код, «подглядывая» в second.rs, так как в мире связных списков очереди в каком-то смысле — это расширение стека. Кроме того, мы начнём с чистого листа, поскольку есть несколько фундаментальных вопросов, которые мы хотим обсудить. Они касаются в первую очередь представления.

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

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

список на входе:
[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.

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

Небезопасный Rust

Это серьёзная, большая, сложная и опасная тема. Настолько серьёзная, что я написала про неё отдельную книгу. (Прим. перевод.: речь идёт о широко известной в среде Rust-программистов книге Растономикон).

На самом деле любой язык, из которого можно вызывать функции на другом языке, уже является небезопасным, потому что, например, в C можно делать любые плохие вещи. Java, Python, Ruby, Haskell... все они крайне небезопасны из-за присутствия в них интерфейса внешних функций (Foreign Function Interfaces, FFI).

Rust следует этой истине, разделяя себя на два языка — Безопасный и Небезопасный Rust. Пока что мы работали только с Безопасным. Это на 100% безопасно... пока мы не используем FFI.

Небезопасный Rust — это надмножество над Безопасным Rust. Он такой же, как и Безопасный Rust со всей семантикой и правилами, но вам доступны дополнительные возможности. Они дико небезопасны и могут привести к ужасному Неопределённому Поведению (Undefined Behaviour, UB), которым так славится C.

Ещё раз скажу, что мы имеем дело с действительно огромной темой, в которой много интересных частных случаев. Я на самом деле не хочу иметь в ними дела (ну, ладно, хочу; уже имела; читайте книгу). К счастью, в случае связных списков мы можем игнорировать большинство из них.

ГОЛОС ЗА КАДРОМ: Это была ложь, но в 2015 она казалась правдой.

Главный инструмент Небезопасного Rust, который мы будем использовать — это сырые указатели. Сырые указатели — это, по сути, указатели из C. У них нет собственных правил псевдонимизации (aliasing). У них нет времени жизни. Они могут быть нулевыми. Они могут быть невыровненными. Они могут быть висячими. Они могут указывать на неинициализированную память. Их можно приводить к целым числам и обратно. Их можно приводить к указателям на другой тип. Неизменяемый указатель? Приводим к изменяемому! Мы можем делать практически всё, что угодно, а это значит, что практически всё, что угодно, может пойти не так.

ГОЛОС ЗА КАДРОМ: нет собственных правил псевдонимизации, да? Эх, молодо-зелено.

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

Есть два вида сырых указателей: *const T и *mut T. Предполагается, что они соответствуют const T* и T* из языка C, но нам, по большому счёту, всё равно, что они означают в C. *const T можно разыменовать только в &T, но, как и в случае с изменяемостью переменной, это всего лишь способ защиты от некорректного использования. Его можно обойти, если сначала надо привести *const к *mut. Впрочем, если у вас нет права менять объект, на который ссылается указатель, у вас возникнут проблемы.

В любом случае, мы лучше разберёмся в теме, если напишем немного кода. А пока для нас *mut T будет значить &unchecked mut T!

Основы

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

Самое время вернуться к основам. Как мы конструируем наш список?

Раньше мы просто делали:

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

Но теперь мы не можем использовать Option для tail:

> cargo build

error[E0308]: mismatched types
  --> src/fifth.rs:15:34
   |
15 |         List { head: None, tail: None }
   |                                  ^^^^ expected *-ptr, found 
   |                                       enum `std::option::Option`
   |
   = note: expected type `*mut fifth::Node<T>`
              found type `std::option::Option<_>`

Мы могли бы использовать Option, но, в отличие от Box, *mut может принимать значение null. Это значит, что мы не получаем преимуществ от оптимизации нулевого указателя. Вместо этого мы используем null, чтобы представить вариант None.

Так как же нам получить указатель на null? Есть несколько способов, но я предпочитаю вызывать std::ptr::null_mut(). Если хотите, можете писать 0 as *mut _, но на мой взгляд это выглядит неряшливо.

use std::ptr;

// определения...

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

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

warning: field is never used: `tail`
 --> src/fifth.rs:5:5
  |
5 |     tail: *mut Node<T>,
  |     ^^^^^^^^^^^^^^^^^^

warning: field is never used: `elem`
  --> src/fifth.rs:11:5
   |
11 |     elem: T,
   |     ^^^^^^^

warning: field is never used: `head`
  --> src/fifth.rs:12:5
   |
12 |     head: Link<T>,
   |     ^^^^^^^^^^^^^

Тише, компилятор, скоро мы используем все эти определения.

Ладно, давайте снова напишем push. Теперь, вместо того, чтобы после вставки получить Option<&mut Node<T>>, мы сразу же получим *mut Node<T> из Box. Мы без проблем можем это сделать, потому что содержимое Box имеет стабильный адрес, даже если мы перемещаем Box. Конечно, это не безопасно, потому что если мы удалим Box, у нас останется указатель на освобождённую память.

Как получить сырой указатель из обычного указателя? С помощью приведения типа! К переменной, объявленной как сырой указатель, можно привести обычную ссылку.

let raw_tail: *mut _ = &mut *new_tail;

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

pub fn push(&mut self, elem: T) {
    let mut new_tail = Box::new(Node {
        elem: elem,
        next: None,
    });

    let raw_tail: *mut _ = &mut *new_tail;

    // .is_null проверяет на null, эквивалент проверки на None
    if !self.tail.is_null() {
        // Если старый хвост был, обновляем его, чтобы он указывал на новый
        self.tail.next = Some(new_tail);
    } else {
        // В противном случае, обновляем голову, чтобы указывала на него
        self.head = Some(new_tail);
    }

    self.tail = raw_tail;
}
> cargo build

error[E0609]: no field `next` on type `*mut fifth::Node<T>`
  --> src/fifth.rs:31:23
   |
31 |             self.tail.next = Some(new_tail);
   |             ----------^^^^
   |             |
   |             help: `self.tail` is a raw pointer; 
   |             try dereferencing it: `(*self.tail).next`

Что? У нас есть указатель на Node, почему нам недоступно поле next?

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

*self.tail.next = Some(new_tail);
> cargo build

error[E0609]: no field `next` on type `*mut fifth::Node<T>`
  --> src/fifth.rs:31:23
   |
31 |             *self.tail.next = Some(new_tail);
   |             -----------^^^^
   |             |
   |             help: `self.tail` is a raw pointer; 
   |             try dereferencing it: `(*self.tail).next`

У-у-у-у, приоритет операторов.

(*self.tail).next = Some(new_tail);
> cargo build

error[E0133]: dereference of raw pointer is unsafe and requires 
              unsafe function or block

  --> src/fifth.rs:31:13
   |
31 |             (*self.tail).next = Some(new_tail);
   |             ^^^^^^^^^^^^^^^^^ dereference of raw pointer
   |
   = note: raw pointers may be NULL, dangling or unaligned; 
     they can violate aliasing rules and cause data races: 
     all of these are undefined behavior

ЭТО. НЕ ДОЛЖНО. БЫТЬ. ТАКИМ. СЛОЖНЫМ.

Помните, я говорила, что Небезопасный Rust — это своего рода FFI для Безопасного Rust? Ну вот, компилятор требует, чтобы мы явно указывали, где именно применяем FFI. У нас есть два варианта. Во-первых, мы можем пометить всю нашу функцию ключевым словом unsafe, при этом она становится функцией Небезопасного Rust и может быть вызвана только в небезопасном контексте. Это не очень здорово, потому что мы хотим, чтобы нашим списком можно было пользоваться в безопасном коде. Во-вторых, мы можем написать блок unsafe внутри нашей функции, чтобы обозначить границы FFI. При этом сама функция будет считаться безопасной. Хорошо, давайте сделаем:

pub fn push(&mut self, elem: T) {
    let mut new_tail = Box::new(Node {
        elem: elem,
        next: None,
    });

    let raw_tail: *mut _ = &mut *new_tail;

    if !self.tail.is_null() {
        // Привет, компилятор. Я знаю, что рискую и обещаю
        // быть хорошим программистом, который никогда не ошибается.
        unsafe {
            (*self.tail).next = Some(new_tail);
        }
    } else {
        self.head = Some(new_tail);
    }

    self.tail = raw_tail;
}
> cargo build
warning: field is never used: `elem`
  --> src/fifth.rs:11:5
   |
11 |     elem: T,
   |     ^^^^^^^
   |
   = note: #[warn(dead_code)] on by default

Ага!

Довольно интересно, но это пока единственное место, где нам пришлось написать небезопасный код. Сырые указатели у нас повсюду, почему unsafe потребовался только здесь?

Дело в том, что, когда речь заходит об unsafe, Rust становится настоящим педантом, помешанным на правилах. Мы вполне обоснованно хотим максимизировать множество программ на Безопасном Rust, потому что в отношении них мы можем быть гораздо более уверены. Чтобы этого достигнуть, Rust старается выделять минимальную возможную небезопасную область. Обратите внимание, что в других местах, где мы работали с сырыми указателями, мы либо присваивали им значение, либо просто проверяли их на null.

Если вы никогда не разыменовываете сырой указатель все эти операции совершенно безопасны. Вы просто читаете и записываете целые числа! Неприятности с сырым указателем могут возникнуть только при разыменовании. Так что Rust считает небезопасной только эту операцию, а все остальные — нет.

Супер. Педантично. Но технически корректно.

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

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

Это то, что я называю небезопасным заражением. Как только вы используете unsafe в модуле, весь модуль заражается небезопасностью. Всё должно быть написано корректно, чтобы гарантировать соблюдение всех инвариантов в небезопасном коде.

Проблема заражения решается благодаря приватности. Вне нашего модуля поля нашей структуры абсолютно приватны, поэтому никто не другой не может произвольно менять наше состояние. До тех пор, пока никакая комбинация вызовов API не приводит к негативным последствиям, с точки зрения внешнего наблюдателя весь наш код безопасен! И, по сути, это ничем не отличается от FFI. Никого не волнует, что математическая библиотека Python вызывает код на C, если она предоставляет безопасный интерфейс.

Ладно, теперь давайте напишем pop, который практически дословно повторяет эталонную версию:

pub fn pop(&mut self) -> Option<T> {
    self.head.take().map(|head| {
        let head = *head;
        self.head = head.next;

        if self.head.is_none() {
            self.tail = ptr::null_mut();
        }

        head.elem
    })
}

Мы встретили ещё один сценарий, в котором безопасность зависит от состояния. Если мы не обнулим указатель на хвост в этой функции, у нас не будет никаких проблем. Однако последующие вызовы push начнут писать в висячий указатель!

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

#[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);

        // Check the exhaustion case fixed the pointer right
        list.push(6);
        list.push(7);

        // Check normal removal
        assert_eq!(list.pop(), Some(6));
        assert_eq!(list.pop(), Some(7));
        assert_eq!(list.pop(), None);
    }
}

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

cargo test

running 12 tests
test fifth::test::basics ... ok
test first::test::basics ... ok
test fourth::test::basics ... ok
test fourth::test::peek ... ok
test second::test::basics ... ok
test fourth::test::into_iter ... ok
test second::test::into_iter ... ok
test second::test::iter ... ok
test second::test::iter_mut ... ok
test second::test::peek ... ok
test third::test::basics ... ok
test third::test::iter ... ok

test result: ok. 12 passed; 0 failed; 0 ignored; 0 measured

Отличная работа!

ГОЛОС ЗА КАДРОМ: Ну-ну.

Miri

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

ГОЛОС ЗА КАДРОМ: 🙂

...правда?

ГОЛОС ЗА КАДРОМ: 🙂

Хорошо, мы написали небезопасный код и компилятор не может нам помочь с поиском ошибок. Возможно, тесты сработали случайно, а на самом деле у нас там что-то недетерминированное. Какое-то Неопределённое Поведение.

Но что мы можем сделать? Мы выбрались из домика, в котором нас защищал rustc. Теперь нам никто не поможет.

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

"Эй, парень, не хочешь поинтерпретировать немного Rust кода?"

Ннннет... Наверное.

"Чувак, это невероятно, интерпретация может проверить, действительно ли исполнение твоей программы в динамике соответствует семантике модели памяти Rust. Крышу сносит..."

Что?

"Она проверяет, есть ли у тебя Неопределённое Поведение."

Думаю, один раз я могу попробовать интерпретацию.

"У тебя же установлен rustup, правда?"

Конечно да, это же та самая утилита для поддержания инструментария Rust в актуальном состоянии!

> rustup +nightly-2022-01-21 component add miri

info: syncing channel updates for 'nightly-2022-01-21-x86_64-pc-windows-msvc'
info: latest update on 2022-01-21, rust version 1.60.0-nightly (777bb86bc 2022-01-20)
info: downloading component 'cargo'
info: downloading component 'clippy'
info: downloading component 'rust-docs'
info: downloading component 'rust-std'
info: downloading component 'rustc'
info: downloading component 'rustfmt'
info: installing component 'cargo'
info: installing component 'clippy'
info: installing component 'rust-docs'
info: installing component 'rust-std'
info: installing component 'rustc'
info: installing component 'rustfmt'
info: downloading component 'miri'
info: installing component 'miri'

Что ты только что установил на мой компьютер!?

"Отличную Вещь"

ГОЛОС ЗА КАДРОМ: Есть кое-какие тонкости, касающиеся версии инструментария:

Инструмент, который мы установили — miri — тесно взаимодействует с внутренностями rustc, поэтому доступен только в ночных сборках.

+nightly-2022-01-21 указывает rustup, что мы хотим установить miri вместе с ночной сборкой инструментария за указанную дату. Я использовала конкретную дату, потому что иногда miri содержит ошибки и не компилируется несколько ночей подряд. rustup автоматически загрузит любой инструментарий, который мы укажем с помощью +, если он ещё не установлен.

2022-01-21 — это сборка, где, я точно знаю, miri работает, что можно проверить на этой странице. Если вы счастливчик, можете использовать просто +nightly.

Мы будем использовать синтаксис + при каждом вызове miri через cargo miri для указания версии совместимого инструментария. Если не хотите каждый раз писать версию, запустите rustup override set

> cargo +nightly-2022-01-21 miri test

I will run `"cargo.exe" "install" "xargo"` to install 
a recent enough xargo. Proceed? [Y/n]

Э-Э-Э, ЧТО ЭТО ЗА XARGO?

Не бери в голову, всё в порядке."

> y

    Updating crates.io index
  Installing xargo v0.3.24
...
    Finished release [optimized] target(s) in 10.65s
  Installing C:\Users\ninte\.cargo\bin\xargo-check.exe
  Installing C:\Users\ninte\.cargo\bin\xargo.exe
   Installed package `xargo v0.3.24` (executables `xargo-check.exe`, `xargo.exe`)

I will run `"rustup" "component" "add" "rust-src"` to install 
the `rust-src` component for the selected toolchain. Proceed? [Y/n]

Э-Э??

"Кто же не любит иметь копию исходного кода Rust?"

> y

info: downloading component 'rust-src'
info: installing component 'rust-src'

"Ну, вот, всё готово, а теперь самое интересное."

   Compiling lists v0.1.0 (C:\Users\ninte\dev\tmp\lists)
    Finished test [unoptimized + debuginfo] target(s) in 0.25s
     Running unittests (lists-5cc11d9ee5c3e924.exe)

error: Undefined Behavior: trying to reborrow for Unique at alloc84055, 
       but parent tag <209678> does not have an appropriate item in 
       the borrow stack

   --> \lib\rustlib\src\rust\library\core\src\option.rs:846:18
    |
846 |             Some(x) => Some(f(x)),
    |                  ^ trying to reborrow for Unique at alloc84055, 
    |                    but parent tag <209678> does not have an 
    |                    appropriate item in the borrow stack
    |
    = help: this indicates a potential bug in the program: 
      it performed an invalid operation, but the rules it 
      violated are still experimental
    = help: see https://github.com/rust-lang/unsafe-code-guidelines/blob/master/wip/stacked-borrows.md 
      for further information

    = note: inside `std::option::Option::<std::boxed::Box<fifth::Node<i32>>>::map::<i32, [closure@src\fifth.rs:31:30: 40:10]>` at \lib\rustlib\src\rust\library\core\src\option.rs:846:18

note: inside `fifth::List::<i32>::pop` at src\fifth.rs:31:9
   --> src\fifth.rs:31:9
    |
31  | /         self.head.take().map(|head| {
32  | |             let head = *head;
33  | |             self.head = head.next;
34  | |
...   |
39  | |             head.elem
40  | |         })
    | |__________^
note: inside `fifth::test::basics` at src\fifth.rs:74:20
   --> src\fifth.rs:74:20
    |
74  |         assert_eq!(list.pop(), Some(1));
    |                    ^^^^^^^^^^
note: inside closure at src\fifth.rs:62:5
   --> src\fifth.rs:62:5
    |
61  |       #[test]
    |       ------- in this procedural macro expansion
62  | /     fn basics() {
63  | |         let mut list = List::new();
64  | |
65  | |         // Check empty list behaves right
...   |
96  | |         assert_eq!(list.pop(), None);
97  | |     }
    | |_____^
 ...
error: aborting due to previous error

Ого. Какая ужасная ошибка.

"Да, взгляни на это дерьмо. Тебе понравится на него натыкаться."

Благодарю?

"Вот возьми и бутылочку эстрадиола, она тебе пригодится."

Подожди, зачем?

"Поверь мне, скоро ты начнёшь думать о моделях памяти."

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


Таинственная лиса оказалась права не только по поводу моего пола: miri — действительно Отличная Вещь.

Так что же такое miri?

Экспериментальный интерпретатор промежуточного представления среднего уровня (Mid-level Intermediate Representation, MIR). Он может запускать бинарники и тестовые наборы из проектов cargo и выявлять целые классы неопределённого поведения, например:

  • Выход за границы допустимого диапазона памяти и использование памяти после освобождения
  • Недопустимое использование неинициализированных данных
  • Нарушение внутренних предусловий (достижение unreachable_unchecked, вызов copy_nonoverlapping с перекрывающимися диапазонами, ...)
  • Доступ к памяти и ссылкам с недостаточно строгим выравниванием
  • Нарушение некоторых базовых инвариантов для типов (значение bool, отличное от 0 и 1 или неизвестная метка перечисления)
  • Экспериментально: Нарушение правил многоуровневого заимствования, регулирующих псевдонимы для ссылочных типов
  • Экспериментально: Состояние гонки данных (но без эффектов ослабленной модели памяти)

Кроме того, miri сообщит вам об утечках памяти. Если после завершения программы останется выделенная память, недоступная из глобальной статической переменной, miri выдаст ошибку.

...

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

В двух словах: он интерпретирует вашу программу и сообщает о всех нарушениях правил во время выполнения, приведших к Неопределённому Поведению. Это нужно, поскольку Неопределённое Поведение происходит именно во время выполнения. Если бы проблему можно было обнаружить на этапе компиляции, компилятор просто выдал бы ошибку!

Возможно, вы знакомы с такими инструментами, как ubsan и tsan. По сути, miri — такой же инструмент, но более полный и продвинутый.


Выглядит так, что за стенами нашего домика miri шастает с ножом в руках. Правда, нож учебный.

Чтобы miri проверил нашу работу, надо запустить его с такими параметрами:

> cargo +nightly-2022-01-21 miri test

Давайте посмотрим, что он вырезал на нашей парте:

error: Undefined Behavior: trying to reborrow for Unique at alloc84055, but parent tag <209678> does not have an appropriate item in the borrow stack

   --> \lib\rustlib\src\rust\library\core\src\option.rs:846:18
    |
846 |             Some(x) => Some(f(x)),
    |                  ^ trying to reborrow for Unique at alloc84055, 
    |                    but parent tag <209678> does not have an 
    |                    appropriate item in the borrow stack
    |

    = help: this indicates a potential bug in the program: it 
      performed an invalid operation, but the rules it 
      violated are still experimental
    
    = help: see 
      https://github.com/rust-lang/unsafe-code-guidelines/blob/master/wip/stacked-borrows.md 
      for further information

Ладно, я вижу, что мы допустили ошибку, но сообщение об ошибке сбивает меня с толку. Что такое «стек заимствований».

Постараемся разобраться с этим в следующей части.

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

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

Обычно я разбираю доку, но не в этот раз. На самом деле мы не являемся целевой аудиторией этой документации. Она написана для разработчиков компилятора и академиков, которые работают над семантикой Rust.

Так что я собираюсь дать вам высокоуровневое представление о «стековых заимствований» и рассказать несколько простых правил.

ГОЛОС ЗА КАДРОМ: стековые заимствования остаются «экспериментальными» в качестве семантической модели Rust. Нарушение этих правил не означает, что у вас «неправильная» программа. Но если вы — не разработчик компилятора, лучше просто исправьте программу, если miri на неё ругается. Это намного безопаснее, чем жалеть, когда возникнет Неопределённое Поведение.

Причина: псевдономизация указателей

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

Мы называем два указателя псевдонимами, если они указывают на перекрывающиеся области памяти. Как кого-то «известного под псевдонимом» можно называть двумя различными именами, так и область памяти может быть доступна через два различных указателя. И это может приводить к проблемам.

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

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

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


Однажды Михил осматривал свои книжные полки и увидел незнакомую книгу. Сняв её с полки, он посмотрел на обложку.

«Ах, да, мой старый экземпляр Войны и мира, книга, которую я наверняка прочитал. Люблю часть, посвящённую миру.»

Внезапно в дверь постучали. Михил вернул книгу на полку и открыл дверь — там стояла его непримиримая противница Хамслава.

«Привет, Хамслава, ты когда-нибудь читала Войну и мир?»

«Пффф, на самом деле никто не читал Войну и мир.»

«Ну, а я читал, смотри, она у меня прямо на полке, что очевидно означает, что я её прочёл.»

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

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

«Это не Война и мир, это Война и тир!

Слёзы текли по лицу Хамславы. Несомненно, это был лучший момент в её жизни.

«Н-нет! Я же только что смотрел!»

Он вырвал книгу из рук Хамславы и посмотрел на обложку. Действительно, слово «мир» было зачёркнуто и исправлено на «тир». Михил в ужасе застыл. Несомненно, это был худший момент в его жизни.

Он упал на колени и беспомощно уставился на книжный шкаф. Как это могло произойти? Он же видел обложку мгновенье назад!

Тут он заметил какое-то движение в шкафу. Это был человечек. Человечек с самым сердитым выражением лица, которое Михил когда-либо видел. Он показал Михилу средний палец и, сказав «тебе никто не поверит», скрылся между книгами.

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

А Хамслава в красках описала свою невероятную победу в стенгазете, так что репутация Михила в местном Интернет-кафе была разрушена навсегда.


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

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

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

Безопасные стековые заимствования

Ладно, значит мы хотим, чтобы у компилятора была полная информация о псевдономизации указателей. Можно ли её предоставить? Ну, выглядит так, что Rust для этого и спроектирован. Изменяемые ссылки по определению не могут иметь псевдонимов, а разделяемые ссылки, хотя и могут быть псевдонимами друг друга, не могут изменяться. Прекрасно. Отправляй в прод!

На самом деле всё гораздо сложнее. Мы можем «повторно заимствовать» изменяемые указатели. Например:

#![allow(unused)]
fn main() {
let mut data = 10;
let ref1 = &mut data;
let ref2 = &mut *ref1;

*ref2 += 2;
*ref1 += 1;

println!("{}", data);
}

Компилируется и запускается без проблем. Почему?

Мы поймём, что здесь происходит, если поменяем две строки местами:

let mut data = 10;
let ref1 = &mut data;
let ref2 = &mut *ref1;

// ПОРЯДОК ИЗМЕНИЛСЯ!
*ref1 += 1;
*ref2 += 2;

println!("{}", data);
error[E0503]: cannot use `*ref1` because it was mutably borrowed
 --> src/main.rs:6:5
  |
4 |     let ref2 = &mut *ref1;
  |                ---------- borrow of `*ref1` occurs here
5 |     
6 |     *ref1 += 1;
  |     ^^^^^^^^^^ use of borrowed `*ref1`
7 |     *ref2 += 2;
  |     ---------- borrow later used here

For more information about this error, try `rustc --explain E0503`.
error: could not compile `playground` due to previous error

Внезапно теперь мы получаем ошибку компилятора!

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

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

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

Эй, а вы знаете, как лучше всего представить штуки, упорядоченные явным образом? В виде стека. Стека заимствований.

Ага, вот и стек заимствований!

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

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

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

Поэтому и «нельзя использовать *ref1, поскольку это изменяемое заимствование». Тот же самый результат, но, возможно, оформленный в более интуитивном виде (особенно,когда речь идёт о не-лексическом времени жизни).

Но анализатор заимствований не может помочь нам, если мы используем небезопасные указатели!

Небезопасные стековые заимствования

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

Это трудная проблема, и я не знаю, как её решить, но ребята, работавшие над стековыми заимствованиями, придумали что-то, что внушает доверие, и miri пытается воплотить эти идеи.

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

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

Но подождите, вы можете превратить сырой указатель в ссылку! И вы можете копировать сырые указатели! Что будет, если вы сделаете &mut -> *mut -> &mut -> *mut, а затем обратитесь к первому *mut? Как, блин, стековые заимствования работают в этом случае?

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

Именно эта неразбериха является причиной появления экстра-экспериментального экстра-строгого режима miri: -Zmiri-tag-raw-pointers.

Чтобы включить этот режим, надо передать флаг через переменную окружения MIRIFLAGS:

MIRIFLAGS="-Zmiri-tag-raw-pointers" cargo +nightly-2022-01-21 miri test

Или можно установить переменную глобально, как в Windows:

$env:MIRIFLAGS="-Zmiri-tag-raw-pointers"
cargo +nightly-2022-01-21 miri test

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

Управление стековыми заимствованиями

При использовании сырых указателей мы будем придерживаться простой и понятной эвристики, которая, как я надеюсь, имеет большую толерантность к ошибкам:

Как только вы стали использовать сырые указатели, старайтесь использовать ТОЛЬКО сырые указатели.

Это сильно снижает возможность непредумышленной потери «права» сырого указателя на доступ к памяти.

ГОЛОС ЗА КАДРОМ: у этого упрощения есть два аспекта:

  1. У безопасных указателей часто есть другие свойства, помимо псевдономизации: память выделена, выровнена, её достаточно для хранения объекта указывания, объект указывания инициализирован и т. д. Поэтому так опасно разбрасывать их везде, когда они находятся в нестабильном состоянии.
  2. Даже если вы используете только сырые указатели, вы не можете использовать псевдонимы для доступа к любой памяти. Указатели концептуально привязаны к определённым «областям выделенной памяти» (которые могут быть такими же мелкими, как и локальная переменная на стеке). Нельзя просто взять указатель на одну область, прибавить к нему смещение и получить указатель на другую область. Если бы это было возможно, угроза сердитых человечков была бы всегда и везде. Именно по этой причине точка зрения «указатели — всего лишь целые числа» является проблематичной.

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

Вот что мы будем делать:

  1. В начале метода используем входные ссылки, чтобы получить сырые указатели
  2. Далее будем использовать только сырые указатели
  3. В конце, если нужно, преобразуем сырые указатели в безопасные указатели

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

Фактически, часть большой ошибки, которую мы совершили, заключается в том, что мы продолжили использовать Box! Box имеет специальную аннотацию, которая говорит компилятору, что «эта штука очень похожа на &mut, потому что эксклюзивно владеет указателем». И это правда!

Но сырой указатель, в котором мы хранили конец списка, указывает на Box, поэтому всякий раз, когда мы обращаемся к Box, мы, возможно, ломаем повторное заимствование этого сырого указателя!

В следующем разделе мы вернёмся в нашему привычному формату и разберёмся с целым ворохом непростых примеров.

Тестирование стековых заимствований

Подытожим, что мы (упрощённо) знаем о подели памяти Rust, опираясь на предыдущие разделы:

  • Концептуально, обработка повторных заимствований в Rust осуществляется через «стек заимствований»
  • Единственный указатель на вершине стека считается «живым» (имеет эксклюзивный доступ)
  • При доступе через указатель из глубины стека, «живым» становится он, а указатели выше него удаляются из стека
  • Нельзя использовать указатели, которые были удалены из стека заимствований
  • Анализатор зависимостей гарантирует, что безопасный код соответствуем этим требованиям
  • Miri (в теории) проверяет, что сырые указатели соответствуют этим правилам во время выполнения

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

ГОЛОС ЗА КАДРОМ: обнаружение Неопределённого Поведения — это хлопотное дело. Помимо прочего, мы имеем дело с ситуациями, которые компилятор буквально полагает невозможными.

Если вы счастливчик, программа будет «казаться работающей», при этом она будет содержать бомбу замедленного действия для Более Умного Компилятора или небольшого изменения в коде. Если вы настоящий счастливчик, программа гарантированно не будет работать, так что вы сможете найти ошибку и исправить её. Но если вы не счастливчик, программа будет ломаться странным и непонятным образом.

Miri пытается работать обойти эту проблему, получая от rustc простейшее не оптимизированное представление программы и следя за её состоянием при интерпретации. «Средства динамического анализа» (sanitizers) — довольно детерминированный и надёжный подход, он он никогда не будет совершенным. Ваша тестовая программа должна дойти до точки с UB, однако во многих программах пышным цветом цветёт разного рода недетерминированное поведение (скажем, HashMap по умолчанию использует датчик случайных чисел).

Ни при каких обстоятельствах мы не должны считать, что отсутствие предупреждений miri означает полное отсутствие UB в нашей программе. В то же время, бывает, что miri думает, будто нашла UB, но на самом деле это не так. Если у нас есть ментальная модель работы указателей, и miri с нами согласна — это хороший знак, что мы на правильном пути.

Базовые заимствования

В предыдущих разделах мы видели, что анализатору заимствований не нравится такой код:

let mut data = 10;
let ref1 = &mut data;
let ref2 = &mut *ref1;

// ПОРЯДОК ИЗМЕНИЛСЯ!
*ref1 += 1;
*ref2 += 2;

println!("{}", data);

Давайте посмотрим, что случится, если мы заменим ref2 на *mut:

unsafe {
    let mut data = 10;
    let ref1 = &mut data;
    let ptr2 = ref1 as *mut _;

    // ПОРЯДОК ИЗМЕНИЛСЯ!
    *ref1 += 1;
    *ptr2 += 2;

    println!("{}", data);
}
cargo run
   Compiling miri-sandbox v0.1.0
    Finished dev [unoptimized + debuginfo] target(s) in 0.71s
     Running `target\debug\miri-sandbox.exe`
13

Кажется, rustc всё устраивает: никаких предупреждений и программа выдала ожидаемый результат! Теперь посмотрим, что об этом думает miri (в строгом режиме):

MIRIFLAGS="-Zmiri-tag-raw-pointers" cargo +nightly-2022-01-21 miri run

    Finished dev [unoptimized + debuginfo] target(s) in 0.00s
     Running cargo-miri.exe target\miri

error: Undefined Behavior: no item granting read access 
to tag <untagged> at alloc748 found in borrow stack.

 --> src\main.rs:9:9
  |
9 |         *ptr2 += 2;
  |         ^^^^^^^^^^ no item granting read access to tag <untagged> 
  |                    at alloc748 found in borrow stack.
  |
  = help: this indicates a potential bug in the program: 
    it performed an invalid operation, but the rules it 
    violated are still experimental
 

Отлично! Наша интуитивная модель подтвердилась: хотя компилятор не смог обнаружить проблему, miri смогла.

Давайте попробуем более сложное преобразование &mut -> *mut -> &mut -> *mut, о котором мы говорили ранее:

unsafe {
    let mut data = 10;
    let ref1 = &mut data;
    let ptr2 = ref1 as *mut _;
    let ref3 = &mut *ptr2;
    let ptr4 = ref3 as *mut _;

    // Сначала обращаемся к первому сырому указателю
    *ptr2 += 2;

    // Затем обращаемся к переменным в порядке «стека заимствований»
    *ptr4 += 4;
    *ref3 += 3;
    *ptr2 += 2;
    *ref1 += 1;

    println!("{}", data);
}
cargo run
22

MIRIFLAGS="-Zmiri-tag-raw-pointers" cargo +nightly-2022-01-21 miri run

error: Undefined Behavior: no item granting read access 
to tag <1621> at alloc748 found in borrow stack.

  --> src\main.rs:13:5
   |
13 |     *ptr4 += 4;
   |     ^^^^^^^^^^ no item granting read access to tag <1621> 
   |                at alloc748 found in borrow stack.
   |

Да, да! В строгом режиме miri смогла «различить» два сырых указателя, обнаружив, что использование второго ведёт к порче первого. Давайте посмотрим, начнёт ли всё работать, когда мы удалим первое использование, которое всё сломало:

unsafe {
    let mut data = 10;
    let ref1 = &mut data;
    let ptr2 = ref1 as *mut _;
    let ref3 = &mut *ptr2;
    let ptr4 = ref3 as *mut _;

    // Обращаемся к переменным в порядке «стека заимствований»
    *ptr4 += 4;
    *ref3 += 3;
    *ptr2 += 2;
    *ref1 += 1;

    println!("{}", data);
}
cargo run
20

MIRIFLAGS="-Zmiri-tag-raw-pointers" cargo +nightly-2022-01-21 miri run
20

ОТЛИЧНО.

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

ГОЛОС ЗА КАДРОМ: это не так, но я всё равно тобой горжусь.

Тестирование массивов

Поэкспериментируем с массивами и смещениями указателей (сложением и вычитанием). Будет ли работать этот код?

unsafe {
    let mut data = [0; 10];
    let ref1_at_0 = &mut data[0];           // Ссылка на 0-й элемент
    let ptr2_at_0 = ref1_at_0 as *mut i32;  // Указатель на 0-й элемент
    let ptr3_at_1 = ptr2_at_0.add(1);       // Указатель на 1-й элемент

    *ptr3_at_1 += 3;
    *ptr2_at_0 += 2;
    *ref1_at_0 += 1;

    // Должно быть [3, 3, 0, ...]
    println!("{:?}", &data[..]);
}
cargo run
[3, 3, 0, 0, 0, 0, 0, 0, 0, 0]

MIRIFLAGS="-Zmiri-tag-raw-pointers" cargo +nightly-2022-01-21 miri run

error: Undefined Behavior: no item granting read access 
to tag <1619> at alloc748+0x4 found in borrow stack.
 --> src\main.rs:8:5
  |
8 |     *ptr3_at_1 += 3;
  |     ^^^^^^^^^^^^^^^ no item granting read access to tag <1619>
  |                     at alloc748+0x4 found in borrow stack.

Рвёт заявление в докторантуру

Что случилось? Мы же прекрасно используем стек заимствований! Можем быть, причина в том, что мы преобразуем ptr -> ptr? Попробуем просто скопировать указатель, чтобы оба указателя указывали на один и тот же элемент:

#![allow(unused)]
fn main() {
unsafe {
    let mut data = [0; 10];
    let ref1_at_0 = &mut data[0];           // Ссылка на 0-й элемент
    let ptr2_at_0 = ref1_at_0 as *mut i32;  // Указатель на 0-й элемент
    let ptr3_at_0 = ptr2_at_0;              // Указатель на 0-й элемент

    *ptr3_at_0 += 3;
    *ptr2_at_0 += 2;
    *ref1_at_0 += 1;

    // Должно быть [6, 0, 0, ...]
    println!("{:?}", &data[..]);
}
}
cargo run
[6, 0, 0, 0, 0, 0, 0, 0, 0, 0]

MIRIFLAGS="-Zmiri-tag-raw-pointers" cargo +nightly-2022-01-21 miri run
[6, 0, 0, 0, 0, 0, 0, 0, 0, 0]

Нет, такой код прекрасно работает. Может быть, нам просто повезло. Давайте как следует перетасуем указатели:

#![allow(unused)]
fn main() {
unsafe {
    let mut data = [0; 10];
    let ref1_at_0 = &mut data[0];            // Ссылка на 0-й элемент
    let ptr2_at_0 = ref1_at_0 as *mut i32;   // Указатель на 0-й элемент
    let ptr3_at_0 = ptr2_at_0;               // Указатель на 0-й элемент
    let ptr4_at_0 = ptr2_at_0.add(0);        // Указатель на 0-й элемент
    let ptr5_at_0 = ptr3_at_0.add(1).sub(1); // Указатель на 0-й элемент

    // Абсолютно беспорядочная мешанина использования указателей
    *ptr3_at_0 += 3;
    *ptr2_at_0 += 2;
    *ptr4_at_0 += 4;
    *ptr5_at_0 += 5;
    *ptr3_at_0 += 3;
    *ptr2_at_0 += 2;
    *ref1_at_0 += 1;

    // Должно быть [20, 0, 0, ...]
    println!("{:?}", &data[..]);
}
}
cargo run
[20, 0, 0, 0, 0, 0, 0, 0, 0, 0]

MIRIFLAGS="-Zmiri-tag-raw-pointers" cargo +nightly-2022-01-21 miri run
[20, 0, 0, 0, 0, 0, 0, 0, 0, 0]

Нет! На самом деле, miri гораздо снисходительнее, если речь идёт о сырых указателях, производных от других сырых указателей. Все они разделяют одно и то же «заимствование» (или, как miri его называет, маркер).

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

ГОЛОС ЗА КАДРОМ: Если код достаточно простой, компилятор отслеживает производные указатели и продолжает оптимизацию, если это возможно. Однако эта часть компилятора достаточно капризная.

Так в чём же реальная проблема?

Несмотря на то, что data — это один «кусок памяти» (одна локальная переменная), ref1_at_0 заимствует только первый элемент. Rust позволяет разбивать заимствования на части, чтобы они применялись к разным частям выделенной памяти! Давайте попробуем:

unsafe {
    let mut data = [0; 10];
    let ref1_at_0 = &mut data[0];           // Ссылка на 0-й элемент
    let ref2_at_1 = &mut data[1];           // Ссылка на 1-й элемент
    let ptr3_at_0 = ref1_at_0 as *mut i32;  // Указатель на 0-й элемент
    let ptr4_at_1 = ref2_at_1 as *mut i32;  // Указатель на 1-й элемент

    *ptr4_at_1 += 4;
    *ptr3_at_0 += 3;
    *ref2_at_1 += 2;
    *ref1_at_0 += 1;

    // Должно быть [4, 6, 0, ...]
    println!("{:?}", &data[..]);
}
error[E0499]: cannot borrow `data[_]` as mutable more than once at a time
 --> src\main.rs:5:21
  |
4 |     let ref1_at_0 = &mut data[0];           // Reference to 0th element
  |                     ------------ first mutable borrow occurs here
5 |     let ref2_at_1 = &mut data[1];           // Reference to 1th element
  |                     ^^^^^^^^^^^^ second mutable borrow occurs here
6 |     let ptr3_at_0 = ref1_at_0 as *mut i32;  // Ptr to 0th element
  |                     --------- first borrow later used here
  |
  = help: consider using `.split_at_mut(position)` or similar method 
    to obtain two mutable non-overlapping sub-slices

Блин! Rust не отслеживает индексы массива, чтобы доказать, что эти заимствования не пересекаются, но он предоставляет нам метод split_at_mut позволяющий безопасности разделить срез на несколько частей:

#![allow(unused)]
fn main() {
unsafe {
    let mut data = [0; 10];

    let slice1 = &mut data[..];
    let (slice2_at_0, slice3_at_1) = slice1.split_at_mut(1); 
    
    let ref4_at_0 = &mut slice2_at_0[0];    // Ссылка на 0-й элемент
    let ref5_at_1 = &mut slice3_at_1[0];    // Ссылка на 1-й элемент
    let ptr6_at_0 = ref4_at_0 as *mut i32;  // Указатель на 0-й элемент
    let ptr7_at_1 = ref5_at_1 as *mut i32;  // Указатель на 1-й элемент

    *ptr7_at_1 += 7;
    *ptr6_at_0 += 6;
    *ref5_at_1 += 5;
    *ref4_at_0 += 4;

    // Должно быть [10, 12, 0, ...]
    println!("{:?}", &data[..]);
}
}
cargo run
[10, 12, 0, 0, 0, 0, 0, 0, 0, 0]

MIRIFLAGS="-Zmiri-tag-raw-pointers" cargo +nightly-2022-01-21 miri run
[10, 12, 0, 0, 0, 0, 0, 0, 0, 0]

Да, так работает! Срезы сообщают компилятору и miri: «Эй, я владею куском памяти в этом диапазоне», так что они знают, что все элементы могут быть изменены.

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

(Мне кажется, что в самой модели заимствований всё продолжает считаться стеком, потому что стек концептуально отслеживает права доступа к каждому байту программы.)

А что, если мы напрямую преобразуем срез в указатель? Будет ли этот указатель иметь доступ к полному срезу?

#![allow(unused)]
fn main() {
unsafe {
    let mut data = [0; 10];

    let slice1_all = &mut data[..];         // Срез всего массива
    let ptr2_all = slice1_all.as_mut_ptr(); // Указатель на весь массив
    
    let ptr3_at_0 = ptr2_all;               // Указатель на (тот же) 0-й элемент
    let ptr4_at_1 = ptr2_all.add(1);        // Указатель на 1-й элемент
    let ref5_at_0 = &mut *ptr3_at_0;        // Ссылка на 0-й элемент
    let ref6_at_1 = &mut *ptr4_at_1;        // Ссылка на 1-й элемент

    *ref6_at_1 += 6;
    *ref5_at_0 += 5;
    *ptr4_at_1 += 4;
    *ptr3_at_0 += 3;

    // Просто для смеха, поменяем все элементы в цикле
    // (Для этого можно использовать любой из сырых указателей, так как они разделяют заимствование!)
    for idx in 0..10 {
        *ptr2_all.add(idx) += idx;
    }

    // Безопасная версия того же самого кода, для развлечения
    for (idx, elem_ref) in slice1_all.iter_mut().enumerate() {
        *elem_ref += idx; 
    }

    // Должно быть [8, 12, 4, 6, 8, 10, 12, 14, 16, 18]
    println!("{:?}", &data[..]);
}
}
cargo run
[8, 12, 4, 6, 8, 10, 12, 14, 16, 18]

MIRIFLAGS="-Zmiri-tag-raw-pointers" cargo +nightly-2022-01-21 miri run
[8, 12, 4, 6, 8, 10, 12, 14, 16, 18]

Прекрасно! Указатели — это не просто целые числа: у них есть область памяти, ассоциированная с ними, и с помощью Rust мы можем сузить эту область!

Тестирование разделяемых ссылок

Во всех наших примерах я использовала только изменяемые ссылки и применяла операции чтения-изменения-записи (+=), чтобы не привносить в код ненужной сложности.

Но в Rust есть разделяемые ссылки, которые предназначены только для чтения и могут быть свободно скопированы. Как будут работать они? Ладно, мы видели, что сырые указатели можно свободно копировать и что с ними можно работать, считая, что копии «разделяют» единственное заимствование. Нельзя ли подобным образом думать и о разделяемых ссылках?

Давайте проверим наше предположение с помощью функции, которая читает значение. (Макрос `println! может работать волшебным образом, когда речь заходит о получении и разыменовании ссылок, так что я завернула его в функцию, чтобы быть уверенной, что мы тестируем то, что нужно):

fn opaque_read(val: &i32) {
    println!("{}", val);
}

unsafe {
    let mut data = 10;
    let mref1 = &mut data;
    let sref2 = &mref1;
    let sref3 = sref2;
    let sref4 = &*sref2;

    // Читаем разделяемые ссылки в случайном порядке
    opaque_read(sref3);
    opaque_read(sref2);
    opaque_read(sref4);
    opaque_read(sref2);
    opaque_read(sref3);

    *mref1 += 1;

    opaque_read(&data);
}
cargo run

warning: unnecessary `unsafe` block
 --> src\main.rs:6:1
  |
6 | unsafe {
  | ^^^^^^ unnecessary `unsafe` block
  |
  = note: `#[warn(unused_unsafe)]` on by default

warning: `miri-sandbox` (bin "miri-sandbox") generated 1 warning

10
10
10
10
10
11

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

fn opaque_read(val: &i32) {
    println!("{}", val);
}

unsafe {
    let mut data = 10;
    let mref1 = &mut data;
    let ptr2 = mref1 as *mut i32;
    let sref3 = &mref1;
    let ptr4 = sref3 as *mut i32;

    *ptr4 += 4;
    opaque_read(sref3);
    *ptr2 += 2;
    *mref1 += 1;

    opaque_read(&data);
}
cargo run

error[E0606]: casting `&&mut i32` as `*mut i32` is invalid
  --> src\main.rs:11:16
   |
11 |     let ptr4 = sref3 as *mut i32;
   |                ^^^^^^^^^^^^^^^^^

Ой, извините, на самом деле мы работали с & &mut вместо &! Rust очень хорошо умеет скрывать такие вещи, когда они не имеют значения. Сделаем правильное повторное заимствование: let sref3 = &*mref1:

cargo run

error[E0606]: casting `&i32` as `*mut i32` is invalid
  --> src\main.rs:11:16
   |
11 |     let ptr4 = sref3 as *mut i32;
   |                ^^^^^^^^^^^^^^^^^

Нет, Rust всё ещё не в восторге от нашего кода! Разделяемую ссылку можно приводить только к указателю *const, доступному только для чтения. Но что, если мы просто... сделаем... так...?

    let ptr4 = sref3 as *const i32 as *mut i32;
cargo run

14
17

ЧТО? ПРОСТО РАБОТАЕТ? Отличная система приведения типов, Rust. Выглядит так, как будто *const — это практически бесполезный тип, нужный только для описания C API и правильных сценариев использования (на самом деле, так и есть). А что по этому поводу думает miri?

MIRIFLAGS="-Zmiri-tag-raw-pointers" cargo +nightly-2022-01-21 miri run

error: Undefined Behavior: no item granting write access to 
tag <1621> at alloc742 found in borrow stack.
  --> src\main.rs:13:5
   |
13 |     *ptr4 += 4;
   |     ^^^^^^^^^^ no item granting write access to tag <1621>
   |                at alloc742 found in borrow stack.

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

Это важно, потому что когда мы удаляем разделяемое заимствование из стека заимствований, изменяемые указатели перед ним могут быть уверены, что память не изменилась. Может быть, какие-то сердитые человечки и читали память, но у них не было возможности её изменить, так что изменяемые указатели могут предполагать, что последнее значение, которое они записали, всё ещё там!

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

Однако мы можем сделать так:

#![allow(unused)]
fn main() {
fn opaque_read(val: &i32) {
    println!("{}", val);
}

unsafe {
    let mut data = 10;
    let mref1 = &mut data;
    let ptr2 = mref1 as *mut i32;
    let sref3 = &*mref1;
    let ptr4 = sref3 as *const i32 as *mut i32;

    opaque_read(&*ptr4);
    opaque_read(sref3);
    *ptr2 += 2;
    *mref1 += 1;

    opaque_read(&data);
}
}

Обратите внимание, что создание изменяемого сырого указателя считается «нормальным», если мы только читаем его данные!

cargo run
10
10
13

MIRIFLAGS="-Zmiri-tag-raw-pointers" cargo +nightly-2022-01-21 miri run
10
10
13

И, на всякий случай давайте убедимся, что разделяемая ссылка удаляется из стека заимствований как надо:

fn opaque_read(val: &i32) {
    println!("{}", val);
}

unsafe {
    let mut data = 10;
    let mref1 = &mut data;
    let ptr2 = mref1 as *mut i32;
    let sref3 = &*mref1;

    *ptr2 += 2;
    opaque_read(sref3); // Читаем в неправильном порядке?
    *mref1 += 1;

    opaque_read(&data);
}
cargo run
12
13

MIRIFLAGS="-Zmiri-tag-raw-pointers" cargo +nightly-2022-01-21 miri run

error: Undefined Behavior: trying to reborrow for SharedReadOnly 
at alloc742, but parent tag <1620> does not have an appropriate 
item in the borrow stack

  --> src\main.rs:13:17
   |
13 |     opaque_read(sref3); // Read in the wrong order?
   |                 ^^^^^ trying to reborrow for SharedReadOnly 
   |                       at alloc742, but parent tag <1620> 
   |                       does not have an appropriate item 
   |                       in the borrow stack
   |

Смотрите, мы получили другое сообщение об ошибке. Теперь вместо вообщения о конкретном маркере, речь идёт о SharedReadOnly. Это логично: как только появляется несколько разделяемых ссылок, все они объединяются в один большой объект SharedReadOnly, так как незачем различать конкретные ссылки!

Тестирование внутренней изменчивости

Помните ту действително ужасную главу, где мы пытались написать связный список с помощью RefCell и Rc? Ту, где всё было даже хуже, чем обычно?

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

#![allow(unused)]
fn main() {
use std::cell::Cell;

unsafe {
    let mut data = Cell::new(10);
    let mref1 = &mut data;
    let ptr2 = mref1 as *mut Cell<i32>;
    let sref3 = &*mref1;

    sref3.set(sref3.get() + 3);
    (*ptr2).set((*ptr2).get() + 2);
    mref1.set(mref1.get() + 1);

    println!("{}", data.get());
}
}

Ах, какая великолепная мешанина! Будет здорово увидеть, как miri её обругает.

cargo run
16

MIRIFLAGS="-Zmiri-tag-raw-pointers" cargo +nightly-2022-01-21 miri run
16

Подождите, правда? Тут всё нормально? Почему? Как? Что вообще такое эта ячейка Cell?

Расчехляет стандартную библиотеку

pub struct Cell<T: ?Sized> {
    value: UnsafeCell<T>,
}

Что это за ерундовина, UnsafeCell?

Расчехляет дальше, просто чтобы показать стандартной библиотеке, что мы настроены серьёзно

#[lang = "unsafe_cell"]
#[repr(transparent)]
#[repr(no_niche)]
pub struct UnsafeCell<T: ?Sized> {
    value: T,
}

А, так это магия. Понятно. Наверное. #[lang = "unsafe_cell"] — это буквально способ сказать, что UnsafeCell — это UnsafeCell. Давайте не будем больше расчехлять код, а проверим актуальную документацию на std::cell::UnsafeCell.

Базовый примитив для реализации внутренней изменчивости в Rust.

Если у вас есть ссылка &T, компилятор Rust оптимизирует код, рассчитывая на то, что &T ссылается на неизменяемые данные. Изменение этих данных, например, через псевдоним или после преобразования &T в &mut T ведёт к неопределённому поведению. UnsafeCell<T> отключает гарантию неизменяемости для &T: разделяемая ссылка &UnsaveCell<T> ссылается на данные, которые могут измениться. Это называется «внутренней изменчивостью».

А, так это на самом деле просто магия.

По сути, UnsafeCell говорит компилятору: «Эй, послушай, мы будем прикалываться с этой памятью, так что не делай по поводу её никаких обычных предположений». Это как повесить большой знак «ВНИМАНИЕ: ПЕРЕХОД ДЛЯ СЕРДИТЫХ ЧЕЛОВЕЧКОВ».

Провеим, действительно ли UnsafeCell может осчастливить miri:

use std::cell::UnsafeCell;

fn opaque_read(val: &i32) {
    println!("{}", val);
}

unsafe {
    let mut data = UnsafeCell::new(10);
    let mref1 = data.get_mut();      // Получаем изменяемиую ссылку на содержимое
    let ptr2 = mref1 as *mut i32;
    let sref3 = &*ptr2;

    *ptr2 += 2;
    opaque_read(sref3);
    *mref1 += 1;

    println!("{}", *data.get());
}
cargo run
12
13

MIRIFLAGS="-Zmiri-tag-raw-pointers" cargo +nightly-2022-01-21 miri run

error: Undefined Behavior: trying to reborrow for SharedReadOnly
at alloc748, but parent tag <1629> does not have an appropriate
item in the borrow stack

  --> src\main.rs:15:17
   |
15 |     opaque_read(sref3);
   |                 ^^^^^ trying to reborrow for SharedReadOnly 
   |                       at alloc748, but parent tag <1629> does
   |                       not have an appropriate item in the
   |                       borrow stack
   |

Подождите, что? Мы же сказали заклинание! Куда теперь мне девать всю эту сертифицированную кровь для жертвоприношений?

Что ж, начали мы правильно, но затем всё испортили, вызвав get_mut, который взял UnsafeCell и превратил его в полноценный &mut i32.

Подумайте об этом: если компилятор считает, что &mut i32 может изменить UnsafeCell, он вообще не может делать никаких предположений! Код, полный сердитых человечков.

Нам надо оставить UnsafeCel в наших типах указателей, чтобы компилятор понимал, что мы хотим сделать.

#![allow(unused)]
fn main() {
use std::cell::UnsafeCell;

fn opaque_read(val: &i32) {
    println!("{}", val);
}

unsafe {
    let mut data = UnsafeCell::new(10);
    let mref1 = &mut data;              // Изменяемая ссылка на *весь объект*
    let ptr2 = mref1.get();             // Получаем сырой указатель на содержимое
    let sref3 = &*mref1;                // Получаем разделяемую ссылку на *весь объект*

    *ptr2 += 2;                         // Меняем значение по сырому указателю
    opaque_read(&*sref3.get());         // Читаем из разделяемой ссылки
    *sref3.get() += 3;                  // Пишем через разделяемую ссылку
    *mref1.get() += 1;                  // Меняем через изменяемую ссылку

    println!("{}", *data.get());
}
}
cargo run
12
16

MIRIFLAGS="-Zmiri-tag-raw-pointers" cargo +nightly-2022-01-21 miri run
12
16

Заработало! В итоге мне не придётся выливать всю эту жертвенную кровь.

На самом деле, эй, подождите. Мы всё ещё не до конца разобрались с порядком. Сначала мы создали ptr2, а затем создали sref3 из изменяемого указателя. А потом мы использовали сырой указатель перед разделяемым указателем. Это кажется... неправильным.

Но ведь мы делали то же самое и в примере с Cell. Хммм.

Мы должны один из двух выводов:

  • Miri несовершенна и на самом деле у нас здесь UB.
  • Наша упрощённая модель оказалсь чрезмерно упрощённой.

Я бы поставила на второй вариант, но, просто чтобы быть уверенной, сделаю версию, которая будет абсолютно надёжной в нашей упрощённой модели:

#![allow(unused)]
fn main() {
use std::cell::UnsafeCell;

fn opaque_read(val: &i32) {
    println!("{}", val);
}

unsafe {
    let mut data = UnsafeCell::new(10);
    let mref1 = &mut data;
    // Меняем местами, теперь заимствования в *точном* соответствии со стеком
    let sref2 = &*mref1;
    // Создаём ptr из разделяемой ссылки для максимальной безопасности!
    let ptr3 = sref2.get();             

    *ptr3 += 3;
    opaque_read(&*sref2.get());
    *sref2.get() += 2;
    *mref1.get() += 1;

    println!("{}", *data.get());
}
}
cargo run
13
16

MIRIFLAGS="-Zmiri-tag-raw-pointers" cargo +nightly-2022-01-21 miri run
13
16

Одна из причин, почему первая реализация, могла быть совершенно корректной, заключается в том, что &UnsafeCell<T> по сути не отличается от *mut T. Его можно бесконечно копировать его и менять!

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

Строка let sref2 = &*mref1 — довольно хитрая штука. Синтаксически кажется, что мы разыменовали указатель, но разыменование само по себе не являтеся чем-то реальным. Сравните с &my_tuple.0: вы ничего не делаете ни с my_tuple, ни с .0, вы просто используете выражение, чтобы получить адрес в памяти, а, написав перед ним & как бы говорите: «не загружай содержимое, просто запомни адрес».

&* — это то же самое: * говорит «обсудим местоположение, на которое указывает этот указатель», а & говорит «теперь запиши этот адрес». Естественно, адрес остался тем же. Но тип указателя изменился, потому что... ну, типы!

С другой стороны, если вы пишете &**, то, по факту, загружаете значение сразу после первой операции *! Эта * такая странная!

ГОЛОС ЗА КАДРОМ: Никого не волнует, что вы знаете, что такое «л-значение», Джонатан. В Rust мы называем л-значения местами (places), что гораздо круче, не находите?

Тестирование Box

Помните, почему мы начали это затянувшееся отступление? Нет? Странно.

Это было потому, что мы смешали Box и сырой указатель. Box — это что-то вроде &mut, поскольку он заявляет об единоличном владении памятью, на которую указывает. Проверим это утверждение:

unsafe {
    let mut data = Box::new(10);
    let ptr1 = (&mut *data) as *mut i32;

    *data += 10;
    *ptr1 += 1;

    // Должно быть 21
    println!("{}", data);
}
cargo run
21

MIRIFLAGS="-Zmiri-tag-raw-pointers" cargo +nightly-2022-01-21 miri run

error: Undefined Behavior: no item granting read access 
       to tag <1707> at alloc763 found in borrow stack.

 --> src\main.rs:7:5
  |
7 |     *ptr1 += 1;
  |     ^^^^^^^^^^ no item granting read access to tag <1707> 
  |                at alloc763 found in borrow stack.
  |

Да, miri это не нравится. Убедимся, что выполнение действий в правильном порядке не приводит к ошибкам:

#![allow(unused)]
fn main() {
unsafe {
    let mut data = Box::new(10);
    let ptr1 = (&mut *data) as *mut i32;

    *ptr1 += 1;
    *data += 10;

    // Должно быть 21
    println!("{}", data);
}
}
cargo run
21

MIRIFLAGS="-Zmiri-tag-raw-pointers" cargo +nightly-2022-01-21 miri run
21

Так!

Что ж, на этом всё, мы, наконец, закончили обсуждать стековые заимствования!

...хотя, как нам решить проблему с Box? Конечно, мы можем писать игрушечные программы, но всё-таки — нам надо хранить Box и удерживать значения сырых указателей в течение довольно долгого времени. Наверняка всё перепутается и станет недействительным?

Хороший вопрос! Чтобы на него ответить, вернёмся, наконец, к нашей истинной задаче — написанию богом проклятых связных списков.

Подождите, мы снова станем писать связные списки? Не будем торопиться, друзья. Будем благоразумны. Я уверена, что у меня есть несколько интересных вопросов для обсуждения.

Представление и основы 2: добавляем сырости

Краткое содержание трёх предыдущих частей: перемешивание ссылок &, & mut и Box с небезопасными указателями *mut и *const — верный способ получить Неопределённое Поведение, поскольку безопасные указатели вводят дополнительные ограничения, которым сырые указатели не обязаны подчиняться.

Господи, мне опять придётся писать связные списки. Прекрасно. ПРЕКРАСНО. Всё прекрасно. У нас всё прекрасно.

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

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

Итак, в новом представлении мы собираемся использовать только сырые указатели. Всё будет нормально и мы никогда больше не ошибёмся.

Вот наше старое проблемное представление:

#![allow(unused)]
fn main() {
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>,
}
}

А вот наше новое представление.

#![allow(unused)]
fn main() {
pub struct List<T> {
    head: Link<T>,
    tail: *mut Node<T>,
}

type Link<T> = *mut Node<T>; // ГОРАЗДО ЛУЧШЕ

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

Запомните: Option не удобен и не полезен, если мы используем сырые указатели, так что мы можем от него отказаться. Чуть позже мы познакомимся с типом NotNull, но пока будем пользоваться чистыми указателями.

Основы

List::new по сути остаётся тем же самым.

use ptr;

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

push в основе так...

pub fn push(&mut self, elem: T) {
    let mut new_tail = Box::new(

Подождите, мы же больше не используем Box. Как нам выделить память без Box?

Мы могли бы вызывать std::alloc::alloc, но в данном случае это как брать катану на кухню. Свою работу мы сделаем, но это будет и неудобно, и слишком наворочено.

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

#![allow(unused)]
fn main() {
struct Node<T> {
    elem: T,
    real_next: Option<Box<Node<T>>>,
    next: *mut Node<T>,
}
}

Идея в том, что мы создаём бокс и храним его в своём узле. Затем получаем сырой указатель на содержимое бокса, и используем его, пока мы не закончим с узлом и не захотим его удалить. Тогда мы можем забрать (take) и освободить Box из переменной real_next. Кажется, это решение совместимо с нашей упрощённой моделью стековых заимствований?

Если вы хотите поэкспериментировать с этой идеей, пожалуйста, развлекайтесь, но выглядит она уродливо, не так ли? Это не глава про Rc или RefCell и мы больше не будем играть в эту игру. Будем делать простые и ясные вещи.

Используем милейшую функцию Box::into_raw.

  pub fn into_raw(b: Box<T>) -> *mut T

Возвращает сырой указатель на содержимое Box.

Указатель будет правильно выровнен и не равен null.

После вызова этой функции вызывающая сторона отвечает за память, которой прежде владел Box. В частности, вызывающая сторона должна правильно уничтожить T и освободить память с учётом внутреннего устройства Box. Простейший способ это сделать — преобразовать сырой указатель обратно в Box с помощью функции Box::from_raw, позволив деструктору Box выполнить очистку.

Обратите внимание, что это ассоциированная функция, то есть вы должны вызывать её как Box::into_raw(b) вместо b.into_raw(). Это нужно, чтобы избежать возможного конфликта с методом внутреннего типа.

Примеры

Конвертируем сырой указатель обратно в Box с помощью Box::from_raw для автоматической очистки:

#![allow(unused)]
fn main() {
let x = Box::new(String::from("Hello"));
let ptr = Box::into_raw(x);
let x = unsafe { Box::from_raw(ptr) };
}

Прекрасно, эти методы как будто разработаны буквально для нашего случая. Они также следует нашим правилам: начинают с безопасного Box, превращают его в сырой указатель, и затем преобразуют его обратно в Box (когда мы хотим вызывать drop).

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

Теперь, когда мы везде используем сырые указатели, давайте не беспокоиться о том, чтобы блоки unsafe были небольшими: весь наш код небезопасен. (Так всегда и было, но иногда обманывать самих себя так приятно.)

pub fn push(&mut self, elem: T) {
    unsafe {
        // Сразу преобразуем Box в сырой указатель
        let new_tail = Box::into_raw(Box::new(Node {
            elem: elem,
            next: ptr::null_mut(),
        }));

        if !self.tail.is_null() {
            (*self.tail).next = new_tail;
        } else {
            self.head = new_tail;
        }

        self.tail = new_tail;
    }
}

Смотрите, сейчас, когда мы используем только сырые указатели, код выглядит гораздо чище!

Перейдём к методу pop, который тоже достаточно похож на старую реализацию, хотя мы помним, что надо вызывать Box::from_raw перед очисткой:

pub fn pop(&mut self) -> Option<T> {
    unsafe {
        if self.head.is_null() {
            None
        } else {
            // ВОССТАНЬ ИЗ МОГИЛЫ
            let head = Box::from_raw(self.head);
            self.head = head.next;

            if self.head.is_null() {
                self.tail = ptr::null_mut();
            }

            Some(head.elem)
        }
    }
}

Наши прекрасные маленькие take и map больше не нужны, теперь мы проверяем и устанавливаем null вручную.

И, раз уже мы заговорили об этом, давайте добавим деструктор. Пока реализуем через повторяющийся вызов pop, потому что это легко и просто:

impl<T> Drop for List<T> {
    fn drop(&mut self) {
        while let Some(_) = self.pop() { }
    }
}

А теперь настал момент истины:

#[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);

        // Проверяем push после pop из пустого списка
        list.push(6);
        list.push(7);

        // Проверяем обычное удаление
        assert_eq!(list.pop(), Some(6));
        assert_eq!(list.pop(), Some(7));
        assert_eq!(list.pop(), None);
    }
}
cargo test

running 12 tests
test fifth::test::basics ... ok
test first::test::basics ... ok
test fourth::test::basics ... ok
test fourth::test::peek ... ok
test second::test::basics ... ok
test fourth::test::into_iter ... ok
test second::test::into_iter ... ok
test second::test::iter ... ok
test second::test::iter_mut ... ok
test second::test::peek ... ok
test third::test::basics ... ok
test third::test::iter ... ok

test result: ok. 12 passed; 0 failed; 0 ignored; 0 measured

Хорошо, но согласен ли miri?

MIRIFLAGS="-Zmiri-tag-raw-pointers" cargo +nightly-2022-01-21 miri test

running 12 tests
test fifth::test::basics ... ok
test first::test::basics ... ok
test fourth::test::basics ... ok
test fourth::test::peek ... ok
test second::test::basics ... ok
test fourth::test::into_iter ... ok
test second::test::into_iter ... ok
test second::test::iter ... ok
test second::test::iter_mut ... ok
test second::test::peek ... ok
test third::test::basics ... ok
test third::test::iter ... ok

test result: ok. 12 passed; 0 failed; 0 ignored; 0 measured

ЙЕЕЕЕ!!!!!

ЭТА ХРЕНЬ СРАБОТАЛА!

ВОЗМОЖНО!

НЕВОЗМОЖНО ДОКАЗАТЬ ОТСУТСТВИЕ НЕОПРЕДЕЛЁННОГО ПОВЕДЕНИЯ. ЕСТЬ ШАНС, ЧТО ОНО ВСЁ ЕЩЁ СУЩЕСТВУЕТ ГДЕ-ТО ТАМ, НО ЕСТЬ ПРЕДЕЛ ТОМУ, НАСКОЛЬКО Я ГОТОВА БЫТЬ СТРОГОЙ В ШУТОЧНОЙ КНИГЕ О СВЯЗНЫХ СПИСКАХ. В ОБЩЕМ, МЫ БУДЕМ СЧИТАТЬ ЭТО ДОКАЗАТЕЛЬСТВО 100% ПРОВЕРЕННЫМ МАШИНОЙ И ЛЮБОЙ, КТО СКАЖЕТ ОБРАТНОЕ, МОЖЕТ ОБЛИЗАТЬ МОЕГО ПЕТУШКА!

(Прим. перев.: в оригинальном тексте именно так — довольно грубо — и написано. Речь, конечно, идёт об языке программирования Coq, который используется для доказательства корректности программ. В то же время Coq созвучен английскому cock — петух. Кроме того, в разговорном английском слово cock означает мужской половой орган.)

Что и требовалось доказать.

Всё остальное

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

Конечно, поскольку у нас везде небезопасные указатели, придётся подкорректировать код! И, раз уж мы будем его корректировать, почему бы не воспользоваться случаем и не убедиться, что мы ничего не упустили!

Что ж, начнём копи-пастить код из старой реализации:

// ...

pub struct IntoIter<T>(List<T>);

pub struct Iter<'a, T> {
    next: Option<&'a Node<T>>,
}

pub struct IterMut<'a, T> {
    next: Option<&'a mut Node<T>>,
}

IntoIter выглядит нормально, но Iter и IterMut нарушают наше правило никогда не использовать безопасные указатели в наших типах. Перестрахуемся и перепишем их на сырые указатели:

pub struct IntoIter<T>(List<T>);

pub struct Iter<'a, T> {
    next: *mut Node<T>,
}

pub struct IterMut<'a, T> {
    next: *mut Node<T>,
}

impl<T> List<T> {
    pub fn into_iter(self) -> IntoIter<T> {
        IntoIter(self)
    }

    pub fn iter(&self) -> Iter<'_, T> {
        Iter { next: self.head }
    }

    pub fn iter_mut(&mut self) -> IterMut<'_, T> {
        IterMut { next: self.head }
    }
}

Выглядит хорошо!

error[E0392]: parameter `'a` is never used
  --> src\fifth.rs:17:17
   |
17 | pub struct Iter<'a, T> {
   |                 ^^ unused parameter
   |
   = help: consider removing `'a`, referring to it in a field, 
     or using a marker such as `PhantomData`

error[E0392]: parameter `'a` is never used
  --> src\fifth.rs:21:20
   |
21 | pub struct IterMut<'a, T> {
   |                    ^^ unused parameter
   |
   = help: consider removing `'a`, referring to it in a field, 
     or using a marker such as `PhantomData`

Выглядит нехорошо! О каком PhantomData они пишут?

Тип нулевого размера. Используется для маркировки объектов, которые «ведут себя так», словно владеют T.

Добавление поля PhantomData<T> к вашему типу говорит компилятору, что он действует так, будто хранит значение типа T, хотя на самом деле это не так. Эта информация используется при вычислении определённых свойств безопасности.

Для более глубокого объяснения, как использовать PhantomData<T>, читайте, пожалуйста, Nomicon.

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

Неиспользуемые параметры времени жизни

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

А, так мы объявили в нашем типе время жизни, но на самом деле его не используем. Мы могли бы пойти по пути PhantomData, но я оставлю его для двусвязного списка из следующей главы, где такое решение на самом деле имеет смысл.

Сейчас мы в интересной ситуации, когда на самом деле нам не нужен тип PhantomData. Мне кажется. Я просто надеюсь, что это правда, а если miri будет ругаться, я уступлю и попробую прикрутить PhantomData.

Что мы на самом деле собираемся сделать: вернём ссылки в итераторы, поскольку они удобны для перебора элементов. Кажется, что с этим не должно быть проблем, поскольку мы соблюдаем вложенность: создаём итератор, использует безопасные ссылки, удаляем итератор.

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

Я даже не уверена на 100%, но я хочу попробовать и проверить!

pub struct IntoIter<T>(List<T>);

pub struct Iter<'a, T> {
    next: Option<&'a Node<T>>,
}

pub struct IterMut<'a, T> {
    next: Option<&'a mut Node<T>>,
}

impl<T> List<T> {
    pub fn into_iter(self) -> IntoIter<T> {
        IntoIter(self)
    }

    pub fn iter(&self) -> Iter<'_, T> {
        unsafe {
            Iter { next: self.head.as_ref() }
        }
    }

    pub fn iter_mut(&mut self) -> IterMut<'_, T> {
        unsafe {
            IterMut { next: self.head.as_mut() }
        }
    }
}

Если мы будем хранить ссылки, нам надо преобразовать сырые указатели в Options для ссылок. Мы могли бы проверять указатели на null, но это один из тех редких случаев, когда я допускаю использование неприятных методов ptr::as_ref и ptr::as_mut.

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

Эти методы сопровождаются множеством предупреждений, но самое интересное вот это:

Необходимо соблюдать правила Rust о псевдонимах, поскольку возвращаемое время жизни 'a выбирается произвольно и не отражает реальное время жизни данных. В частности, в течение этого времени жизни, доступ к этой памяти (чтение или запись) не должен осуществляться ни через один другой указатель

Смотрите, это то, о чём мы говорили на протяжении 25 страниц! Я уже говорила, что использование ссылок здесь определённо допустимо, так что проблема псевдонимов решена! Но есть и другая проблема — сигнатура:

pub unsafe fn as_mut<'a>(self) -> Option<&'a mut T>

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

Способ справиться с ним — поместить его туда, где оно будет ограничено, что обычно означает «вернуть его из функции как можно быстрее, так что теперь его будет ограничивать сигнатура функции».

Боже, я нервничаю, но мы будем двигаться дальше! Позаимствуем несколько реализаций итератора из стека:

impl<T> Iterator for IntoIter<T> {
    type Item = T;
    fn next(&mut self) -> Option<Self::Item> {
        self.0.pop()
    }
}

impl<'a, T> Iterator for Iter<'a, T> {
    type Item = &'a T;

    fn next(&mut self) -> Option<Self::Item> {
        unsafe {
            self.next.map(|node| {
                self.next = node.next.as_ref();
                &node.elem
            })
        }
    }
}

impl<'a, T> Iterator for IterMut<'a, T> {
    type Item = &'a mut T;

    fn next(&mut self) -> Option<Self::Item> {
        unsafe {
            self.next.take().map(|node| {
                self.next = node.next.as_mut();
                &mut node.elem
            })
        }
    }
}

Moment of truth time...

Время момента истины...

cargo test

running 15 tests
test fifth::test::basics ... ok
test fifth::test::into_iter ... ok
test fifth::test::iter ... ok
test fifth::test::iter_mut ... ok
test first::test::basics ... ok
test fourth::test::basics ... ok
test fourth::test::into_iter ... ok
test fourth::test::peek ... ok
test second::test::basics ... ok
test second::test::into_iter ... ok
test second::test::iter ... ok
test second::test::iter_mut ... ok
test second::test::peek ... ok
test third::test::iter ... ok
test third::test::basics ... ok

test result: ok. 15 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out;
MIRIFLAGS="-Zmiri-tag-raw-pointers" cargo +nightly-2022-01-21 miri test

running 15 tests
test fifth::test::basics ... ok
test fifth::test::into_iter ... ok
test fifth::test::iter ... ok
test fifth::test::iter_mut ... ok
test first::test::basics ... ok
test fourth::test::basics ... ok
test fourth::test::into_iter ... ok
test fourth::test::peek ... ok
test second::test::basics ... ok
test second::test::into_iter ... ok
test second::test::iter ... ok
test second::test::iter_mut ... ok
test second::test::peek ... ok
test third::test::basics ... ok
test third::test::iter ... ok

test result: ok. 15 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out

ДА!!! Вот тебе, ГОЛОС ЗА КАДРОМ! Иногда и я не ошибаюсь!

ГОЛОС ЗА КАДРОМ: но разве вся суть не в том, что ошибки нужны, чтобы научить читателя?

ДА, НО ИНОГДА СУТЬ В ТОМ, ЧТО Я ПРАВА И ВСЕ ДОЛЖНЫ ПРИСЛУШИВАТЬСЯ КО МНЕ, КОГДА Я ГОВОРЮ О НЕБЕЗОПАСНОМ КОДЕ, ПОТОМУ ЧТО Я ПРОВЕЛА МНОГО ВРЕМЕНИ В РАЗМЫШЛЕНИЯХ О НАДЁЖНОСТИ РЕАЛИЗАЦИИ ИТЕРАТОРОВ! ЛАДЫ?! ЛАДЫ.

Наконец, вот peek и peek_mut.

pub fn peek(&self) -> Option<&T> {
    unsafe {
        self.head.as_ref()
    }
}

pub fn peek_mut(&mut self) -> Option<&mut T> {
    unsafe {
        self.head.as_mut()
    }
}

Я даже не буду их тестировать, потому что я больше не ошибаюсь.

ГОЛОС ЗА КАДРОМ: cargo build

error[E0308]: mismatched types
  --> src\fifth.rs:66:13
   |
25 | impl<T> List<T> {
   |      - this type parameter
...
64 |     pub fn peek(&self) -> Option<&T> {
   |                           ---------- expected `Option<&T>` 
   |                                      because of return type
65 |         unsafe {
66 |             self.head.as_ref()
   |             ^^^^^^^^^^^^^^^^^^ expected type parameter `T`, 
   |                                found struct `fifth::Node`
   |
   = note: expected enum `Option<&T>`
              found enum `Option<&fifth::Node<T>>`

ПРЕКРАСНО.

pub fn peek(&self) -> Option<&T> {
    unsafe {
        self.head.as_ref().map(|node| &node.elem)
    }
}

pub fn peek_mut(&mut self) -> Option<&mut T> {
    unsafe {
        self.head.as_mut().map(|node| &mut node.elem)
    }
}

Полагаю, я всё-таки буду ошибаться время от времени, так что мы добавим новый тест, который я назову «приманка для miri»: что-то, что вызывает наши методы в произвольном порядке и помогает miri искать наши ошибки.

#[test]
fn miri_food() {
    let mut list = List::new();

    list.push(1);
    list.push(2);
    list.push(3);

    assert!(list.pop() == Some(1));
    list.push(4);
    assert!(list.pop() == Some(2));
    list.push(5);

    assert!(list.peek() == Some(&3));
    list.push(6);
    list.peek_mut().map(|x| *x *= 10);
    assert!(list.peek() == Some(&30));
    assert!(list.pop() == Some(30));

    for elem in list.iter_mut() {
        *elem *= 100;
    }

    let mut iter = list.iter();
    assert_eq!(iter.next(), Some(&400));
    assert_eq!(iter.next(), Some(&500));
    assert_eq!(iter.next(), Some(&600));
    assert_eq!(iter.next(), None);
    assert_eq!(iter.next(), None);

    assert!(list.pop() == Some(400));
    list.peek_mut().map(|x| *x *= 10);
    assert!(list.peek() == Some(&5000));
    list.push(7);

    // Ну а здесь пусть запуститься деструктор
}
cargo test

running 16 tests
test fifth::test::basics ... ok
test fifth::test::into_iter ... ok
test fifth::test::iter ... ok
test fifth::test::iter_mut ... ok
test fifth::test::miri_food ... ok
test first::test::basics ... ok
test fourth::test::basics ... ok
test fourth::test::into_iter ... ok
test fourth::test::peek ... ok
test second::test::into_iter ... ok
test second::test::basics ... ok
test second::test::iter_mut ... ok
test second::test::peek ... ok
test third::test::iter ... ok
test second::test::iter ... ok
test third::test::basics ... ok

test result: ok. 16 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out



MIRIFLAGS="-Zmiri-tag-raw-pointers" cargo +nightly-2022-01-21 miri test

running 16 tests
test fifth::test::basics ... ok
test fifth::test::into_iter ... ok
test fifth::test::iter ... ok
test fifth::test::iter_mut ... ok
test fifth::test::miri_food ... ok
test first::test::basics ... ok
test fourth::test::basics ... ok
test fourth::test::into_iter ... ok
test fourth::test::peek ... ok
test second::test::into_iter ... ok
test second::test::basics ... ok
test second::test::iter_mut ... ok
test second::test::peek ... ok
test third::test::iter ... ok
test second::test::iter ... ok
test third::test::basics ... ok

test result: ok. 16 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out

Идеально.

Финальный код

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

За исключением той части, где miri полностью нас подвёл и нам пришлось писать небольшую магистерскую работу о модели памяти в Rust. Как это обычно и бывает.

Но светлая сторона в том, что нам не пришлось писать всех этих ужасных Rc или RefCell.

#![allow(unused)]
fn main() {
use std::ptr;

pub struct List<T> {
    head: Link<T>,
    tail: *mut Node<T>,
}

type Link<T> = *mut Node<T>;

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

pub struct IntoIter<T>(List<T>);

pub struct Iter<'a, T> {
    next: Option<&'a Node<T>>,
}

pub struct IterMut<'a, T> {
    next: Option<&'a mut Node<T>>,
}

impl<T> List<T> {
    pub fn new() -> Self {
        List { head: ptr::null_mut(), tail: ptr::null_mut() }
    }
    pub fn push(&mut self, elem: T) {
        unsafe {
            let new_tail = Box::into_raw(Box::new(Node {
                elem: elem,
                next: ptr::null_mut(),
            }));

            if !self.tail.is_null() {
                (*self.tail).next = new_tail;
            } else {
                self.head = new_tail;
            }

            self.tail = new_tail;
        }
    }
    pub fn pop(&mut self) -> Option<T> {
        unsafe {
            if self.head.is_null() {
                None
            } else {
                let head = Box::from_raw(self.head);
                self.head = head.next;

                if self.head.is_null() {
                    self.tail = ptr::null_mut();
                }

                Some(head.elem)
            }
        }
    }

    pub fn peek(&self) -> Option<&T> {
        unsafe {
            self.head.as_ref().map(|node| &node.elem)
        }
    }

    pub fn peek_mut(&mut self) -> Option<&mut T> {
        unsafe {
            self.head.as_mut().map(|node| &mut node.elem)
        }
    }

    pub fn into_iter(self) -> IntoIter<T> {
        IntoIter(self)
    }

    pub fn iter(&self) -> Iter<'_, T> {
        unsafe {
            Iter { next: self.head.as_ref() }
        }
    }

    pub fn iter_mut(&mut self) -> IterMut<'_, T> {
        unsafe {
            IterMut { next: self.head.as_mut() }
        }
    }
}

impl<T> Drop for List<T> {
    fn drop(&mut self) {
        while let Some(_) = self.pop() { }
    }
}

impl<T> Iterator for IntoIter<T> {
    type Item = T;
    fn next(&mut self) -> Option<Self::Item> {
        self.0.pop()
    }
}

impl<'a, T> Iterator for Iter<'a, T> {
    type Item = &'a T;

    fn next(&mut self) -> Option<Self::Item> {
        unsafe {
            self.next.map(|node| {
                self.next = node.next.as_ref();
                &node.elem
            })
        }
    }
}

impl<'a, T> Iterator for IterMut<'a, T> {
    type Item = &'a mut T;

    fn next(&mut self) -> Option<Self::Item> {
        unsafe {
            self.next.take().map(|node| {
                self.next = node.next.as_mut();
                &mut node.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);

        // Проверяем push после pop из пустого списка
        list.push(6);
        list.push(7);

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

    #[test]
    fn into_iter() {
        let mut list = List::new();
        list.push(1); list.push(2); list.push(3);

        let mut iter = list.into_iter();
        assert_eq!(iter.next(), Some(1));
        assert_eq!(iter.next(), Some(2));
        assert_eq!(iter.next(), Some(3));
        assert_eq!(iter.next(), None);
    }

    #[test]
    fn iter() {
        let mut list = List::new();
        list.push(1); list.push(2); list.push(3);

        let mut iter = list.iter();
        assert_eq!(iter.next(), Some(&1));
        assert_eq!(iter.next(), Some(&2));
        assert_eq!(iter.next(), Some(&3));
        assert_eq!(iter.next(), None);
    }

    #[test]
    fn iter_mut() {
        let mut list = List::new();
        list.push(1); list.push(2); list.push(3);

        let mut iter = list.iter_mut();
        assert_eq!(iter.next(), Some(&mut 1));
        assert_eq!(iter.next(), Some(&mut 2));
        assert_eq!(iter.next(), Some(&mut 3));
        assert_eq!(iter.next(), None);
    }

    #[test]
    fn miri_food() {
        let mut list = List::new();

        list.push(1);
        list.push(2);
        list.push(3);

        assert!(list.pop() == Some(1));
        list.push(4);
        assert!(list.pop() == Some(2));
        list.push(5);

        assert!(list.peek() == Some(&3));
        list.push(6);
        list.peek_mut().map(|x| *x *= 10);
        assert!(list.peek() == Some(&30));
        assert!(list.pop() == Some(30));

        for elem in list.iter_mut() {
            *elem *= 100;
        }

        let mut iter = list.iter();
        assert_eq!(iter.next(), Some(&400));
        assert_eq!(iter.next(), Some(&500));
        assert_eq!(iter.next(), Some(&600));
        assert_eq!(iter.next(), None);
        assert_eq!(iter.next(), None);

        assert!(list.pop() == Some(400));
        list.peek_mut().map(|x| *x *= 10);
        assert!(list.peek() == Some(&5000));
        list.push(7);

        // Ну а здесь пусть запуститься деструктор
    }
}
}

Небезопасный двусвязный дек продуктового уровня

Наконец, мы добрались. Мой величайший враг: std::collections::LinkedList, двусвязный дек.

Который я безуспешно пыталась уничтожить.

Наша история начинается в 2014 году, когда мы стремительно приближались к выпуску Rust 1.0, первой стабильной версии Rust. Я была ответственна за std::collections или, как мы тогда называли эту библиотеку, libcollections.

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

До той поры я поддерживала их всех, но пришло время для них предстать перед судом за их прегрешения.

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

Удалить TreeMap, TreeSet, TrieMap, TrieSet, LruCache и EnumSet

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

Объявить устаревшими BitSet и BitVec

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

Объявить устаревшим VecMap

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

Я оглянулась и увидела тех, кто остался:

  • Vec и VecDeque — простые и надёжные, сердце вычислений.
  • HashMap и HashSet — мощные и требующие знаний, мозг вычислений.
  • BTreeMap и BTreeSet — неуклюжие, но нужные, печень вычислений.
  • BinaryHeap — хитрая и ловкая, лодыжка вычислений.

Я удовлетворённо кивнула. Просто и эффективно. Моя работа подошла к концу.

Нет, DList, этого не может быть! Я думала, ты погиб в том трагическом инциденте со сборкой мусора! Единственный, кто появился случайно, а не вследствие какого-то намерения!

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

Я рассказывала о его злодеяниях всем, кто готов был слушать, но их сердца оставались непоколебимыми. Связный список оказался красноречивым дьяволом, который убедил всех вокруг, что является фундаментальной и естественной структурой данных. Он даже убедил C++, что был тем самым списком!

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

Легко! Тривиально!

«Это нетривиальный небезопасный код, поэтому вполне логично включить его в стандартную библиотеку!»

То же самое можно сказать про драйверы GPU и видео кодеки. Библиотека libcollections должна быть минималистична!

Увы, связный список обзавёлся слишком большим числом сторонников и стал слишком сильным, пока я занималась его братьями.

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

Связный список победил. Я потерпела поражение и отправилась в изгнание.

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

Действительно продуктового уровня? Ну, я собираюсь полностью переписать мой старую реализацию связного списка из Rust 1.0, ту самую, которая объективно лучше реализации из стандартной библиотеки. Ту, в которой были Курсоры, из стабильной версии Rust 2015 года! Ту, которой всё ещё нет в стандартной библиотеке 2022 года!

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

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

Так что вместо этого (для большей ясности прячу Some/None):

... -> (A, ptr) -> (B, ptr) -> ...

Мы получаем:

... <-> (ptr, A, ptr) <-> (ptr, B, ptr) <-> ...

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

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

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

Есть два известных способа реализации: «традиционный» и «фиктивный узел».

Традиционный подход — простое расширение способа, который мы использовали для стека — просто хранить указатели на голову и хвост:

[ptr, ptr] <-> (ptr, A, ptr) <-> (ptr, B, ptr)
  ^                                        ^
  +----------------------------------------+

Он работает, но у его есть обратная сторона — граничные случаи. А теперь в нашем списке два конца, что означает в два раза больше граничных случаев. Легко забыть об одном из них и получить серьёзную ошибку.

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

[ptr] -> (ptr, ?DUMMY?, ptr) <-> (ptr, A, ptr) <-> (ptr, B, ptr)
           ^                                                 ^
           +-------------------------------------------------+ 

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

[ptr] -> (ptr, ?DUMMY?, ptr) 
           ^             ^
           +-------------+

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

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

  • Не создавать фиктивный узел до тех пор, пока в список не будет вставлен первый элемент: просто и эффективно, но это возвращает некоторые из граничных случаев, от которых мы пытались избавиться, используя фиктивные указатели!
  • Использовать статический одиночный фиктивный узел с копированием при записи и с каким-нибудь хитроумным методом, позволяющим совместить проверки Copy-On-Write с обычными проверками. Этот подход мне очень нравится, но мы не станем заниматься в этой книге тёмной магией. Читайте исходный код ThinVec, если хотите познакомиться с подобной странной реализацией.
  • Хранить фиктивный узел на стеке — не практично в языке, где не поддерживаются конструкторы перемещения (move constructors) из C++. Я уверена, что можно было бы придумать странное решение с закреплением (pin), но и этого мы делать не будем.

Проблема 2: Какое значение хранить в фиктивном узле? Ладно бы, речь шла о целом числе, но что если в нашем списке хранятся сплошь экземпляры Box? Мы бы не смогли инициализировать такое значение! Потенциальные решения включают:

  • Хранить в каждом узле Option<T>: просто и эффективно, в то же время неэкономно и «по ощущениям» неправильно.
  • Хранить в каждом узле MaybeUninit<T>. Не только неправильно, но и ужасно.
  • По настоящему осторожно и хитроумно, в стиле наследования, сделать так, чтобы фиктивный узел не хранил данных. Тоже заманчиво, но крайне опасно и неправильно. Читайте исходный код BTreeMap если хотите познакомиться с подобной странной реализацией.

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

#![allow(unused)]
fn main() {
pub struct LinkedList<T> {
    front: Link<T>,
    back: Link<T>,
    len: usize,
}

type Link<T> = *mut Node<T>;

struct Node<T> {
    front: Link<T>,
    back: Link<T>,
    elem: T, 
}
}

(Теперь, когда мы добрались до двусвязного дека, мы, наконец, заслужили право назвать структуру LinkedList, поскольку речь идёт об истинном связном списке.)

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

Вариантность и PhantomData

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

Есть пять ужасных всадников создания небезопасных коллекций в Rust:

  1. Вариантность
  2. Дроп-чек
  3. Оптимизации NonNull
  4. Правило выделения isize::MAX
  5. Типы нулевой длины

Слава небесам, последние два не являются для нас проблемой.

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

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

Остаётся Вариантность. Честно говоря, на ней, наверное, тоже можно было не настаивать, но я всё ещё горжусь своей работой в качестве человека, разработавшего std::collections, так что мы всё-таки займёмся Этой Штукой.

Так вот, сюрприз: в Rust есть подтипы. В частности, &'big T — это подтип &'small T. (Прим. перевод.: время жизни big больше, чем время жизни small). Почему? Ну, потому что если какому-то коду нужна ссылка, живущая в течение определённого времени, вполне допустимо дать ему ссылку, которая живёт дольше. Интуитивно это же понятно, так?

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

fn take_two<T>(_val1: T, _val2: T) { }

Это поистине скучный код, поэтому мы ожидаем, что он будет работать при T=&u32, так?

#![allow(unused)]
fn main() {
fn two_refs<'big: 'small, 'small>(
    big: &'big u32, 
    small: &'small u32,
) {
    take_two(big, small);
}

fn take_two<T>(_val1: T, _val2: T) { }
}

Да, без проблем компилируется!

А теперь давайте немного повеселимся и завернём его, ну, я не знаю, в std::cell::Cell:

#![allow(unused)]
fn main() {
use std::cell::Cell;

fn two_refs<'big: 'small, 'small>(
    // ОБРАТИТЕ ВНИМАНИЕ: эти две строки изменились
    big: Cell<&'big u32>, 
    small: Cell<&'small u32>,
) {
    take_two(big, small);
}

fn take_two<T>(_val1: T, _val2: T) { }
}
error[E0623]: lifetime mismatch
 --> src/main.rs:7:19
  |
4 |     big: Cell<&'big u32>, 
  |               ---------
5 |     small: Cell<&'small u32>,
  |                 ----------- these two types are declared with different lifetimes...
6 | ) {
7 |     take_two(big, small);
  |                   ^^^^^ ...but data from `small` flows into `big` here

Что??? Мы не трогали время жизни, почему компилятор стал ругаться?

Хорошо, штука со временем жизни «подтипов» должна быть очень простой, поэтому она даёт сбой, если завернуть ссылки во что-то ещё. Смотрите, она ломается и с Vec:

#![allow(unused)]
fn main() {
fn two_refs<'big: 'small, 'small>(
    big: Vec<&'big u32>, 
    small: Vec<&'small u32>,
) {
    take_two(big, small);
}

fn take_two<T>(_val1: T, _val2: T) { }
}
    Finished dev [unoptimized + debuginfo] target(s) in 1.07s
     Running `target/debug/playground`

Видите, этот код тоже не компилиру... — подождите, что??? Vec что, магический???

Ну, да. Но, и в то же время — нет. Эта магия всегда была с нами и имя ей — ✨Вариантность✨.

Прочитайте главу про подтипы в Растономиконе, если вам нужны кровавые подробности, но, вкратце: подтипы не всегда безопасны. В частности, небезопасно когда смешиваются изменяемые ссылки, потому что вы можете вызывать что-то вроде mem::swap и получить висячие указатели!

Штуки, которые выглядят как «изменяемые ссылки» являются инвариантными. Это значит, что они не разрешают использовать подтипы вместо типов. Так что, в целях безопасности, &mut T — инвариантен относительно T, и Cell<T> — инвариантен относительно T, потому что &Cell<T> по сути является обычным &mut T (из-за внутренней изменчивости).

Почти всё, что не является инвариантным, ковариантно. Это значит, что подтипы можно использовать везде, где можно использовать типы, и всё будет работать. (Есть также контравариантные типы, где типы можно использовать вместо подтипов, но они встречаются редко и никому не нравятся, так что я не буду про них говорить).

Обычно коллекции содержат изменяемый указатель на свои данные, так что они должны быть инвариантными, но на самом деле, это не так! Из-за системы владения Rust, Vec<T> семантически эквивалентен T и это значит, что его можно сделать ковариантным и это будет безопасно!

К сожалению, такое определение инвариантно:

#![allow(unused)]
fn main() {
pub struct LinkedList<T> {
    front: Link<T>,
    back: Link<T>,
    len: usize,
}

type Link<T> = *mut Node<T>;

struct Node<T> {
    front: Link<T>,
    back: Link<T>,
    elem: T, 
}
}

Но как на самом деле Rust принимает решение о вариантности типов? В старые добрые времена (до версии 1.0) мы разрешали людям просто указывать нужную им вариантность... и это был полный провал! Подтипы и вариантность — действительно сложные понятия. Разработчики ядра вполне искренне не могли договориться даже о базовой терминологии! В конце концов мы остановились на «вариантности на примере»: компилятор смотрит на ваши поля и копирует их вариантность. Если возникают противоречия, побеждает инвариантность, потому что это безопасно.

Так что же такого есть в наших определениях, что так не нравится Rust? *mut!

Сырые указатели в Rust и в самом деле позволяют вам делать всё, что угодно, но и они имеют одну безопасную черту: из-за того, что большинство людей не имеют представления о вариантности и подтипах, и из-за того, что некорректная ковариантность может привести к ужасным последствиям, *mut T инвариантен, поскольку обычно его используют «как» &mut T.

Это крайне раздражает меня, человека, потратившего много времени на реализую коллекций в Rust. Именно поэтому, когда я писала std::ptr::NonNull, я добавила этот кусочек магии:

В отличие от *mut T, NonNull<T> ковариантен относительно T. Благодаря этому можно использовать NonNull<T> для построения ковариантных типов. Это может привести к ошибкам, если тип не должен быть ковариантным.

Однако интерфейс NonNull<T> построен на базе *mut T, почему всё работает? Опять магия? Давайте посмотрим:

#![allow(unused)]
fn main() {
pub struct NonNull<T> {
    pointer: *const T,
}

impl<T> NonNull<T> {
    pub unsafe fn new_unchecked(ptr: *mut T) -> Self {
        // БЕЗОПАСНОСТЬ: вызывающая сторона должна гарантировать, что `ptr` не равен null.
        unsafe { NonNull { pointer: ptr as *const T } }
    }
}
}

НЕТ. НИКАКОЙ МАГИИ! NonNull полагается на то, что *const T ковариантен и хранит его вместо *mut T, преобразовывая значения туда и обратно, чтобы создать впечатление, будто он хранит *mut T. Весь трюк в этом! Вот почему коллекции в Rust ковариантны. Было бы довольно грустно всё время писать так. Поэтому я заставила Правильный Тип Указателя делать всю грязную работу! Пожалуйста! Наслаждайтесь своими подтипами!

Решение в том, чтобы использовать NonNull<T>, а если вам потребуются нулевые указатели, использовать Option<NonNull<T>>. Мы действительно станем так делать?

Ага! Мы пишем связные списки продуктового уровня, поэтому будем героически разбираться в деталях и писать сложный код. (Можно было бы просто использовать *const T и приводить типы, но я искренне хочу узнать, насколько это больно... в Интересах Науки).

Так что принимайте окончательное определение нашего типа:

#![allow(unused)]
fn main() {
use std::ptr::NonNull;

// !!!Изменилось!!!
pub struct LinkedList<T> {
    front: Link<T>,
    back: Link<T>,
    len: usize,
}

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

struct Node<T> {
    front: Link<T>,
    back: Link<T>,
    elem: T, 
}
}

...хотя, подождите, ещё одна деталь. Всякий раз, используя сырые указатели, добавляйте поле PhantomData для их защиты:

use std::marker::PhantomData;

pub struct LinkedList<T> {
    front: Link<T>,
    back: Link<T>,
    len: usize,
    /// Логически мы владеем объектами типа T и храним их по значению.
    _boo: PhantomData<T>,
}

В этом случае я не уверена, что нам действительно нужен PhantomData, но всякий раз, когда вы используете NonNull (или просто сырые указатели), вы должны добавлять его для безопасности, чтобы компилятору и всем другим было понятно, что вы подумали над тем, что делаете.

PhantomData — это способ предоставить компилятору «пример» поля, которое концептуально присутствует в вашем типе, но физически — нет. (Прим. перевод.: физически LinkedList содержит два сырых указателя на значения типа T. Компилятор не знает, владеет ли LinkedList этими значениями и будет ли их освобождать при вызове drop. С помощью PhantomData мы даём подсказку: LinkedList владеет значениями типа T — не &T и не &mut T.) В данном случае мы используем NonNull, поэтому можем утверждать, что наш тип ведёт себя так, как будто хранит значения T, и мы добавляем PhantomData, чтобы явно это выразить.

Раньше в стандартной библиотеке PhantomData часто использовался из-за сложных и опасных трюков с деструкторами, но эти правила менялись столько раз, что я уже не уверена в необходимости фантомного поля. Впрочем, успела выработать привычку, так что буду следовать карго-культу и ставить PhantomData, даже если он не обязателен!

(Кстати, узел действительно хранит значение типа T, так что маркер для LinkedList не обязателен!)

...ладно, с представлением мы закончили! Переходим к основным функциям!

Основы

Ладно, самая ужасная часть книги. Вот почему мне потребовалось 7 лет, чтобы написать эту главу! Настало время ещё раз написать множеству скучнейших функций, которые мы писали уже 5 раз, но теперь они стали ещё многословнее и длиннее, потому теперь у нас два указателя формата Option<NonNull<Node<T>>>!

impl<T> LinkedList<T> {
    pub fn new() -> Self {
        Self {
            front: None,
            back: None,
            len: 0,
            _boo: PhantomData,
        }
    }
}

PhantomData — тип без полей и его можно создать, просто написав его имя. пожимает плечами

pub fn push_front(&mut self, elem: T) {
    // БЕЗОПАСНОСТЬ: это связный список, что вы делаете?
    unsafe {
        let new = NonNull::new_unchecked(Box::into_raw(Box::new(Node {
            front: None,
            back: None,
            elem,
        })));
        if let Some(old) = self.front {
            // Вставляем новый передний узел перед старым
            (*old).front = Some(new);
            (*new).back = Some(old);
        } else {
            // Если переднего узла нет, у нас пустой список и нам надо
            // установить и значение заднего узла. Кроме того, мы добавили
            // несколько проверок, на случай, если что-то напутали.
            debug_assert!(self.back.is_none());
            debug_assert!(self.front.is_none());
            debug_assert!(self.len == 0);
            self.back = Some(new);
        }
        self.front = Some(new);
        self.len += 1;
    }
}
error[E0614]: type `NonNull<Node<T>>` cannot be dereferenced
  --> src\lib.rs:39:17
   |
39 |                 (*old).front = Some(new);
   |                 ^^^^^^

Ах, да, именно поэтому я ненавижу своих детей, напичканных указателями. Нам нужно явно извлечь сырой указатель из NotNull с помощью as_ptr, потому что DerefMut определён через &mut, а мы не хотим вводить безопасные ссылки в наш небезопасный код!

            (*old.as_ptr()).front = Some(new);
            (*new.as_ptr()).back = Some(old);
   Compiling linked-list v0.0.3
warning: field is never read: `elem`
  --> src\lib.rs:16:5
   |
16 |     elem: T,
   |     ^^^^^^^
   |
   = note: `#[warn(dead_code)]` on by default

warning: `linked-list` (lib) generated 1 warning (1 duplicate)
warning: `linked-list` (lib test) generated 1 warning
    Finished test [unoptimized + debuginfo] target(s) in 0.33s

Прекрасно, теперь pop (и len):

pub fn pop_front(&mut self) -> Option<T> {
    unsafe {
        // Будем что-то делать, только если у списка есть передний узел.
        // Обратите внимание, что мы больше не должны беспокоиться
        // о `take`, потому что копирование сырых указателей не приводит к
        // вызову деструкторов, если мы что-то напутаем... правда? :)
        // Праааавда? :)))
        self.front.map(|node| {
            // Возвращаем к жизни Box, так что можно извлечь и уничтожить его
            // значение (Box магически делает это за нас).
            let boxed_node = Box::from_raw(node.as_ptr());
            let result = boxed_node.elem;

            // Делаем следующий узел новым передним узлом.
            self.front = boxed_node.back;
            if let Some(new) = self.front {
                // Убираем ссылку переднего узла на удалённый узел.
                (*new.as_ptr()).front = None;
            } else {
                // Если передний узел равен null, значит, список пустой!
                debug_assert!(self.len == 1);
                self.back = None;
            }

            self.len -= 1;
            result
            // Здесь Box неявным образом освобождается, потому что больше нет T.
        })
    }
}

pub fn len(&self) -> usize {
    self.len
}
   Compiling linked-list v0.0.3
    Finished dev [unoptimized + debuginfo] target(s) in 0.37s

Мне кажется, всё в порядке, пора писать тест!

#[cfg(test)]
mod test {
    use super::LinkedList;

    #[test]
    fn test_basic_front() {
        let mut list = LinkedList::new();

        // Пытаемся сломать пустой список
        assert_eq!(list.len(), 0);
        assert_eq!(list.pop_front(), None);
        assert_eq!(list.len(), 0);

        // Пытаемся сломать список из одного элемента
        list.push_front(10);
        assert_eq!(list.len(), 1);
        assert_eq!(list.pop_front(), Some(10));
        assert_eq!(list.len(), 0);
        assert_eq!(list.pop_front(), None);
        assert_eq!(list.len(), 0);

        // Всё перемешиваем
        list.push_front(10);
        assert_eq!(list.len(), 1);
        list.push_front(20);
        assert_eq!(list.len(), 2);
        list.push_front(30);
        assert_eq!(list.len(), 3);
        assert_eq!(list.pop_front(), Some(30));
        assert_eq!(list.len(), 2);
        list.push_front(40);
        assert_eq!(list.len(), 3);
        assert_eq!(list.pop_front(), Some(40));
        assert_eq!(list.len(), 2);
        assert_eq!(list.pop_front(), Some(20));
        assert_eq!(list.len(), 1);
        assert_eq!(list.pop_front(), Some(10));
        assert_eq!(list.len(), 0);
        assert_eq!(list.pop_front(), None);
        assert_eq!(list.len(), 0);
        assert_eq!(list.pop_front(), None);
        assert_eq!(list.len(), 0);
    }
}
   Compiling linked-list v0.0.3
    Finished test [unoptimized + debuginfo] target(s) in 0.40s
     Running unittests src\lib.rs

running 1 test
test test::test_basic_front ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

Ура, у нас всё идеально!

...Правда?

Метод drop и устойчивость к панике

А вы заметили этот комментарий:

#![allow(unused)]
fn main() {
// Обратите внимание, что мы больше не должны беспокоиться
// о `take`, потому что копирование сырых указателей не приводит к
// вызову деструкторов, если мы что-то напутаем... правда? :)
// Праааавда? :)))
}

Это правда?

Простите, вы забыли, какую книгу читаете? Конечно, нет! (В каком-то смысле.)

Снова заглянем внутрь pop_front:

// Возвращаем к жизни Box, так что можно извлечь и уничтожить его
// значение (Box магически делает это за нас).
let boxed_node = Box::from_raw(node.as_ptr());
let result = boxed_node.elem;

// Делаем следующий узел новым передним узлом.
self.front = boxed_node.back;
if let Some(new) = self.front {
    // Убираем ссылку переднего узла на удалённый узел.
    (*new.as_ptr()).front = None;
} else {
    // Если передний узел равен null, значит, список пустой!
    debug_assert!(self.len == 1);
    self.back = None;
}

self.len -= 1;
result
// Здесь Box неявным образом освобождается, потому что больше нет T.

Видите ошибку? Ужасно, но она в этой строчке:

debug_assert!(self.len == 1);

Серьёзно? Проклятая проверка целостности, сделанная для тестов — ошибка? Да!!! Впрочем, если коллекция реализована корректно, ошибка не должна возникнуть. Но может получиться так, что вполне безобидная штука вроде «некорректного обновления len» превратится в Уязвимость, вызванную ошибкой безопасности памяти! Почему? Потому что может возникнуть паника! Большую часть времени вам не стоит беспокоиться о панике, но однажды вы начинаете писать поистине небезопасный код и вольно обращаетесь с «инвариантностью», и вам приходится проявлять повышенную бдительность!

Нам нужно поговорить об устойчивости к исключениям (ИЛИ устойчивости к панике, ИЛИ устойчивости к раскрутке стека...).

Итак, паника по умолчанию приводит к раскрутке стека (unwinding). Раскрутка — это красивый способ сказать «немедленно завершить каждую функцию». Вы можете подумать «Ладно, если всё завершается, значит, программа тоже завершается, так что не о чем беспокоиться?», но вы не правы!

Мы должны беспокоиться по двум причинам: из-за запуска деструкторов, и возможного перехвата раскрутки. В обоих случаях, код будет выполняться после паники, так что нам надо быть очень осторожными и убедиться, что наши небезопасные коллекции находятся в каком-то когерентном состоянии независимо от паники, потому что паника неявно означает ранний возврат (early return)!

Подумайте, в каком состоянии находится коллекция, когда мы доходим до строки с ошибкой.

У нас есть boxed_node на стеке, и мы извлекли из него элемент. Если бы мы завершили функцию в этой точке, Box должен был быть уничтожен, и узел должен был быть освобождён. Теперь вы видите?.. self.back всё ещё указывает на освобождённый узел! Как только мы реализуем оставшиеся функции и начнём использовать self.back для разных вещей, это приведёт к ошибке использование-после-освобождения (use-after-free)! Кошмар!

Интересно, что в этой строке встречается похожая проблема, но гораздо более безопасная:

self.len -= 1;

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

Я уже несколько раз употребляла термин «инвариант», а всё потому, что это действительно полезная концепция для разговора про устойчивость к панике! В каком-то смысле, для стороннего наблюдателя у нашей коллекции есть определённые свойства, которые всегда соблюдаются. Для связного списка одним из таких свойств является следующее: для любого достижимого узла в нашем списке выделена и проинициализирована память.

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

Возможно, наилучшим воплощением этой идеи является Vec::drain, который, фактически, позволяет вам полностью нарушить основной инвариант Vec и начать извлекать значения не только из начала Vec, но и из середины. Причина, по которой это правильно заключается в том, что итератор Drain, который мы возвращаем, хранит &mut на Vec, поэтому любой доступ к нему ограничен! Никто не сможет пользоваться Vec до тех пор, пока итератор Drain не завершится, и тогда деструктор «восстановит» Vec до того, как кто-либо это заметит, что идеа—

Нет, это не идеально. К сожалению вы не можете полагаться на выполнение деструкторов в коде, который вы не контролируете, так что даже с Drain нам нужно делать небольшую дополнительную работу, чтобы инварианты нашего типа всегда сохранялись, но в несколько нелепой манере: в начале мы просто сбрасываем длину Vec в 0, так что если у кого-то утечёт Drain, он получит безопасный Vec... потеряв при этом все данные. Ты устроил утечку мне? Я устрою утечку тебе! Око за око! Истинная справедливость!

Чтобы разобраться, когда для устойчивости при панике действительно можно использовать деструкторы, познакомьтесь с учебным примером использования BinaryHeap::sift_up.

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

В этом случае у нас есть две возможности сделать код надёжнее:

  • Чаще использовать такие операции, как Option::take, потому что они более «транзакционные» и имеют тенденцию сохранять инварианты.
  • Удалить все debug_assert и доверять себе в написании лучших тестов со специальными функциями «проверки целостности», которые никогда не будут выполняться в пользовательском коде.

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

(Возможно про них следует думать, как о предусловиях и постусловиях, но на самом деле к ним правильнее относиться, как к инвариантам!)

Вот наша полная текущая реализация:

#![allow(unused)]
fn main() {
use std::ptr::NonNull;
use std::marker::PhantomData;

pub struct LinkedList<T> {
    front: Link<T>,
    back: Link<T>,
    len: usize,
    _boo: PhantomData<T>,
}

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

struct Node<T> {
    front: Link<T>,
    back: Link<T>,
    elem: T, 
}

impl<T> LinkedList<T> {
    pub fn new() -> Self {
        Self {
            front: None,
            back: None,
            len: 0,
            _boo: PhantomData,
        }
    }

    pub fn push_front(&mut self, elem: T) {
        // БЕЗОПАСНОСТЬ: это связный список, что вы делаете?
        unsafe {
            let new = NonNull::new_unchecked(Box::into_raw(Box::new(Node {
                front: None,
                back: None,
                elem,
            })));
            if let Some(old) = self.front {
                // Вставляем новый передний узел перед старым
                (*old.as_ptr()).front = Some(new);
                (*new.as_ptr()).back = Some(old);
            } else {
                // Если переднего узла нет, у нас пустой список и нам надо
                // установить и значение заднего узла. Кроме того, мы добавили
                // несколько проверок, на случай, если что-то напутали.
                self.back = Some(new);
            }
            // Этот код выполняется всегда!
            self.front = Some(new);
            self.len += 1;
        }
    }

    pub fn pop_front(&mut self) -> Option<T> {
        unsafe {
            // Будем что-то делать, только если у списка есть передний узел.
            self.front.map(|node| {
                // Возвращаем к жизни Box, так что можно извлечь и уничтожить его
                // значение (Box магически делает это за нас).
                let boxed_node = Box::from_raw(node.as_ptr());
                let result = boxed_node.elem;

                // Делаем следующий узел новым передним узлом.
                self.front = boxed_node.back;
                if let Some(new) = self.front {
                    // Убираем ссылку переднего узла на удалённый узел.
                    (*new.as_ptr()).front = None;
                } else {
                    // Если передний узел равен null, значит, список пустой!
                    self.back = None;
                }

                self.len -= 1;
                result
                // Здесь Box неявным образом освобождается, потому что больше нет T.
            })
        }
    }

    pub fn len(&self) -> usize {
        self.len
    }
}
}

Что здесь может привести к панике? Чтобы ответить на этот вопрос, надо быть экспертом по Rust, но, к счастью, я им являюсь!

Единственные места, которые я вижу, где код может вызвать панику (за исключением совершенно безумной идеи перекомпилировать стандартную библиотеку с включенными debug_assert, чего никогда не следует делать) — это Box::new (в случае нехватки памяти) и арифметические операции с len. Всё это находится или в самом конце или в самом начале наших методов, так что да, наш код красивый и безопасный!

...удивились, что Bos::new может вызывать панику? Паника может достать вас самым непредсказуемым образом! Держите свои инварианты в целости, чтобы вас это не беспокоило!

Скучная комбинаторика

Ладно, возвращаемся к нашим ежедневным связным спискам!

Для начала разберёмся с Drop, который тривиально реализуется с помощью pop:

impl<T> Drop for LinkedList<T> {
    fn drop(&mut self) {
        // Вызываем pop, пока есть элементы
        while let Some(_) = self.pop_front() { }
    }
}

Нам предстоит написать множество действительно скучных комбинаторных реализаций, таких как front, front_mut, back, back_mut, iter, iter_mut, into_iter, ...

Можно было бы воспользоваться макросами или чем то подобным, но, честно говоря, это хуже, чем обычная копи-паста. Нам предстоит много копи-пасты. Я очень тщательно подошла к реализации предыдущих push/pop. Нам осталось буквально поменять местами front и back, и код будет работать! Да здравствует болезненный опыт! (Заманчиво говорить о «предыдущем и следующем» узлах, но я обнаружила, что термины «передний» и «задний» ведут к большей согласованности и позволяют избегать ошибок.)

Хорошо, начнём с front:

pub fn front(&self) -> Option<&T> {
    unsafe {
        self.front.map(|node| &(*node.as_ptr()).elem)
    }
}

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

pub fn front(&self) -> Option<&T> {
    unsafe {
        Some(&(*self.front?.as_ptr()).elem)
    }
}

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

pub fn front_mut(&mut self) -> Option<&mut T> {
    unsafe {
        self.front.map(|node| &mut (*node.as_ptr()).elem)
    }
}

Чуть позже я добавлю все версии back.

Далее, итераторы. В отличие от наших прошлых списков, мы наконец можем реализовать [DoubleEndedIterator](https://doc.rust-lang.org/std/iter/trait.DoubleEndedIterator.html, и, поскольку мы вышли на продуктовый уровень — и ExactSizeIterator.

Таким образом, помимо next и size_hint, мы реализуем next_back и len.

Самые внимательные читатели могут заметить, что IterMut в случае итерации с двумя концами выглядит гораздо подозрительнее, но но самом деле он всё ещё работает!

...боже, будет тонна рутинного кода. Возможно, мне следует написать макрос... нет, нет, это всё ещё плохая идея.

pub struct Iter<'a, T> {
    front: Link<T>,
    back: Link<T>,
    len: usize,
    _boo: PhantomData<&'a T>,
}

impl<T> LinkedList<T> {
    pub fn iter(&self) -> Iter<T> {
        Iter { 
            front: self.front, 
            back: self.back,
            len: self.len,
            _boo: PhantomData,
        }
    }
}

impl<'a, T> IntoIterator for &'a LinkedList<T> {
    type IntoIter = Iter<'a, T>;
    type Item = &'a T;

    fn into_iter(self) -> Self::IntoIter {
        self.iter()
    }
}

impl<'a, T> Iterator for Iter<'a, T> {
    type Item = &'a T;
    
    fn next(&mut self) -> Option<Self::Item> {
        // Хотя условие self.front == self.back кажется очевидным,
        // оно не подходит для возврата последнего элемента! Подобный код
        // работает только с массивами из-за указателя «на элемент, следующий
        // за последним»
        if self.len > 0 {
            // Мы могли бы извлечь значение из front, но так быстрее и безопаснее
            self.front.map(|node| unsafe {
                self.len -= 1;
                self.front = (*node.as_ptr()).back;
                &(*node.as_ptr()).elem
            })
        } else {
            None
        }
    }

    fn size_hint(&self) -> (usize, Option<usize>) {
        (self.len, Some(self.len))
    }
}

impl<'a, T> DoubleEndedIterator for Iter<'a, T> {
    fn next_back(&mut self) -> Option<Self::Item> {
        if self.len > 0 {
            self.back.map(|node| unsafe {
                self.len -= 1;
                self.back = (*node.as_ptr()).front;
                &(*node.as_ptr()).elem
            })
        } else {
            None
        }
    }
}

impl<'a, T> ExactSizeIterator for Iter<'a, T> {
    fn len(&self) -> usize {
        self.len
    }
}

...это всего лишь .iter()...

Позже мы добавим IterMut. Будет буквально тот же код, но с большим количеством mut. Для начала разберёмся с into_iter. К счастью, мы всё ещё можем положиться на проверенное решение: завернуть нашу коллекцию и использовать pop для next:

pub struct IntoIter<T> {
    list: LinkedList<T>,
}

impl<T> LinkedList<T> {
    pub fn into_iter(self) -> IntoIter<T> {
        IntoIter { 
            list: self
        }
    }
}


impl<T> IntoIterator for LinkedList<T> {
    type IntoIter = IntoIter<T>;
    type Item = T;

    fn into_iter(self) -> Self::IntoIter {
        self.into_iter()
    }
}

impl<T> Iterator for IntoIter<T> {
    type Item = T;

    fn next(&mut self) -> Option<Self::Item> {
        self.list.pop_front()
    }

    fn size_hint(&self) -> (usize, Option<usize>) {
        (self.list.len, Some(self.list.len))
    }
}

impl<T> DoubleEndedIterator for IntoIter<T> {
    fn next_back(&mut self) -> Option<Self::Item> {
        self.list.pop_back()
    }
}

impl<T> ExactSizeIterator for IntoIter<T> {
    fn len(&self) -> usize {
        self.list.len
    }
}

Всё ещё много рутинного кода, но, по крайней мере, это удовлетворительный рутинный код.

Хорошо, вот весь наш код со всеми комбинаторными вариантами:

#![allow(unused)]
fn main() {
use std::ptr::NonNull;
use std::marker::PhantomData;

pub struct LinkedList<T> {
    front: Link<T>,
    back: Link<T>,
    len: usize,
    _boo: PhantomData<T>,
}

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

struct Node<T> {
    front: Link<T>,
    back: Link<T>,
    elem: T, 
}

pub struct Iter<'a, T> {
    front: Link<T>,
    back: Link<T>,
    len: usize,
    _boo: PhantomData<&'a T>,
}

pub struct IterMut<'a, T> {
    front: Link<T>,
    back: Link<T>,
    len: usize,
    _boo: PhantomData<&'a mut T>,
}

pub struct IntoIter<T> {
    list: LinkedList<T>,
}

impl<T> LinkedList<T> {
    pub fn new() -> Self {
        Self {
            front: None,
            back: None,
            len: 0,
            _boo: PhantomData,
        }
    }

    pub fn push_front(&mut self, elem: T) {
        // БЕЗОПАСНОСТЬ: это связный список, что вы делаете?
        unsafe {
            let new = NonNull::new_unchecked(Box::into_raw(Box::new(Node {
                front: None,
                back: None,
                elem,
            })));
            if let Some(old) = self.front {
                // Вставляем новый передний узел перед старым
                (*old.as_ptr()).front = Some(new);
                (*new.as_ptr()).back = Some(old);
            } else {
                // Если переднего узла нет, у нас пустой список и нам надо
                // установить и значение заднего узла.
                self.back = Some(new);
            }
            // Этот код выполняется всегда!
            self.front = Some(new);
            self.len += 1;
        }
    }

    pub fn push_back(&mut self, elem: T) {
        // БЕЗОПАСНОСТЬ: это связный список, что вы делаете?
        unsafe {
            let new = NonNull::new_unchecked(Box::into_raw(Box::new(Node {
                back: None,
                front: None,
                elem,
            })));
            if let Some(old) = self.back {
                // Вставляем новый задний узел перед старым
                (*old.as_ptr()).back = Some(new);
                (*new.as_ptr()).front = Some(old);
            } else {
                // Если заднего узла нет, у нас пустой список и нам надо
                // установить и значение переднего узла.
                self.front = Some(new);
            }
            // Этот код выполняется всегда!
            self.back = Some(new);
            self.len += 1;
        }
    }

    pub fn pop_front(&mut self) -> Option<T> {
        unsafe {
            // Будем что-то делать, только если у списка есть передний узел.
            self.front.map(|node| {
                // Возвращаем к жизни Box, так что можно извлечь и уничтожить его
                // значение (Box магически делает это за нас).
                let boxed_node = Box::from_raw(node.as_ptr());
                let result = boxed_node.elem;

                // Делаем следующий узел новым передним узлом.
                self.front = boxed_node.back;
                if let Some(new) = self.front {
                    // Убираем ссылку переднего узла на удалённый узел.
                    (*new.as_ptr()).front = None;
                } else {
                    // Если передний узел равен null, значит, список пустой!
                    self.back = None;
                }

                self.len -= 1;
                result
                // Здесь Box неявным образом освобождается, потому что больше нет T.
            })
        }
    }

    pub fn pop_back(&mut self) -> Option<T> {
        unsafe {
            // Будем что-то делать, только если у списка есть задний узел.
            self.back.map(|node| {
                // Возвращаем к жизни Box, так что можно извлечь и уничтожить его
                // значение (Box магически делает это за нас).
                let boxed_node = Box::from_raw(node.as_ptr());
                let result = boxed_node.elem;

                // Делаем следующий узел новым задним узлом.
                self.back = boxed_node.front;
                if let Some(new) = self.back {
                    // Убираем ссылку заднего узла на удалённый узел.
                    (*new.as_ptr()).back = None;
                } else {
                    // Если задний узел равен null, значит, список пустой!
                    self.front = None;
                }

                self.len -= 1;
                result
                // Здесь Box неявным образом освобождается, потому что больше нет T.
            })
        }
    }

    pub fn front(&self) -> Option<&T> {
        unsafe {
            self.front.map(|node| &(*node.as_ptr()).elem)
        }
    }

    pub fn front_mut(&mut self) -> Option<&mut T> {
        unsafe {
            self.front.map(|node| &mut (*node.as_ptr()).elem)
        }
    }

    pub fn back(&self) -> Option<&T> {
        unsafe {
            self.back.map(|node| &(*node.as_ptr()).elem)
        }
    }

    pub fn back_mut(&mut self) -> Option<&mut T> {
        unsafe {
            self.back.map(|node| &mut (*node.as_ptr()).elem)
        }
    }

    pub fn len(&self) -> usize {
        self.len
    }

    pub fn iter(&self) -> Iter<T> {
        Iter { 
            front: self.front, 
            back: self.back,
            len: self.len,
            _boo: PhantomData,
        }
    }

    pub fn iter_mut(&mut self) -> IterMut<T> {
        IterMut { 
            front: self.front, 
            back: self.back,
            len: self.len,
            _boo: PhantomData,
        }
    }

    pub fn into_iter(self) -> IntoIter<T> {
        IntoIter { 
            list: self
        }
    }
}

impl<T> Drop for LinkedList<T> {
    fn drop(&mut self) {
        // Вызываем pop, пока есть элементы
        while let Some(_) = self.pop_front() { }
    }
}

impl<'a, T> IntoIterator for &'a LinkedList<T> {
    type IntoIter = Iter<'a, T>;
    type Item = &'a T;

    fn into_iter(self) -> Self::IntoIter {
        self.iter()
    }
}

impl<'a, T> Iterator for Iter<'a, T> {
    type Item = &'a T;

    fn next(&mut self) -> Option<Self::Item> {
        // Хотя условие self.front == self.back кажется очевидным,
        // оно не подходит для возврата последнего элемента! Подобный код
        // работает только с массивами из-за указателя «на элемент, следующий
        // за последним»
        if self.len > 0 {
            // Мы могли бы извлечь значение из front, но так быстрее и безопаснее
            self.front.map(|node| unsafe {
                self.len -= 1;
                self.front = (*node.as_ptr()).back;
                &(*node.as_ptr()).elem
            })
        } else {
            None
        }
    }

    fn size_hint(&self) -> (usize, Option<usize>) {
        (self.len, Some(self.len))
    }
}

impl<'a, T> DoubleEndedIterator for Iter<'a, T> {
    fn next_back(&mut self) -> Option<Self::Item> {
        if self.len > 0 {
            self.back.map(|node| unsafe {
                self.len -= 1;
                self.back = (*node.as_ptr()).front;
                &(*node.as_ptr()).elem
            })
        } else {
            None
        }
    }
}

impl<'a, T> ExactSizeIterator for Iter<'a, T> {
    fn len(&self) -> usize {
        self.len
    }
}

impl<'a, T> IntoIterator for &'a mut LinkedList<T> {
    type IntoIter = IterMut<'a, T>;
    type Item = &'a mut T;

    fn into_iter(self) -> Self::IntoIter {
        self.iter_mut()
    }
}

impl<'a, T> Iterator for IterMut<'a, T> {
    type Item = &'a mut T;

    fn next(&mut self) -> Option<Self::Item> {
        // Хотя условие self.front == self.back кажется очевидным,
        // оно не подходит для возврата последнего элемента! Подобный код
        // работает только с массивами из-за указателя «на элемент, следующий
        // за последним»
        if self.len > 0 {
            // Мы могли бы извлечь значение из front, но так быстрее и безопаснее
            self.front.map(|node| unsafe {
                self.len -= 1;
                self.front = (*node.as_ptr()).back;
                &mut (*node.as_ptr()).elem
            })
        } else {
            None
        }
    }

    fn size_hint(&self) -> (usize, Option<usize>) {
        (self.len, Some(self.len))
    }
}

impl<'a, T> DoubleEndedIterator for IterMut<'a, T> {
    fn next_back(&mut self) -> Option<Self::Item> {
        if self.len > 0 {
            self.back.map(|node| unsafe {
                self.len -= 1;
                self.back = (*node.as_ptr()).front;
                &mut (*node.as_ptr()).elem
            })
        } else {
            None
        }
    }
}

impl<'a, T> ExactSizeIterator for IterMut<'a, T> {
    fn len(&self) -> usize {
        self.len
    }
}

impl<T> IntoIterator for LinkedList<T> {
    type IntoIter = IntoIter<T>;
    type Item = T;

    fn into_iter(self) -> Self::IntoIter {
        self.into_iter()
    }
}

impl<T> Iterator for IntoIter<T> {
    type Item = T;

    fn next(&mut self) -> Option<Self::Item> {
        self.list.pop_front()
    }

    fn size_hint(&self) -> (usize, Option<usize>) {
        (self.list.len, Some(self.list.len))
    }
}

impl<T> DoubleEndedIterator for IntoIter<T> {
    fn next_back(&mut self) -> Option<Self::Item> {
        self.list.pop_back()
    }
}

impl<T> ExactSizeIterator for IntoIter<T> {
    fn len(&self) -> usize {
        self.list.len
    }
}


#[cfg(test)]
mod test {
    use super::LinkedList;

    #[test]
    fn test_basic_front() {
        let mut list = LinkedList::new();

        // Пытаемся сломать пустой список
        assert_eq!(list.len(), 0);
        assert_eq!(list.pop_front(), None);
        assert_eq!(list.len(), 0);

        // Пытаемся сломать список из одного элемента
        list.push_front(10);
        assert_eq!(list.len(), 1);
        assert_eq!(list.pop_front(), Some(10));
        assert_eq!(list.len(), 0);
        assert_eq!(list.pop_front(), None);
        assert_eq!(list.len(), 0);

        // Всё перемешиваем
        list.push_front(10);
        assert_eq!(list.len(), 1);
        list.push_front(20);
        assert_eq!(list.len(), 2);
        list.push_front(30);
        assert_eq!(list.len(), 3);
        assert_eq!(list.pop_front(), Some(30));
        assert_eq!(list.len(), 2);
        list.push_front(40);
        assert_eq!(list.len(), 3);
        assert_eq!(list.pop_front(), Some(40));
        assert_eq!(list.len(), 2);
        assert_eq!(list.pop_front(), Some(20));
        assert_eq!(list.len(), 1);
        assert_eq!(list.pop_front(), Some(10));
        assert_eq!(list.len(), 0);
        assert_eq!(list.pop_front(), None);
        assert_eq!(list.len(), 0);
        assert_eq!(list.pop_front(), None);
        assert_eq!(list.len(), 0);
    }
}
}

Всякая всячина

Вы говорили, что хотите продуктового кода, нет?

Вот немного всякой всячины, чтобы получилась «хорошая» коллекция:

impl<T> LinkedList<T> {
    pub fn is_empty(&self) -> bool {
        self.len == 0
    }

    pub fn clear(&mut self) {
        // О, смотрите, это снова drop
        while let Some(_) = self.pop_front() { }
    }
}

Теперь реализуем набор типажей, которые должны быть у коллекции:

impl<T> Default for LinkedList<T> {
    fn default() -> Self {
        Self::new()
    }
}

impl<T: Clone> Clone for LinkedList<T> {
    fn clone(&self) -> Self {
        let mut new_list = Self::new();
        for item in self {
            new_list.push_back(item.clone());
        }
        new_list
    }
}

impl<T> Extend<T> for LinkedList<T> {
    fn extend<I: IntoIterator<Item = T>>(&mut self, iter: I) {
        for item in iter {
            self.push_back(item);
        }
    }
}

impl<T> FromIterator<T> for LinkedList<T> {
    fn from_iter<I: IntoIterator<Item = T>>(iter: I) -> Self {
        let mut list = Self::new();
        list.extend(iter);
        list
    }
}

impl<T: Debug> Debug for LinkedList<T> {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.debug_list().entries(self).finish()
    }
}

impl<T: PartialEq> PartialEq for LinkedList<T> {
    fn eq(&self, other: &Self) -> bool {
        self.len() == other.len() && self.iter().eq(other)
    }

    fn ne(&self, other: &Self) -> bool {
        self.len() != other.len() || self.iter().ne(other)
    }
}

impl<T: Eq> Eq for LinkedList<T> { }

impl<T: PartialOrd> PartialOrd for LinkedList<T> {
    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
        self.iter().partial_cmp(other)
    }
}

impl<T: Ord> Ord for LinkedList<T> {
    fn cmp(&self, other: &Self) -> Ordering {
        self.iter().cmp(other)
    }
}

impl<T: Hash> Hash for LinkedList<T> {
    fn hash<H: Hasher>(&self, state: &mut H) {
        self.len().hash(state);
        for item in self {
            item.hash(state);
        }
    }
}

Я написала весь код с нуля, а не просто скопировала из стандартной библиотеки. Здесь всё такое интересное и я определённо помню все тонкости ручной реализации Hash. Да, я постоянно думаю об этом...

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

Во-первых, неприятный конфликт пространств имён. По какой-то причине в стандартной библиотеке теперь есть макросы с именами Hash и Debug, так что если вы не импортировали типажи, вы получите поистине таинственные ошибки о макросах вместо простого «типаж не найден».

Другая интересная вещь касается самого Hash. Вы видите, что мы вызываем hash у len? Это на самом деле важно! Если коллекции не учитывают свою длину при вычислении хеша, они могут случайно стать уязвимыми для коллизии префиксов. Например, чем отличаются ["he", "llo"] и ["hello"]? Если при вычислении хеша вы не используете длину или какой-то разделитель, то ничем! Слишком высокая вероятность коллизий при вычислении хеша может привести к большим неприятностям, так что просто напишите правильно!

Хорошо, вот наш текущий код:

#![allow(unused)]
fn main() {
use std::cmp::Ordering;
use std::fmt::{self, Debug};
use std::hash::{Hash, Hasher};
use std::iter::FromIterator;
use std::ptr::NonNull;
use std::marker::PhantomData;

pub struct LinkedList<T> {
    front: Link<T>,
    back: Link<T>,
    len: usize,
    _boo: PhantomData<T>,
}

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

struct Node<T> {
    front: Link<T>,
    back: Link<T>,
    elem: T, 
}

pub struct Iter<'a, T> {
    front: Link<T>,
    back: Link<T>,
    len: usize,
    _boo: PhantomData<&'a T>,
}

pub struct IterMut<'a, T> {
    front: Link<T>,
    back: Link<T>,
    len: usize,
    _boo: PhantomData<&'a mut T>,
}

pub struct IntoIter<T> {
    list: LinkedList<T>,
}

impl<T> LinkedList<T> {
    pub fn new() -> Self {
        Self {
            front: None,
            back: None,
            len: 0,
            _boo: PhantomData,
        }
    }

    pub fn push_front(&mut self, elem: T) {
        // БЕЗОПАСНОСТЬ: это связный список, что вы делаете?
        unsafe {
            let new = NonNull::new_unchecked(Box::into_raw(Box::new(Node {
                front: None,
                back: None,
                elem,
            })));
            if let Some(old) = self.front {
                // Вставляем новый передний узел перед старым
                (*old.as_ptr()).front = Some(new);
                (*new.as_ptr()).back = Some(old);
            } else {
                // Если переднего узла нет, у нас пустой список и нам надо
                // установить и значение заднего узла.
                debug_assert!(self.back.is_none());
                debug_assert!(self.front.is_none());
                debug_assert!(self.len == 0);
                self.back = Some(new);
            }
            // Этот код выполняется всегда!
            self.front = Some(new);
            self.len += 1;
        }
    }

    pub fn push_back(&mut self, elem: T) {
        // БЕЗОПАСНОСТЬ: это связный список, что вы делаете?
        unsafe {
            let new = NonNull::new_unchecked(Box::into_raw(Box::new(Node {
                back: None,
                front: None,
                elem,
            })));
            if let Some(old) = self.back {
                // Вставляем новый задний узел перед старым
                (*old.as_ptr()).back = Some(new);
                (*new.as_ptr()).front = Some(old);
            } else {
                // Если заднего узла нет, у нас пустой список и нам надо
                // установить и значение переднего узла.
                self.front = Some(new);
            }
            // Этот код выполняется всегда!
            self.back = Some(new);
            self.len += 1;
        }
    }

    pub fn pop_front(&mut self) -> Option<T> {
        unsafe {
            // Будем что-то делать, только если у списка есть передний узел.
            self.front.map(|node| {
                // Возвращаем к жизни Box, так что можно извлечь и уничтожить его
                // значение (Box магически делает это за нас).
                let boxed_node = Box::from_raw(node.as_ptr());
                let result = boxed_node.elem;

                // Делаем следующий узел новым передним узлом.
                self.front = boxed_node.back;
                if let Some(new) = self.front {
                    // Убираем ссылку переднего узла на удалённый узел.
                    (*new.as_ptr()).front = None;
                } else {
                    // Если передний узел равен null, значит, список пустой!
                    debug_assert!(self.len == 1);
                    self.back = None;
                }

                self.len -= 1;
                result
                // Здесь Box неявным образом освобождается, потому что больше нет T.
            })
        }
    }

    pub fn pop_back(&mut self) -> Option<T> {
        unsafe {
            // Будем что-то делать, только если у списка есть задний узел.
            self.back.map(|node| {
                // Возвращаем к жизни Box, так что можно извлечь и уничтожить его
                // значение (Box магически делает это за нас).
                let boxed_node = Box::from_raw(node.as_ptr());
                let result = boxed_node.elem;

                // Делаем следующий узел новым задним узлом.
                self.back = boxed_node.front;
                if let Some(new) = self.back {
                    // Убираем ссылку заднего узла на удалённый узел.
                    (*new.as_ptr()).back = None;
                } else {
                    // Если задний узел равен null, значит, список пустой!
                    self.front = None;
                }

                self.len -= 1;
                result
                // Здесь Box неявным образом освобождается, потому что больше нет T.
            })
        }
    }

    pub fn front(&self) -> Option<&T> {
        unsafe {
            self.front.map(|node| &(*node.as_ptr()).elem)
        }
    }

    pub fn front_mut(&mut self) -> Option<&mut T> {
        unsafe {
            self.front.map(|node| &mut (*node.as_ptr()).elem)
        }
    }

    pub fn back(&self) -> Option<&T> {
        unsafe {
            self.back.map(|node| &(*node.as_ptr()).elem)
        }
    }

    pub fn back_mut(&mut self) -> Option<&mut T> {
        unsafe {
            self.back.map(|node| &mut (*node.as_ptr()).elem)
        }
    }

    pub fn len(&self) -> usize {
        self.len
    }

    pub fn is_empty(&self) -> bool {
        self.len == 0
    }

    pub fn clear(&mut self) {
        // Oh look it's drop again
        while let Some(_) = self.pop_front() { }
    }

    pub fn iter(&self) -> Iter<T> {
        Iter { 
            front: self.front, 
            back: self.back,
            len: self.len,
            _boo: PhantomData,
        }
    }

    pub fn iter_mut(&mut self) -> IterMut<T> {
        IterMut { 
            front: self.front, 
            back: self.back,
            len: self.len,
            _boo: PhantomData,
        }
    }

    pub fn into_iter(self) -> IntoIter<T> {
        IntoIter { 
            list: self
        }
    }
}

impl<T> Drop for LinkedList<T> {
    fn drop(&mut self) {
        // Вызываем pop, пока есть элементы
        while let Some(_) = self.pop_front() { }
    }
}

impl<T> Default for LinkedList<T> {
    fn default() -> Self {
        Self::new()
    }
}

impl<T: Clone> Clone for LinkedList<T> {
    fn clone(&self) -> Self {
        let mut new_list = Self::new();
        for item in self {
            new_list.push_back(item.clone());
        }
        new_list
    }
}

impl<T> Extend<T> for LinkedList<T> {
    fn extend<I: IntoIterator<Item = T>>(&mut self, iter: I) {
        for item in iter {
            self.push_back(item);
        }
    }
}

impl<T> FromIterator<T> for LinkedList<T> {
    fn from_iter<I: IntoIterator<Item = T>>(iter: I) -> Self {
        let mut list = Self::new();
        list.extend(iter);
        list
    }
}

impl<T: Debug> Debug for LinkedList<T> {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.debug_list().entries(self).finish()
    }
}

impl<T: PartialEq> PartialEq for LinkedList<T> {
    fn eq(&self, other: &Self) -> bool {
        self.len() == other.len() && self.iter().eq(other)
    }

    fn ne(&self, other: &Self) -> bool {
        self.len() != other.len() || self.iter().ne(other)
    }
}

impl<T: Eq> Eq for LinkedList<T> { }

impl<T: PartialOrd> PartialOrd for LinkedList<T> {
    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
        self.iter().partial_cmp(other)
    }
}

impl<T: Ord> Ord for LinkedList<T> {
    fn cmp(&self, other: &Self) -> Ordering {
        self.iter().cmp(other)
    }
}

impl<T: Hash> Hash for LinkedList<T> {
    fn hash<H: Hasher>(&self, state: &mut H) {
        self.len().hash(state);
        for item in self {
            item.hash(state);
        }
    }
}

impl<'a, T> IntoIterator for &'a LinkedList<T> {
    type IntoIter = Iter<'a, T>;
    type Item = &'a T;

    fn into_iter(self) -> Self::IntoIter {
        self.iter()
    }
}

impl<'a, T> Iterator for Iter<'a, T> {
    type Item = &'a T;

    fn next(&mut self) -> Option<Self::Item> {
        // Хотя условие self.front == self.back кажется очевидным,
        // оно не подходит для возврата последнего элемента! Подобный код
        // работает только с массивами из-за указателя «на элемент, следующий
        // за последним»
        if self.len > 0 {
            // Мы могли бы извлечь значение из front, но так быстрее и безопаснее
            self.front.map(|node| unsafe {
                self.len -= 1;
                self.front = (*node.as_ptr()).back;
                &(*node.as_ptr()).elem
            })
        } else {
            None
        }
    }

    fn size_hint(&self) -> (usize, Option<usize>) {
        (self.len, Some(self.len))
    }
}

impl<'a, T> DoubleEndedIterator for Iter<'a, T> {
    fn next_back(&mut self) -> Option<Self::Item> {
        if self.len > 0 {
            self.back.map(|node| unsafe {
                self.len -= 1;
                self.back = (*node.as_ptr()).front;
                &(*node.as_ptr()).elem
            })
        } else {
            None
        }
    }
}

impl<'a, T> ExactSizeIterator for Iter<'a, T> {
    fn len(&self) -> usize {
        self.len
    }
}

impl<'a, T> IntoIterator for &'a mut LinkedList<T> {
    type IntoIter = IterMut<'a, T>;
    type Item = &'a mut T;

    fn into_iter(self) -> Self::IntoIter {
        self.iter_mut()
    }
}

impl<'a, T> Iterator for IterMut<'a, T> {
    type Item = &'a mut T;

    fn next(&mut self) -> Option<Self::Item> {
        // Хотя условие self.front == self.back кажется очевидным,
        // оно не подходит для возврата последнего элемента! Подобный код
        // работает только с массивами из-за указателя «на элемент, следующий
        // за последним»
        if self.len > 0 {
            // Мы могли бы извлечь значение из front, но так быстрее и безопаснее
            self.front.map(|node| unsafe {
                self.len -= 1;
                self.front = (*node.as_ptr()).back;
                &mut (*node.as_ptr()).elem
            })
        } else {
            None
        }
    }

    fn size_hint(&self) -> (usize, Option<usize>) {
        (self.len, Some(self.len))
    }
}

impl<'a, T> DoubleEndedIterator for IterMut<'a, T> {
    fn next_back(&mut self) -> Option<Self::Item> {
        if self.len > 0 {
            self.back.map(|node| unsafe {
                self.len -= 1;
                self.back = (*node.as_ptr()).front;
                &mut (*node.as_ptr()).elem
            })
        } else {
            None
        }
    }
}

impl<'a, T> ExactSizeIterator for IterMut<'a, T> {
    fn len(&self) -> usize {
        self.len
    }
}

impl<T> Iterator for IntoIter<T> {
    type Item = T;

    fn next(&mut self) -> Option<Self::Item> {
        self.list.pop_front()
    }

    fn size_hint(&self) -> (usize, Option<usize>) {
        (self.list.len, Some(self.list.len))
    }
}

impl<T> DoubleEndedIterator for IntoIter<T> {
    fn next_back(&mut self) -> Option<Self::Item> {
        self.list.pop_back()
    }
}

impl<T> ExactSizeIterator for IntoIter<T> {
    fn len(&self) -> usize {
        self.list.len
    }
}

#[cfg(test)]
mod test {
    use super::LinkedList;

    #[test]
    fn test_basic_front() {
        let mut list = LinkedList::new();

        // Пытаемся сломать пустой список
        assert_eq!(list.len(), 0);
        assert_eq!(list.pop_front(), None);
        assert_eq!(list.len(), 0);

        // Пытаемся сломать список из одного элемента
        list.push_front(10);
        assert_eq!(list.len(), 1);
        assert_eq!(list.pop_front(), Some(10));
        assert_eq!(list.len(), 0);
        assert_eq!(list.pop_front(), None);
        assert_eq!(list.len(), 0);

        // Всё перемешиваем
        list.push_front(10);
        assert_eq!(list.len(), 1);
        list.push_front(20);
        assert_eq!(list.len(), 2);
        list.push_front(30);
        assert_eq!(list.len(), 3);
        assert_eq!(list.pop_front(), Some(30));
        assert_eq!(list.len(), 2);
        list.push_front(40);
        assert_eq!(list.len(), 3);
        assert_eq!(list.pop_front(), Some(40));
        assert_eq!(list.len(), 2);
        assert_eq!(list.pop_front(), Some(20));
        assert_eq!(list.len(), 1);
        assert_eq!(list.pop_front(), Some(10));
        assert_eq!(list.len(), 0);
        assert_eq!(list.pop_front(), None);
        assert_eq!(list.len(), 0);
        assert_eq!(list.pop_front(), None);
        assert_eq!(list.len(), 0);
    }
}
}

Тестирование

Я отложила тестирования на потом, потому что, ну, теперь мы оба знаем, что мы мастера Rust и больше не допускаем ошибок! Кроме того, это переписанная версия старого крейта, так что у меня уже были все тесты. Вы их уже видели, и не раз. Вот они:

#[cfg(test)]
mod test {
    use super::LinkedList;

    fn generate_test() -> LinkedList<i32> {
        list_from(&[0, 1, 2, 3, 4, 5, 6])
    }

    fn list_from<T: Clone>(v: &[T]) -> LinkedList<T> {
        v.iter().map(|x| (*x).clone()).collect()
    }

    #[test]
    fn test_basic_front() {
        let mut list = LinkedList::new();

        // Пытаемся сломать пустой список
        assert_eq!(list.len(), 0);
        assert_eq!(list.pop_front(), None);
        assert_eq!(list.len(), 0);

        // Пытаемся сломать список из одного элемента
        list.push_front(10);
        assert_eq!(list.len(), 1);
        assert_eq!(list.pop_front(), Some(10));
        assert_eq!(list.len(), 0);
        assert_eq!(list.pop_front(), None);
        assert_eq!(list.len(), 0);

        // Всё перемешиваем
        list.push_front(10);
        assert_eq!(list.len(), 1);
        list.push_front(20);
        assert_eq!(list.len(), 2);
        list.push_front(30);
        assert_eq!(list.len(), 3);
        assert_eq!(list.pop_front(), Some(30));
        assert_eq!(list.len(), 2);
        list.push_front(40);
        assert_eq!(list.len(), 3);
        assert_eq!(list.pop_front(), Some(40));
        assert_eq!(list.len(), 2);
        assert_eq!(list.pop_front(), Some(20));
        assert_eq!(list.len(), 1);
        assert_eq!(list.pop_front(), Some(10));
        assert_eq!(list.len(), 0);
        assert_eq!(list.pop_front(), None);
        assert_eq!(list.len(), 0);
        assert_eq!(list.pop_front(), None);
        assert_eq!(list.len(), 0);
    }

    #[test]
    fn test_basic() {
        let mut m = LinkedList::new();
        assert_eq!(m.pop_front(), None);
        assert_eq!(m.pop_back(), None);
        assert_eq!(m.pop_front(), None);
        m.push_front(1);
        assert_eq!(m.pop_front(), Some(1));
        m.push_back(2);
        m.push_back(3);
        assert_eq!(m.len(), 2);
        assert_eq!(m.pop_front(), Some(2));
        assert_eq!(m.pop_front(), Some(3));
        assert_eq!(m.len(), 0);
        assert_eq!(m.pop_front(), None);
        m.push_back(1);
        m.push_back(3);
        m.push_back(5);
        m.push_back(7);
        assert_eq!(m.pop_front(), Some(1));

        let mut n = LinkedList::new();
        n.push_front(2);
        n.push_front(3);
        {
            assert_eq!(n.front().unwrap(), &3);
            let x = n.front_mut().unwrap();
            assert_eq!(*x, 3);
            *x = 0;
        }
        {
            assert_eq!(n.back().unwrap(), &2);
            let y = n.back_mut().unwrap();
            assert_eq!(*y, 2);
            *y = 1;
        }
        assert_eq!(n.pop_front(), Some(0));
        assert_eq!(n.pop_front(), Some(1));
    }

    #[test]
    fn test_iterator() {
        let m = generate_test();
        for (i, elt) in m.iter().enumerate() {
            assert_eq!(i as i32, *elt);
        }
        let mut n = LinkedList::new();
        assert_eq!(n.iter().next(), None);
        n.push_front(4);
        let mut it = n.iter();
        assert_eq!(it.size_hint(), (1, Some(1)));
        assert_eq!(it.next().unwrap(), &4);
        assert_eq!(it.size_hint(), (0, Some(0)));
        assert_eq!(it.next(), None);
    }

    #[test]
    fn test_iterator_double_end() {
        let mut n = LinkedList::new();
        assert_eq!(n.iter().next(), None);
        n.push_front(4);
        n.push_front(5);
        n.push_front(6);
        let mut it = n.iter();
        assert_eq!(it.size_hint(), (3, Some(3)));
        assert_eq!(it.next().unwrap(), &6);
        assert_eq!(it.size_hint(), (2, Some(2)));
        assert_eq!(it.next_back().unwrap(), &4);
        assert_eq!(it.size_hint(), (1, Some(1)));
        assert_eq!(it.next_back().unwrap(), &5);
        assert_eq!(it.next_back(), None);
        assert_eq!(it.next(), None);
    }

    #[test]
    fn test_rev_iter() {
        let m = generate_test();
        for (i, elt) in m.iter().rev().enumerate() {
            assert_eq!(6 - i as i32, *elt);
        }
        let mut n = LinkedList::new();
        assert_eq!(n.iter().rev().next(), None);
        n.push_front(4);
        let mut it = n.iter().rev();
        assert_eq!(it.size_hint(), (1, Some(1)));
        assert_eq!(it.next().unwrap(), &4);
        assert_eq!(it.size_hint(), (0, Some(0)));
        assert_eq!(it.next(), None);
    }

    #[test]
    fn test_mut_iter() {
        let mut m = generate_test();
        let mut len = m.len();
        for (i, elt) in m.iter_mut().enumerate() {
            assert_eq!(i as i32, *elt);
            len -= 1;
        }
        assert_eq!(len, 0);
        let mut n = LinkedList::new();
        assert!(n.iter_mut().next().is_none());
        n.push_front(4);
        n.push_back(5);
        let mut it = n.iter_mut();
        assert_eq!(it.size_hint(), (2, Some(2)));
        assert!(it.next().is_some());
        assert!(it.next().is_some());
        assert_eq!(it.size_hint(), (0, Some(0)));
        assert!(it.next().is_none());
    }

    #[test]
    fn test_iterator_mut_double_end() {
        let mut n = LinkedList::new();
        assert!(n.iter_mut().next_back().is_none());
        n.push_front(4);
        n.push_front(5);
        n.push_front(6);
        let mut it = n.iter_mut();
        assert_eq!(it.size_hint(), (3, Some(3)));
        assert_eq!(*it.next().unwrap(), 6);
        assert_eq!(it.size_hint(), (2, Some(2)));
        assert_eq!(*it.next_back().unwrap(), 4);
        assert_eq!(it.size_hint(), (1, Some(1)));
        assert_eq!(*it.next_back().unwrap(), 5);
        assert!(it.next_back().is_none());
        assert!(it.next().is_none());
    }

    #[test]
    fn test_eq() {
        let mut n: LinkedList<u8> = list_from(&[]);
        let mut m = list_from(&[]);
        assert!(n == m);
        n.push_front(1);
        assert!(n != m);
        m.push_back(1);
        assert!(n == m);

        let n = list_from(&[2, 3, 4]);
        let m = list_from(&[1, 2, 3]);
        assert!(n != m);
    }

    #[test]
    fn test_ord() {
        let n = list_from(&[]);
        let m = list_from(&[1, 2, 3]);
        assert!(n < m);
        assert!(m > n);
        assert!(n <= n);
        assert!(n >= n);
    }

    #[test]
    fn test_ord_nan() {
        let nan = 0.0f64 / 0.0;
        let n = list_from(&[nan]);
        let m = list_from(&[nan]);
        assert!(!(n < m));
        assert!(!(n > m));
        assert!(!(n <= m));
        assert!(!(n >= m));

        let n = list_from(&[nan]);
        let one = list_from(&[1.0f64]);
        assert!(!(n < one));
        assert!(!(n > one));
        assert!(!(n <= one));
        assert!(!(n >= one));

        let u = list_from(&[1.0f64, 2.0, nan]);
        let v = list_from(&[1.0f64, 2.0, 3.0]);
        assert!(!(u < v));
        assert!(!(u > v));
        assert!(!(u <= v));
        assert!(!(u >= v));

        let s = list_from(&[1.0f64, 2.0, 4.0, 2.0]);
        let t = list_from(&[1.0f64, 2.0, 3.0, 2.0]);
        assert!(!(s < t));
        assert!(s > one);
        assert!(!(s <= one));
        assert!(s >= one);
    }

    #[test]
    fn test_debug() {
        let list: LinkedList<i32> = (0..10).collect();
        assert_eq!(format!("{:?}", list), "[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]");

        let list: LinkedList<&str> = vec!["just", "one", "test", "more"]
            .iter().copied()
            .collect();
        assert_eq!(format!("{:?}", list), r#"["just", "one", "test", "more"]"#);
    }

    #[test]
    fn test_hashmap() {
        // Убеждаемся, что HashMap работает со списком в качестве ключа

        let list1: LinkedList<i32> = (0..10).collect();
        let list2: LinkedList<i32> = (1..11).collect();
        let mut map = std::collections::HashMap::new();

        assert_eq!(map.insert(list1.clone(), "list1"), None);
        assert_eq!(map.insert(list2.clone(), "list2"), None);

        assert_eq!(map.len(), 2);

        assert_eq!(map.get(&list1), Some(&"list1"));
        assert_eq!(map.get(&list2), Some(&"list2"));

        assert_eq!(map.remove(&list1), Some("list1"));
        assert_eq!(map.remove(&list2), Some("list2"));

        assert!(map.is_empty());
    }
}

А сейчас момент истины:

cargo test
    Finished test [unoptimized + debuginfo] target(s) in 0.00s
     Running unittests src\lib.rs

running 12 tests
test test::test_basic ... ok
test test::test_basic_front ... ok
test test::test_eq ... ok
test test::test_iterator ... ok
test test::test_iterator_mut_double_end ... ok
test test::test_ord_nan ... ok
test test::test_iterator_double_end ... ok
test test::test_mut_iter ... ok
test test::test_rev_iter ... ok
test test::test_hashmap ... ok
test test::test_ord ... ok
test test::test_debug ... ok

test result: ok. 12 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
$env:MIRIFLAGS="-Zmiri-tag-raw-pointers"
cargo miri test
   Compiling linked-list v0.0.3
    Finished test [unoptimized + debuginfo] target(s) in 0.35s
     Running unittests src\lib.rs

running 12 tests
test test::test_basic ... ok
test test::test_basic_front ... ok
test test::test_debug ... ok
test test::test_eq ... ok
test test::test_hashmap ... ok
test test::test_iterator ... ok
test test::test_iterator_double_end ... ok
test test::test_iterator_mut_double_end ... ok
test test::test_mut_iter ... ok
test test::test_ord ... ok
test test::test_ord_nan ... ok
test test::test_rev_iter ... ok

test result: ok. 12 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out

😭

У нас получилось и мы не облажались. Это не трюк! Вся наша практика, все тренировки наконец окупились, мы, наконец, написали хороший код!!!

Теперь, разобравшись со всякой ерунда, мы можем вернуться к Интересным Вещам!

Send, Sync и тесты этапа компиляции

У нас есть ещё пара типажей, о которых стоит подумать, но они особенные. Нам предстоит иметь дело со Древним Римом языка Rust: небезопасными встроенными явно-включаемыми типажами (The Unsafe Opt-In Built-In Traits, OIBITs): Send и Sync, которые на самом деле и не-встроенные и явно-отключаемые (есть 1 свойство из 3 — уже неплохо!).

Как и у Copy, у этих типажей нет никакого связанного с ними кода — они являются маркерами, показывающими, что ваш тип обладает определённым свойством. Send говорит, что ваш тип можно безопасно передать в другой поток. Sync говорит, что ваш тип можно безопасно разделять между потоками (&Self: Send).

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

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

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

#[allow(dead_code)]
fn assert_properties() {
    fn is_send<T: Send>() {}
    fn is_sync<T: Sync>() {}

    is_send::<LinkedList<i32>>();
    is_sync::<LinkedList<i32>>();

    is_send::<IntoIter<i32>>();
    is_sync::<IntoIter<i32>>();

    is_send::<Iter<i32>>();
    is_sync::<Iter<i32>>();

    is_send::<IterMut<i32>>();
    is_sync::<IterMut<i32>>();

    fn linked_list_covariant<'a, T>(x: LinkedList<&'static T>) -> LinkedList<&'a T> { x }
    fn iter_covariant<'i, 'a, T>(x: Iter<'i, &'static T>) -> Iter<'i, &'a T> { x }
    fn into_iter_covariant<'a, T>(x: IntoIter<&'static T>) -> IntoIter<&'a T> { x }
}
cargo build
   Compiling linked-list v0.0.3 
error[E0277]: `NonNull<Node<i32>>` cannot be sent between threads safely
   --> src\lib.rs:433:5
    |
433 |     is_send::<LinkedList<i32>>();
    |     ^^^^^^^^^^^^^^^^^^^^^^^^^^ `NonNull<Node<i32>>` cannot be sent between threads safely
    |
    = help: within `LinkedList<i32>`, the trait `Send` is not implemented for `NonNull<Node<i32>>`
    = note: required because it appears within the type `Option<NonNull<Node<i32>>>`
note: required because it appears within the type `LinkedList<i32>`
   --> src\lib.rs:8:12
    |
8   | pub struct LinkedList<T> {
    |            ^^^^^^^^^^
note: required by a bound in `is_send`
   --> src\lib.rs:430:19
    |
430 |     fn is_send<T: Send>() {}
    |                   ^^^^ required by this bound in `is_send`

<и ещё миллион ошибок>

Боже, ну что за дела? А я ведь уже заготовила отличную шутку про Древний Рим!

Ладно, я соврала вам, когда сказала, что у сырых указателей есть только одна защита. Есть и вторая. У *const И *mut в целях безопасности явно отключены Send и Sync, так что нам на самом деле надо включить их обратно:

unsafe impl<T: Send> Send for LinkedList<T> {}
unsafe impl<T: Sync> Sync for LinkedList<T> {}

unsafe impl<'a, T: Send> Send for Iter<'a, T> {}
unsafe impl<'a, T: Sync> Sync for Iter<'a, T> {}

unsafe impl<'a, T: Send> Send for IterMut<'a, T> {}
unsafe impl<'a, T: Sync> Sync for IterMut<'a, T> {}

Обратите внимание, что здесь мы должны писать unsafe impl: ведь это небезопасные типажи! Небезопасный код (например, конкурентные библиотеки) зависят от того, насколько правильно мы их реализуем! Но, поскольку в них нет никакого кода, мы гарантируем лишь то, что да, мы действительно можем передавать и разделять список между потоками!

Не стоит добавлять их просто так, но я, как Сертифицированный Специалист, могу сказать, да, с ними всё в порядке. Обратите внимание, что нам не надо реализовывать Send и Sync для IntoIter: поскольку он просто содержит LinkedList, они для него определены автоматически. Я же говорила, что Send и Sync явно отключаемые!

cargo build
   Compiling linked-list v0.0.3
    Finished dev [unoptimized + debuginfo] target(s) in 0.18s

Что ж, прекрасно!

...Подождите, на самом деле это очень опасно, когда типы обладают свойством, которым не должны обладать. Например, IterMut определённо не должен быть ковариантным, потому что он «как бы» &mut T. Но как нам это проверить?

С помощью Магии! Ну, на самом деле, с помощью rustdoc! Ладно, нам не обязательно использовать для этого rustdoc, но это самый весёлый способ проверки. Смотрите, если вы напишите документацию и включите в неё блок кода, rustdoc попытается скомпилировать и запустить его, и с помощью этого мы можем создавать новые безымянные «программы», которые не оказывают влияния на главный код:

    /// ```
    /// use linked_list::IterMut;
    /// 
    /// fn iter_mut_covariant<'i, 'a, T>(x: IterMut<'i, &'static T>) -> IterMut<'i, &'a T> { x }
    /// ```
    fn iter_mut_invariant() {}
cargo test

...

   Doc-tests linked-list

running 1 test
test src\lib.rs - assert_properties::iter_mut_invariant (line 458) ... FAILED

failures:

---- src\lib.rs - assert_properties::iter_mut_invariant (line 458) stdout ----
error[E0308]: mismatched types
 --> src\lib.rs:461:86
  |
6 | fn iter_mut_covariant<'i, 'a, T>(x: IterMut<'i, &'static T>) -> IterMut<'i, &'a T> { x }
  |                                                                                      ^ lifetime mismatch
  |
  = note: expected struct `linked_list::IterMut<'_, &'a T>`
             found struct `linked_list::IterMut<'_, &'static T>`

Хорошо, мы доказали что наш тип инвариантный, но, хм, теперь наши тесты не проходят. Не беспокойтесь, rustdoc позволяет написать аннотацию, которая подскажет, что это ожидаемое поведение: compile_fail!

(На самом деле мы всего лишь доказали, что тип «не ковариантный», но если вам удастся случайно сделать неправильный «контравариантный» тип, то... мои поздравления?)

    /// ```compile_fail
    /// use linked_list::IterMut;
    /// 
    /// fn iter_mut_covariant<'i, 'a, T>(x: IterMut<'i, &'static T>) -> IterMut<'i, &'a T> { x }
    /// ```
    fn iter_mut_invariant() {}
cargo test
   Compiling linked-list v0.0.3
    Finished test [unoptimized + debuginfo] target(s) in 0.49s
     Running unittests src\lib.rs

...

   Doc-tests linked-list

running 1 test
test src\lib.rs - assert_properties::iter_mut_invariant (line 458) - compile fail ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.12s

Ура! Я рекомендую в начале всегда создавать тест без compile_fail, чтобы убедиться, что он не компилируется по правильной причине. Например, в этом же тесте возникнет ошибка (и, соответственно, он будет пройден), если вы забудете написать use, но это ведь не то, что нам нужно! Хотя теоретически было бы правильно «требовать» у компилятора проверки на определённую ошибку, это стало бы абсолютным кошмаром, поскольку всякий раз, когда в компиляторе улучшали бы выдачу ошибок, это ломало бы тесты. А мы хотим, чтобы компилятор улучшался, поэтому нет, вы этого не получите.

(Впрочем, мы можем просто указать нужный код ошибки вместе с compile_fail, но это работает только в nightly-версиях и на это не стоит полагаться по причинам, озвученным выше. В не-nightly-версиях код ошибки молча игнорируется.)

    /// ```compile_fail,E0308
    /// use linked_list::IterMut;
    /// 
    /// fn iter_mut_covariant<'i, 'a, T>(x: IterMut<'i, &'static T>) -> IterMut<'i, &'a T> { x }
    /// ```
    fn iter_mut_invariant() {}

...кстати, вы заметили, когда мы на самом деле сделали IterMut инвариантным? Пропустить было легко, поскольку я «всего-навсего» скопировала Iter и вставила его в конец. Вот, в последней строке:

pub struct IterMut<'a, T> {
    front: Link<T>,
    back: Link<T>,
    len: usize,
    _boo: PhantomData<&'a mut T>,
}

Попробуем удалить PhantomData:

 cargo build
   Compiling linked-list v0.0.3 (C:\Users\ninte\dev\contain\linked-list)
error[E0392]: parameter `'a` is never used
  --> src\lib.rs:30:20
   |
30 | pub struct IterMut<'a, T> {
   |                    ^^ unused parameter
   |
   = help: consider removing `'a`, referring to it in a field, or using a marker such as `PhantomData`

Ха! Компилятор поддерживает нас и не позволяет нам просто не указывать время жизни. Давайте теперь попробуем использовать неправильный пример:

    _boo: PhantomData<&'a T>,
cargo build
   Compiling linked-list v0.0.3 (C:\Users\ninte\dev\contain\linked-list)
    Finished dev [unoptimized + debuginfo] target(s) in 0.17s

Он собрался! Смогут ли сейчас наши тесты выявить проблему?

cargo test

...

   Doc-tests linked-list

running 1 test
test src\lib.rs - assert_properties::iter_mut_invariant (line 458) - compile fail ... FAILED

failures:

---- src\lib.rs - assert_properties::iter_mut_invariant (line 458) stdout ----
Test compiled successfully, but it's marked `compile_fail`.

failures:
    src\lib.rs - assert_properties::iter_mut_invariant (line 458)

test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.15s

Йееее!!! Система работает! Мне очень нравится, когда тесты делают свою работу, так что мне не приходиться переживать из-за возможных ошибок!

Введение в Курсоры

Здорово!!! Теперь у нас есть LinkedList, не уступающий по качеству реализации списку из стандартной библиотеки 1.0! Что, конечно, означает, что наш LinkedList всё ещё совершенно бесполезен. Мы смирились с огромными потерями производительности, реализуя дек как связный список, и у нас нет ни одного API, который бы сделал его полезным.

Вот как у нас обстоят дела с «киллер-фичами» связных списков:

Ну... 1 из 6... лучше, чем ничего! Понимаете, почему я хотела выдрать эту штуку из стандартной библиотеки?

Мы не станем реализовывать «странные» штуки, потому что они зависят от предметной области и обычно делаются для решения конкретной задачи (adhoc). Но вот разбиение и вставка — совсем другое дело!

Однако, здесь есть проблема: на самом деле получение k-того элемента в связном списке требует времени O(k), так как же мы можем произвольно разбить или вставить список за O(1)? Ну, трюк в том, что вы не предоставляете API наподобие split_at(index) — вы создаёте систему, где пользователь может добраться до нужной позиции в списке, а затем, в этой точке, модифицировать его за O(1)!

Эй, а ведь у нас уже есть итераторы! Можем ли мы использовать их для перебора? Вроде бы... но мешает одна из их супер-способностей. Возможно, вы помните, что способ, которым мы указываем время жизни итераторов, возвращающих ссылку на элемент, работает так, что они возвращают ссылку, которая не привязана к итератору. Это позволяет нам повторно вызывать next и хранить элементы:

let mut list = ...;
let iter = list.iter_mut();
let elem1 = list.next();
let elem2 = list.next();

if elem1 == elem2 { ... }

Если бы возвращаемые ссылки заимствовали итератор, этот код вообще бы не работал. Компилятор бы просто жаловался на второй вызов next! Эта гибкость великолепна, но она накладывает на нас некоторые неявные ограничения:

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

К сожалению, нам от LinkedList API нужны именно эти два пункта! Поэтому мы не можем просто использовать итераторы, нам нужно что-то новое: Курсоры.

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

Смотрите, если я просто

нажму

Enter

весь

текст

сломается.

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

Если вам не посчастливилось иметь клавиатуру с клавишей «Insert» и время от времени её нажимать, вы знаете, что технически есть две интерпретации курсоров: они могут находиться между элементами (символами) или над элементами. Я почти уверена, что никто никогда в жизни целенаправленно не нажимал клавишу «Insert», и что она существует исключительно, как Клавиша Страдания. Ведь совершенно очевидно, какая интерпретация лучше: курсоры находятся между элементами!

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

Простите, что? В 2018 был RFC по добавлению Курсоров в LinkedList библиотеки Rust?

С помощью Cursor можно перемещаться по списку вперёд и назад и получать текущий элемент. С помощью CursorMut можно перемещаться по списку вперёд и назад, получать изменяемые ссылки на элементы, а также вставлять и удалять элементы до/после текущего (наряду с такими операциями, как разбиение и вставка).

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

Эй, подождите:

Курсоры всегда располагаются между двумя элементами в списке и нумеруются циклически. Чтобы это работало, между концом и началом списка существует специальный «псевдоэлемент», который возвращает None.

ЭЙ, ПОДОЖДИТЕ. Это ведь противоречит тому, что написано в RFC??? Однако вся дока по методам продолжается ссылаться на «текущие» элементы... так, минутку, я ведь где-то уже видела этот псевдоэлемент раньше. Разве не я написала его в своём старом прототипе связного списка?

Курсоры всегда располагаются между двумя элементами в списке и нумеруются циклически. Чтобы это работало, между концом и началом списка существует специальный «псевдоэлемент», который возвращает None.

Что за хрень здесь творится? Я не шучу, я действительно прямо сейчас пытаюсь Читать Доки. Стандартная библиотека взяла дизайн, отличный от того, что я предложила в 2015, а затем просто скопи-пастила документацию из моего прототипа??? Они что, троллят меня за то, что я пишу книгу, как сильно я ненавижу связные списки??? Да, я написала этот прототип, чтобы продемонстрировать концепцию, чтобы мне разрешили добавить его в стандартную библиотеку и сделать LinkedList не таким бесполезным. Но... простите мой французский, это что за хрень?

Ладно, знаете, очевидно, что стандартная библиотека одобрила мой дизайн, как объективно лучший, так что именно его мы и будем использовать. А, кроме того, я целую главу буду писать с нуля свою же библиотеку, что меня полностью устраивает!

Вот полный текст документации верхнего уровня, которую я написала:

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

Курсоры всегда располагаются между двумя элементами в списке и нумеруются циклически. Чтобы это работало, между концом и началом списка существует специальный «псевдоэлемент», который возвращает None.

Будучи созданными, курсоры находятся между псевдоэлементом и передним элементом списка. Таким образом, вызов next вернёт передний элемент списка, а вызов prev вернёт None. Повторный вызов prev вернёт хвост.

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

Ещё раз пролистывает свои старые API

fn splice(&mut self, other: &mut LinkedList<T>)

Вставляет содержимое целого списка прямо после курсора.

О, да, начинаю вспоминать. Я писала это в жутком расстройстве из-за комбинаторного взрыва и пыталась придумать, как сделать так, чтобы была только одна копия каждой операции. К сожалению, это... семантически проблематично. Смотрите, когда пользовать вставляет один список в другой, ему может быть надо, чтобы курсор остался перед вставляемым списком или за ним. Вставляемый список может быть произвольно большим, поэтому разрешать только один способ и ожидать, что пользователь переберёт весь вставленный список, чтобы добраться до другого конца — это подлинная проблема.

We're gonna have to rework this design from the ground up after all. What does our Cursor type need? Well it needs to:

Похоже, нам придётся заново разработать весь дизайн. Что должен делать наш тип Cursor? Ну, он должен:

  • указывать между двумя элементами
  • отслеживать «индекс» следующего элемента — в качестве удобной небольшой функции
  • обновлять сам список, чтобы менять значения front/back/len

Как вы можете указывать между двумя элементами? Ладно, вы не можете. Вы просто указываете на «следующий» элемент. Так что да, несмотря на то. что мы подразумеваем семантику «курсор между элементами», на самом деле мы реализуем «курсор над элементом» и просто притворяемся, что всё происходит перед этой точкой, или за ней.

Но для этого есть причина! В сценарии вставки пользователь может выбрать, должен ли курсор оказаться за вставляемым списком или перед ним... но всё это ужасно трудно выразить с помощью стандартного API! У них есть splice_after и splice_before, но они не меняют позицию курсора, поэтому нам потребуются также splice_after_before и splice_after_after...

Подождите, я шучу. В стандартном API вы можете выбрать узел, где хотите оказаться и затем просто вызывать splice_after/splice_before в зависимости от того, что вам нужно.

прищуривается

Хм, а стандартный API действительно хорош.

пробегает код глазами

Правда, стандартный API действительно хорош.

Ладно, к чёрту всё, будем реализовывать RFC. Или по крайней мере самые интересные его части.

У меня есть пара претензий к терминологии, которую использует стандартная библиотека, но курсоры всегда немного сбивают с толку: iter().next_back() означает по сути back() (то есть назад), что в целом правильно, но каждый вызов next_back() делает нас ближе к переднему элементу, а это значит, что мы двигаемся не назад, а вперёд! Когда я начинаю слишком много думать об этом парадоксе, у меня начинает болеть голова. Так что я могу понять необходимость в другой терминологии, чтобы избежать таких проблем.

В стандартному API операции описаны, как «before» (в сторону переднего элемента) и «after» (в сторону заднего элемента), а вместо next/back_next... есть вызовы move_next и move_prev. ГРМ. Ладно, они позаимствовали терминологию итераторов, но, в конце концов next не вызывает неверных ассоциаций по отношению к front/back. И, по аналогии с итераторами, помогает понять, как всё работает.

С этим можно работать.

Реализация Курсоров

Мы займёмся типом CursorMut из стандартной библиотеки, поскольку неизменяемая версия на самом деле не так интересна. Как и в оригинальном дизайне, у неё есть «псевдоэлемент», который содержит None, чтобы обозначить начало/конец списка, и через который можно «пройти», чтобы перепрыгнуть на другую сторону списка. Для реализации нам потребуются:

  • Указатель на текущий узел
  • Указатель на список
  • Текущий индекс

Так, а каково должно быть значение индекса, когда мы указываем на «псевдоэлемент»?

хмурит брови... проверяет стандартную библиотеку... отвергает решение из стандартной библиотеки...

Ладно, вполне логично, что index Курсора возвращает Option<usize>. Стандартная реализация делает кучу ненужных вещей, чтобы избежать хранения индекса, как Option, но... у нас же связный список, это вполне нормально. Так же в стандартной библиотеке есть функции cursor_front/cursor_back, которые передвигают курсор на передний/задний элементы, что кажется интуитивным, пока речь не заходит о пустом списке.

Вы можете написать такой код, какой захотите, но я собираюсь выкинуть весь повторяющийся мусор, все граничные случае и сделать простой метод cursor_mut, который начинается в псевдоэлементе, и который можно передвигать вперёд/назад, чтобы получить нужную позицию. И, если, очень хотите, можете написать свой cursor_front.

Приступим:

pub struct CursorMut<'a, T> {
    cur: Link<T>,
    list: &'a mut LinkedList<T>,
    index: Option<usize>,
}

Всё довольно просто: одно поле на каждый пункт нашего списка! Теперь напишем метод cursor_mut:

impl<T> LinkedList<T> {
    pub fn cursor_mut(&mut self) -> CursorMut<T> {
        CursorMut { 
            list: self, 
            cur: None, 
            index: None,
        }
    }
}

Поскольку мы стартуем на псевдоэлементе, мы можем проинициализировать все поля значением None: красиво и и просто! Далее, перемещение:

impl<'a, T> CursorMut<'a, T> {
    pub fn index(&self) -> Option<usize> {
        self.index
    }

    pub fn move_next(&mut self) {
        if let Some(cur) = self.cur {
            unsafe {
                // Мы на реальном элементе, двигаемся к следующему (в сторону заднего)
                self.cur = (*cur.as_ptr()).back;
                if self.cur.is_some() {
                    *self.index.as_mut().unwrap() += 1;
                } else {
                    // Мы попали на псевдоэлемент, убираем индекс
                    self.index = None;
                }
            }
        } else if !self.list.is_empty() {
            // Мы на псевдоэлементе и у нас есть реальный передний элемент, так что перемещаемся на него!
            self.cur = self.list.front;
            self.index = Some(0)
        } else {
            // Мы на псевдоэлементе, но никаких других элементов нет... ничего не делаем.
        }
    }
}

Итак, есть 4 интересные варианта:

  • Обычный вариант
  • Обычный вариант, но мы перемещаемся к псевдоэлементу
  • Вариант псевдоэлемента, когда мы перемещаемся к переднему элементу
  • Вариант псевдоэлемента, но список пуст, так что ничего не делаем

В move_prev точно такая же логика, но мы меняем front/back местами и инвертируем изменение индекса:

pub fn move_prev(&mut self) {
    if let Some(cur) = self.cur {
        unsafe {
            // Мы на реальном элементе, двигаемся к предыдущему (в сторону переднего)
            self.cur = (*cur.as_ptr()).front;
            if self.cur.is_some() {
                *self.index.as_mut().unwrap() -= 1;
            } else {
                // Мы попали на псевдоэлемент, убираем индекс
                self.index = None;
            }
        }
    } else if !self.list.is_empty() {
        // Мы на псевдоэлементе и у нас есть реальный задний элемент, так что перемещаемся на него!
        self.cur = self.list.back;
        self.index = Some(self.list.len - 1)
    } else {
        // Мы на псевдоэлементе, но никаких других элементов нет... ничего не делаем.
    }
}

Добавим методы для просмотра элементов вокруг курсора: current, peek_next и peek_prev. Очень важное примечание: эти методы должны заимствовать наш курсор через &mut self и результаты должны быть привязаны к этому заимствованию. Мы не можем позволить пользователю получить несколько копий изменяемой ссылки и мы не можем позволить ему использовать любой из методов insert/remove/split/splice, пока у него есть такая ссылка!

К счастью, неявное выведение времени жизни работает в Rust именно так, как нужно, поэтому мы по умолчанию пишем правильный код!

pub fn current(&mut self) -> Option<&mut T> {
    unsafe {
        self.cur.map(|node| &mut (*node.as_ptr()).elem)
    }
}

pub fn peek_next(&mut self) -> Option<&mut T> {
    unsafe {
        self.cur
            .and_then(|node| (*node.as_ptr()).back)
            .map(|node| &mut (*node.as_ptr()).elem)
    }
}

pub fn peek_prev(&mut self) -> Option<&mut T> {
    unsafe {
        self.cur
            .and_then(|node| (*node.as_ptr()).front)
            .map(|node| &mut (*node.as_ptr()).elem)
    }
}

Даже думать не пришлось, всю работу сделали методы Option и (опущенные) ошибки компилятора. Я была настроена скептически по отношению к Option<NonNull>, но, будь я проклята, этот тип позволил мне писать код на автопилоте. Я потратила слишком много времени на написание коллекций на основе массивов, где вам никогда не приходится использовать Option, так что мне есть, с чем сравнивать! ((*node.as_ptr()), конечно, выглядит кринжово, но, с другой стороны, для вас это всего лишь обычные указатели Rust...)

Далее у нас есть выбор: мы можем сразу заняться split и splice, центральной темой этого API, или мы можем сделать небольшой шажок, реализовав insert/remove для одного элемента. У меня такое чувство, что нам придётся реализовать insert/remove через split и splice, так что... давайте попробуем и посмотрим, как ляжет карта (честно говоря, понятия не имею, пока пишу эти строки).

Разбиение

Итак, split_before и split_after возвращают всё до/после текущего элемента в виде LinkedList (и останавливаются на псевдоэлементе, а если они сразу находятся на псевдоэлементе, то возвращают весь список, а курсор после этого указывает на пустой список).

прищуривается ладно, здесь и правда есть нетривиальная логика, так что давайте разобьём её на части.

Я вижу 4 потенциально интересных варианта для split_before:

  • Обычный вариант
  • Обычный вариант, но предыдущий элемент является псевдоэлементом
  • Вариант псевдоэлемента, где мы возвращаем весь список, а оставляем пустой
  • Вариант псевдоэлемента, но список пустой, так что ничего не делаем и возвращаем пустой список

Начнём с граничных вариантов. Третий вариант, мне кажется, самый простой.

#![allow(unused)]
fn main() {
mem::replace(self.list, LinkedList::new())
}

Так? Список становится пустым, мы возвращаем весь предыдущий список и теперь наши поля хранят None, так что нам нечего исправлять. Прекрасно. О, кстати, этот метод закрывает и четвёртый вариант!

Так, теперь обычные варианты... ладно, здесь мне потребуется несколько ASCII-диаграмм. В самом общем случае, у нас есть что-то такое:

list.front -> A <-> B <-> C <-> D <- list.back
                          ^
                         cur

А мы хотим получить вот это:

list.front -> C <-> D <- list.back
              ^
             cur

return.front -> A <-> B <- return.back

Так что нам надо разорвать связь между cur и prev, и... боже, сколько всего надо менять. Ладно, мне просто надо разбить весь путь на отдельные шаги, чтобы быть уверенной, что я нигде не ошиблась. Будет чуток многословно, но я по крайней мере ничего не пропущу:

pub fn split_before(&mut self) -> LinkedList<T> {
    if let Some(cur) = self.cur {
        // Указываем на реальный элемент, так что список не пустой.
        unsafe {
            // Текущее состояние
            let old_len = self.list.len;
            let old_idx = self.index.unwrap();
            let prev = (*cur.as_ptr()).front;
            
            // Во что должен превратиться self
            let new_len = old_len - old_idx;
            let new_front = self.cur;
            let new_back = self.list.back;
            let new_idx = Some(0);

            // Что мы должны вернуть
            let output_len = old_len - new_len;
            let output_front = self.list.front;
            let output_back = prev;

            // Разрываем связь между cur и prev
            if let Some(prev) = prev {
                (*cur.as_ptr()).front = None;
                (*prev.as_ptr()).back = None;
            }

            // Создаём результат:
            self.list.len = new_len;
            self.list.front = new_front;
            self.list.back = new_back;
            self.index = new_idx;

            LinkedList {
                front: output_front,
                back: output_back,
                len: output_len,
                _boo: PhantomData,
            }
        }
    } else {
        // Мы на псевдолементе, просто меняем наш список на пустой.
        // Никаких других изменений состояния не нужно.
        std::mem::replace(self.list, LinkedList::new())
    }
}

Обратите внимание, что конструкция if-let помогает справиться с «обычным вариантом, когда предыдущий элемент — это псевдоэлемент»:

if let Some(prev) = prev {
    (*cur.as_ptr()).front = None;
    (*prev.as_ptr()).back = None;
}

Если вы хотите, можете рассмотреть весь код, как единое целое и сделать такие оптимизации:

  • заменить два обращения к (*cur.as_ptr()).front на вызов (*cur.as_ptr()).front.take()
  • заметить, что new_back не выполняет никакой работы, поэтому переменную можно удалить

Насколько я могу судить, оставшийся код должен работать. Узнаем, когда напишем тесты. (копи-пастим метод split_after)

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

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

Не пишите на Rust небезопасный код! Безопасный Rust гораздо лучше!!!

Вставка

Остался последний босс, с которым нужно сразиться — splice_before и splice_after. И в них, похоже, будет больше всего граничных случаев. Эти функции принимают на вход один LinkedList и вставляют его содержимое в наш список. Наш список может быть пустым, их список может быть пустым, нужно что-то решать с псевдоэлементами... вздыхает Давайте просто действовать шаг за шагом на примере splice_before.

  • Если их список пустой, ничего не делаем
  • Если наш список пустой, тогда их список становится нашим списком
  • Если мы указываем на псевдоэлемент, их список добавляется сзади (изменяя list.back)
  • Если мы указываем на первый элемент (0), их список добавляется спереди (изменяя list.front)
  • В других случаях, мы очень много возимся с указателями.

Общий случай:

input.front -> 1 <-> 2 <- input.back

 list.front -> A <-> B <-> C <- list.back
                     ^
                    cur

Превращается в:

list.front -> A <-> 1 <-> 2 <-> B <-> C <- list.back

Так? Так. Будем писать... **ГЛУБОКО ВЗДЫХАЕТ И ПИШЕТ:

    pub fn splice_before(&mut self, mut input: LinkedList<T>) {
        unsafe {
            if input.is_empty() {
                // Пустой входной список, ничего не делаем
            } else if let Some(cur) = self.cur {
                if let Some(0) = self.index {
                    // Добавляем спереди, сравните с добавлением сзади
                    (*cur.as_ptr()).front = input.back.take();
                    (*input.back.unwrap().as_ptr()).back = Some(cur);
                    self.list.front = input.front.take();

                    // Индекс увеличивается на длину входного списка
                    *self.index.as_mut().unwrap() += input.len;
                    self.list.len += input.len;
                    input.len = 0;
                } else {
                    // Общий случай, никаких граничных случаев:
                    // просто правим приватные поля
                    let prev = (*cur.as_ptr()).front.unwrap();
                    let in_front = input.front.take().unwrap();
                    let in_back = input.back.take().unwrap();

                    (*prev.as_ptr()).back = Some(in_front);
                    (*in_front.as_ptr()).front = Some(prev);
                    (*cur.as_ptr()).front = Some(in_back);
                    (*in_back.as_ptr()).back = Some(cur);

                    // Индекс увеличивается на длину входного списка
                    *self.index.as_mut().unwrap() += input.len;
                    self.list.len += input.len;
                    input.len = 0;
                }
            } else if let Some(back) = self.list.back {
                // Мы на псевдоэлементе и список не пуст, добавляем сзади.
                // Мы можем вызывать для входных указателей либо `take`,
                // либо `mem::forget`. Take корректнее, если мы используем
                // собственный аллокатор или что-то, что тоже требует очистки!
                (*back.as_ptr()).back = input.front.take();
                (*input.front.unwrap().as_ptr()).front = Some(back);
                self.list.back = input.back.take();
                self.list.len += input.len;
                // Не обязательно, но вежливо
                input.len = 0;
            } else {
                // Наш список пуст, заменяем его на входной, остаёмся на псевдоэлементе
                *self.list = input;
            }
        }
    }

Этот код по настоящему ужасен и я ощущаю всю боль от выбора Option<NonNull>. Но многое можно исправить. Во-первых, эти две строки можно вынести в самый конец, поскольку они нужны всегда. (согласна, в некоторых сценариях они не обязательны, но ничего не ломают, а установка input.len вызвана, скорее, паранойей):

self.list.len += input.len;
input.len = 0;

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

Ах, да, в случае «наш список пуст» мы перемещаем list. Заменим эту строку на вызов swap:

// Наш список пуст, заменяем его на входной, остаёмся на псевдоэлементе
std::mem::swap(self.list, &mut input);

В этом случае операторы, меняющие значения self.list.len и input.len, не делают ничего, так как значения уже корректны, но они ничего и не ломают (и, в принципе, мы могли бы сделать в этом месте ранний возврат из функции).

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

if let Some(0) = self.index {

} else {
    let prev = (*cur.as_ptr()).front.unwrap();
}

Корректировка индекса повторяется в обоих ветках, так что и её можно вынести наружу:

#![allow(unused)]
fn main() {
*self.index.as_mut().unwrap() += input.len;
}

Сложив всё воедино, получаем:

#![allow(unused)]
fn main() {
if input.is_empty() {
    // Пустой входной список, ничего не делаем
} else if let Some(cur) = self.cur {
    // Оба списка не пустые
    if let Some(prev) = (*cur.as_ptr()).front {
        // Общий случай, никаких граничных случаев:
        // просто правим приватные поля
        let in_front = input.front.take().unwrap();
        let in_back = input.back.take().unwrap();

        (*prev.as_ptr()).back = Some(in_front);
        (*in_front.as_ptr()).front = Some(prev);
        (*cur.as_ptr()).front = Some(in_back);
        (*in_back.as_ptr()).back = Some(cur);
    } else {
        // Добавляем спереди, сравните с добавлением сзади
        (*cur.as_ptr()).front = input.back.take();
        (*input.back.unwrap().as_ptr()).back = Some(cur);
        self.list.front = input.front.take();
    }
    // Индекс увеличивается на длину входного списка
    *self.index.as_mut().unwrap() += input.len;
} else if let Some(back) = self.list.back {
    // Мы на псевдоэлементе и список не пуст, добавляем сзади.
    // Мы можем вызывать для входных указателей либо `take`,
    // либо `mem::forget`. Take корректнее, если мы используем
    // собственный аллокатор или что-то, что тоже требует очистки!
    (*back.as_ptr()).back = input.front.take();
    (*input.front.unwrap().as_ptr()).front = Some(back);
    self.list.back = input.back.take();

} else {
    // Наш список пуст, заменяем его на входной, остаёмся на псевдоэлементе
    std::mem::swap(self.list, &mut input);
}

self.list.len += input.len;
// Не обязательно, но вежливо
input.len = 0;

// input освобождается здесь
}

Ладно, это всё ещё отстой, но, в основном из-за... нет, так, я только что нашла ошибку:

#![allow(unused)]
fn main() {
    (*back.as_ptr()).back = input.front.take();
    (*input.front.unwrap().as_ptr()).front = Some(back);
}

Мы вызываем take у input.front и затем, буквально на следующей строке вызываем unwrap! вздыхает и то же самое мы делаем в эквивалентном зеркальном методе. Мы бы нашли эту ошибку с помощью тестов, но сейчас я пытаюсь быть Идеальной и пишу код в режиме реального времени, так что я увидела эту ошибку только что. Вот что бывает, когда выполняешь работу не так, как привыкла, и не в том порядке. Даёшь больше ясности в коде!

#![allow(unused)]
fn main() {
// Мы можем вызывать для входных указателей либо `take`,
// либо `mem::forget`. Take корректнее, если мы используем
// собственный аллокатор или что-то, что тоже требует очистки!
if input.is_empty() {
    // Пустой входной список, ничего не делаем
} else if let Some(cur) = self.cur {
    // Оба списка не пустые
    let in_front = input.front.take().unwrap();
    let in_back = input.back.take().unwrap();

    if let Some(prev) = (*cur.as_ptr()).front {
        // Общий случай, никаких граничных случаев:
        // просто правим приватные поля
        (*prev.as_ptr()).back = Some(in_front);
        (*in_front.as_ptr()).front = Some(prev);
        (*cur.as_ptr()).front = Some(in_back);
        (*in_back.as_ptr()).back = Some(cur);
    } else {
        // Нет предыдущего элемента, добавляем спереди
        (*cur.as_ptr()).front = Some(in_back);
        (*in_back.as_ptr()).back = Some(cur);
        self.list.front = Some(in_front);
    }
    // Индекс увеличивается на длину входного списка
    *self.index.as_mut().unwrap() += input.len;
} else if let Some(back) = self.list.back {
    // Мы на псевдоэлементе и список не пуст, добавляем сзади.
    let in_front = input.front.take().unwrap();
    let in_back = input.back.take().unwrap();

    (*back.as_ptr()).back = Some(in_front);
    (*in_front.as_ptr()).front = Some(back);
    self.list.back = Some(in_back);
} else {
    // Наш список пуст, заменяем его на входной, остаёмся на псевдоэлементе
    std::mem::swap(self.list, &mut input);
}

self.list.len += input.len;
// Не обязательно, но вежливо
input.len = 0;

// input освобождается здесь
}

Ладно, с таким кодом уже можно смириться. Единственный его недостаток в том, что мы два раза инициализируем in_front/in_back (возможно мы могли бы поправить наши условия, но не суть). Подобный код мы бы написали и на C, но Option<NonNull> делает его многословным. С этим можно жить. По уму, надо сделать сырые указатели удобнее для решения таких задач. Но это выходит за рамки этой книги.

В любом случае, я абсолютно измучена, так что insert, remove и все остальные методы оставляю читателю в качестве упражнения.

Финальный код нашего курсора вместе с копи-пастой комбинаторных методов. Всё ли здесь правильно? Это я узнаю только после написания следующего раздела и тестирования этого монстра!

pub struct CursorMut<'a, T> {
    list: &'a mut LinkedList<T>,
    cur: Link<T>,
    index: Option<usize>,
}

impl<T> LinkedList<T> {
    pub fn cursor_mut(&mut self) -> CursorMut<T> {
        CursorMut { 
            list: self, 
            cur: None, 
            index: None,
        }
    }
}

impl<'a, T> CursorMut<'a, T> {
    pub fn index(&self) -> Option<usize> {
        self.index
    }

    pub fn move_next(&mut self) {
        if let Some(cur) = self.cur {
            unsafe {
                // Мы на реальном элементе, двигаемся к следующему (в сторону заднего)
                self.cur = (*cur.as_ptr()).back;
                if self.cur.is_some() {
                    *self.index.as_mut().unwrap() += 1;
                } else {
                    // Мы попали на псевдоэлемент, убираем индекс
                    self.index = None;
                }
            }
        } else if !self.list.is_empty() {
            // Мы на псевдоэлементе и у нас есть реальный передний элемент, так что перемещаемся на него!
            self.cur = self.list.front;
            self.index = Some(0)
        } else {
            // Мы на псевдоэлементе, но никаких других элементов нет... ничего не делаем.
        }
    }

    pub fn move_prev(&mut self) {
        if let Some(cur) = self.cur {
            unsafe {
                // Мы на реальном элементе, двигаемся к предыдущему (в сторону переднего)
                self.cur = (*cur.as_ptr()).front;
                if self.cur.is_some() {
                    *self.index.as_mut().unwrap() -= 1;
                } else {
                    // Мы попали на псевдоэлемент, убираем индекс
                    self.index = None;
                }
            }
        } else if !self.list.is_empty() {
            // Мы на псевдоэлементе и у нас есть реальный задний элемент, так что перемещаемся на него!
            self.cur = self.list.back;
            self.index = Some(self.list.len - 1)
        } else {
            // Мы на псевдоэлементе, но никаких других элементов нет... ничего не делаем.
        }
    }

    pub fn current(&mut self) -> Option<&mut T> {
        unsafe {
            self.cur.map(|node| &mut (*node.as_ptr()).elem)
        }
    }

    pub fn peek_next(&mut self) -> Option<&mut T> {
        unsafe {
            self.cur
                .and_then(|node| (*node.as_ptr()).back)
                .map(|node| &mut (*node.as_ptr()).elem)
        }
    }

    pub fn peek_prev(&mut self) -> Option<&mut T> {
        unsafe {
            self.cur
                .and_then(|node| (*node.as_ptr()).front)
                .map(|node| &mut (*node.as_ptr()).elem)
        }
    }

    pub fn split_before(&mut self) -> LinkedList<T> {
        // У нас есть что-то такое:
        //
        //     list.front -> A <-> B <-> C <-> D <- list.back
        //                               ^
        //                              cur
        //
        // А мы хотим получить вот это:
        //
        //     list.front -> C <-> D <- list.back
        //                   ^
        //                  cur
        //
        //     return.front -> A <-> B <- return.back
        //
        if let Some(cur) = self.cur {
            // Указываем на реальный элемент, так что список не пустой.
            unsafe {
                // Текущее состояние
                let old_len = self.list.len;
                let old_idx = self.index.unwrap();
                let prev = (*cur.as_ptr()).front;
                
                // Во что должен превратиться self
                let new_len = old_len - old_idx;
                let new_front = self.cur;
                let new_back = self.list.back;
                let new_idx = Some(0);

                // Что мы должны вернуть
                let output_len = old_len - new_len;
                let output_front = self.list.front;
                let output_back = prev;

                // Разрываем связь между cur и prev
                if let Some(prev) = prev {
                    (*cur.as_ptr()).front = None;
                    (*prev.as_ptr()).back = None;
                }

                // Создаём результат:
                self.list.len = new_len;
                self.list.front = new_front;
                self.list.back = new_back;
                self.index = new_idx;

                LinkedList {
                    front: output_front,
                    back: output_back,
                    len: output_len,
                    _boo: PhantomData,
                }
            }
        } else {
            // Мы на псевдолементе, просто меняем наш список на пустой.
            // Никаких других изменений состояния не нужно.
            std::mem::replace(self.list, LinkedList::new())
        }
    }

    pub fn split_after(&mut self) -> LinkedList<T> {
        // У нас есть что-то такое:
        //
        //     list.front -> A <-> B <-> C <-> D <- list.back
        //                         ^
        //                        cur
        // 
        //
        // А мы хотим получить вот это:
        // 
        //     list.front -> A <-> B <- list.back
        //                         ^
        //                        cur
        //
        // 
        //     return.front -> C <-> D <- return.back
        //
        if let Some(cur) = self.cur {
            // Указываем на реальный элемент, так что список не пустой.
            unsafe {
                // Текущее состояние
                let old_len = self.list.len;
                let old_idx = self.index.unwrap();
                let next = (*cur.as_ptr()).back;
                
                // Во что должен превратиться self
                let new_len = old_idx + 1;
                let new_back = self.cur;
                let new_front = self.list.front;
                let new_idx = Some(old_idx);

                // Что мы должны вернуть
                let output_len = old_len - new_len;
                let output_front = next;
                let output_back = self.list.back;

                // Разрываем связь между cur и next
                if let Some(next) = next {
                    (*cur.as_ptr()).back = None;
                    (*next.as_ptr()).front = None;
                }

                // Создаём результат:
                self.list.len = new_len;
                self.list.front = new_front;
                self.list.back = new_back;
                self.index = new_idx;

                LinkedList {
                    front: output_front,
                    back: output_back,
                    len: output_len,
                    _boo: PhantomData,
                }
            }
        } else {
            // Мы на псевдолементе, просто меняем наш список на пустой.
            // Никаких других изменений состояния не нужно.
            std::mem::replace(self.list, LinkedList::new())
        }
    }

    pub fn splice_before(&mut self, mut input: LinkedList<T>) {
        // Наше:
        //
        // input.front -> 1 <-> 2 <- input.back
        //
        //  list.front -> A <-> B <-> C <- list.back
        //                      ^
        //                     cur
        //
        // Превращается в:
        //
        // list.front -> A <-> 1 <-> 2 <-> B <-> C <- list.back
        //
        unsafe {
            // Мы можем вызывать для входных указателей либо `take`,
            // либо `mem::forget`. Take корректнее, если мы используем
            // собственный аллокатор или что-то, что тоже требует очистки!
            if input.is_empty() {
                // Пустой входной список, ничего не делаем
            } else if let Some(cur) = self.cur {
                // Оба списка не пустые
                let in_front = input.front.take().unwrap();
                let in_back = input.back.take().unwrap();

                if let Some(prev) = (*cur.as_ptr()).front {
                    // Общий случай, никаких граничных случаев:
                    // просто правим приватные поля
                    (*prev.as_ptr()).back = Some(in_front);
                    (*in_front.as_ptr()).front = Some(prev);
                    (*cur.as_ptr()).front = Some(in_back);
                    (*in_back.as_ptr()).back = Some(cur);
                } else {
                    // Нет предыдущего элемента, добавляем спереди
                    (*cur.as_ptr()).front = Some(in_back);
                    (*in_back.as_ptr()).back = Some(cur);
                    self.list.front = Some(in_front);
                }
                // Индекс увеличивается на длину входного списка
                *self.index.as_mut().unwrap() += input.len;
            } else if let Some(back) = self.list.back {
                // Мы на псевдоэлементе и список не пуст, добавляем сзади.
                let in_front = input.front.take().unwrap();
                let in_back = input.back.take().unwrap();

                (*back.as_ptr()).back = Some(in_front);
                (*in_front.as_ptr()).front = Some(back);
                self.list.back = Some(in_back);
            } else {
                // Наш список пуст, заменяем его на входной, остаёмся на псевдоэлементе
                std::mem::swap(self.list, &mut input);
            }

            self.list.len += input.len;
            // Не обязательно, но вежливо
            input.len = 0;
            
            // input освобождается здесь
        }        
    }

    pub fn splice_after(&mut self, mut input: LinkedList<T>) {
        // Наше:
        //
        // input.front -> 1 <-> 2 <- input.back
        //
        // list.front -> A <-> B <-> C <- list.back
        //                     ^
        //                    cur
        //
        //
        // Превращается в :
        //
        // list.front -> A <-> B <-> 1 <-> 2 <-> C <- list.back
        //                     ^
        //                    cur
        //
        unsafe {
            // Мы можем вызывать для входных указателей либо `take`,
            // либо `mem::forget`. Take корректнее, если мы используем
            // собственный аллокатор или что-то, что тоже требует очистки!
            if input.is_empty() {
                // Пустой входной список, ничего не делаем
            } else if let Some(cur) = self.cur {
                // Оба списка не пустые
                let in_front = input.front.take().unwrap();
                let in_back = input.back.take().unwrap();

                if let Some(next) = (*cur.as_ptr()).back {
                    // Общий случай, никаких граничных случаев:
                    // просто правим приватные поля
                    (*next.as_ptr()).front = Some(in_back);
                    (*in_back.as_ptr()).back = Some(next);
                    (*cur.as_ptr()).back = Some(in_front);
                    (*in_front.as_ptr()).front = Some(cur);
                } else {
                    // Нет следующего элемента, добавляем сзади
                    (*cur.as_ptr()).back = Some(in_front);
                    (*in_front.as_ptr()).front = Some(cur);
                    self.list.back = Some(in_back);
                }
                // Индекс не меняется
            } else if let Some(front) = self.list.front {
                // Мы на псевдоэлементе и список не пуст, добавляем спереди.
                let in_front = input.front.take().unwrap();
                let in_back = input.back.take().unwrap();

                (*front.as_ptr()).front = Some(in_back);
                (*in_back.as_ptr()).back = Some(front);
                self.list.front = Some(in_front);
            } else {
                // Наш список пуст, заменяем его на входной, остаёмся на псевдоэлементе
                std::mem::swap(self.list, &mut input);
            }

            self.list.len += input.len;
            // Не обязательно, но вежливо
            input.len = 0;
            
            // input освобождается здесь
        }        
    }
}

Тестирование Курсоров

Время узнать, сколько ужасных и неловких ошибок я допустила в предыдущем разделе!

К несчастью, наш API отличается, как от стандартной библиотеки, так и от моей старой реализации, так что мне придётся собирать работающие тесты из них обеих. Эти тесты «позаимствуем» из стандартной реализации:

    #[test]
    fn test_cursor_move_peek() {
        let mut m: LinkedList<u32> = LinkedList::new();
        m.extend([1, 2, 3, 4, 5, 6]);
        let mut cursor = m.cursor_mut();
        cursor.move_next();
        assert_eq!(cursor.current(), Some(&mut 1));
        assert_eq!(cursor.peek_next(), Some(&mut 2));
        assert_eq!(cursor.peek_prev(), None);
        assert_eq!(cursor.index(), Some(0));
        cursor.move_prev();
        assert_eq!(cursor.current(), None);
        assert_eq!(cursor.peek_next(), Some(&mut 1));
        assert_eq!(cursor.peek_prev(), Some(&mut 6));
        assert_eq!(cursor.index(), None);
        cursor.move_next();
        cursor.move_next();
        assert_eq!(cursor.current(), Some(&mut 2));
        assert_eq!(cursor.peek_next(), Some(&mut 3));
        assert_eq!(cursor.peek_prev(), Some(&mut 1));
        assert_eq!(cursor.index(), Some(1));

        let mut cursor = m.cursor_mut();
        cursor.move_prev();
        assert_eq!(cursor.current(), Some(&mut 6));
        assert_eq!(cursor.peek_next(), None);
        assert_eq!(cursor.peek_prev(), Some(&mut 5));
        assert_eq!(cursor.index(), Some(5));
        cursor.move_next();
        assert_eq!(cursor.current(), None);
        assert_eq!(cursor.peek_next(), Some(&mut 1));
        assert_eq!(cursor.peek_prev(), Some(&mut 6));
        assert_eq!(cursor.index(), None);
        cursor.move_prev();
        cursor.move_prev();
        assert_eq!(cursor.current(), Some(&mut 5));
        assert_eq!(cursor.peek_next(), Some(&mut 6));
        assert_eq!(cursor.peek_prev(), Some(&mut 4));
        assert_eq!(cursor.index(), Some(4));
    }

    #[test]
    fn test_cursor_mut_insert() {
        let mut m: LinkedList<u32> = LinkedList::new();
        m.extend([1, 2, 3, 4, 5, 6]);
        let mut cursor = m.cursor_mut();
        cursor.move_next();
        cursor.splice_before(Some(7).into_iter().collect());
        cursor.splice_after(Some(8).into_iter().collect());
        // check_links(&m);
        assert_eq!(m.iter().cloned().collect::<Vec<_>>(), &[7, 1, 8, 2, 3, 4, 5, 6]);
        let mut cursor = m.cursor_mut();
        cursor.move_next();
        cursor.move_prev();
        cursor.splice_before(Some(9).into_iter().collect());
        cursor.splice_after(Some(10).into_iter().collect());
        check_links(&m);
        assert_eq!(m.iter().cloned().collect::<Vec<_>>(), &[10, 7, 1, 8, 2, 3, 4, 5, 6, 9]);
        
        /* удаляем, сейчас не реализовано:
        let mut cursor = m.cursor_mut();
        cursor.move_next();
        cursor.move_prev();
        assert_eq!(cursor.remove_current(), None);
        cursor.move_next();
        cursor.move_next();
        assert_eq!(cursor.remove_current(), Some(7));
        cursor.move_prev();
        cursor.move_prev();
        cursor.move_prev();
        assert_eq!(cursor.remove_current(), Some(9));
        cursor.move_next();
        assert_eq!(cursor.remove_current(), Some(10));
        check_links(&m);
        assert_eq!(m.iter().cloned().collect::<Vec<_>>(), &[1, 8, 2, 3, 4, 5, 6]);
        */

        let mut cursor = m.cursor_mut();
        cursor.move_next();
        let mut p: LinkedList<u32> = LinkedList::new();
        p.extend([100, 101, 102, 103]);
        let mut q: LinkedList<u32> = LinkedList::new();
        q.extend([200, 201, 202, 203]);
        cursor.splice_after(p);
        cursor.splice_before(q);
        check_links(&m);
        assert_eq!(
            m.iter().cloned().collect::<Vec<_>>(),
            &[200, 201, 202, 203, 1, 100, 101, 102, 103, 8, 2, 3, 4, 5, 6]
        );
        let mut cursor = m.cursor_mut();
        cursor.move_next();
        cursor.move_prev();
        let tmp = cursor.split_before();
        assert_eq!(m.into_iter().collect::<Vec<_>>(), &[]);
        m = tmp;
        let mut cursor = m.cursor_mut();
        cursor.move_next();
        cursor.move_next();
        cursor.move_next();
        cursor.move_next();
        cursor.move_next();
        cursor.move_next();
        cursor.move_next();
        let tmp = cursor.split_after();
        assert_eq!(tmp.into_iter().collect::<Vec<_>>(), &[102, 103, 8, 2, 3, 4, 5, 6]);
        check_links(&m);
        assert_eq!(m.iter().cloned().collect::<Vec<_>>(), &[200, 201, 202, 203, 1, 100, 101]);
    }

    fn check_links<T>(_list: &LinkedList<T>) {
        // было бы неплохо написать!
    }

Момент истины!

cargo test

   Compiling linked-list v0.0.3
    Finished test [unoptimized + debuginfo] target(s) in 1.03s
     Running unittests src\lib.rs

running 14 tests
test test::test_basic_front ... ok
test test::test_basic ... ok
test test::test_debug ... ok
test test::test_iterator_mut_double_end ... ok
test test::test_ord ... ok
test test::test_cursor_move_peek ... FAILED
test test::test_cursor_mut_insert ... FAILED
test test::test_iterator ... ok
test test::test_mut_iter ... ok
test test::test_eq ... ok
test test::test_rev_iter ... ok
test test::test_iterator_double_end ... ok
test test::test_hashmap ... ok
test test::test_ord_nan ... ok

failures:

---- test::test_cursor_move_peek stdout ----
thread 'test::test_cursor_move_peek' panicked at 'assertion failed: `(left == right)`
  left: `None`,
 right: `Some(1)`', src\lib.rs:1079:9
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

---- test::test_cursor_mut_insert stdout ----
thread 'test::test_cursor_mut_insert' panicked at 'assertion failed: `(left == right)`
  left: `[200, 201, 202, 203, 10, 100, 101, 102, 103, 7, 1, 8, 2, 3, 4, 5, 6, 9]`,
 right: `[200, 201, 202, 203, 1, 100, 101, 102, 103, 8, 2, 3, 4, 5, 6]`', src\lib.rs:1153:9


failures:
    test::test_cursor_move_peek
    test::test_cursor_mut_insert

test result: FAILED. 12 passed; 2 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

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

В чём первая ошибка?

let mut m: LinkedList<u32> = LinkedList::new();
m.extend([1, 2, 3, 4, 5, 6]);
let mut cursor = m.cursor_mut();

cursor.move_next();
assert_eq!(cursor.current(), Some(&mut 1));
assert_eq!(cursor.peek_next(), Some(&mut 2));
assert_eq!(cursor.peek_prev(), None);
assert_eq!(cursor.index(), Some(0));

cursor.move_prev();
assert_eq!(cursor.current(), None);
assert_eq!(cursor.peek_next(), Some(&mut 1)); // DIES HERE

Блин, я и правда накосячила с базовой функциональностью. Подождите,

Даже думать не пришлось, всю работу сделали методы Option и (опущенные) ошибки компилятора.

Что ж, я человек честный.

pub fn peek_next(&mut self) -> Option<&mut T> {
    unsafe {
        self.cur
            .and_then(|node| (*node.as_ptr()).back)
            .map(|node| &mut (*node.as_ptr()).elem)
    }
}

Да, это просто неправильно. Если self.cur равен None, мы не должны завершать проверку, мы должны также проверить self.list.front, потому что находимся над псевдоэлементом! Так что нам надо добавить or_else в цепочку вызов:

pub fn peek_next(&mut self) -> Option<&mut T> {
    unsafe {
        self.cur
            .and_then(|node| (*node.as_ptr()).back)
            .or_else(|| self.list.front)
            .map(|node| &mut (*node.as_ptr()).elem)
    }
}

pub fn peek_prev(&mut self) -> Option<&mut T> {
    unsafe {
        self.cur
            .and_then(|node| (*node.as_ptr()).front)
            .or_else(|| self.list.back)
            .map(|node| &mut (*node.as_ptr()).elem)
    }
}

Помогло?

---- test::test_cursor_move_peek stdout ----
thread 'test::test_cursor_move_peek' panicked at 'assertion failed: `(left == right)`
  left: `Some(6)`,
 right: `None`', src\lib.rs:1078:9

Подождите, теперь ошибка сдвинулась на элемент назад. Ладно, мне надо остановить своё «даже думать не пришлось» в отношении peek, потому что, оказывается, всё намного сложнее, чем я представляла. Просто пытаться связать в цепочку все эти варианты — как мы видим, не работает. Давайте добавим явное условие if для псевдоэлемента и обычного элемента:

pub fn peek_next(&mut self) -> Option<&mut T> {
    unsafe {
        let next = if let Some(cur) = self.cur {
            // Обычный вариант, пытаемся следовать заднему указателю текущего узла
            (*cur.as_ptr()).back
        } else {
            // Псевдоэлемент, пытаемся использовать передний указатель списка
            self.list.front
        };

        // Возвращаем элемент, если следующий узел существует
        next.map(|node| &mut (*node.as_ptr()).elem)
    }
}

pub fn peek_prev(&mut self) -> Option<&mut T> {
    unsafe {
        let prev = if let Some(cur) = self.cur {
            // Обычный вариант, пытаемся следовать переднему указателю текущего узла
            (*cur.as_ptr()).front
        } else {
            // Псевдоэлемент, пытаемся использовать задний указатель списка
            self.list.back
        };

        // Возвращаем элемент, если предыдущий узел существует
        prev.map(|node| &mut (*node.as_ptr()).elem)
    }
}

Ну, в этом коде я уверена!

failures:

---- test::test_cursor_mut_insert stdout ----
thread 'test::test_cursor_mut_insert' panicked at 'assertion failed: `(left == right)`
  left: `[200, 201, 202, 203, 10, 100, 101, 102, 103, 7, 1, 8, 2, 3, 4, 5, 6, 9]`,
 right: `[200, 201, 202, 203, 1, 100, 101, 102, 103, 8, 2, 3, 4, 5, 6]`', src\lib.rs:1168:9
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace


failures:
    test::test_cursor_mut_insert

test result: FAILED. 13 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

Даааа. Что ж, осталась одна ошибка... уф.

Вы заметили, что я закомментировала часть кода, предназначенного для тестирования remove_current? Оказывается, я не обратила внимания, что этот тест меняет состояние. Давайте просто создадим новый список в том виде, в каком бы его оставил вызов remove_current:

let mut m: LinkedList<u32> = LinkedList::new();
m.extend([1, 8, 2, 3, 4, 5, 6]);
 cargo test
   Compiling linked-list v0.0.3
    Finished test [unoptimized + debuginfo] target(s) in 0.70s
     Running unittests src\lib.rs

running 14 tests
test test::test_basic_front ... ok
test test::test_basic ... ok
test test::test_cursor_move_peek ... ok
test test::test_eq ... ok
test test::test_cursor_mut_insert ... ok
test test::test_iterator ... ok
test test::test_iterator_double_end ... ok
test test::test_ord_nan ... ok
test test::test_mut_iter ... ok
test test::test_hashmap ... ok
test test::test_debug ... ok
test test::test_ord ... ok
test test::test_iterator_mut_double_end ... ok
test test::test_rev_iter ... ok

test result: ok. 14 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

   Doc-tests linked-list

running 1 test
test src\lib.rs - assert_properties::iter_mut_invariant (line 803) - compile fail ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.12s

Эээй, только взгляните на... ладно, у уже начинаю параноить. Давайте аккуратно заполним check_links и протестируем его в miri:

fn check_links<T: Eq + std::fmt::Debug>(list: &LinkedList<T>) {
    let from_front: Vec<_> = list.iter().collect();
    let from_back: Vec<_> = list.iter().rev().collect();
    let re_reved: Vec<_> = from_back.into_iter().rev().collect();

    assert_eq!(from_front, re_reved);
}

Это лучшим способ проверки? Нет. Нормальный? Да.

$env:MIRIFLAGS="-Zmiri-tag-raw-pointers"
cargo miri test
   Compiling linked-list v0.0.3
    Finished test [unoptimized + debuginfo] target(s) in 0.25s
     Running unittests src\lib.rs

running 14 tests
test test::test_basic ... ok
test test::test_basic_front ... ok
test test::test_cursor_move_peek ... ok
test test::test_cursor_mut_insert ... ok
test test::test_debug ... ok
test test::test_eq ... ok
test test::test_hashmap ... ok
test test::test_iterator ... ok
test test::test_iterator_double_end ... ok
test test::test_iterator_mut_double_end ... ok
test test::test_mut_iter ... ok
test test::test_ord ... ok
test test::test_ord_nan ... ok
test test::test_rev_iter ... ok

test result: ok. 14 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out

   Doc-tests linked-list

running 1 test
test src\lib.rs - assert_properties::iter_mut_invariant (line 803) - compile fail ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.10s

ГОТОВО.

Готово.

У нас получилось. Мы сделали связный список офигенно продуктового уровня, практически со всеми функциями из стандартной библиотеки. Нам не хватает пары удобных методов? Никаких сомнений. Добавлю ли я их позже? Возможно!

Но Сейчас Я Очень Устала.

В любом случае. Мы победили.

Блин, подождите. Нам же нужно продуктовое качество. Ладно, последний финальный босс: clippy.

cargo clippy

cargo clippy
    Checking linked-list v0.0.3 (C:\Users\ninte\dev\contain\linked-list)
warning: redundant pattern matching, consider using `is_some()`
   --> src\lib.rs:189:19
    |
189 |         while let Some(_) = self.pop_front() { }
    |         ----------^^^^^^^------------------- help: try this: `while self.pop_front().is_some()`
    |
    = note: `#[warn(clippy::redundant_pattern_matching)]` on by default
    = note: this will change drop order of the result, as well as all temporaries
    = note: add `#[allow(clippy::redundant_pattern_matching)]` if this is important
    = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#redundant_pattern_matching

warning: method `into_iter` can be confused for the standard trait method `std::iter::IntoIterator::into_iter`
   --> src\lib.rs:210:5
    |
210 | /     pub fn into_iter(self) -> IntoIter<T> {
211 | |         IntoIter {
212 | |             list: self
213 | |         }
214 | |     }
    | |_____^
    |
    = note: `#[warn(clippy::should_implement_trait)]` on by default
    = help: consider implementing the trait `std::iter::IntoIterator` or choosing a less ambiguous method name
    = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#should_implement_trait

warning: redundant pattern matching, consider using `is_some()`
   --> src\lib.rs:228:19
    |
228 |         while let Some(_) = self.pop_front() { }
    |         ----------^^^^^^^------------------- help: try this: `while self.pop_front().is_some()`
    |
    = note: this will change drop order of the result, as well as all temporaries
    = note: add `#[allow(clippy::redundant_pattern_matching)]` if this is important
    = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#redundant_pattern_matching

warning: re-implementing `PartialEq::ne` is unnecessary
   --> src\lib.rs:275:5
    |
275 | /     fn ne(&self, other: &Self) -> bool {
276 | |         self.len() != other.len() || self.iter().ne(other)
277 | |     }
    | |_____^
    |
    = note: `#[warn(clippy::partialeq_ne_impl)]` on by default
    = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#partialeq_ne_impl

warning: `linked-list` (lib) generated 4 warnings
    Finished dev [unoptimized + debuginfo] target(s) in 0.29s

Ладно, clippy, добьём.

Предупреждение 1 (и 3): мы используем while let Some(_) = вместо while .is_some(). Тело цикла пустое, так что писать можно как угодно, но, хорошо, clippy, я сделаю по твоему.

Предупреждение 2: У нас уже есть унаследованный метод into_iter. Подождите, что... проверяет стандартную библиотеку... ладно, очко уходит clippy. IntoIterator уже есть в прелюдии (и по сути является языковым примитивом), так что нам не нужна своя версия into_iter.

Предупреждение 4: мы скопировали кусок странного карго-кода из стандартной библиотеки. пожимает плечами ладно, просто удалю его.

cargo clippy
    Finished dev [unoptimized + debuginfo] target(s) in 0.00s

Прекрасно. Ещё одно, прежде чем мы сможем считать наш код продуктовым: fmt.

cargo fmt

...так, он добавил несколько новый строк и удалил несколько конечных пробелов. Ничего интересного.

НАКОНЕЦ, МЫ И ПРАВДА ЗАКОНЧИЛИ!!!

Финальный код

Не могу поверить, что заставила вас пройти через переработку std::collections::LinkedList от начала и до конца, с разбором всех ошибок, которые я сделала.

Ну, моя работа завершена, книга готова и я, наконец, могу отдохнуть.

Хорошо, вот 1200 строк нашего нового кода во всей красе. Это должен быть тот же код, что и в коммите.

Позже я немного его отполирую, документирую, и опубликую версию 0.1.0.

#![allow(unused)]
fn main() {
use std::cmp::Ordering;
use std::fmt::{self, Debug};
use std::hash::{Hash, Hasher};
use std::iter::FromIterator;
use std::ptr::NonNull;
use std::marker::PhantomData;

pub struct LinkedList<T> {
    front: Link<T>,
    back: Link<T>,
    len: usize,
    _boo: PhantomData<T>,
}

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

struct Node<T> {
    front: Link<T>,
    back: Link<T>,
    elem: T, 
}

pub struct Iter<'a, T> {
    front: Link<T>,
    back: Link<T>,
    len: usize,
    _boo: PhantomData<&'a T>,
}

pub struct IterMut<'a, T> {
    front: Link<T>,
    back: Link<T>,
    len: usize,
    _boo: PhantomData<&'a mut T>,
}

pub struct IntoIter<T> {
    list: LinkedList<T>,
}

pub struct CursorMut<'a, T> {
    cur: Link<T>,
    list: &'a mut LinkedList<T>,
    index: Option<usize>,
}

impl<T> LinkedList<T> {
    pub fn new() -> Self {
        Self {
            front: None,
            back: None,
            len: 0,
            _boo: PhantomData,
        }
    }

    pub fn push_front(&mut self, elem: T) {
        // БЕЗОПАСНОСТЬ: это связный список, что вы делаете?
        unsafe {
            let new = NonNull::new_unchecked(Box::into_raw(Box::new(Node {
                front: None,
                back: None,
                elem,
            })));
            if let Some(old) = self.front {
                // Вставляем новый передний узел перед старым
                (*old.as_ptr()).front = Some(new);
                (*new.as_ptr()).back = Some(old);
            } else {
                // Если переднего узла нет, у нас пустой список и нам надо
                // установить и значение заднего узла.
                debug_assert!(self.back.is_none());
                debug_assert!(self.front.is_none());
                debug_assert!(self.len == 0);
                self.back = Some(new);
            }
            // Этот код выполняется всегда!
            self.front = Some(new);
            self.len += 1;
        }
    }

    pub fn push_back(&mut self, elem: T) {
        // БЕЗОПАСНОСТЬ: это связный список, что вы делаете?
        unsafe {
            let new = NonNull::new_unchecked(Box::into_raw(Box::new(Node {
                back: None,
                front: None,
                elem,
            })));
            if let Some(old) = self.back {
                // Вставляем новый задний узел перед старым
                (*old.as_ptr()).back = Some(new);
                (*new.as_ptr()).front = Some(old);
            } else {
                // Если заднего узла нет, у нас пустой список и нам надо
                // установить и значение переднего узла.
                self.front = Some(new);
            }
            // Этот код выполняется всегда!
            self.back = Some(new);
            self.len += 1;
        }
    }

    pub fn pop_front(&mut self) -> Option<T> {
        unsafe {
            // Будем что-то делать, только если у списка есть передний узел.
            self.front.map(|node| {
                // Возвращаем к жизни Box, так что можно извлечь и уничтожить его
                // значение (Box магически делает это за нас).
                let boxed_node = Box::from_raw(node.as_ptr());
                let result = boxed_node.elem;

                // Делаем следующий узел новым передним узлом.
                self.front = boxed_node.back;
                if let Some(new) = self.front {
                    // Убираем ссылку переднего узла на удалённый узел.
                    (*new.as_ptr()).front = None;
                } else {
                    // Если передний узел равен null, значит, список пустой!
                    debug_assert!(self.len == 1);
                    self.back = None;
                }

                self.len -= 1;
                result
                // Здесь Box неявным образом освобождается, потому что больше нет T.
            })
        }
    }

    pub fn pop_back(&mut self) -> Option<T> {
        unsafe {
            // Будем что-то делать, только если у списка есть задний узел.
            self.back.map(|node| {
                // Возвращаем к жизни Box, так что можно извлечь и уничтожить его
                // значение (Box магически делает это за нас).
                let boxed_node = Box::from_raw(node.as_ptr());
                let result = boxed_node.elem;

                // Делаем следующий узел новым задним узлом.
                self.back = boxed_node.front;
                if let Some(new) = self.back {
                    // Убираем ссылку заднего узла на удалённый узел.
                    (*new.as_ptr()).back = None;
                } else {
                    // Если задний узел равен null, значит, список пустой!
                    self.front = None;
                }

                self.len -= 1;
                result
                // Здесь Box неявным образом освобождается, потому что больше нет T.
            })
        }
    }

    pub fn front(&self) -> Option<&T> {
        unsafe {
            self.front.map(|node| &(*node.as_ptr()).elem)
        }
    }

    pub fn front_mut(&mut self) -> Option<&mut T> {
        unsafe {
            self.front.map(|node| &mut (*node.as_ptr()).elem)
        }
    }

    pub fn back(&self) -> Option<&T> {
        unsafe {
            self.back.map(|node| &(*node.as_ptr()).elem)
        }
    }

    pub fn back_mut(&mut self) -> Option<&mut T> {
        unsafe {
            self.back.map(|node| &mut (*node.as_ptr()).elem)
        }
    }

    pub fn len(&self) -> usize {
        self.len
    }

    pub fn is_empty(&self) -> bool {
        self.len == 0
    }

    pub fn clear(&mut self) {
        // О, смотрите, это снова drop
        while self.pop_front().is_some() { }
    }

    pub fn iter(&self) -> Iter<'_, T> {
        Iter { 
            front: self.front, 
            back: self.back,
            len: self.len,
            _boo: PhantomData,
        }
    }

    pub fn iter_mut(&mut self) -> IterMut<'_, T> {
        IterMut { 
            front: self.front, 
            back: self.back,
            len: self.len,
            _boo: PhantomData,
        }
    }

    pub fn cursor_mut(&mut self) -> CursorMut<'_, T> {
        CursorMut { 
            list: self, 
            cur: None, 
            index: None,
        }
    }
}

impl<T> Drop for LinkedList<T> {
    fn drop(&mut self) {
        // Вызываем pop, пока есть элементы
        while self.pop_front().is_some() { }
    }
}

impl<T> Default for LinkedList<T> {
    fn default() -> Self {
        Self::new()
    }
}

impl<T: Clone> Clone for LinkedList<T> {
    fn clone(&self) -> Self {
        let mut new_list = Self::new();
        for item in self {
            new_list.push_back(item.clone());
        }
        new_list
    }
}

impl<T> Extend<T> for LinkedList<T> {
    fn extend<I: IntoIterator<Item = T>>(&mut self, iter: I) {
        for item in iter {
            self.push_back(item);
        }
    }
}

impl<T> FromIterator<T> for LinkedList<T> {
    fn from_iter<I: IntoIterator<Item = T>>(iter: I) -> Self {
        let mut list = Self::new();
        list.extend(iter);
        list
    }
}

impl<T: Debug> Debug for LinkedList<T> {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.debug_list().entries(self).finish()
    }
}

impl<T: PartialEq> PartialEq for LinkedList<T> {
    fn eq(&self, other: &Self) -> bool {
        self.len() == other.len() && self.iter().eq(other)
    }
}

impl<T: Eq> Eq for LinkedList<T> { }

impl<T: PartialOrd> PartialOrd for LinkedList<T> {
    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
        self.iter().partial_cmp(other)
    }
}

impl<T: Ord> Ord for LinkedList<T> {
    fn cmp(&self, other: &Self) -> Ordering {
        self.iter().cmp(other)
    }
}

impl<T: Hash> Hash for LinkedList<T> {
    fn hash<H: Hasher>(&self, state: &mut H) {
        self.len().hash(state);
        for item in self {
            item.hash(state);
        }
    }
}

impl<'a, T> IntoIterator for &'a LinkedList<T> {
    type IntoIter = Iter<'a, T>;
    type Item = &'a T;

    fn into_iter(self) -> Self::IntoIter {
        self.iter()
    }
}

impl<'a, T> Iterator for Iter<'a, T> {
    type Item = &'a T;
    
    fn next(&mut self) -> Option<Self::Item> {
        // Хотя условие self.front == self.back кажется очевидным,
        // оно не подходит для возврата последнего элемента! Подобный код
        // работает только с массивами из-за указателя «на элемент, следующий
        // за последним»
        if self.len > 0 {
            // Мы могли бы извлечь значение из front, но так быстрее и безопаснее
            self.front.map(|node| unsafe {
                self.len -= 1;
                self.front = (*node.as_ptr()).back;
                &(*node.as_ptr()).elem
            })
        } else {
            None
        }
    }

    fn size_hint(&self) -> (usize, Option<usize>) {
        (self.len, Some(self.len))
    }
}

impl<'a, T> DoubleEndedIterator for Iter<'a, T> {
    fn next_back(&mut self) -> Option<Self::Item> {
        if self.len > 0 {
            self.back.map(|node| unsafe {
                self.len -= 1;
                self.back = (*node.as_ptr()).front;
                &(*node.as_ptr()).elem
            })
        } else {
            None
        }
    }
}

impl<'a, T> ExactSizeIterator for Iter<'a, T> {
    fn len(&self) -> usize {
        self.len
    }
}

impl<'a, T> IntoIterator for &'a mut LinkedList<T> {
    type IntoIter = IterMut<'a, T>;
    type Item = &'a mut T;

    fn into_iter(self) -> Self::IntoIter {
        self.iter_mut()
    }
}

impl<'a, T> Iterator for IterMut<'a, T> {
    type Item = &'a mut T;

    fn next(&mut self) -> Option<Self::Item> {
        // Хотя условие self.front == self.back кажется очевидным,
        // оно не подходит для возврата последнего элемента! Подобный код
        // работает только с массивами из-за указателя «на элемент, следующий
        // за последним»
        if self.len > 0 {
            // Мы могли бы извлечь значение из front, но так быстрее и безопаснее
            self.front.map(|node| unsafe {
                self.len -= 1;
                self.front = (*node.as_ptr()).back;
                &mut (*node.as_ptr()).elem
            })
        } else {
            None
        }
    }

    fn size_hint(&self) -> (usize, Option<usize>) {
        (self.len, Some(self.len))
    }
}

impl<'a, T> DoubleEndedIterator for IterMut<'a, T> {
    fn next_back(&mut self) -> Option<Self::Item> {
        if self.len > 0 {
            self.back.map(|node| unsafe {
                self.len -= 1;
                self.back = (*node.as_ptr()).front;
                &mut (*node.as_ptr()).elem
            })
        } else {
            None
        }
    }
}

impl<'a, T> ExactSizeIterator for IterMut<'a, T> {
    fn len(&self) -> usize {
        self.len
    }
}

impl<T> IntoIterator for LinkedList<T> {
    type IntoIter = IntoIter<T>;
    type Item = T;

    fn into_iter(self) -> Self::IntoIter {
        IntoIter { list: self }
    }
}

impl<T> Iterator for IntoIter<T> {
    type Item = T;

    fn next(&mut self) -> Option<Self::Item> {
        self.list.pop_front()
    }

    fn size_hint(&self) -> (usize, Option<usize>) {
        (self.list.len, Some(self.list.len))
    }
}

impl<T> DoubleEndedIterator for IntoIter<T> {
    fn next_back(&mut self) -> Option<Self::Item> {
        self.list.pop_back()
    }
}

impl<T> ExactSizeIterator for IntoIter<T> {
    fn len(&self) -> usize {
        self.list.len
    }
}

impl<'a, T> CursorMut<'a, T> {
    pub fn index(&self) -> Option<usize> {
        self.index
    }

    pub fn move_next(&mut self) {
        if let Some(cur) = self.cur {
            unsafe {
                // Мы на реальном элементе, двигаемся к следующему (в сторону заднего)
                self.cur = (*cur.as_ptr()).back;
                if self.cur.is_some() {
                    *self.index.as_mut().unwrap() += 1;
                } else {
                    // Мы попали на псевдоэлемент, убираем индекс
                    self.index = None;
                }
            }
        } else if !self.list.is_empty() {
            // Мы на псевдоэлементе и у нас есть реальный передний элемент, так что перемещаемся на него!
            self.cur = self.list.front;
            self.index = Some(0)
        } else {
            // Мы на псевдоэлементе, но никаких других элементов нет... ничего не делаем.
        }
    }

    pub fn move_prev(&mut self) {
        if let Some(cur) = self.cur {
            unsafe {
                // Мы на реальном элементе, двигаемся к предыдущему (в сторону переднего)
                self.cur = (*cur.as_ptr()).front;
                if self.cur.is_some() {
                    *self.index.as_mut().unwrap() -= 1;
                } else {
                    // Мы попали на псевдоэлемент, убираем индекс
                    self.index = None;
                }
            }
        } else if !self.list.is_empty() {
            // Мы на псевдоэлементе и у нас есть реальный задний элемент, так что перемещаемся на него!
            self.cur = self.list.back;
            self.index = Some(self.list.len - 1)
        } else {
            // Мы на псевдоэлементе, но никаких других элементов нет... ничего не делаем.
        }
    }

    pub fn current(&mut self) -> Option<&mut T> {
        unsafe {
            self.cur.map(|node| &mut (*node.as_ptr()).elem)
        }
    }

    pub fn peek_next(&mut self) -> Option<&mut T> {
        unsafe {
            let next = if let Some(cur) = self.cur {
                // Обычный вариант, пытаемся следовать заднему указателю текущего узла
                (*cur.as_ptr()).back
            } else {
                // Псевдоэлемент, пытаемся использовать передний указатель списка
                self.list.front
            };

            // Возвращаем элемент, если следующий узел существует
            next.map(|node| &mut (*node.as_ptr()).elem)
        }
    }

    pub fn peek_prev(&mut self) -> Option<&mut T> {
        unsafe {
            let prev = if let Some(cur) = self.cur {
                // Normal case, try to follow the cur node's front pointer
                // Обычный вариант, пытаемся следовать переднему указателю текущего узла
                (*cur.as_ptr()).front
            } else {
                // Псевдоэлемент, пытаемся использовать задний указатель списка
                self.list.back
            };

            // Возвращаем элемент, если предыдущий узел существует
            prev.map(|node| &mut (*node.as_ptr()).elem)
        }
    }

    pub fn split_before(&mut self) -> LinkedList<T> {
        // У нас есть что-то такое:
        //
        //     list.front -> A <-> B <-> C <-> D <- list.back
        //                               ^
        //                              cur
        //
        // А мы хотим получить вот это:
        //
        //     list.front -> C <-> D <- list.back
        //                   ^
        //                  cur
        //
        //     return.front -> A <-> B <- return.back
        //
        if let Some(cur) = self.cur {
            // Указываем на реальный элемент, так что список не пустой.
            unsafe {
                // Текущее состояние
                let old_len = self.list.len;
                let old_idx = self.index.unwrap();
                let prev = (*cur.as_ptr()).front;
                
                // Во что должен превратиться self
                let new_len = old_len - old_idx;
                let new_front = self.cur;
                let new_back = self.list.back;
                let new_idx = Some(0);

                // Что мы должны вернуть
                let output_len = old_len - new_len;
                let output_front = self.list.front;
                let output_back = prev;

                // Разрываем связь между cur и prev
                if let Some(prev) = prev {
                    (*cur.as_ptr()).front = None;
                    (*prev.as_ptr()).back = None;
                }

                // Создаём результат:
                self.list.len = new_len;
                self.list.front = new_front;
                self.list.back = new_back;
                self.index = new_idx;

                LinkedList {
                    front: output_front,
                    back: output_back,
                    len: output_len,
                    _boo: PhantomData,
                }
            }
        } else {
            // Мы на псевдолементе, просто меняем наш список на пустой.
            // Никаких других изменений состояния не нужно.
            std::mem::replace(self.list, LinkedList::new())
        }
    }

    pub fn split_after(&mut self) -> LinkedList<T> {
        // У нас есть что-то такое:
        //
        //     list.front -> A <-> B <-> C <-> D <- list.back
        //                         ^
        //                        cur
        // 
        //
        // А мы хотим получить вот это:
        // 
        //     list.front -> A <-> B <- list.back
        //                         ^
        //                        cur
        //
        // 
        //     return.front -> C <-> D <- return.back
        //
        if let Some(cur) = self.cur {
            // Указываем на реальный элемент, так что список не пустой.
            unsafe {
                // Текущее состояние
                let old_len = self.list.len;
                let old_idx = self.index.unwrap();
                let next = (*cur.as_ptr()).back;
                
                // Во что должен превратиться self
                let new_len = old_idx + 1;
                let new_back = self.cur;
                let new_front = self.list.front;
                let new_idx = Some(old_idx);

                // Что мы должны вернуть
                let output_len = old_len - new_len;
                let output_front = next;
                let output_back = self.list.back;

                // Разрываем связь между cur и next
                if let Some(next) = next {
                    (*cur.as_ptr()).back = None;
                    (*next.as_ptr()).front = None;
                }

                // Создаём результат:
                self.list.len = new_len;
                self.list.front = new_front;
                self.list.back = new_back;
                self.index = new_idx;

                LinkedList {
                    front: output_front,
                    back: output_back,
                    len: output_len,
                    _boo: PhantomData,
                }
            }
        } else {
            // Мы на псевдолементе, просто меняем наш список на пустой.
            // Никаких других изменений состояния не нужно.
            std::mem::replace(self.list, LinkedList::new())
        }
    }

    pub fn splice_before(&mut self, mut input: LinkedList<T>) {
        // Наше:
        //
        // input.front -> 1 <-> 2 <- input.back
        //
        //  list.front -> A <-> B <-> C <- list.back
        //                      ^
        //                     cur
        //
        // Превращается в:
        //
        // list.front -> A <-> 1 <-> 2 <-> B <-> C <- list.back
        //
        unsafe {
            // Мы можем вызывать для входных указателей либо `take`,
            // либо `mem::forget`. Take корректнее, если мы используем
            // собственный аллокатор или что-то, что тоже требует очистки!
            if input.is_empty() {
                // Пустой входной список, ничего не делаем
            } else if let Some(cur) = self.cur {
                // Оба списка не пустые
                let in_front = input.front.take().unwrap();
                let in_back = input.back.take().unwrap();

                if let Some(prev) = (*cur.as_ptr()).front {
                    // Общий случай, никаких граничных случаев:
                    // просто правим приватные поля
                    (*prev.as_ptr()).back = Some(in_front);
                    (*in_front.as_ptr()).front = Some(prev);
                    (*cur.as_ptr()).front = Some(in_back);
                    (*in_back.as_ptr()).back = Some(cur);
                } else {
                    // Нет предыдущего элемента, добавляем спереди
                    (*cur.as_ptr()).front = Some(in_back);
                    (*in_back.as_ptr()).back = Some(cur);
                    self.list.front = Some(in_front);
                }
                // Индекс увеличивается на длину входного списка
                *self.index.as_mut().unwrap() += input.len;
            } else if let Some(back) = self.list.back {
                // Мы на псевдоэлементе и список не пуст, добавляем сзади.
                let in_front = input.front.take().unwrap();
                let in_back = input.back.take().unwrap();

                (*back.as_ptr()).back = Some(in_front);
                (*in_front.as_ptr()).front = Some(back);
                self.list.back = Some(in_back);
            } else {
                // Наш список пуст, заменяем его на входной, остаёмся на псевдоэлементе
                std::mem::swap(self.list, &mut input);
            }

            self.list.len += input.len;
            // Не обязательно, но вежливо
            input.len = 0;

            // input освобождается здесь
        }
    }

    pub fn splice_after(&mut self, mut input: LinkedList<T>) {
        // Наше:
        //
        // input.front -> 1 <-> 2 <- input.back
        //
        // list.front -> A <-> B <-> C <- list.back
        //                     ^
        //                    cur
        //
        //
        // Превращается в :
        //
        // list.front -> A <-> B <-> 1 <-> 2 <-> C <- list.back
        //                     ^
        //                    cur
        //
        unsafe {
            // Мы можем вызывать для входных указателей либо `take`,
            // либо `mem::forget`. Take корректнее, если мы используем
            // собственный аллокатор или что-то, что тоже требует очистки!
            if input.is_empty() {
                // Пустой входной список, ничего не делаем
            } else if let Some(cur) = self.cur {
                // Оба списка не пустые
                let in_front = input.front.take().unwrap();
                let in_back = input.back.take().unwrap();

                if let Some(next) = (*cur.as_ptr()).back {
                    // Общий случай, никаких граничных случаев:
                    // просто правим приватные поля
                    (*next.as_ptr()).front = Some(in_back);
                    (*in_back.as_ptr()).back = Some(next);
                    (*cur.as_ptr()).back = Some(in_front);
                    (*in_front.as_ptr()).front = Some(cur);
                } else {
                    // Нет следующего элемента, добавляем сзади
                    (*cur.as_ptr()).back = Some(in_front);
                    (*in_front.as_ptr()).front = Some(cur);
                    self.list.back = Some(in_back);
                }
                // Индекс не меняется
            } else if let Some(front) = self.list.front {
                // Мы на псевдоэлементе и список не пуст, добавляем спереди.
                let in_front = input.front.take().unwrap();
                let in_back = input.back.take().unwrap();

                (*front.as_ptr()).front = Some(in_back);
                (*in_back.as_ptr()).back = Some(front);
                self.list.front = Some(in_front);
            } else {
                // Наш список пуст, заменяем его на входной, остаёмся на псевдоэлементе
                std::mem::swap(self.list, &mut input);
            }

            self.list.len += input.len;
            // Не обязательно, но вежливо
            input.len = 0;
            
            // input освобождается здесь
        }        
    }
}

unsafe impl<T: Send> Send for LinkedList<T> {}
unsafe impl<T: Sync> Sync for LinkedList<T> {}

unsafe impl<'a, T: Send> Send for Iter<'a, T> {}
unsafe impl<'a, T: Sync> Sync for Iter<'a, T> {}

unsafe impl<'a, T: Send> Send for IterMut<'a, T> {}
unsafe impl<'a, T: Sync> Sync for IterMut<'a, T> {}

#[allow(dead_code)]
fn assert_properties() {
    fn is_send<T: Send>() {}
    fn is_sync<T: Sync>() {}

    is_send::<LinkedList<i32>>();
    is_sync::<LinkedList<i32>>();

    is_send::<IntoIter<i32>>();
    is_sync::<IntoIter<i32>>();

    is_send::<Iter<i32>>();
    is_sync::<Iter<i32>>();

    is_send::<IterMut<i32>>();
    is_sync::<IterMut<i32>>();

    fn linked_list_covariant<'a, T>(x: LinkedList<&'static T>) -> LinkedList<&'a T> { x }
    fn iter_covariant<'i, 'a, T>(x: Iter<'i, &'static T>) -> Iter<'i, &'a T> { x }
    fn into_iter_covariant<'a, T>(x: IntoIter<&'static T>) -> IntoIter<&'a T> { x }

    /// ```compile_fail
    /// use lists::linked_list::IterMut;
    /// 
    /// fn iter_mut_covariant<'i, 'a, T>(x: IterMut<'i, &'static T>) -> IterMut<'i, &'a T> { x }
    /// ```
    fn iter_mut_invariant() {}    
}

#[cfg(test)]
mod test {
    use super::LinkedList;

    fn generate_test() -> LinkedList<i32> {
        list_from(&[0, 1, 2, 3, 4, 5, 6])
    }

    fn list_from<T: Clone>(v: &[T]) -> LinkedList<T> {
        v.iter().map(|x| (*x).clone()).collect()
    }

    #[test]
    fn test_basic_front() {
        let mut list = LinkedList::new();

        // Пытаемся сломать пустой список
        assert_eq!(list.len(), 0);
        assert_eq!(list.pop_front(), None);
        assert_eq!(list.len(), 0);

        // Пытаемся сломать список из одного элемента
        list.push_front(10);
        assert_eq!(list.len(), 1);
        assert_eq!(list.pop_front(), Some(10));
        assert_eq!(list.len(), 0);
        assert_eq!(list.pop_front(), None);
        assert_eq!(list.len(), 0);

        // Всё перемешиваем
        list.push_front(10);
        assert_eq!(list.len(), 1);
        list.push_front(20);
        assert_eq!(list.len(), 2);
        list.push_front(30);
        assert_eq!(list.len(), 3);
        assert_eq!(list.pop_front(), Some(30));
        assert_eq!(list.len(), 2);
        list.push_front(40);
        assert_eq!(list.len(), 3);
        assert_eq!(list.pop_front(), Some(40));
        assert_eq!(list.len(), 2);
        assert_eq!(list.pop_front(), Some(20));
        assert_eq!(list.len(), 1);
        assert_eq!(list.pop_front(), Some(10));
        assert_eq!(list.len(), 0);
        assert_eq!(list.pop_front(), None);
        assert_eq!(list.len(), 0);
        assert_eq!(list.pop_front(), None);
        assert_eq!(list.len(), 0);
    }

    #[test]
    fn test_basic() {
        let mut m = LinkedList::new();
        assert_eq!(m.pop_front(), None);
        assert_eq!(m.pop_back(), None);
        assert_eq!(m.pop_front(), None);
        m.push_front(1);
        assert_eq!(m.pop_front(), Some(1));
        m.push_back(2);
        m.push_back(3);
        assert_eq!(m.len(), 2);
        assert_eq!(m.pop_front(), Some(2));
        assert_eq!(m.pop_front(), Some(3));
        assert_eq!(m.len(), 0);
        assert_eq!(m.pop_front(), None);
        m.push_back(1);
        m.push_back(3);
        m.push_back(5);
        m.push_back(7);
        assert_eq!(m.pop_front(), Some(1));

        let mut n = LinkedList::new();
        n.push_front(2);
        n.push_front(3);
        {
            assert_eq!(n.front().unwrap(), &3);
            let x = n.front_mut().unwrap();
            assert_eq!(*x, 3);
            *x = 0;
        }
        {
            assert_eq!(n.back().unwrap(), &2);
            let y = n.back_mut().unwrap();
            assert_eq!(*y, 2);
            *y = 1;
        }
        assert_eq!(n.pop_front(), Some(0));
        assert_eq!(n.pop_front(), Some(1));
    }

    #[test]
    fn test_iterator() {
        let m = generate_test();
        for (i, elt) in m.iter().enumerate() {
            assert_eq!(i as i32, *elt);
        }
        let mut n = LinkedList::new();
        assert_eq!(n.iter().next(), None);
        n.push_front(4);
        let mut it = n.iter();
        assert_eq!(it.size_hint(), (1, Some(1)));
        assert_eq!(it.next().unwrap(), &4);
        assert_eq!(it.size_hint(), (0, Some(0)));
        assert_eq!(it.next(), None);
    }

    #[test]
    fn test_iterator_double_end() {
        let mut n = LinkedList::new();
        assert_eq!(n.iter().next(), None);
        n.push_front(4);
        n.push_front(5);
        n.push_front(6);
        let mut it = n.iter();
        assert_eq!(it.size_hint(), (3, Some(3)));
        assert_eq!(it.next().unwrap(), &6);
        assert_eq!(it.size_hint(), (2, Some(2)));
        assert_eq!(it.next_back().unwrap(), &4);
        assert_eq!(it.size_hint(), (1, Some(1)));
        assert_eq!(it.next_back().unwrap(), &5);
        assert_eq!(it.next_back(), None);
        assert_eq!(it.next(), None);
    }

    #[test]
    fn test_rev_iter() {
        let m = generate_test();
        for (i, elt) in m.iter().rev().enumerate() {
            assert_eq!(6 - i as i32, *elt);
        }
        let mut n = LinkedList::new();
        assert_eq!(n.iter().rev().next(), None);
        n.push_front(4);
        let mut it = n.iter().rev();
        assert_eq!(it.size_hint(), (1, Some(1)));
        assert_eq!(it.next().unwrap(), &4);
        assert_eq!(it.size_hint(), (0, Some(0)));
        assert_eq!(it.next(), None);
    }

    #[test]
    fn test_mut_iter() {
        let mut m = generate_test();
        let mut len = m.len();
        for (i, elt) in m.iter_mut().enumerate() {
            assert_eq!(i as i32, *elt);
            len -= 1;
        }
        assert_eq!(len, 0);
        let mut n = LinkedList::new();
        assert!(n.iter_mut().next().is_none());
        n.push_front(4);
        n.push_back(5);
        let mut it = n.iter_mut();
        assert_eq!(it.size_hint(), (2, Some(2)));
        assert!(it.next().is_some());
        assert!(it.next().is_some());
        assert_eq!(it.size_hint(), (0, Some(0)));
        assert!(it.next().is_none());
    }

    #[test]
    fn test_iterator_mut_double_end() {
        let mut n = LinkedList::new();
        assert!(n.iter_mut().next_back().is_none());
        n.push_front(4);
        n.push_front(5);
        n.push_front(6);
        let mut it = n.iter_mut();
        assert_eq!(it.size_hint(), (3, Some(3)));
        assert_eq!(*it.next().unwrap(), 6);
        assert_eq!(it.size_hint(), (2, Some(2)));
        assert_eq!(*it.next_back().unwrap(), 4);
        assert_eq!(it.size_hint(), (1, Some(1)));
        assert_eq!(*it.next_back().unwrap(), 5);
        assert!(it.next_back().is_none());
        assert!(it.next().is_none());
    }

    #[test]
    fn test_eq() {
        let mut n: LinkedList<u8> = list_from(&[]);
        let mut m = list_from(&[]);
        assert!(n == m);
        n.push_front(1);
        assert!(n != m);
        m.push_back(1);
        assert!(n == m);

        let n = list_from(&[2, 3, 4]);
        let m = list_from(&[1, 2, 3]);
        assert!(n != m);
    }

    #[test]
    fn test_ord() {
        let n = list_from(&[]);
        let m = list_from(&[1, 2, 3]);
        assert!(n < m);
        assert!(m > n);
        assert!(n <= n);
        assert!(n >= n);
    }

    #[test]
    fn test_ord_nan() {
        let nan = 0.0f64 / 0.0;
        let n = list_from(&[nan]);
        let m = list_from(&[nan]);
        assert!(!(n < m));
        assert!(!(n > m));
        assert!(!(n <= m));
        assert!(!(n >= m));

        let n = list_from(&[nan]);
        let one = list_from(&[1.0f64]);
        assert!(!(n < one));
        assert!(!(n > one));
        assert!(!(n <= one));
        assert!(!(n >= one));

        let u = list_from(&[1.0f64, 2.0, nan]);
        let v = list_from(&[1.0f64, 2.0, 3.0]);
        assert!(!(u < v));
        assert!(!(u > v));
        assert!(!(u <= v));
        assert!(!(u >= v));

        let s = list_from(&[1.0f64, 2.0, 4.0, 2.0]);
        let t = list_from(&[1.0f64, 2.0, 3.0, 2.0]);
        assert!(!(s < t));
        assert!(s > one);
        assert!(!(s <= one));
        assert!(s >= one);
    }

    #[test]
    fn test_debug() {
        let list: LinkedList<i32> = (0..10).collect();
        assert_eq!(format!("{:?}", list), "[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]");

        let list: LinkedList<&str> = vec!["just", "one", "test", "more"]
            .iter().copied()
            .collect();
        assert_eq!(format!("{:?}", list), r#"["just", "one", "test", "more"]"#);
    }

    #[test]
    fn test_hashmap() {
        // Убеждаемся, что HashMap работает со списком в качестве ключа

        let list1: LinkedList<i32> = (0..10).collect();
        let list2: LinkedList<i32> = (1..11).collect();
        let mut map = std::collections::HashMap::new();

        assert_eq!(map.insert(list1.clone(), "list1"), None);
        assert_eq!(map.insert(list2.clone(), "list2"), None);

        assert_eq!(map.len(), 2);

        assert_eq!(map.get(&list1), Some(&"list1"));
        assert_eq!(map.get(&list2), Some(&"list2"));

        assert_eq!(map.remove(&list1), Some("list1"));
        assert_eq!(map.remove(&list2), Some("list2"));

        assert!(map.is_empty());
    }

    #[test]
    fn test_cursor_move_peek() {
        let mut m: LinkedList<u32> = LinkedList::new();
        m.extend([1, 2, 3, 4, 5, 6]);
        let mut cursor = m.cursor_mut();
        cursor.move_next();
        assert_eq!(cursor.current(), Some(&mut 1));
        assert_eq!(cursor.peek_next(), Some(&mut 2));
        assert_eq!(cursor.peek_prev(), None);
        assert_eq!(cursor.index(), Some(0));
        cursor.move_prev();
        assert_eq!(cursor.current(), None);
        assert_eq!(cursor.peek_next(), Some(&mut 1));
        assert_eq!(cursor.peek_prev(), Some(&mut 6));
        assert_eq!(cursor.index(), None);
        cursor.move_next();
        cursor.move_next();
        assert_eq!(cursor.current(), Some(&mut 2));
        assert_eq!(cursor.peek_next(), Some(&mut 3));
        assert_eq!(cursor.peek_prev(), Some(&mut 1));
        assert_eq!(cursor.index(), Some(1));

        let mut cursor = m.cursor_mut();
        cursor.move_prev();
        assert_eq!(cursor.current(), Some(&mut 6));
        assert_eq!(cursor.peek_next(), None);
        assert_eq!(cursor.peek_prev(), Some(&mut 5));
        assert_eq!(cursor.index(), Some(5));
        cursor.move_next();
        assert_eq!(cursor.current(), None);
        assert_eq!(cursor.peek_next(), Some(&mut 1));
        assert_eq!(cursor.peek_prev(), Some(&mut 6));
        assert_eq!(cursor.index(), None);
        cursor.move_prev();
        cursor.move_prev();
        assert_eq!(cursor.current(), Some(&mut 5));
        assert_eq!(cursor.peek_next(), Some(&mut 6));
        assert_eq!(cursor.peek_prev(), Some(&mut 4));
        assert_eq!(cursor.index(), Some(4));
    }

    #[test]
    fn test_cursor_mut_insert() {
        let mut m: LinkedList<u32> = LinkedList::new();
        m.extend([1, 2, 3, 4, 5, 6]);
        let mut cursor = m.cursor_mut();
        cursor.move_next();
        cursor.splice_before(Some(7).into_iter().collect());
        cursor.splice_after(Some(8).into_iter().collect());
        // check_links(&m);
        assert_eq!(m.iter().cloned().collect::<Vec<_>>(), &[7, 1, 8, 2, 3, 4, 5, 6]);
        let mut cursor = m.cursor_mut();
        cursor.move_next();
        cursor.move_prev();
        cursor.splice_before(Some(9).into_iter().collect());
        cursor.splice_after(Some(10).into_iter().collect());
        check_links(&m);
        assert_eq!(m.iter().cloned().collect::<Vec<_>>(), &[10, 7, 1, 8, 2, 3, 4, 5, 6, 9]);
        
        /* удаляем, сейчас не реализовано:
        let mut cursor = m.cursor_mut();
        cursor.move_next();
        cursor.move_prev();
        assert_eq!(cursor.remove_current(), None);
        cursor.move_next();
        cursor.move_next();
        assert_eq!(cursor.remove_current(), Some(7));
        cursor.move_prev();
        cursor.move_prev();
        cursor.move_prev();
        assert_eq!(cursor.remove_current(), Some(9));
        cursor.move_next();
        assert_eq!(cursor.remove_current(), Some(10));
        check_links(&m);
        assert_eq!(m.iter().cloned().collect::<Vec<_>>(), &[1, 8, 2, 3, 4, 5, 6]);
        */

        check_links(&m);
        let mut m: LinkedList<u32> = LinkedList::new();
        m.extend([1, 8, 2, 3, 4, 5, 6]);
        let mut cursor = m.cursor_mut();
        cursor.move_next();
        let mut p: LinkedList<u32> = LinkedList::new();
        p.extend([100, 101, 102, 103]);
        let mut q: LinkedList<u32> = LinkedList::new();
        q.extend([200, 201, 202, 203]);
        cursor.splice_after(p);
        cursor.splice_before(q);
        assert_eq!(
            m.iter().cloned().collect::<Vec<_>>(),
            &[200, 201, 202, 203, 1, 100, 101, 102, 103, 8, 2, 3, 4, 5, 6]
        );
        let mut cursor = m.cursor_mut();
        cursor.move_next();
        cursor.move_prev();
        let tmp = cursor.split_before();
        assert_eq!(m.into_iter().collect::<Vec<_>>(), &[]);
        m = tmp;
        let mut cursor = m.cursor_mut();
        cursor.move_next();
        cursor.move_next();
        cursor.move_next();
        cursor.move_next();
        cursor.move_next();
        cursor.move_next();
        cursor.move_next();
        let tmp = cursor.split_after();
        assert_eq!(tmp.into_iter().collect::<Vec<_>>(), &[102, 103, 8, 2, 3, 4, 5, 6]);
        check_links(&m);
        assert_eq!(m.iter().cloned().collect::<Vec<_>>(), &[200, 201, 202, 203, 1, 100, 101]);
    }

    fn check_links<T: Eq + std::fmt::Debug>(list: &LinkedList<T>) {
        let from_front: Vec<_> = list.iter().collect();
        let from_back: Vec<_> = list.iter().rev().collect();
        let re_reved: Vec<_> = from_back.into_iter().rev().collect();

        assert_eq!(from_front, re_reved);
    }
}
}

Немного дурацких списков

Ладно. Вот и всё. Мы написали все списки.

ахахахаха

Нет

Всегда есть ещё немного списков.

Эта глава — живой документ про самые нелепые связные списки и про их реализацию в Rust.

  1. Двойной односвязный
  2. Список размещённый на стеке
  3. Само-ссылающийся список-арена?
  4. Список GhostCell?

Двойной односвязный список

У нас были трудности при работе с двусвязными списками, поскольку у них сложная семантика владения: строго говоря, ни один узел не владеет никаким другим узлом. Однако эти трудности вызваны, скорее, нашим ограниченным представлением о том, чем являются связные списки. А именно, мы считаем, что все ссылки должны идти в одном и том же направлении.

Вместо этого мы можем разбить наш список на две половины: одна направлена влево, а вторая вправо:

// lib.rs
// ...
pub mod silly1;     // НОВЫЙ!
// silly1.rs
use crate::second::List as Stack;

struct List<T> {
    left: Stack<T>,
    right: Stack<T>,
}

Теперь вместо обычного безопасного стека мы получили список общего назначения. Мы можем расширять список влево или вправо, добавляя значения в любой из стеков. Мы также можем «прокручивать» по списку, извлекая значения с одного конца и вставляя их в другой. Чтобы избежать ненужного выделения памяти, скопируем исходный код нашего безопасного стека, чтобы получить доступ к его приватным полям:

pub struct Stack<T> {
    head: Link<T>,
}

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

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

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

    pub fn push(&mut self, elem: T) {
        let new_node = Box::new(Node {
            elem: elem,
            next: self.head.take(),
        });

        self.head = Some(new_node);
    }

    pub fn pop(&mut self) -> Option<T> {
        self.head.take().map(|node| {
            let node = *node;
            self.head = node.next;
            node.elem
        })
    }

    pub fn peek(&self) -> Option<&T> {
        self.head.as_ref().map(|node| {
            &node.elem
        })
    }

    pub fn peek_mut(&mut self) -> Option<&mut T> {
        self.head.as_mut().map(|node| {
            &mut node.elem
        })
    }
}

impl<T> Drop for Stack<T> {
    fn drop(&mut self) {
        let mut cur_link = self.head.take();
        while let Some(mut boxed_node) = cur_link {
            cur_link = boxed_node.next.take();
        }
    }
}

И немного переработаем push и pop:

pub fn push(&mut self, elem: T) {
    let new_node = Box::new(Node {
        elem: elem,
        next: None,
    });

    self.push_node(new_node);
}

fn push_node(&mut self, mut node: Box<Node<T>>) {
    node.next = self.head.take();
    self.head = Some(node);
}

pub fn pop(&mut self) -> Option<T> {
    self.pop_node().map(|node| {
        node.elem
    })
}

fn pop_node(&mut self) -> Option<Box<Node<T>>> {
    self.head.take().map(|mut node| {
        self.head = node.next.take();
        node
    })
}

Теперь мы можем написать наш список:

pub struct List<T> {
    left: Stack<T>,
    right: Stack<T>,
}

impl<T> List<T> {
    fn new() -> Self {
        List { left: Stack::new(), right: Stack::new() }
    }
}

И выполнять над ним обычные операции:

pub fn push_left(&mut self, elem: T) { self.left.push(elem) }
pub fn push_right(&mut self, elem: T) { self.right.push(elem) }
pub fn pop_left(&mut self) -> Option<T> { self.left.pop() }
pub fn pop_right(&mut self) -> Option<T> { self.right.pop() }
pub fn peek_left(&self) -> Option<&T> { self.left.peek() }
pub fn peek_right(&self) -> Option<&T> { self.right.peek() }
pub fn peek_left_mut(&mut self) -> Option<&mut T> { self.left.peek_mut() }
pub fn peek_right_mut(&mut self) -> Option<&mut T> { self.right.peek_mut() }

Но, что гораздо интереснее, мы можем его прокручивать!

pub fn go_left(&mut self) -> bool {
    self.left.pop_node().map(|node| {
        self.right.push_node(node);
    }).is_some()
}

pub fn go_right(&mut self) -> bool {
    self.right.pop_node().map(|node| {
        self.left.push_node(node);
    }).is_some()
}

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

#[cfg(test)]
mod test {
    use super::List;

    #[test]
    fn walk_aboot() {
        let mut list = List::new();             // [_]

        list.push_left(0);                      // [0,_]
        list.push_right(1);                     // [0, _, 1]
        assert_eq!(list.peek_left(), Some(&0));
        assert_eq!(list.peek_right(), Some(&1));

        list.push_left(2);                      // [0, 2, _, 1]
        list.push_left(3);                      // [0, 2, 3, _, 1]
        list.push_right(4);                     // [0, 2, 3, _, 4, 1]

        while list.go_left() {}                 // [_, 0, 2, 3, 4, 1]

        assert_eq!(list.pop_left(), None);
        assert_eq!(list.pop_right(), Some(0));  // [_, 2, 3, 4, 1]
        assert_eq!(list.pop_right(), Some(2));  // [_, 3, 4, 1]

        list.push_left(5);                      // [5, _, 3, 4, 1]
        assert_eq!(list.pop_right(), Some(3));  // [5, _, 4, 1]
        assert_eq!(list.pop_left(), Some(5));   // [_, 4, 1]
        assert_eq!(list.pop_right(), Some(4));  // [_, 1]
        assert_eq!(list.pop_right(), Some(1));  // [_]

        assert_eq!(list.pop_right(), None);
        assert_eq!(list.pop_left(), None);

    }
}
> cargo test

     Running target/debug/lists-5c71138492ad4b4a

running 16 tests
test fifth::test::into_iter ... ok
test fifth::test::basics ... ok
test fifth::test::iter ... ok
test fifth::test::iter_mut ... ok
test fourth::test::into_iter ... ok
test fourth::test::basics ... ok
test fourth::test::peek ... ok
test first::test::basics ... ok
test second::test::into_iter ... ok
test second::test::basics ... ok
test second::test::iter ... ok
test second::test::iter_mut ... ok
test third::test::basics ... ok
test third::test::iter ... ok
test second::test::peek ... ok
test silly1::test::walk_aboot ... ok

test result: ok. 16 passed; 0 failed; 0 ignored; 0 measured

Перед нами — экстремальный пример структуры данных с указателем (finger data structure). Благодаря своеобразному указателю, структура позволяет выполнять операции над элементами за время, пропорциональное расстоянию до него.

Можно быстро вносить изменения рядом с указателем, но если нам надо поправить список где-то далеко, туда придётся добраться. Можно либо идти по списку, перекладывая элементы из одного стека в другой, либо двигаться с помощью временного изменяемого итератора &mut. Но помните, итератор не позволит нам вернуться назад, в то время как указатель позволит!

Связный список, размещённый на стеке

Эта книга посвящена связным спискам, размещаемым в куче, поскольку они и распространены, и практичны. Но не обязательно выделять память в куче, хотя это удобно, поскольку позволяет выделять память динамически. В этом смысле выделение памяти в стеке менее дружелюбно: такие вещи, как alloca из C достаточно Ужасны и Проблемны.

Будем выделять память в стеке простым способом: вызвав функцию и получая новый фрейм стека для нового узла! Это очень простое решение нашей проблемы, в то же время практичное и полезное. Многие так делают, даже не подозревая, что реализуют связный список!

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

Теперь, когда мы перешли к нелепой части книги, мы будем программировать нелепым способом: связный список будет главной звездой а весь пользовательский код окажется среди функций обратного вызова (callback)! Все любят вложенные функции обратного вызова!

Наш тип списка будет обычным узлом со ссылкой на другой узел.

#![allow(unused)]
fn main() {
pub struct List<'a, T> {
    pub data: T,
    pub prev: Option<&'a List<'a, T>>,
}
}

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

impl<'a, T> List<'a, T> {
    pub fn push<U>(
        prev: Option<&'a List<'a, T>>, 
        data: T, 
        callback: impl FnOnce(&List<'a, T>) -> U,
    ) -> U {
        let list = List { data, prev };
        callback(&list)
    }
}

Вот и всё! Использовать этот код можно так:

List::push(None, 3, |list| {
    println!("{}", list.data);
    List::push(Some(list), 5, |list| {
        println!("{}", list.data);
        List::push(Some(list), 13, |list| {
            println!("{}", list.data);
        })
    })
})

Красиво. 😿

Пользователь может перемещаться по списку, используя конструкцию while-let и извлекая значения prev, но, веселья ради, давайте реализуем наш обычный итератор:

impl<'a, T> List<'a, T> {
    pub fn iter(&'a self) -> Iter<'a, T> {
        Iter { next: Some(self) }
    }
}

impl<'a, T> Iterator for Iter<'a, T> {
    type Item = &'a T;

    fn next(&mut self) -> Option<Self::Item> {
        self.next.map(|node| {
            self.next = node.prev;
            &node.data
        })
    }
}

Протестируем:

#[cfg(test)]
mod test {
    use super::List;

    #[test]
    fn elegance() {
        List::push(None, 3, |list| {
            assert_eq!(list.iter().copied().sum::<i32>(), 3);
            List::push(Some(list), 5, |list| {
                assert_eq!(list.iter().copied().sum::<i32>(), 5 + 3);
                List::push(Some(list), 13, |list| {
                    assert_eq!(list.iter().copied().sum::<i32>(), 13 + 5 + 3);
                })
            })
        })
    }
}
> cargo test

running 18 tests
test fifth::test::into_iter ... ok
test fifth::test::iter ... ok
test fifth::test::iter_mut ... ok
test fifth::test::basics ... ok
test fifth::test::miri_food ... ok
test first::test::basics ... ok
test second::test::into_iter ... ok
test fourth::test::peek ... ok
test fourth::test::into_iter ... ok
test second::test::iter_mut ... ok
test fourth::test::basics ... ok
test second::test::basics ... ok
test second::test::iter ... ok
test third::test::basics ... ok
test silly1::test::walk_aboot ... ok
test silly2::test::elegance ... ok
test second::test::peek ... ok
test third::test::iter ... ok

test result: ok. 18 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out;

Сейчас, вероятно, вы задаётесь вопросом, можно ли менять данные внутри узла? Давайте проверим. Будем хранить в списке изменяемые ссылки вместо разделяемых:

#![allow(unused)]
fn main() {
pub struct List<'a, T> {
    pub data: T,
    pub prev: Option<&'a mut List<'a, T>>,
}

pub struct Iter<'a, T> {
    next: Option<&'a List<'a, T>>,
}

impl<'a, T> List<'a, T> {
    pub fn push<U>(
        prev: Option<&'a mut List<'a, T>>, 
        data: T, 
        callback: impl FnOnce(&mut List<'a, T>) -> U,
    ) -> U {
        let mut list = List { data, prev };
        callback(&mut list)
    }

    pub fn iter(&'a self) -> Iter<'a, T> {
        Iter { next: Some(self) }
    }
}

impl<'a, T> Iterator for Iter<'a, T> {
    type Item = &'a T;

    fn next(&mut self) -> Option<Self::Item> {
        self.next.map(|node| {
            self.next = node.prev.as_ref().map(|prev| &**prev);
            &node.data
        })
    }
}

}
> cargo test

error[E0521]: borrowed data escapes outside of closure
  --> src\silly2.rs:47:32
   |
46 |  List::push(Some(list), 13, |list| {
   |                              ----
   |                              |
   |              `list` declared here, outside of the closure body
   |              `list` is a reference that is only valid in the closure body
47 |      assert_eq!(list.iter().copied().sum::<i32>(), 13 + 5 + 3);
   |                 ^^^^^^^^^^^ `list` escapes the closure body here

error[E0521]: borrowed data escapes outside of closure
  --> src\silly2.rs:45:28
   |
44 |  List::push(Some(list), 5, |list| {
   |                             ----
   |                             |
   |              `list` declared here, outside of the closure body
   |              `list` is a reference that is only valid in the closure body
45 |      assert_eq!(list.iter().copied().sum::<i32>(), 5 + 3);
   |                 ^^^^^^^^^^^ `list` escapes the closure body here


<до бесконечности>

Что ж. Кажется, компилятору не понравился наш итератор. Возможно, мы что-то упустили? Попробуем упростить наш тест:

#[test]
fn elegance() {
    List::push(None, 3, |list| {
        assert_eq!(list.data, 3);
        List::push(Some(list), 5, |list| {
            assert_eq!(list.data, 5);
            List::push(Some(list), 13, |list| {
                assert_eq!(list.data, 13);
            })
        })
    })
}
> cargo test

error[E0521]: borrowed data escapes outside of closure
  --> src\silly2.rs:46:17
   |
44 |   List::push(Some(list), 5, |list| {
   |                              ----
   |                              |
   |              `list` declared here, outside of the closure body
   |              `list` is a reference that is only valid in the closure body
45 |       assert_eq!(list.data, 5);
46 | /     List::push(Some(list), 13, |list| {
47 | |         assert_eq!(list.data, 13);
48 | |     })
   | |______^ `list` escapes the closure body here

error[E0521]: borrowed data escapes outside of closure
  --> src\silly2.rs:44:13
   |
42 |   List::push(None, 3, |list| {
   |                        ----
   |                        |
   |              `list` declared here, outside of the closure body
   |              `list` is a reference that is only valid in the closure body
43 |       assert_eq!(list.data, 3);
44 | /     List::push(Some(list), 5, |list| {
45 | |         assert_eq!(list.data, 5);
46 | |         List::push(Some(list), 13, |list| {
47 | |             assert_eq!(list.data, 13);
48 | |         })
49 | |     })
   | |______________^ `list` escapes the closure body here

Хмм, нет, это всё ещё ерунда.

Проблема в том, что наш список случайно (😉) полагается на вариантность. Вариантность — сложная тема, но давайте рассмотрим её в упрощённом виде:

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

Так... почему всё компилировалось, когда мы использовали разделяемые ссылки? Потому что в большинстве случаев компилятор знает, что «долгожители» безопасны! Когда мы помещаем ссылку на список в следующий список, компилятор втихую «урезает» время жизни, чтобы оно совпадало с ожиданиями нового списка. Это урезание времени жизни и создаёт вариантность.

Тот же самый трюк, как в языках с наследованием, где вы можете передать Cat туда, где ожидается Animal (базовый тип Cat). Интуитивно мы понимаем, что передавать Cat, когда ожидается Animal — это нормально, потому что Animal включает Cat и что-то ещё. Нормально временно забыть про что-то ещё, так ведь?

Точно также, меньшее время жизни включает большее время жизни и что-то ещё. Нормально и здесь забыть про что-то ещё!

Теперь вы наверняка удивляетесь, почему не работает версия с изменяемой ссылкой?

Ну, потому что вариантность не всегда безопасна. Если бы наш код компилировался, мы могли бы реализовать использование-после-освобождения (use-after-free), например, вот так:

List::push(None, 3, |list| {
    List::push(Some(list), 5, |list| {
        List::push(Some(list), 13, |list| {
            // ХАХАХА все временя жизни одинаковы, так что компилятор
            // позволит мне поменять моего родителя, чтобы оставить
            // мутабельную ссылку на меня!
            // Я создам объект, который можно использовать-после-освобождения!!!
            *list.prev.as_mut().unwrap().prev = Some(list);
        })
    })
})

Главная беда забытых мелочей в том, что где-то помнят про них и рассчитывают на них. Изменяемые данные остаются очень большой проблемой. Если вы не проявите осторожность, код, который не помнит про что-то ещё, может заменить что-то ещё и сломать те части программы, которые «помнят» и ожидают, что что-то ещё будет на месте.

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

let mut my_kitty = Cat;                  // Создаём Кота (длинное время жизни)
let animal: &mut Animal = &mut my_kitty; // Забываем, что это Кот (кратчайшее время жизни)
*animal = Dog;                           // Меняем на Собаку (короткое время жизни)
my_kitty.meow();                         // Мяукающая Собака! (использование-после-освобождения)

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

В частности, &mut &'big mut T нельзя преобразовывать в &mut &'small mut T, где 'big живёт дольше, чем 'small. Или, более формально &'a mut T ковариантен относительно 'a, но инвариантен относительно T.

Занимательный факт: Java в принципе позволяет проворачивать такие трюки, но в ней есть проверка времени выполнения, которая предотвращает мяуканье собак.


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

Вернём предыдущую версию нашего кода с разделяемыми ссылками и используем Cell в новом тесте:

#[test]
fn cell() {
    use std::cell::Cell;

    List::push(None, Cell::new(3), |list| {
        List::push(Some(list), Cell::new(5), |list| {
            List::push(Some(list), Cell::new(13), |list| {
                // Умножаем каждое значение в списке на 10
                for val in list.iter() {
                    val.set(val.get() * 10)
                }

                let mut vals = list.iter();
                assert_eq!(vals.next().unwrap().get(), 130);
                assert_eq!(vals.next().unwrap().get(), 50);
                assert_eq!(vals.next().unwrap().get(), 30);
                assert_eq!(vals.next(), None);
                assert_eq!(vals.next(), None);
            })
        })
    })
}
> cargo test

running 19 tests
test fifth::test::into_iter ... ok
test fifth::test::basics ... ok
test fifth::test::iter_mut ... ok
test fifth::test::iter ... ok
test fourth::test::basics ... ok
test fourth::test::into_iter ... ok
test second::test::into_iter ... ok
test first::test::basics ... ok
test fourth::test::peek ... ok
test second::test::basics ... ok
test fifth::test::miri_food ... ok
test silly2::test::cell ... ok
test third::test::iter ... ok
test second::test::iter_mut ... ok
test second::test::peek ... ok
test silly1::test::walk_aboot ... ok
test silly2::test::elegance ... ok
test third::test::basics ... ok
test second::test::iter ... ok

test result: ok. 19 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out;

Проще рекурсивной репы! ✨