Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Язык программирования Rust

авторы: Steve Klabnik, Carol Nichols и Chris Krycho при участии сообщества Rust

Эта версия текста предполагает, что вы используете Rust 1.90.0 (выпущенный 18.09.2025) или более позднюю версию с edition = "2024" в файле Cargo.toml всех проектов для настройки их в соответствии с идиомами редакции Rust 2024. Инструкции по установке или обновлению Rust см. в разделе «Установка» главы 1, а информацию о редакциях Rust — в Приложении E.

HTML-версия доступна онлайн по адресу

https://doc.rust-lang.org/stable/book/

а также офлайн при установке Rust через rustup; выполните rustup doc --book, чтобы открыть книгу.

Также доступны несколько переводов, подготовленных сообществом.

Этот текст доступен в печатном формате и формате электронных книг от No Starch Press.

🚨 Хотите получить более интерактивный опыт обучения? Попробуйте другую версию Rust Book, в которой доступны: тесты, подсветка, визуализации и многое другое: https://rust-book.cs.brown.edu

Предисловие

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

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

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

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

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

Добро пожаловать в сообщество Rust!

  • Bec Rumbul, исполнительный директор Rust Foundation

Введение

Примечание: Это издание книги идентично книге The Rust Programming Language, доступной в печатном формате и в формате электронной книги от No Starch Press.

Добро пожаловать в The Rust Programming Language — вводную книгу по Rust. Язык программирования Rust помогает писать более быстрое и надёжное программное обеспечение. Высокоуровневое удобство разработки и низкоуровневый контроль часто противоречат друг другу при проектировании языков программирования; Rust бросает вызов этому противоречию. За счёт баланса между мощными техническими возможностями и качественным опытом разработки Rust даёт вам возможность контролировать низкоуровневые детали (например, использование памяти) без всех сложностей, которые традиционно связаны с таким уровнем контроля.

Для кого предназначен Rust

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

Команды разработчиков

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

Rust также приносит в мир системного программирования современные инструменты разработки:

  • Cargo — встроенный менеджер зависимостей и инструмент сборки — делает добавление, компиляцию и управление зависимостями простыми и единообразными для всей экосистемы Rust.
  • Инструмент форматирования rustfmt обеспечивает единый стиль написания кода для всех разработчиков.
  • Rust Language Server обеспечивает интеграцию со средами разработки (IDE), предоставляя автодополнение кода и встроенные сообщения об ошибках.

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

Студенты

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

Компании

Сотни компаний — как крупных, так и небольших — используют Rust в производственной среде для самых разных задач, включая инструменты командной строки, веб-сервисы, DevOps-инструменты, встроенные устройства, анализ и транскодирование аудио и видео, криптовалюты, биоинформатику, поисковые системы, приложения Интернета вещей (IoT), машинное обучение и даже значительные части веб-браузера Firefox.

Разработчики открытого исходного кода

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

Люди, которые ценят скорость и стабильность

Rust предназначен для людей, которым важны скорость и стабильность языка. Под скоростью мы подразумеваем как скорость выполнения кода Rust, так и скорость, с которой Rust позволяет создавать программы. Проверки компилятора Rust обеспечивают стабильность при добавлении новых возможностей и рефакторинге. Это противопоставляется хрупкому устаревшему коду в языках без подобных проверок, который разработчики часто опасаются изменять. Стремясь к созданию абстракций с нулевой стоимостью (zero-cost abstractions) — высокоуровневых возможностей, которые компилируются в низкоуровневый код с той же скоростью, что и написанный вручную, — Rust стремится сделать безопасный код также и быстрым.

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

Для кого предназначена эта книга

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

Как использовать эту книгу

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

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

Глава 1 объясняет, как установить Rust, как написать программу «Привет, мир!», а также как использовать Cargo — менеджер пакетов и инструмент сборки Rust. Глава 2 представляет собой практическое введение в написание программ на Rust, в котором вы создадите игру по угадыванию числа. Здесь мы рассматриваем концепции на высоком уровне, а в последующих главах будут представлены дополнительные подробности. Если вы хотите сразу приступить к практике, то Глава 2 — подходящее место. Если же вы относитесь к числу особенно внимательных учащихся и предпочитаете изучить каждую деталь перед переходом к следующему материалу, вы можете пропустить Главу 2 и сразу перейти к Главе 3, которая рассматривает возможности Rust, похожие на возможности других языков программирования; после этого вы сможете вернуться к Главе 2, когда захотите поработать над проектом, применяя изученные детали.

В Главе 4 вы познакомитесь с системой владения (ownership) Rust. Глава 5 рассматривает структуры и методы. Глава 6 посвящена перечислениям, выражениям match, а также конструкциям управления потоком if let и let...else. Вы будете использовать структуры и перечисления для создания собственных типов.

В Главе 7 вы познакомитесь с системой модулей Rust и правилами приватности для организации вашего кода и его публичного интерфейса программирования приложений (API). Глава 8 рассматривает некоторые распространённые структуры данных коллекций, предоставляемые стандартной библиотекой: векторы, строки и HashMap. Глава 9 исследует философию и подходы Rust к обработке ошибок.

Глава 10 подробно рассматривает дженерики (generics), трейты и времена жизни (lifetimes), которые дают возможность определять код, применимый сразу к нескольким типам. Глава 11 полностью посвящена тестированию, которое, даже при наличии гарантий безопасности Rust, остаётся необходимым для обеспечения корректности логики программы. В Главе 12 мы создадим собственную реализацию части функциональности инструмента командной строки grep, который выполняет поиск текста внутри файлов. Для этого мы будем использовать многие концепции, обсуждавшиеся в предыдущих главах.

Глава 13 рассматривает замыкания и итераторы — возможности Rust, пришедшие из функциональных языков программирования. В Главе 14 мы более подробно изучим Cargo и обсудим лучшие практики распространения ваших библиотек. Глава 15 рассматривает умные указатели, предоставляемые стандартной библиотекой, а также трейты, обеспечивающие их функциональность.

В Главе 16 мы рассмотрим различные модели конкурентного программирования и поговорим о том, как Rust помогает бесстрашно работать с несколькими потоками. В Главе 17 мы продолжим эту тему, изучив синтаксис async и await в Rust, а также задачи (tasks), Future и потоки (streams), вместе с лёгкой моделью конкурентности, которую они предоставляют.

Глава 18 рассматривает, как идиомы Rust соотносятся с принципами объектно-ориентированного программирования, с которыми вы, возможно, уже знакомы. Глава 19 представляет собой справочник по шаблонам и сопоставлению с шаблонами (pattern matching) — мощным способам выражения идей в программах на Rust. Глава 20 содержит набор продвинутых тем, включая unsafe Rust, макросы и дополнительные сведения о временах жизни, трейтах, типах, функциях и замыканиях.

В Главе 21 мы завершим проект, реализовав низкоуровневый многопоточный веб-сервер!

Наконец, несколько приложений содержат полезную справочную информацию о языке. Приложение A посвящено ключевым словам Rust, Приложение B — операторам и символам Rust, Приложение C — автоматически выводимым трейтам, предоставляемым стандартной библиотекой, Приложение D содержит некоторые полезные инструменты разработки, а Приложение E объясняет редакции Rust. В Приложении F вы сможете найти переводы книги, а в Приложении G мы рассмотрим, как создаётся Rust и что представляет собой nightly Rust.

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

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

FerrisЗначение
Ferris with a question markЭтот код не компилируется!
Ferris throwing up their handsЭтот код вызывает панику (panic!)!
Ferris with one claw up, shruggingЭтот код не даёт ожидаемого поведения.

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

Исходный код

Исходные файлы, на основе которых генерируется эта книга, доступны на GitHub.

Начало работы

Давайте начнём ваше путешествие в мир Rust! Впереди много нового, но любое путешествие начинается с чего-то. В этой главе мы рассмотрим:

  • установку Rust на Linux, macOS и Windows
  • написание программы, которая выводит Hello, world!
  • использование cargo — менеджера пакетов и системы сборки Rust

Установка

Установка

Первым шагом является установка Rust. Мы загрузим Rust с помощью rustup — инструмента командной строки для управления версиями Rust и связанными инструментами. Для загрузки вам потребуется подключение к интернету.

Примечание: Если по какой-либо причине вы предпочитаете не использовать rustup, ознакомьтесь со страницей Другие способы установки Rust, где доступны дополнительные варианты.

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

Обозначения командной строки

В этой главе и далее по всей книге мы будем показывать команды, используемые в терминале. Строки, которые вы должны вводить в терминале, всегда начинаются с символа $. Вам не нужно вводить символ $ — это приглашение командной строки, показывающее начало команды. Строки, не начинающиеся с $, обычно показывают результат выполнения предыдущей команды. Кроме того, примеры для PowerShell будут использовать > вместо $.

Установка rustup в Linux или macOS

Если вы используете Linux или macOS, откройте терминал и выполните следующую команду:

$ curl --proto '=https' --tlsv1.2 https://sh.rustup.rs -sSf | sh

Эта команда загружает скрипт и запускает установку инструмента rustup, который устанавливает последнюю стабильную версию Rust. У вас могут запросить пароль. Если установка завершится успешно, появится следующая строка:

Rust is installed now. Great!

Также вам понадобится компоновщик (linker) — программа, которую Rust использует для объединения результатов компиляции в единый файл. Скорее всего, он у вас уже установлен. Если вы получаете ошибки компоновщика, следует установить C-компилятор, который обычно включает и компоновщик. C-компилятор также полезен потому, что некоторые распространённые пакеты Rust зависят от C-кода и требуют наличия C-компилятора.

В macOS вы можете установить C-компилятор, выполнив:

$ xcode-select --install

Пользователям Linux обычно следует устанавливать GCC или Clang в соответствии с документацией их дистрибутива. Например, если вы используете Ubuntu, можно установить пакет build-essential.

Установка rustup в Windows

В Windows перейдите по адресу https://www.rust-lang.org/tools/install и следуйте инструкциям по установке Rust. В определённый момент установки вам будет предложено установить Visual Studio. Это предоставит компоновщик и нативные библиотеки, необходимые для компиляции программ. Если вам нужна дополнительная помощь по этому шагу, см.:

https://rust-lang.github.io/rustup/installation/windows-msvc.html.

Оставшаяся часть книги использует команды, работающие как в cmd.exe, так и в PowerShell. Если будут существовать специфические различия, мы отдельно объясним, что именно использовать.

Устранение неполадок

Чтобы проверить, правильно ли установлен Rust, откройте терминал и выполните следующую команду:

$ rustc --version

Вы должны увидеть номер версии, хеш коммита и дату коммита последней выпущенной стабильной версии в следующем формате:

rustc x.y.z (abcabcabc yyyy-mm-dd)

Если вы видите эту информацию, значит Rust был установлен успешно! Если эта информация не отображается, проверьте наличие Rust в системной переменной %PATH% следующим образом.

В Windows CMD используйте:

> echo %PATH%

В PowerShell используйте:

> echo $env:Path

В Linux и macOS используйте:

$ echo $PATH

Если всё выглядит правильно, но Rust по-прежнему не работает, существует несколько мест, где можно получить помощь. Узнайте, как связаться с другими Rustacean (так мы в шутку называем себя), на странице сообщества.

Обновление и удаление

После установки Rust через rustup обновление до новой выпущенной версии выполняется очень просто. В командной строке выполните следующий сценарий обновления:

$ rustup update

Чтобы удалить Rust и rustup, выполните следующий сценарий удаления:

$ rustup self uninstall

Чтение локальной документации

Установка Rust также включает локальную копию документации, чтобы вы могли читать её без подключения к интернету. Выполните команду rustup doc, чтобы открыть локальную документацию в браузере.

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

Использование текстовых редакторов и IDE

Эта книга не предполагает использование каких-либо конкретных инструментов для написания Rust-кода. Практически любой текстовый редактор справится с этой задачей! Однако многие текстовые редакторы и интегрированные среды разработки (IDE) имеют встроенную поддержку Rust. Вы всегда можете найти достаточно актуальный список множества редакторов и IDE на странице инструментов сайта Rust.

Работа с этой книгой без подключения к интернету

В нескольких примерах мы будем использовать пакеты Rust, выходящие за пределы стандартной библиотеки. Чтобы пройти эти примеры, вам потребуется либо подключение к интернету, либо предварительно загруженные зависимости. Чтобы заранее загрузить зависимости, выполните следующие команды. (Позже мы подробно объясним, что такое cargo и что делает каждая из этих команд.)

$ cargo new get-dependencies
$ cd get-dependencies
$ cargo add rand@0.8.5 trpl@0.2.0

Эти команды сохранят загруженные пакеты в кэше, поэтому позже их не придётся загружать снова. После выполнения команды каталог get-dependencies можно не сохранять. После выполнения этих команд вы сможете использовать флаг --offline со всеми командами cargo в остальной части книги, чтобы использовать кэшированные версии вместо попыток обращения к сети.

Привет, мир!

Привет, мир!

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

Примечание: Эта книга предполагает базовое знакомство с командной строкой. Rust не предъявляет особых требований к редактору, инструментам или месту, где будет находиться ваш код, поэтому, если вы предпочитаете использовать IDE вместо командной строки, можете свободно использовать свою любимую IDE. Многие IDE уже имеют определённую степень поддержки Rust; подробности смотрите в документации вашей IDE. Команда Rust сосредоточила внимание на обеспечении качественной поддержки IDE через rust-analyzer. Подробнее смотрите в Приложении D.

Настройка каталога проекта

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

Откройте терминал и введите следующие команды, чтобы создать каталог projects и каталог для проекта «Hello, world!» внутри каталога projects.

Для Linux, macOS и PowerShell в Windows введите следующее:

$ mkdir ~/projects
$ cd ~/projects
$ mkdir hello_world
$ cd hello_world

Для Windows CMD введите:

> mkdir "%USERPROFILE%\projects"
> cd /d "%USERPROFILE%\projects"
> mkdir hello_world
> cd hello_world

Основы программы на Rust

Теперь создайте новый исходный файл и назовите его main.rs. Файлы Rust всегда имеют расширение .rs. Если в имени файла используется более одного слова, общепринято разделять их символом подчёркивания. Например, используйте hello_world.rs, а не helloworld.rs.

Теперь откройте только что созданный файл main.rs и введите код из листинга 1-1.

Filename: main.rs
fn main() {
    println!("Hello, world!");
}
Listing 1-1: Программа, выводящая Hello, world!

Сохраните файл и вернитесь в окно терминала, находясь в каталоге ~/projects/hello_world. В Linux или macOS введите следующие команды для компиляции и запуска файла:

$ rustc main.rs
$ ./main
Hello, world!

В Windows вместо ./main используйте команду .\main:

> rustc main.rs
> .\main
Hello, world!

Независимо от используемой операционной системы, в терминале должна появиться строка Hello, world!. Если этот вывод не появился, вернитесь к разделу «Устранение неполадок» в разделе установки, чтобы узнать способы получения помощи.

Если Hello, world! успешно вывелось, поздравляем! Вы официально написали программу на Rust. Это делает вас программистом Rust — добро пожаловать!

Анатомия программы на Rust

Давайте подробно разберём программу «Hello, world!». Вот первая часть:

fn main() {

}

Эти строки определяют функцию с именем main. Функция main является особенной: именно она всегда выполняется первой в каждой исполняемой программе на Rust. Здесь первая строка объявляет функцию с именем main, которая не имеет параметров и ничего не возвращает. Если бы параметры были, они располагались бы внутри круглых скобок (()).

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

Примечание: Если вы хотите придерживаться единого стиля во всех проектах Rust, вы можете использовать инструмент автоматического форматирования rustfmt, который форматирует код в определённом стиле (подробнее о rustfmt см. в Приложении D). Команда Rust включает этот инструмент в стандартную поставку Rust, как и rustc, поэтому он уже должен быть установлен на вашем компьютере!

Тело функции main содержит следующий код:

#![allow(unused)]
fn main() {
println!("Hello, world!");
}

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

Во-первых, println! вызывает макрос Rust. Если бы вызывалась функция, запись выглядела бы как println (без !). Макросы Rust представляют собой способ написания кода, который генерирует код для расширения синтаксиса Rust, и мы рассмотрим их подробнее в Главе 20. Сейчас вам достаточно знать, что использование ! означает вызов макроса вместо обычной функции и что макросы не всегда подчиняются тем же правилам, что и функции.

Во-вторых, обратите внимание на строку "Hello, world!". Мы передаём эту строку в качестве аргумента для println!, и строка выводится на экран.

В-третьих, мы завершаем строку точкой с запятой (;), которая показывает, что текущее выражение завершено и следующее готово к началу выполнения. Большинство строк кода Rust заканчиваются точкой с запятой.

Компиляция и выполнение

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

Перед запуском программы на Rust её необходимо скомпилировать с помощью компилятора Rust, введя команду rustc и передав ей имя исходного файла:

$ rustc main.rs

Если у вас есть опыт работы с C или C++, вы заметите сходство с gcc или clang. После успешной компиляции Rust создаёт исполняемый бинарный файл.

В Linux, macOS и PowerShell в Windows вы можете увидеть исполняемый файл, выполнив в оболочке команду ls:

$ ls
main  main.rs

В Linux и macOS вы увидите два файла. В PowerShell на Windows вы увидите те же три файла, что и при использовании CMD. В CMD на Windows нужно выполнить следующую команду:

> dir /B %= параметр /B указывает выводить только имена файлов =%
main.exe
main.pdb
main.rs

Это показывает исходный файл кода с расширением .rs, исполняемый файл (main.exe в Windows и main на всех остальных платформах), а также, при использовании Windows, файл с отладочной информацией с расширением .pdb. После этого вы запускаете файл main или main.exe следующим образом:

$ ./main # или .\main в Windows

Если файл main.rs содержит программу «Hello, world!», эта команда выведет Hello, world! в ваш терминал.

Если вы больше знакомы с динамическими языками, такими как Ruby, Python или JavaScript, вы, возможно, не привыкли к тому, что компиляция и выполнение программы являются отдельными шагами. Rust является языком с предварительной компиляцией (ahead-of-time compiled), что означает, что вы можете скомпилировать программу и передать исполняемый файл другому человеку, и он сможет запустить её даже без установленного Rust. Если вы передадите кому-либо файл .rb, .py или .js, у него должна быть установлена соответствующая реализация Ruby, Python или JavaScript. Но в этих языках обычно требуется только одна команда для компиляции и запуска программы. В проектировании языков всё является компромиссом.

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

Привет, Cargo!

Привет, Cargo!

Cargo — это система сборки и менеджер пакетов Rust. Большинство Rustacean используют этот инструмент для управления своими проектами на Rust, поскольку Cargo берёт на себя множество задач, таких как сборка вашего кода, загрузка библиотек, от которых зависит код, и сборка этих библиотек. (Библиотеки, которые требуются вашему коду, называются зависимостями.)

Простейшие программы Rust, подобные той, что мы писали до этого, не имеют зависимостей. Если бы мы создавали проект «Hello, world!» с использованием Cargo, использовалась бы только часть Cargo, отвечающая за сборку кода. Однако по мере написания более сложных программ вы начнёте добавлять зависимости, и если проект изначально создан с использованием Cargo, добавление зависимостей будет значительно проще.

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

$ cargo --version

Если вы видите номер версии, значит Cargo установлен! Если появляется ошибка, например command not found, обратитесь к документации используемого метода установки, чтобы узнать, как установить Cargo отдельно.

Создание проекта с помощью Cargo

Давайте создадим новый проект с использованием Cargo и посмотрим, чем он отличается от нашего первоначального проекта «Hello, world!». Вернитесь в каталог projects (или туда, где вы решили хранить код). Затем в любой операционной системе выполните:

$ cargo new hello_cargo
$ cd hello_cargo

Первая команда создаёт новый каталог и проект с именем hello_cargo. Мы назвали проект hello_cargo, а Cargo создаёт свои файлы в каталоге с таким же именем.

Перейдите в каталог hello_cargo и выведите список файлов. Вы увидите, что Cargo сгенерировал два файла и один каталог: файл Cargo.toml и каталог src, содержащий файл main.rs.

Cargo также инициализировал новый Git-репозиторий вместе с файлом .gitignore. Git-файлы не будут созданы, если cargo new выполняется внутри уже существующего Git-репозитория; это поведение можно изменить, используя cargo new --vcs=git.

Примечание: Git является распространённой системой контроля версий. Вы можете изменить cargo new, чтобы использовать другую систему контроля версий или не использовать её вовсе, применяя флаг --vcs. Выполните cargo new --help, чтобы увидеть доступные параметры.

Откройте Cargo.toml в предпочитаемом вами текстовом редакторе. Он должен выглядеть примерно так, как показано в листинге 1-2.

Filename: Cargo.toml
[package]
name = "hello_cargo"
version = "0.1.0"
edition = "2024"

[dependencies]
Listing 1-2: Содержимое Cargo.toml, созданного cargo new

Этот файл использует формат TOML (Tom’s Obvious, Minimal Language), который является форматом конфигурации Cargo.

Первая строка [package] представляет собой заголовок секции, указывающий, что последующие параметры относятся к настройке пакета. По мере добавления новой информации в этот файл мы будем добавлять и другие секции.

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

Последняя строка [dependencies] обозначает начало секции, в которой перечисляются зависимости проекта. В Rust пакеты кода называются крейтами (crates). Для данного проекта нам не понадобятся дополнительные крейты, однако они понадобятся нам в первом проекте Главы 2, поэтому мы вернёмся к этой секции позже.

Теперь откройте src/main.rs и посмотрите его содержимое:

Имя файла: src/main.rs

fn main() {
    println!("Hello, world!");
}

Cargo сгенерировал для вас программу «Hello, world!», точно такую же, как мы написали в листинге 1-1! Пока различия между нашим проектом и проектом, созданным Cargo, заключаются в том, что Cargo поместил код в каталог src, а в корневом каталоге находится конфигурационный файл Cargo.toml.

Cargo предполагает, что исходные файлы находятся внутри каталога src. Каталог верхнего уровня проекта предназначен только для файлов README, лицензионной информации, файлов конфигурации и всего остального, что не относится непосредственно к коду. Использование Cargo помогает организовать проекты: для каждой вещи существует своё место, и всё находится на своих местах.

Если вы начали проект без использования Cargo, как мы делали с проектом «Hello, world!», вы можете преобразовать его в проект Cargo. Переместите код проекта в каталог src и создайте соответствующий файл Cargo.toml. Один из простых способов получить этот файл — выполнить cargo init, который создаст его автоматически.

Сборка и запуск проекта Cargo

Теперь посмотрим, чем отличается сборка и запуск программы «Hello, world!» с использованием Cargo. Из каталога hello_cargo соберите проект, выполнив следующую команду:

$ cargo build
   Compiling hello_cargo v0.1.0 (file:///projects/hello_cargo)
    Finished dev [unoptimized + debuginfo] target(s) in 2.85 secs

Эта команда создаёт исполняемый файл в каталоге target/debug/hello_cargo (или target\debug\hello_cargo.exe в Windows), а не в текущем каталоге. Поскольку по умолчанию выполняется сборка в режиме отладки (debug build), Cargo помещает бинарный файл в каталог с именем debug. Вы можете запустить исполняемый файл следующей командой:

$ ./target/debug/hello_cargo # или .\target\debug\hello_cargo.exe в Windows
Hello, world!

Если всё прошло успешно, в терминале должна появиться строка Hello, world!. Первый запуск cargo build также заставляет Cargo создать новый файл верхнего уровня: Cargo.lock. Этот файл отслеживает точные версии зависимостей вашего проекта. В данном проекте нет зависимостей, поэтому файл содержит совсем немного информации. Вам никогда не придётся изменять этот файл вручную; Cargo управляет его содержимым самостоятельно.

Мы только что собрали проект с помощью cargo build и запустили его через ./target/debug/hello_cargo, но также можно использовать cargo run, чтобы выполнить компиляцию и запуск исполняемого файла одной командой:

$ cargo run
    Finished dev [unoptimized + debuginfo] target(s) in 0.0 secs
     Running `target/debug/hello_cargo`
Hello, world!

Использование cargo run удобнее, чем необходимость помнить про запуск cargo build, а затем указывать полный путь к бинарному файлу, поэтому большинство разработчиков используют именно cargo run.

Обратите внимание, что на этот раз мы не увидели вывода, сообщающего, что Cargo компилирует hello_cargo. Cargo определил, что файлы не изменились, поэтому не выполнял повторную сборку, а просто запустил бинарный файл. Если бы исходный код был изменён, Cargo пересобрал бы проект перед запуском, и вы увидели бы следующий вывод:

$ cargo run
   Compiling hello_cargo v0.1.0 (file:///projects/hello_cargo)
    Finished dev [unoptimized + debuginfo] target(s) in 0.33 secs
     Running `target/debug/hello_cargo`
Hello, world!

Cargo также предоставляет команду cargo check. Эта команда быстро проверяет, что код компилируется, но не создаёт исполняемый файл:

$ cargo check
   Checking hello_cargo v0.1.0 (file:///projects/hello_cargo)
    Finished dev [unoptimized + debuginfo] target(s) in 0.32 secs

Почему вам может не понадобиться исполняемый файл? Часто cargo check работает значительно быстрее, чем cargo build, потому что пропускает этап создания исполняемого файла. Если вы постоянно проверяете свою работу во время написания кода, использование cargo check ускорит процесс получения информации о том, продолжает ли ваш проект успешно компилироваться! Поэтому многие Rustacean периодически запускают cargo check во время написания программы, чтобы убедиться, что она по-прежнему компилируется. Затем, когда исполняемый файл уже нужен, они запускают cargo build.

Подведём итог тому, что мы узнали о Cargo:

  • Мы можем создать проект с помощью cargo new.
  • Мы можем собрать проект с помощью cargo build.
  • Мы можем собрать и запустить проект одним действием с помощью cargo run.
  • Мы можем проверить проект на наличие ошибок без создания бинарного файла, используя cargo check.
  • Вместо сохранения результатов сборки в том же каталоге, где находится код, Cargo сохраняет их в каталоге target/debug.

Дополнительным преимуществом использования Cargo является то, что команды остаются одинаковыми независимо от используемой операционной системы. Поэтому с этого момента мы больше не будем приводить отдельные инструкции для Linux и macOS по сравнению с Windows.

Сборка для выпуска

Когда ваш проект наконец будет готов к выпуску, вы можете использовать cargo build --release, чтобы выполнить компиляцию с оптимизациями. Эта команда создаст исполняемый файл в каталоге target/release, а не target/debug. Оптимизации позволяют вашему Rust-коду работать быстрее, но их включение увеличивает время компиляции программы. Именно поэтому существуют два различных профиля: один для разработки, когда необходимы быстрые и частые пересборки, и другой для сборки финальной версии программы, которую вы предоставите пользователю и которую не придётся постоянно пересобирать, но которая должна работать максимально быстро. Если вы измеряете производительность кода, обязательно используйте cargo build --release и выполняйте тестирование с исполняемым файлом из каталога target/release.

Использование соглашений Cargo

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

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

$ git clone example.org/someproject
$ cd someproject
$ cargo build

Для получения дополнительной информации о Cargo ознакомьтесь с его документацией.

Итоги

Вы уже сделали отличный старт в изучении Rust! В этой главе вы узнали, как:

  • установить последнюю стабильную версию Rust с помощью rustup;
  • обновить Rust до новой версии;
  • открыть локально установленную документацию;
  • написать и запустить программу «Hello, world!» напрямую с использованием rustc;
  • создать и запустить новый проект, используя соглашения Cargo.

Сейчас хороший момент для создания более серьёзной программы, чтобы привыкнуть к чтению и написанию Rust-кода. Поэтому в Главе 2 мы создадим программу «Угадай число». Если же вы предпочитаете сначала изучить работу основных концепций программирования в Rust, перейдите к Главе 3, а затем вернитесь к Главе 2.

Программирование игры «Угадай число»

Давайте погрузимся в Rust через практический проект! Эта глава познакомит вас с несколькими распространёнными концепциями Rust, показывая, как использовать их в реальной программе. Вы познакомитесь с let, match, методами, связанными функциями (associated functions), внешними крейтами и многим другим! В следующих главах мы изучим эти идеи более подробно. В этой главе вы просто познакомитесь с основами на практике.

Мы реализуем классическую задачу для начинающих программистов: игру «Угадай число». Вот как она работает: программа генерирует случайное целое число от 1 до 100. Затем она предлагает игроку ввести предположение. После ввода программа сообщает, является ли число слишком маленьким или слишком большим. Если число угадано правильно, игра выведет поздравительное сообщение и завершится.

Создание нового проекта

Чтобы подготовить новый проект, перейдите в каталог projects, который вы создали в Главе 1, и создайте новый проект с помощью Cargo следующим образом:

$ cargo new guessing_game
$ cd guessing_game

Первая команда cargo new принимает имя проекта (guessing_game) в качестве первого аргумента. Вторая команда переходит в каталог нового проекта.

Посмотрите на созданный файл Cargo.toml:

Имя файла: Cargo.toml

[package]
name = "guessing_game"
version = "0.1.0"
edition = "2024"

[dependencies]

Как вы уже видели в Главе 1, cargo new генерирует программу «Hello, world!» для вас. Посмотрите содержимое файла src/main.rs:

Имя файла: src/main.rs

fn main() {
    println!("Hello, world!");
}

Теперь скомпилируем и запустим программу «Hello, world!» за один шаг с помощью команды cargo run:

$ cargo run
   Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.08s
     Running `target/debug/guessing_game`
Hello, world!

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

Снова откройте файл src/main.rs. Весь код этой главы вы будете писать внутри этого файла.

Обработка предположения

Первая часть программы игры «Угадай число» будет запрашивать пользовательский ввод, обрабатывать его и проверять, соответствует ли он ожидаемому формату. Для начала мы позволим игроку вводить своё предположение. Введите код из листинга 2-1 в файл src/main.rs.

Filename: src/main.rs
use std::io;

fn main() {
    println!("Guess the number!");

    println!("Please input your guess.");

    let mut guess = String::new();

    io::stdin()
        .read_line(&mut guess)
        .expect("Failed to read line");

    println!("You guessed: {guess}");
}
Listing 2-1: Код, который получает предположение от пользователя и выводит его

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

use std::io;

fn main() {
    println!("Guess the number!");

    println!("Please input your guess.");

    let mut guess = String::new();

    io::stdin()
        .read_line(&mut guess)
        .expect("Failed to read line");

    println!("You guessed: {guess}");
}

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

Если нужный вам тип отсутствует в prelude, его необходимо явно добавить в область видимости с помощью инструкции use. Использование библиотеки std::io предоставляет множество полезных возможностей, включая получение ввода пользователя.

Как вы уже видели в Главе 1, функция main является точкой входа в программу:

use std::io;

fn main() {
    println!("Guess the number!");

    println!("Please input your guess.");

    let mut guess = String::new();

    io::stdin()
        .read_line(&mut guess)
        .expect("Failed to read line");

    println!("You guessed: {guess}");
}

Синтаксис fn объявляет новую функцию; круглые скобки () означают отсутствие параметров; а фигурная скобка { начинает тело функции.

Как вы также узнали в Главе 1, println! — это макрос, выводящий строку на экран:

use std::io;

fn main() {
    println!("Guess the number!");

    println!("Please input your guess.");

    let mut guess = String::new();

    io::stdin()
        .read_line(&mut guess)
        .expect("Failed to read line");

    println!("You guessed: {guess}");
}

Этот код выводит приглашение, сообщающее о сути игры и предлагающее пользователю ввести значение.

Сохранение значений с помощью переменных

Теперь создадим переменную для хранения пользовательского ввода:

use std::io;

fn main() {
    println!("Guess the number!");

    println!("Please input your guess.");

    let mut guess = String::new();

    io::stdin()
        .read_line(&mut guess)
        .expect("Failed to read line");

    println!("You guessed: {guess}");
}

Теперь программа становится интереснее! В этой небольшой строке происходит многое. Мы используем инструкцию let для создания переменной. Вот ещё один пример:

let apples = 5;

Эта строка создаёт новую переменную с именем apples и связывает её со значением 5. В Rust переменные по умолчанию неизменяемы (immutable), то есть после присвоения значения оно больше не может изменяться. Мы подробно рассмотрим эту концепцию в разделе «Переменные и изменяемость» Главы 3. Чтобы сделать переменную изменяемой, перед её именем добавляется mut:

let apples = 5; // неизменяемая
let mut bananas = 5; // изменяемая

Примечание: Синтаксис // начинает комментарий, который продолжается до конца строки. Rust игнорирует всё, что находится внутри комментариев. Более подробно комментарии рассматриваются в Главе 3.

Возвращаясь к программе игры «Угадай число», теперь вы знаете, что let mut guess создаёт изменяемую переменную с именем guess. Знак равенства (=) сообщает Rust, что мы хотим прямо сейчас связать переменную с каким-либо значением. Справа от знака равенства находится значение, с которым связывается guess: результат вызова String::new — функции, возвращающей новый экземпляр String. String представляет собой строковый тип, предоставляемый стандартной библиотекой; это расширяемый фрагмент текста в кодировке UTF-8.

Синтаксис :: в строке ::new показывает, что new является связанной функцией (associated function) типа String. Связанная функция — это функция, реализованная для определённого типа, в данном случае String. Функция new создаёт новую пустую строку. Вы встретите функцию new у многих типов, потому что это распространённое имя для функции, создающей новое значение.

В полном виде строка let mut guess = String::new(); создаёт изменяемую переменную, которая сейчас связана с новым пустым экземпляром String. Уф!

Получение пользовательского ввода

Напомним, что в первой строке программы мы подключили функциональность ввода-вывода из стандартной библиотеки с помощью use std::io;. Теперь вызовем функцию stdin из модуля io, которая позволит работать с пользовательским вводом:

use std::io;

fn main() {
    println!("Guess the number!");

    println!("Please input your guess.");

    let mut guess = String::new();

    io::stdin()
        .read_line(&mut guess)
        .expect("Failed to read line");

    println!("You guessed: {guess}");
}

Если бы мы не импортировали модуль io с помощью use std::io; в начале программы, мы всё равно могли бы использовать эту функцию, записав вызов следующим образом: std::io::stdin. Функция stdin возвращает экземпляр std::io::Stdin — типа, представляющего дескриптор стандартного ввода вашего терминала.

Затем строка .read_line(&mut guess) вызывает метод read_line для дескриптора стандартного ввода, чтобы получить ввод от пользователя. Мы также передаём &mut guess в качестве аргумента в read_line, чтобы сообщить, в какую строку нужно сохранить пользовательский ввод. Полная задача read_line состоит в том, чтобы взять всё, что пользователь вводит через стандартный ввод, и добавить это в строку (не перезаписывая её содержимое), поэтому мы передаём эту строку в качестве аргумента. Строковый аргумент должен быть изменяемым, чтобы метод мог изменять его содержимое.

Символ & показывает, что аргумент является ссылкой (reference), которая даёт возможность нескольким частям кода получать доступ к одному и тому же участку данных без необходимости многократного копирования этих данных в память. Ссылки являются довольно сложной возможностью, и одно из главных преимуществ Rust заключается в том, насколько безопасно и удобно они используются. Для завершения этой программы вам пока не нужно знать все подробности. Сейчас достаточно помнить, что, как и переменные, ссылки по умолчанию являются неизменяемыми. Поэтому необходимо писать &mut guess, а не &guess, чтобы сделать ссылку изменяемой. (Глава 4 объясняет ссылки более подробно.)

Обработка возможных ошибок с помощью Result

Мы всё ещё работаем с этой строкой кода. Сейчас мы обсуждаем третью строку текста, но обратите внимание, что она всё ещё является частью одной логической строки кода. Следующая часть выглядит так:

use std::io;

fn main() {
    println!("Guess the number!");

    println!("Please input your guess.");

    let mut guess = String::new();

    io::stdin()
        .read_line(&mut guess)
        .expect("Failed to read line");

    println!("You guessed: {guess}");
}

Мы могли бы записать этот код следующим образом:

io::stdin().read_line(&mut guess).expect("Failed to read line");

Однако одну длинную строку трудно читать, поэтому её лучше разделить. Часто полезно добавлять переносы строк и дополнительные пробелы, чтобы разбивать длинные конструкции при использовании синтаксиса .method_name(). Теперь рассмотрим, что именно делает эта строка.

Как упоминалось ранее, read_line помещает всё, что вводит пользователь, в переданную строку, но также возвращает значение Result. Result представляет собой перечисление (enumeration), часто называемое enum, то есть тип, который может находиться в одном из нескольких возможных состояний. Каждое возможное состояние называется вариантом (variant).

В Главе 6 перечисления будут рассмотрены более подробно. Назначение типов Result состоит в кодировании информации об обработке ошибок.

Вариантами Result являются Ok и Err. Вариант Ok указывает, что операция завершилась успешно, и содержит успешно полученное значение. Вариант Err означает, что операция завершилась неудачей, и содержит информацию о том, как или почему произошла ошибка.

Значения типа Result, как и значения любого другого типа, имеют определённые для них методы. Экземпляр Result имеет метод expect, который можно вызвать. Если данный экземпляр Result содержит значение Err, expect приведёт к аварийному завершению программы и отобразит сообщение, переданное ему в качестве аргумента. Если метод read_line возвращает Err, скорее всего, ошибка возникла на уровне операционной системы.

Если же данный экземпляр Result содержит значение Ok, expect извлечёт значение, находящееся внутри Ok, и вернёт только его, чтобы вы могли использовать его дальше. В данном случае этим значением является количество байтов, введённых пользователем.

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

$ cargo build
   Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
warning: unused `Result` that must be used
  --> src/main.rs:10:5
   |
10 |     io::stdin().read_line(&mut guess);
   |     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
   |
   = note: this `Result` may be an `Err` variant, which should be handled
   = note: `#[warn(unused_must_use)]` on by default
help: use `let _ = ...` to ignore the resulting value
   |
10 |     let _ = io::stdin().read_line(&mut guess);
   |     +++++++

warning: `guessing_game` (bin "guessing_game") generated 1 warning
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.59s

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

Правильным способом устранения такого предупреждения является написание кода обработки ошибок, но в нашем случае при возникновении проблемы мы просто хотим аварийно завершить программу, поэтому используем expect. О восстановлении после ошибок вы узнаете в Главе 9.

Вывод значений с использованием заполнителей println!

Помимо закрывающей фигурной скобки остаётся обсудить ещё только одну строку кода:

use std::io;

fn main() {
    println!("Guess the number!");

    println!("Please input your guess.");

    let mut guess = String::new();

    io::stdin()
        .read_line(&mut guess)
        .expect("Failed to read line");

    println!("You guessed: {guess}");
}

Эта строка выводит строку, которая теперь содержит пользовательский ввод. Набор фигурных скобок {} представляет собой заполнитель: воспринимайте {} как маленькие клешни краба, удерживающие значение на месте. При выводе значения переменной имя переменной можно поместить внутрь фигурных скобок. При выводе результата вычисления выражения используйте пустые фигурные скобки в строке форматирования, а затем после строки форматирования через запятую укажите список выражений для вывода, по одному для каждого заполнителя, в том же порядке. Вывод значения переменной и результата выражения в одном вызове println! будет выглядеть следующим образом:

#![allow(unused)]
fn main() {
let x = 5;
let y = 10;

println!("x = {x} and y + 2 = {}", y + 2);
}

Этот код выведет x = 5 and y + 2 = 12.

Проверка первой части

Давайте протестируем первую часть игры «Угадай число». Запустите её с помощью cargo run:

$ cargo run
   Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 6.44s
     Running `target/debug/guessing_game`
Guess the number!
Please input your guess.
6
You guessed: 6

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

Генерация секретного числа

Теперь необходимо сгенерировать секретное число, которое пользователь будет пытаться угадать. Секретное число должно быть разным при каждом запуске, чтобы играть было интересно больше одного раза. Мы будем использовать случайное число от 1 до 100, чтобы игра не была слишком сложной. В стандартной библиотеке Rust пока отсутствует встроенная функциональность генерации случайных чисел. Однако команда Rust предоставляет для этого крейт rand.

Продолжаю с сохранением структуры, переносов строк и терминологии Rust.

<!-- Old headings. Do not remove or links may break. -->
<a id="using-a-crate-to-get-more-functionality"></a>

### Расширение функциональности с помощью крейта

Напомним, что крейт представляет собой набор файлов исходного кода Rust.
Проект, который мы создаём, является бинарным крейтом (binary crate), то
есть исполняемой программой. Крейт `rand` является библиотечным крейтом
(library crate), который содержит код, предназначенный для использования
в других программах и не может выполняться самостоятельно.

Именно при работе с внешними крейтами Cargo проявляет свои сильные стороны.
Прежде чем писать код, использующий `rand`, необходимо изменить файл
_Cargo.toml_, добавив крейт `rand` как зависимость. Откройте этот файл
и добавьте следующую строку в конец, ниже заголовка секции
`[dependencies]`, который Cargo создал автоматически. Убедитесь, что
указываете `rand` точно так же, как показано здесь, включая номер версии,
иначе примеры кода из этого руководства могут не работать:

<!-- When updating the version of `rand` used, also update the version of
`rand` used in these files so they all match:
* ch07-04-bringing-paths-into-scope-with-the-use-keyword.md
* ch14-03-cargo-workspaces.md
-->

<span class="filename">Имя файла: Cargo.toml</span>

```toml
[dependencies]
rand = "0.8.5"
```

В файле _Cargo.toml_ всё, что находится после заголовка, относится к данной
секции до тех пор, пока не начинается следующая секция. В `[dependencies]`
вы сообщаете Cargo, от каких внешних крейтов зависит ваш проект и какие
версии этих крейтов вам необходимы.

В данном случае мы указываем крейт `rand` со спецификатором семантической
версии `0.8.5`. Cargo понимает
[семантическое версионирование][semver]<!-- ignore -->
(часто называемое _SemVer_), которое является стандартом записи номеров
версий. Спецификатор `0.8.5` на самом деле является сокращением для
`^0.8.5`, что означает любую версию не ниже `0.8.5`, но меньше `0.9.0`.

Cargo считает, что эти версии имеют публичный API, совместимый с версией 0.8.5, и данная спецификация гарантирует получение последнего исправления (patch release), которое по-прежнему будет компилироваться с кодом этой главы. Для любой версии 0.9.0 или выше не гарантируется сохранение того же API, который используется в следующих примерах.

Теперь, не изменяя код, соберём проект, как показано в листинге 2-2.

$ cargo build
  Updating crates.io index
   Locking 15 packages to latest Rust 1.85.0 compatible versions
    Adding rand v0.8.5 (available: v0.9.0)
 Compiling proc-macro2 v1.0.93
 Compiling unicode-ident v1.0.17
 Compiling libc v0.2.170
 Compiling cfg-if v1.0.0
 Compiling byteorder v1.5.0
 Compiling getrandom v0.2.15
 Compiling rand_core v0.6.4
 Compiling quote v1.0.38
 Compiling syn v2.0.98
 Compiling zerocopy-derive v0.7.35
 Compiling zerocopy v0.7.35
 Compiling ppv-lite86 v0.2.20
 Compiling rand_chacha v0.3.1
 Compiling rand v0.8.5
 Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
  Finished `dev` profile [unoptimized + debuginfo] target(s) in 2.48s
Listing 2-2: Вывод cargo build после добавления крейта rand в качестве зависимости

Вы можете увидеть другие номера версий (но они всё равно будут совместимы с кодом благодаря SemVer), другие строки (в зависимости от операционной системы), а также иной порядок строк.

Когда мы добавляем внешнюю зависимость, Cargo получает из реестра (registry) последние версии всего, что необходимо этой зависимости. Реестр представляет собой копию данных с Crates.io, где участники экосистемы Rust публикуют свои проекты с открытым исходным кодом для использования другими разработчиками.

После обновления реестра Cargo проверяет секцию [dependencies] и загружает все перечисленные крейты, которые ещё не были загружены. В данном случае, несмотря на то что мы указали только rand, Cargo также загрузил другие крейты, необходимые rand для работы. После загрузки крейтов Rust компилирует их, а затем компилирует сам проект с уже доступными зависимостями.

Если вы сразу снова выполните cargo build, не внося изменений, вы не увидите никакого вывода, кроме строки Finished. Cargo знает, что зависимости уже были загружены и скомпилированы, а вы ничего не изменили в файле Cargo.toml. Cargo также знает, что вы не изменяли код, поэтому повторная компиляция тоже не требуется. Не имея задач для выполнения, он просто завершает работу.

Если вы откроете файл src/main.rs, внесёте небольшое изменение, сохраните его и снова выполните сборку, вы увидите только две строки вывода:

$ cargo build
   Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.13s

Эти строки показывают, что Cargo обновляет сборку только с учётом вашего небольшого изменения файла src/main.rs. Зависимости не изменялись, поэтому Cargo знает, что может повторно использовать уже загруженные и скомпилированные данные.

Обеспечение воспроизводимых сборок с помощью файла Cargo.lock

Cargo имеет механизм, который гарантирует, что вы или любой другой человек сможете каждый раз пересобирать один и тот же артефакт: Cargo будет использовать только те версии зависимостей, которые были определены, пока вы явно не укажете иное. Например, предположим, что на следующей неделе выходит версия 0.8.6 крейта rand, содержащая важное исправление ошибки, но также включающая регрессию, которая ломает ваш код. Для решения этой проблемы Rust создаёт файл Cargo.lock при первом запуске cargo build, поэтому теперь в каталоге guessing_game появляется этот файл.

Когда вы собираете проект впервые, Cargo определяет все версии зависимостей, которые удовлетворяют указанным условиям, а затем записывает их в файл Cargo.lock. При последующих сборках Cargo обнаружит существование Cargo.lock и будет использовать указанные там версии вместо повторного поиска подходящих вариантов. Это позволяет автоматически получать воспроизводимые сборки. Иными словами, благодаря файлу Cargo.lock ваш проект останется на версии 0.8.5, пока вы явно не выполните обновление.

Обновление крейта для получения новой версии

Когда вы действительно захотите обновить крейт, Cargo предоставляет команду update, которая игнорирует файл Cargo.lock и определяет самые новые версии, соответствующие ограничениям в Cargo.toml. Затем Cargo запишет эти версии в файл Cargo.lock. В остальных случаях Cargo по умолчанию будет искать только версии выше 0.8.5, но ниже 0.9.0. Если крейт rand выпустил две новые версии 0.8.6 и 0.999.0, вы увидите следующее после выполнения cargo update:

$ cargo update
    Updating crates.io index
     Locking 1 package to latest Rust 1.85.0 compatible version
    Updating rand v0.8.5 -> v0.8.6 (available: v0.999.0)

Cargo проигнорирует выпуск 0.999.0. В этот момент вы также заметите изменение в файле Cargo.lock, указывающее, что используемая версия крейта rand теперь 0.8.6. Чтобы использовать rand версии 0.999.0 или любую версию серии 0.999._x_, потребуется изменить файл Cargo.toml следующим образом (на самом деле не выполняйте это изменение, поскольку дальнейшие примеры предполагают использование rand версии 0.8):

[dependencies]
rand = "0.999.0"

При следующем запуске cargo build Cargo обновит реестр доступных крейтов и заново проверит требования к rand согласно новой версии, которую вы указали.

О Cargo и его экосистеме можно рассказать гораздо больше, и мы поговорим об этом в Главе 14, но пока этого достаточно. Cargo значительно упрощает повторное использование библиотек, поэтому Rustacean могут создавать небольшие проекты, собранные из множества пакетов.

Генерация случайного числа

Теперь начнём использовать rand для генерации числа, которое нужно угадать. Следующим шагом будет обновление файла src/main.rs, как показано в листинге 2-3.

Filename: src/main.rs
use std::io;

use rand::Rng;

fn main() {
    println!("Guess the number!");

    let secret_number = rand::thread_rng().gen_range(1..=100);

    println!("The secret number is: {secret_number}");

    println!("Please input your guess.");

    let mut guess = String::new();

    io::stdin()
        .read_line(&mut guess)
        .expect("Failed to read line");

    println!("You guessed: {guess}");
}
Listing 2-3: Добавление кода для генерации случайного числа

Сначала мы добавляем строку use rand::Rng;. Трейт Rng определяет методы, которые реализуют генераторы случайных чисел, и этот трейт должен находиться в области видимости, чтобы мы могли использовать его методы. Глава 10 рассматривает трейты более подробно.

Далее мы добавляем две строки в середину программы. В первой строке вызывается функция rand::thread_rng, которая предоставляет конкретный генератор случайных чисел, который мы будем использовать: генератор, локальный для текущего потока выполнения и инициализируемый операционной системой.

Затем мы вызываем метод gen_range у генератора случайных чисел. Этот метод определён трейтом Rng, который мы добавили в область видимости с помощью инструкции use rand::Rng;. Метод gen_range принимает выражение диапазона (range expression) в качестве аргумента и генерирует случайное число внутри этого диапазона. Используемое здесь выражение диапазона имеет вид start..=end и включает как нижнюю, так и верхнюю границы, поэтому для получения числа от 1 до 100 необходимо указать 1..=100.

Примечание: Невозможно просто заранее знать, какие трейты необходимо использовать и какие методы или функции следует вызывать из конкретного крейта, поэтому каждый крейт сопровождается документацией с инструкциями по использованию. Ещё одна полезная возможность Cargo состоит в том, что команда cargo doc --open локально создаёт документацию для всех ваших зависимостей и открывает её в браузере. Например, если вам интересно изучить другие возможности крейта rand, выполните команду cargo doc --open и выберите rand на боковой панели слева.

Вторая новая строка выводит секретное число. Это удобно во время разработки, поскольку позволяет проверять работу программы, но перед финальной версией мы удалим эту строку. Игра становится слишком простой, если программа сразу после запуска выводит правильный ответ!

Попробуйте запустить программу несколько раз:

$ cargo run
   Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.02s
     Running `target/debug/guessing_game`
Guess the number!
The secret number is: 7
Please input your guess.
4
You guessed: 4

$ cargo run
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.02s
     Running `target/debug/guessing_game`
Guess the number!
The secret number is: 83
Please input your guess.
5
You guessed: 5

Вы должны получать разные случайные числа, и все они должны находиться в диапазоне от 1 до 100. Отличная работа!

Сравнение предположения с секретным числом

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

Этот этап показан в листинге 2-4. Обратите внимание, что этот код пока ещё не будет компилироваться, и сейчас мы объясним почему.

Filename: src/main.rs
use std::cmp::Ordering;
use std::io;

use rand::Rng;

fn main() {
    // --snip--
    println!("Guess the number!");

    let secret_number = rand::thread_rng().gen_range(1..=100);

    println!("The secret number is: {secret_number}");

    println!("Please input your guess.");

    let mut guess = String::new();

    io::stdin()
        .read_line(&mut guess)
        .expect("Failed to read line");

    println!("You guessed: {guess}");

    match guess.cmp(&secret_number) {
        Ordering::Less => println!("Too small!"),
        Ordering::Greater => println!("Too big!"),
        Ordering::Equal => println!("You win!"),
    }
}
Listing 2-4: Обработка возможных результатов сравнения двух чисел

Сначала мы добавляем ещё одну инструкцию use, помещая в область видимости тип std::cmp::Ordering из стандартной библиотеки. Тип Ordering также является перечислением (enum) и содержит варианты Less, Greater и Equal. Это три возможных результата, которые могут возникнуть при сравнении двух значений.

Затем мы добавляем пять новых строк в нижнюю часть программы, использующих тип Ordering. Метод cmp сравнивает два значения и может быть вызван для любого типа, который поддерживает сравнение. Он принимает ссылку на значение, с которым необходимо выполнить сравнение. В данном случае происходит сравнение guess и secret_number. Затем метод возвращает вариант перечисления Ordering, которое мы импортировали с помощью инструкции use.

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

Выражение match состоит из ветвей (arms). Каждая ветвь включает шаблон (pattern), с которым производится сопоставление, и код, который будет выполнен, если значение, переданное в match, соответствует этому шаблону. Rust берёт значение, переданное в match, и по очереди проверяет каждый шаблон ветвей. Шаблоны и конструкция match являются мощными возможностями Rust: они позволяют выразить различные ситуации, которые могут возникнуть в программе, и гарантируют, что все они будут обработаны. Эти возможности подробно рассматриваются соответственно в Главе 6 и Главе 19.

Разберём пример использования выражения match. Предположим, пользователь ввёл 50, а случайно сгенерированное секретное число равно 38.

Когда код сравнивает 50 и 38, метод cmp возвращает Ordering::Greater, потому что 50 больше 38. Выражение match получает значение Ordering::Greater и начинает проверять шаблоны ветвей. Сначала проверяется шаблон первой ветви — Ordering::Less. Так как Ordering::Greater не совпадает с Ordering::Less, код этой ветви игнорируется и выполняется переход к следующей.

Шаблон следующей ветви — Ordering::Greater, который совпадает со значением Ordering::Greater! Поэтому будет выполнен связанный с этой ветвью код, который выведет на экран Too big!. После первого успешного совпадения выполнение выражения match завершается, поэтому в данном случае последняя ветвь даже не будет проверяться.

Однако код из листинга 2-4 пока не компилируется. Давайте попробуем:

$ cargo build
   Compiling libc v0.2.86
   Compiling getrandom v0.2.2
   Compiling cfg-if v1.0.0
   Compiling ppv-lite86 v0.2.10
   Compiling rand_core v0.6.2
   Compiling rand_chacha v0.3.0
   Compiling rand v0.8.5
   Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
error[E0308]: mismatched types
  --> src/main.rs:23:21
   |
23 |     match guess.cmp(&secret_number) {
   |                 --- ^^^^^^^^^^^^^^ expected `&String`, found `&{integer}`
   |                 |
   |                 arguments to this method are incorrect
   |
   = note: expected reference `&String`
              found reference `&{integer}`
note: method defined here
  --> /rustc/1159e78c4747b02ef996e55082b704c09b970588/library/core/src/cmp.rs:979:8

For more information about this error, try `rustc --explain E0308`.
error: could not compile `guessing_game` (bin "guessing_game") due to 1 previous error

Основная часть ошибки сообщает о наличии несоответствия типов (mismatched types). Rust использует строгую статическую систему типов. Однако он также поддерживает вывод типов (type inference). Когда мы написали let mut guess = String::new(), Rust смог самостоятельно определить, что guess должен иметь тип String, и нам не пришлось явно указывать тип.

С другой стороны, secret_number является числовым типом. Несколько числовых типов Rust могут содержать значения от 1 до 100: i32 (32-битное целое число), u32 (беззнаковое 32-битное целое число), i64 (64-битное целое число) и другие. Если не указано иное, Rust использует тип i32 по умолчанию, поэтому именно такой тип будет у secret_number, если дополнительная информация не позволит Rust вывести другой числовой тип. Причина ошибки заключается в том, что Rust не может сравнивать строку и число.

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

Имя файла: src/main.rs

use std::cmp::Ordering;
use std::io;

use rand::Rng;

fn main() {
    println!("Guess the number!");

    let secret_number = rand::thread_rng().gen_range(1..=100);

    println!("The secret number is: {secret_number}");

    println!("Please input your guess.");

    // --snip--

    let mut guess = String::new();

    io::stdin()
        .read_line(&mut guess)
        .expect("Failed to read line");

    let guess: u32 = guess.trim().parse().expect("Please type a number!");

    println!("You guessed: {guess}");

    match guess.cmp(&secret_number) {
        Ordering::Less => println!("Too small!"),
        Ordering::Greater => println!("Too big!"),
        Ordering::Equal => println!("You win!"),
    }
}

Строка выглядит следующим образом:

let guess: u32 = guess.trim().parse().expect("Please type a number!");

Мы создаём переменную с именем guess. Но подождите, разве в программе уже нет переменной с именем guess? Есть, но Rust позволяет перекрывать (shadow) предыдущее значение guess новым. Затенение (shadowing) позволяет повторно использовать имя переменной guess, вместо того чтобы создавать две разные переменные, например guess_str и guess. Мы рассмотрим это подробнее в Главе 3, а пока достаточно знать, что эта возможность часто используется, когда необходимо преобразовать значение из одного типа в другой.

Мы связываем новую переменную с выражением guess.trim().parse(). Переменная guess внутри выражения ссылается на исходную переменную guess, которая содержала введённую строку. Метод trim для экземпляра String удаляет пробельные символы в начале и конце строки, что необходимо перед преобразованием строки в u32, который может содержать только числовые данные.

Пользователь должен нажать Enter, чтобы завершить ввод через read_line, и это добавляет символ новой строки в строку. Например, если пользователь вводит 5 и нажимает Enter, содержимое guess выглядит следующим образом: 5\n. Символ \n обозначает перевод строки (“newline”). В Windows при нажатии Enter добавляется возврат каретки и перевод строки — \r\n. Метод trim удаляет \n или \r\n, оставляя только 5.

Метод parse для строк преобразует строку в другой тип. Здесь он используется для преобразования строки в число. Нам необходимо сообщить Rust, какой именно числовой тип требуется, используя let guess: u32. Двоеточие (:) после guess указывает Rust, что далее следует аннотация типа переменной. В Rust есть несколько встроенных числовых типов; u32, используемый здесь, представляет собой беззнаковое 32-битное целое число. Это хороший вариант по умолчанию для небольших положительных чисел. Другие числовые типы будут рассмотрены в Главе 3.

Кроме того, аннотация u32 в этой программе вместе со сравнением с secret_number позволяет Rust вывести, что secret_number также должен иметь тип u32. Теперь сравнение будет выполняться между двумя значениями одного типа!

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

Поскольку метод может завершиться ошибкой, parse, как и read_line, возвращает тип Result (о котором говорилось ранее в разделе «Обработка возможных ошибок с помощью Result»). Мы обработаем этот Result таким же образом, снова используя метод expect. Если parse возвращает вариант Err типа Result, потому что не смог преобразовать строку в число, вызов expect аварийно завершит игру и выведет переданное сообщение. Если parse успешно преобразует строку в число, он вернёт вариант Ok типа Result, а expect извлечёт из значения Ok нужное число.

Теперь давайте запустим программу:

$ cargo run
   Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.26s
     Running `target/debug/guessing_game`
Guess the number!
The secret number is: 58
Please input your guess.
  76
You guessed: 76
Too big!

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

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

Разрешение нескольких попыток с помощью циклов

Ключевое слово loop создаёт бесконечный цикл. Мы добавим цикл, чтобы дать пользователю больше попыток угадать число:

Имя файла: src/main.rs

use std::cmp::Ordering;
use std::io;

use rand::Rng;

fn main() {
    println!("Guess the number!");

    let secret_number = rand::thread_rng().gen_range(1..=100);

    // --snip--

    println!("The secret number is: {secret_number}");

    loop {
        println!("Please input your guess.");

        // --snip--


        let mut guess = String::new();

        io::stdin()
            .read_line(&mut guess)
            .expect("Failed to read line");

        let guess: u32 = guess.trim().parse().expect("Please type a number!");

        println!("You guessed: {guess}");

        match guess.cmp(&secret_number) {
            Ordering::Less => println!("Too small!"),
            Ordering::Greater => println!("Too big!"),
            Ordering::Equal => println!("You win!"),
        }
    }
}

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

Пользователь всегда может прервать выполнение программы сочетанием клавиш Ctrl-C. Но существует и другой способ выбраться из этого ненасытного монстра, о котором упоминалось при обсуждении parse в разделе «Сравнение предположения с секретным числом»: если пользователь введёт нечисловое значение, программа завершится с ошибкой. Мы можем воспользоваться этим, чтобы позволить пользователю выйти:

$ cargo run
   Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.23s
     Running `target/debug/guessing_game`
Guess the number!
The secret number is: 59
Please input your guess.
45
You guessed: 45
Too small!
Please input your guess.
60
You guessed: 60
Too big!
Please input your guess.
59
You guessed: 59
You win!
Please input your guess.
quit

thread 'main' panicked at src/main.rs:28:47:
Please type a number!: ParseIntError { kind: InvalidDigit }
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

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

Завершение после правильного ответа

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

Имя файла: src/main.rs

use std::cmp::Ordering;
use std::io;

use rand::Rng;

fn main() {
    println!("Guess the number!");

    let secret_number = rand::thread_rng().gen_range(1..=100);

    println!("The secret number is: {secret_number}");

    loop {
        println!("Please input your guess.");

        let mut guess = String::new();

        io::stdin()
            .read_line(&mut guess)
            .expect("Failed to read line");

        let guess: u32 = guess.trim().parse().expect("Please type a number!");

        println!("You guessed: {guess}");

        // --snip--

        match guess.cmp(&secret_number) {
            Ordering::Less => println!("Too small!"),
            Ordering::Greater => println!("Too big!"),
            Ordering::Equal => {
                println!("You win!");
                break;
            }
        }
    }
}

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

Обработка некорректного ввода

Чтобы ещё больше улучшить поведение игры, вместо аварийного завершения программы при вводе нечислового значения сделаем так, чтобы игра просто игнорировала такой ввод и позволяла пользователю продолжить угадывать. Этого можно добиться, изменив строку, где guess преобразуется из String в u32, как показано в листинге 2-5.

Filename: src/main.rs
use std::cmp::Ordering;
use std::io;

use rand::Rng;

fn main() {
    println!("Guess the number!");

    let secret_number = rand::thread_rng().gen_range(1..=100);

    println!("The secret number is: {secret_number}");

    loop {
        println!("Please input your guess.");

        let mut guess = String::new();

        // --snip--

        io::stdin()
            .read_line(&mut guess)
            .expect("Failed to read line");

        let guess: u32 = match guess.trim().parse() {
            Ok(num) => num,
            Err(_) => continue,
        };

        println!("You guessed: {guess}");

        // --snip--

        match guess.cmp(&secret_number) {
            Ordering::Less => println!("Too small!"),
            Ordering::Greater => println!("Too big!"),
            Ordering::Equal => {
                println!("You win!");
                break;
            }
        }
    }
}
Listing 2-5: Игнорирование нечислового ввода и запрос нового предположения вместо аварийного завершения программы

Мы заменяем вызов expect выражением match, переходя от аварийного завершения программы при ошибке к её обработке. Напомним, что parse возвращает тип Result, а Result является перечислением, имеющим варианты Ok и Err. Здесь мы используем выражение match, как ранее делали с результатом Ordering, полученным из метода cmp.

Если parse успешно преобразует строку в число, он вернёт значение Ok, содержащее полученное число. Это значение Ok совпадёт с шаблоном первой ветви, и выражение match просто вернёт значение num, созданное parse и помещённое внутрь Ok. Это число окажется именно там, где нам нужно — в новой переменной guess, которую мы создаём.

Если parse не сможет преобразовать строку в число, он вернёт значение Err, содержащее дополнительную информацию об ошибке. Значение Err не совпадает с шаблоном Ok(num) первой ветви match, но совпадает с шаблоном Err(_) во второй ветви. Символ подчёркивания _ является универсальным шаблоном (catch-all value); в данном примере мы говорим, что хотим обработать любые значения Err, независимо от того, какие данные они содержат внутри.

Поэтому программа выполнит код второй ветви — continue, который сообщает программе перейти к следующей итерации цикла loop и снова запросить предположение. Таким образом, программа фактически игнорирует все ошибки, которые может вернуть parse.

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

$ cargo run
   Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.13s
     Running `target/debug/guessing_game`
Guess the number!
The secret number is: 61
Please input your guess.
10
You guessed: 10
Too small!
Please input your guess.
99
You guessed: 99
Too big!
Please input your guess.
foo
Please input your guess.
61
You guessed: 61
You win!

Отлично! Остался один небольшой финальный штрих, и мы закончим игру «Угадай число». Напомним, что программа всё ещё выводит секретное число. Это было удобно для тестирования, но портит игру. Удалим println!, который выводит секретное число. Листинг 2-6 показывает итоговый код.

Filename: src/main.rs
use std::cmp::Ordering;
use std::io;

use rand::Rng;

fn main() {
    println!("Guess the number!");

    let secret_number = rand::thread_rng().gen_range(1..=100);

    loop {
        println!("Please input your guess.");

        let mut guess = String::new();

        io::stdin()
            .read_line(&mut guess)
            .expect("Failed to read line");

        let guess: u32 = match guess.trim().parse() {
            Ok(num) => num,
            Err(_) => continue,
        };

        println!("You guessed: {guess}");

        match guess.cmp(&secret_number) {
            Ordering::Less => println!("Too small!"),
            Ordering::Greater => println!("Too big!"),
            Ordering::Equal => {
                println!("You win!");
                break;
            }
        }
    }
}
Listing 2-6: Полный код игры «Угадай число»

На этом этапе вы успешно создали игру «Угадай число». Поздравляем!

Итоги

Этот проект стал практическим способом познакомить вас со многими новыми концепциями Rust: let, match, функциями, использованием внешних крейтов и другими. В следующих главах вы изучите эти концепции подробнее. Глава 3 рассматривает концепции, которые есть в большинстве языков программирования, такие как переменные, типы данных и функции, и показывает, как использовать их в Rust. Глава 4 исследует владение (ownership) — возможность, которая отличает Rust от других языков. Глава 5 рассматривает структуры и синтаксис методов, а Глава 6 объясняет, как работают перечисления.

Распространённые концепции программирования

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

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

Ключевые слова

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

Переменные и изменяемость

Переменные и изменяемость

Как упоминалось в разделе «Сохранение значений с помощью переменных», по умолчанию переменные неизменяемы. Это один из многих способов, которыми Rust подталкивает вас писать код так, чтобы использовать преимущества безопасности и простой конкурентности, предоставляемые Rust. Однако у вас всё ещё есть возможность сделать переменные изменяемыми. Давайте разберёмся, как и почему Rust поощряет отдавать предпочтение неизменяемости и почему иногда вы можете захотеть от неё отказаться.

Когда переменная неизменяема, после привязки значения к имени это значение нельзя изменить. Чтобы показать это, создайте в каталоге projects новый проект с именем variables, используя cargo new variables.

Затем в новом каталоге variables откройте src/main.rs и замените его код следующим кодом, который пока не будет компилироваться:

Имя файла: src/main.rs

fn main() {
    let x = 5;
    println!("The value of x is: {x}");
    x = 6;
    println!("The value of x is: {x}");
}

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

$ cargo run
   Compiling variables v0.1.0 (file:///projects/variables)
error[E0384]: cannot assign twice to immutable variable `x`
 --> src/main.rs:4:5
  |
2 |     let x = 5;
  |         - first assignment to `x`
3 |     println!("The value of x is: {x}");
4 |     x = 6;
  |     ^^^^^ cannot assign twice to immutable variable
  |
help: consider making this binding mutable
  |
2 |     let mut x = 5;
  |         +++

For more information about this error, try `rustc --explain E0384`.
error: could not compile `variables` (bin "variables") due to 1 previous error

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

Вы получили сообщение об ошибке cannot assign twice to immutable variable `x`, потому что попытались присвоить второе значение неизменяемой переменной x.

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

Но изменяемость может быть очень полезной и делать написание кода удобнее. Хотя переменные по умолчанию неизменяемы, вы можете сделать их изменяемыми, добавив mut перед именем переменной, как вы делали в Главе 2. Добавление mut также передаёт намерение будущим читателям кода, указывая, что другие части кода будут изменять значение этой переменной.

Например, изменим src/main.rs следующим образом:

Имя файла: src/main.rs

fn main() {
    let mut x = 5;
    println!("The value of x is: {x}");
    x = 6;
    println!("The value of x is: {x}");
}

Когда мы теперь запустим программу, получим следующее:

$ cargo run
   Compiling variables v0.1.0 (file:///projects/variables)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.30s
     Running `target/debug/variables`
The value of x is: 5
The value of x is: 6

Когда используется mut, нам разрешено изменить значение, связанное с x, с 5 на 6. В конечном счёте решение о том, использовать изменяемость или нет, остаётся за вами и зависит от того, что вы считаете наиболее понятным в конкретной ситуации.

Объявление констант

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

Во-первых, с константами нельзя использовать mut. Константы не просто неизменяемы по умолчанию — они неизменяемы всегда. Константы объявляются с помощью ключевого слова const вместо ключевого слова let, и тип значения должен быть указан явно. Мы рассмотрим типы и аннотации типов в следующем разделе, «Типы данных», поэтому сейчас не беспокойтесь о деталях. Просто знайте, что тип всегда нужно указывать.

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

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

Вот пример объявления константы:

#![allow(unused)]
fn main() {
const THREE_HOURS_IN_SECONDS: u32 = 60 * 60 * 3;
}

Имя константы — THREE_HOURS_IN_SECONDS, а её значение задаётся как результат умножения 60 (числа секунд в минуте) на 60 (числа минут в часе) и на 3 (числа часов, которое мы хотим учитывать в этой программе). Соглашение Rust об именовании констант предполагает использование только заглавных букв с подчёркиваниями между словами. Компилятор способен вычислять ограниченный набор операций во время компиляции, что позволяет записать это значение способом, который легче понять и проверить, вместо того чтобы присваивать этой константе значение 10 800. Дополнительную информацию о том, какие операции можно использовать при объявлении констант, смотрите в разделе Rust Reference о вычислении констант.

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

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

Затенение

Как вы видели в руководстве по игре «Угадай число» в Главе 2, можно объявить новую переменную с тем же именем, что и предыдущая переменная. Разработчики Rust говорят, что первая переменная затеняется второй: это означает, что именно вторую переменную компилятор будет видеть, когда вы используете имя переменной. По сути, вторая переменная перекрывает первую, забирая на себя все использования имени переменной до тех пор, пока либо она сама не будет затенена, либо не завершится область видимости. Мы можем затенить переменную, используя то же имя переменной и снова применив ключевое слово let, как показано ниже:

Имя файла: src/main.rs

fn main() {
    let x = 5;

    let x = x + 1;

    {
        let x = x * 2;
        println!("The value of x in the inner scope is: {x}");
    }

    println!("The value of x is: {x}");
}

Сначала эта программа связывает x со значением 5. Затем она создаёт новую переменную x, повторяя let x =, берёт исходное значение и прибавляет 1, так что значение x становится равным 6. Затем во внутренней области видимости, созданной фигурными скобками, третий оператор let также затеняет x и создаёт новую переменную, умножая предыдущее значение на 2, чтобы получить значение x, равное 12. Когда эта область видимости завершается, внутреннее затенение заканчивается, и x снова становится равным 6. Когда мы запустим эту программу, она выведет следующее:

$ cargo run
   Compiling variables v0.1.0 (file:///projects/variables)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.31s
     Running `target/debug/variables`
The value of x in the inner scope is: 12
The value of x is: 6

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

Другое различие между mut и затенением состоит в том, что, поскольку при повторном использовании ключевого слова let мы фактически создаём новую переменную, мы можем изменить тип значения, но повторно использовать то же имя. Например, предположим, что наша программа просит пользователя показать, сколько пробелов он хочет вставить между некоторым текстом, введя символы пробела, а затем мы хотим сохранить этот ввод как число:

fn main() {
    let spaces = "   ";
    let spaces = spaces.len();
}

Первая переменная spaces имеет строковый тип, а вторая переменная spaces — числовой тип. Таким образом, затенение избавляет нас от необходимости придумывать разные имена, такие как spaces_str и spaces_num; вместо этого мы можем повторно использовать более простое имя spaces. Однако если мы попытаемся использовать для этого mut, как показано здесь, то получим ошибку времени компиляции:

fn main() {
    let mut spaces = "   ";
    spaces = spaces.len();
}

Ошибка говорит, что нам не разрешено изменять тип переменной:

$ cargo run
   Compiling variables v0.1.0 (file:///projects/variables)
error[E0308]: mismatched types
 --> src/main.rs:3:14
  |
2 |     let mut spaces = "   ";
  |                      ----- expected due to this value
3 |     spaces = spaces.len();
  |              ^^^^^^^^^^^^ expected `&str`, found `usize`

For more information about this error, try `rustc --explain E0308`.
error: could not compile `variables` (bin "variables") due to 1 previous error

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

Типы данных

Типы данных

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

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

#![allow(unused)]
fn main() {
let guess: u32 = "42".parse().expect("Not a number!");
}

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

$ cargo build
   Compiling no_type_annotations v0.1.0 (file:///projects/no_type_annotations)
error[E0284]: type annotations needed
 --> src/main.rs:2:9
  |
2 |     let guess = "42".parse().expect("Not a number!");
  |         ^^^^^        ----- type must be known at this point
  |
  = note: cannot satisfy `<_ as FromStr>::Err == _`
help: consider giving `guess` an explicit type
  |
2 |     let guess: /* Type */ = "42".parse().expect("Not a number!");
  |              ++++++++++++

For more information about this error, try `rustc --explain E0284`.
error: could not compile `no_type_annotations` (bin "no_type_annotations") due to 1 previous error

Для других типов данных вы будете видеть другие аннотации типов.

Скалярные типы

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

Целочисленные типы

Целое число — это число без дробной части. В Главе 2 мы использовали один целочисленный тип, u32. Это объявление типа указывает, что связанное с ним значение должно быть беззнаковым целым числом (знаковые целочисленные типы начинаются с i, а не с u), занимающим 32 бита памяти. В таблице 3-1 показаны встроенные целочисленные типы Rust. Мы можем использовать любой из этих вариантов, чтобы объявить тип целочисленного значения.

Таблица 3-1: Целочисленные типы в Rust

ДлинаЗнаковыйБеззнаковый
8 битi8u8
16 битi16u16
32 битаi32u32
64 битаi64u64
128 битi128u128
Зависит от архитектурыisizeusize

Каждый вариант может быть знаковым или беззнаковым и имеет явно заданный размер. Знаковый и беззнаковый означают, может ли число быть отрицательным: другими словами, должен ли у числа быть знак (знаковое) или оно всегда будет только положительным и поэтому может быть представлено без знака (беззнаковое). Это похоже на запись чисел на бумаге: когда знак важен, число записывают со знаком плюс или минус; но когда можно безопасно считать число положительным, его записывают без знака. Знаковые числа хранятся с помощью представления в дополнительном коде.

Каждый знаковый вариант может хранить числа от −(2n − 1) до 2n − 1 − 1 включительно, где n — количество бит, используемых этим вариантом. Например, i8 может хранить числа от −(27) до 27 − 1, то есть от −128 до 127. Беззнаковые варианты могут хранить числа от 0 до 2n − 1, поэтому u8 может хранить числа от 0 до 28 − 1, то есть от 0 до 255.

Кроме того, типы isize и usize зависят от архитектуры компьютера, на котором выполняется ваша программа: 64 бита на 64-битной архитектуре и 32 бита на 32-битной архитектуре.

Целочисленные литералы можно записывать в любой из форм, показанных в таблице 3-2. Обратите внимание, что числовые литералы, которые могут относиться к нескольким числовым типам, допускают суффикс типа, например 57u8, чтобы указать тип. Числовые литералы также могут использовать _ как визуальный разделитель, чтобы число было легче читать, например 1_000, что будет иметь то же значение, как если бы вы указали 1000.

Таблица 3-2: Целочисленные литералы в Rust

Числовые литералыПример
Десятичный98_222
Шестнадцатеричный0xff
Восьмеричный0o77
Двоичный0b1111_0000
Байт (только u8)b'A'

Как же понять, какой целочисленный тип использовать? Если вы не уверены, значения Rust по умолчанию обычно являются хорошей отправной точкой: целочисленные типы по умолчанию имеют тип i32. Основная ситуация, в которой вы будете использовать isize или usize, — индексирование какой-либо коллекции.

Целочисленное переполнение

Допустим, у вас есть переменная типа u8, которая может хранить значения от 0 до 255. Если вы попытаетесь изменить переменную на значение вне этого диапазона, например 256, произойдёт целочисленное переполнение, которое может привести к одному из двух вариантов поведения. При компиляции в режиме отладки Rust включает проверки целочисленного переполнения, из-за которых ваша программа вызовет panic во время выполнения, если такое поведение произойдёт. Rust использует термин panic, когда программа завершается с ошибкой; мы подробнее обсудим panic в разделе «Неустранимые ошибки с panic!» Главы 9.

При компиляции в режиме выпуска с флагом --release Rust не включает проверки целочисленного переполнения, вызывающие panic. Вместо этого, если происходит переполнение, Rust выполняет циклическое переполнение в дополнительном коде. Если кратко, значения больше максимального значения, которое может хранить тип, «оборачиваются» к минимальному значению, которое может хранить этот тип. В случае u8 значение 256 становится 0, значение 257 становится 1 и так далее. Программа не вызовет panic, но переменная получит значение, которое, вероятно, отличается от ожидаемого. Полагаться на поведение циклического переполнения считается ошибкой.

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

  • Выполнять циклическое переполнение во всех режимах с помощью методов wrapping_*, например wrapping_add.
  • Возвращать значение None при переполнении с помощью методов checked_*.
  • Возвращать значение и логический признак наличия переполнения с помощью методов overflowing_*.
  • Насыщать до минимального или максимального значения с помощью методов saturating_*.

Типы с плавающей точкой

В Rust также есть два примитивных типа для чисел с плавающей точкой, то есть чисел с десятичной точкой. Типы Rust с плавающей точкой — f32 и f64, размером 32 и 64 бита соответственно. Тип по умолчанию — f64, потому что на современных процессорах он примерно такой же быстрый, как f32, но способен обеспечить большую точность. Все типы с плавающей точкой являются знаковыми.

Вот пример, показывающий числа с плавающей точкой в действии:

Имя файла: src/main.rs

fn main() {
    let x = 2.0; // f64

    let y: f32 = 3.0; // f32
}

Числа с плавающей точкой представлены в соответствии со стандартом IEEE-754.

Числовые операции

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

Имя файла: src/main.rs

fn main() {
    // addition
    let sum = 5 + 10;

    // subtraction
    let difference = 95.5 - 4.3;

    // multiplication
    let product = 4 * 30;

    // division
    let quotient = 56.7 / 32.2;
    let truncated = -5 / 3; // Results in -1

    // remainder
    let remainder = 43 % 5;
}

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

Логический тип

Как и в большинстве других языков программирования, логический тип в Rust имеет два возможных значения: true и false. Логические значения занимают один байт. Логический тип в Rust обозначается как bool. Например:

Имя файла: src/main.rs

fn main() {
    let t = true;

    let f: bool = false; // with explicit type annotation
}

Основной способ использовать логические значения — условные конструкции, например выражение if. Мы рассмотрим, как выражения if работают в Rust, в разделе «Управление потоком выполнения».

Символьный тип

Тип char в Rust — самый примитивный алфавитный тип языка. Вот несколько примеров объявления значений char:

Имя файла: src/main.rs

fn main() {
    let c = 'z';
    let z: char = 'ℤ'; // with explicit type annotation
    let heart_eyed_cat = '😻';
}

Обратите внимание, что литералы char задаются одинарными кавычками, в отличие от строковых литералов, которые используют двойные кавычки. Тип char в Rust занимает 4 байта и представляет скалярное значение Unicode. Это означает, что он может представлять намного больше, чем только ASCII. Буквы с диакритическими знаками; китайские, японские и корейские символы; эмодзи; и пробелы нулевой ширины — всё это допустимые значения char в Rust. Скалярные значения Unicode находятся в диапазонах от U+0000 до U+D7FF и от U+E000 до U+10FFFF включительно. Однако «символ» на самом деле не является понятием Unicode, поэтому ваше человеческое представление о том, что такое «символ», может не совпадать с тем, чем является char в Rust. Мы подробно обсудим эту тему в разделе «Хранение текста в кодировке UTF-8 с помощью строк» Главы 8.

Составные типы

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

Тип кортежа

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

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

Имя файла: src/main.rs

fn main() {
    let tup: (i32, f64, u8) = (500, 6.4, 1);
}

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

Имя файла: src/main.rs

fn main() {
    let tup = (500, 6.4, 1);

    let (x, y, z) = tup;

    println!("The value of y is: {y}");
}

Сначала эта программа создаёт кортеж и связывает его с переменной tup. Затем она использует шаблон с let, чтобы взять tup и превратить его в три отдельные переменные: x, y и z. Это называется деструктурированием, потому что оно разбивает один кортеж на три части. В конце программа выводит значение y, равное 6.4.

Мы также можем обращаться к элементу кортежа напрямую, используя точку (.), за которой следует индекс значения, к которому мы хотим обратиться. Например:

Имя файла: src/main.rs

fn main() {
    let x: (i32, f64, u8) = (500, 6.4, 1);

    let five_hundred = x.0;

    let six_point_four = x.1;

    let one = x.2;
}

Эта программа создаёт кортеж x, а затем обращается к каждому элементу кортежа по соответствующему индексу. Как и в большинстве языков программирования, первый индекс в кортеже равен 0.

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

Тип массива

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

Значения в массиве записываются внутри квадратных скобок в виде списка, разделённого запятыми:

Имя файла: src/main.rs

fn main() {
    let a = [1, 2, 3, 4, 5];
}

Массивы полезны, когда вы хотите, чтобы ваши данные размещались в стеке, как и другие типы, которые мы уже видели, а не в куче (подробнее стек и кучу мы обсудим в Главе 4), или когда вы хотите гарантировать, что у вас всегда будет фиксированное количество элементов. Однако массив не так гибок, как тип вектора. Вектор — похожий тип коллекции, предоставляемый стандартной библиотекой, которому разрешено увеличиваться или уменьшаться в размере, потому что его содержимое находится в куче. Если вы не уверены, использовать массив или вектор, скорее всего, вам следует использовать вектор. Глава 8 рассматривает векторы подробнее.

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

#![allow(unused)]
fn main() {
let months = ["January", "February", "March", "April", "May", "June", "July",
              "August", "September", "October", "November", "December"];
}

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

#![allow(unused)]
fn main() {
let a: [i32; 5] = [1, 2, 3, 4, 5];
}

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

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

#![allow(unused)]
fn main() {
let a = [3; 5];
}

Массив с именем a будет содержать 5 элементов, и все они изначально будут установлены в значение 3. Это то же самое, что записать let a = [3, 3, 3, 3, 3];, но более кратко.

Доступ к элементам массива

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

Имя файла: src/main.rs

fn main() {
    let a = [1, 2, 3, 4, 5];

    let first = a[0];
    let second = a[1];
}

В этом примере переменная с именем first получит значение 1, потому что это значение находится в массиве по индексу [0]. Переменная с именем second получит значение 2 из индекса [1] в массиве.

Недопустимый доступ к элементу массива

Посмотрим, что произойдёт, если попытаться обратиться к элементу массива, который находится за пределами массива. Допустим, вы запускаете этот код, похожий на игру «Угадай число» из Главы 2, чтобы получить индекс массива от пользователя:

Имя файла: src/main.rs

use std::io;

fn main() {
    let a = [1, 2, 3, 4, 5];

    println!("Please enter an array index.");

    let mut index = String::new();

    io::stdin()
        .read_line(&mut index)
        .expect("Failed to read line");

    let index: usize = index
        .trim()
        .parse()
        .expect("Index entered was not a number");

    let element = a[index];

    println!("The value of the element at index {index} is: {element}");
}

Этот код успешно компилируется. Если вы запустите его с помощью cargo run и введёте 0, 1, 2, 3 или 4, программа выведет соответствующее значение по этому индексу в массиве. Если вместо этого вы введёте число за пределами массива, например 10, то увидите примерно такой вывод:

thread 'main' panicked at src/main.rs:19:19:
index out of bounds: the len is 5 but the index is 10
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

Программа завершилась ошибкой времени выполнения в момент использования недопустимого значения в операции индексирования. Она вышла с сообщением об ошибке и не выполнила последнюю инструкцию println!. Когда вы пытаетесь обратиться к элементу с помощью индексирования, Rust проверяет, что указанный индекс меньше длины массива. Если индекс больше длины или равен ей, Rust вызовет panic. Эта проверка должна происходить во время выполнения, особенно в данном случае, потому что компилятор не может заранее знать, какое значение пользователь введёт, когда позже запустит код.

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

Функции

Функции

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

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

Имя файла: src/main.rs

fn main() {
    println!("Hello, world!");

    another_function();
}

fn another_function() {
    println!("Another function.");
}

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

Мы можем вызвать любую определённую нами функцию, указав её имя, за которым следует пара круглых скобок. Поскольку another_function определена в программе, её можно вызвать внутри функции main. Обратите внимание, что в исходном коде мы определили another_function после функции main; мы могли бы определить её и раньше. Rust не важно, где именно вы определяете функции, важно лишь, чтобы они были определены где-то в области видимости, доступной вызывающему коду.

Давайте создадим новый бинарный проект с именем functions, чтобы подробнее изучить функции. Поместите пример с another_function в src/main.rs и запустите его. Вы должны увидеть следующий вывод:

$ cargo run
   Compiling functions v0.1.0 (file:///projects/functions)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.28s
     Running `target/debug/functions`
Hello, world!
Another function.

Строки выполняются в том порядке, в котором они расположены в функции main. Сначала выводится сообщение «Hello, world!», затем вызывается another_function и выводится её сообщение.

Параметры

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

В этой версии another_function мы добавим параметр:

Имя файла: src/main.rs

fn main() {
    another_function(5);
}

fn another_function(x: i32) {
    println!("The value of x is: {x}");
}

Попробуйте запустить эту программу; вы должны получить следующий вывод:

$ cargo run
   Compiling functions v0.1.0 (file:///projects/functions)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 1.21s
     Running `target/debug/functions`
The value of x is: 5

В объявлении another_function есть один параметр с именем x. Тип x указан как i32. Когда мы передаём 5 в another_function, макрос println! помещает 5 в то место строки форматирования, где находилась пара фигурных скобок с x.

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

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

Имя файла: src/main.rs

fn main() {
    print_labeled_measurement(5, 'h');
}

fn print_labeled_measurement(value: i32, unit_label: char) {
    println!("The measurement is: {value}{unit_label}");
}

В этом примере создаётся функция с именем print_labeled_measurement с двумя параметрами. Первый параметр называется value и имеет тип i32. Второй называется unit_label и имеет тип char. Затем функция выводит текст, содержащий и value, и unit_label.

Давайте попробуем запустить этот код. Замените программу, которая сейчас находится в файле src/main.rs вашего проекта functions, предыдущим примером и запустите её с помощью cargo run:

$ cargo run
   Compiling functions v0.1.0 (file:///projects/functions)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.31s
     Running `target/debug/functions`
The measurement is: 5h

Поскольку мы вызвали функцию со значением 5 для value и значением 'h' для unit_label, вывод программы содержит эти значения.

Инструкции и выражения

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

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

Рассмотрим несколько примеров.

На самом деле мы уже использовали инструкции и выражения. Создание переменной и присваивание ей значения с помощью ключевого слова let — это инструкция. В листинге 3-1 let y = 6; является инструкцией.

Filename: src/main.rs
fn main() {
    let y = 6;
}
Listing 3-1: Объявление функции main, содержащее одну инструкцию

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

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

Имя файла: src/main.rs

fn main() {
    let x = (let y = 6);
}

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

$ cargo run
   Compiling functions v0.1.0 (file:///projects/functions)
error: expected expression, found `let` statement
 --> src/main.rs:2:14
  |
2 |     let x = (let y = 6);
  |              ^^^
  |
  = note: only supported directly in conditions of `if` and `while` expressions

warning: unnecessary parentheses around assigned value
 --> src/main.rs:2:13
  |
2 |     let x = (let y = 6);
  |             ^         ^
  |
  = note: `#[warn(unused_parens)]` on by default
help: remove these parentheses
  |
2 -     let x = (let y = 6);
2 +     let x = let y = 6;
  |

warning: `functions` (bin "functions") generated 1 warning
error: could not compile `functions` (bin "functions") due to 1 previous error; 1 warning emitted

Инструкция let y = 6 не возвращает значение, поэтому x не с чем связываться. Это отличается от поведения в других языках, таких как C и Ruby, где присваивание возвращает значение присваивания. В этих языках можно написать x = y = 6, и тогда и x, и y будут иметь значение 6; в Rust это не так.

Выражения вычисляются в значение и составляют большую часть остального кода, который вы будете писать на Rust. Рассмотрим математическую операцию, например 5 + 6: это выражение, которое вычисляется в значение 11. Выражения могут быть частью инструкций: в листинге 3-1 число 6 в инструкции let y = 6; — это выражение, которое вычисляется в значение 6. Вызов функции — это выражение. Вызов макроса — это выражение. Новый блок области видимости, созданный с помощью фигурных скобок, тоже является выражением, например:

Имя файла: src/main.rs

fn main() {
    let y = {
        let x = 3;
        x + 1
    };

    println!("The value of y is: {y}");
}

Это выражение:

{
    let x = 3;
    x + 1
}

является блоком, который в этом случае вычисляется в 4. Это значение связывается с y как часть инструкции let. Обратите внимание на строку x + 1 без точки с запятой в конце, что отличает её от большинства строк, которые вы видели до сих пор. Выражения не заканчиваются точкой с запятой. Если добавить точку с запятой в конец выражения, вы превратите его в инструкцию, и тогда оно не будет возвращать значение. Помните об этом, когда дальше будете изучать возвращаемые значения функций и выражения.

Функции с возвращаемыми значениями

Функции могут возвращать значения коду, который их вызывает. Мы не даём имена возвращаемым значениям, но должны объявлять их тип после стрелки (->). В Rust возвращаемое значение функции совпадает со значением последнего выражения в блоке тела функции. Можно вернуться из функции раньше, используя ключевое слово return и указывая значение, но большинство функций неявно возвращают последнее выражение. Вот пример функции, которая возвращает значение:

Имя файла: src/main.rs

fn five() -> i32 {
    5
}

fn main() {
    let x = five();

    println!("The value of x is: {x}");
}

В функции five нет вызовов функций, макросов и даже инструкций let — только само число 5. Это вполне допустимая функция в Rust. Обратите внимание, что тип возвращаемого значения функции также указан как -> i32. Попробуйте запустить этот код; вывод должен выглядеть так:

$ cargo run
   Compiling functions v0.1.0 (file:///projects/functions)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.30s
     Running `target/debug/functions`
The value of x is: 5

5 в функции five — это возвращаемое значение функции, поэтому тип возвращаемого значения — i32. Рассмотрим это подробнее. Здесь есть две важные детали. Во-первых, строка let x = five(); показывает, что мы используем возвращаемое значение функции для инициализации переменной. Поскольку функция five возвращает 5, эта строка эквивалентна следующей:

#![allow(unused)]
fn main() {
let x = 5;
}

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

Рассмотрим другой пример:

Имя файла: src/main.rs

fn main() {
    let x = plus_one(5);

    println!("The value of x is: {x}");
}

fn plus_one(x: i32) -> i32 {
    x + 1
}

Запуск этого кода выведет The value of x is: 6. Но что произойдёт, если мы поставим точку с запятой в конце строки, содержащей x + 1, превратив её из выражения в инструкцию?

Имя файла: src/main.rs

fn main() {
    let x = plus_one(5);

    println!("The value of x is: {x}");
}

fn plus_one(x: i32) -> i32 {
    x + 1;
}

Компиляция этого кода приведёт к следующей ошибке:

$ cargo run
   Compiling functions v0.1.0 (file:///projects/functions)
error[E0308]: mismatched types
 --> src/main.rs:7:24
  |
7 | fn plus_one(x: i32) -> i32 {
  |    --------            ^^^ expected `i32`, found `()`
  |    |
  |    implicitly returns `()` as its body has no tail or `return` expression
8 |     x + 1;
  |          - help: remove this semicolon to return this value

For more information about this error, try `rustc --explain E0308`.
error: could not compile `functions` (bin "functions") due to 1 previous error

Основное сообщение об ошибке, mismatched types, раскрывает главную проблему этого кода. Определение функции plus_one говорит, что она вернёт i32, но инструкции не вычисляются в значение; это выражается типом (), unit-типом. Следовательно, ничего не возвращается, что противоречит определению функции и приводит к ошибке. В этом выводе Rust предоставляет сообщение, которое может помочь исправить проблему: он предлагает удалить точку с запятой, что исправит ошибку.

Комментарии

Комментарии

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

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

#![allow(unused)]
fn main() {
// привет, мир
}

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

#![allow(unused)]
fn main() {
// Итак, здесь мы делаем что-то сложное, настолько длинное, что нам нужно
// несколько строк комментариев, чтобы это описать! Уф! Надеюсь, этот
// комментарий объяснит, что происходит.
}

Комментарии также можно размещать в конце строк, содержащих код:

Имя файла: src/main.rs

fn main() {
    let lucky_number = 7; // I'm feeling lucky today
}

Но чаще вы будете видеть их в таком формате: комментарий находится на отдельной строке над кодом, который он поясняет:

Имя файла: src/main.rs

fn main() {
    // I'm feeling lucky today
    let lucky_number = 7;
}

В Rust также есть другой вид комментариев — документационные комментарии, которые мы обсудим в разделе «Публикация крейта в Crates.io» Главы 14.

Управление потоком выполнения

Управление потоком выполнения

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

Выражения if

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

Чтобы изучить выражение if, создайте в каталоге projects новый проект с именем branches. В файл src/main.rs введите следующее:

Имя файла: src/main.rs

fn main() {
    let number = 3;

    if number < 5 {
        println!("condition was true");
    } else {
        println!("condition was false");
    }
}

Все выражения if начинаются с ключевого слова if, за которым следует условие. В этом случае условие проверяет, меньше ли значение переменной number числа 5. Сразу после условия в фигурных скобках мы помещаем блок кода, который нужно выполнить, если условие равно true. Блоки кода, связанные с условиями в выражениях if, иногда называют ветвями (arms), как и ветви в выражениях match, которые мы обсуждали в разделе «Сравнение предположения с секретным числом» Главы 2.

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

Попробуйте запустить этот код; вы должны увидеть следующий вывод:

$ cargo run
   Compiling branches v0.1.0 (file:///projects/branches)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.31s
     Running `target/debug/branches`
condition was true

Попробуем изменить значение number на такое, при котором условие станет false, и посмотрим, что произойдёт:

fn main() {
    let number = 7;

    if number < 5 {
        println!("condition was true");
    } else {
        println!("condition was false");
    }
}

Запустите программу снова и посмотрите на вывод:

$ cargo run
   Compiling branches v0.1.0 (file:///projects/branches)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.31s
     Running `target/debug/branches`
condition was false

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

Имя файла: src/main.rs

fn main() {
    let number = 3;

    if number {
        println!("number was three");
    }
}

На этот раз условие if вычисляется в значение 3, и Rust выдаёт ошибку:

$ cargo run
   Compiling branches v0.1.0 (file:///projects/branches)
error[E0308]: mismatched types
 --> src/main.rs:4:8
  |
4 |     if number {
  |        ^^^^^^ expected `bool`, found integer

For more information about this error, try `rustc --explain E0308`.
error: could not compile `branches` (bin "branches") due to 1 previous error

Ошибка указывает, что Rust ожидал bool, но получил целое число. В отличие от таких языков, как Ruby и JavaScript, Rust не будет автоматически пытаться преобразовывать нелогические типы в логический. Нужно быть явными и всегда передавать в if логическое значение в качестве условия. Например, если мы хотим, чтобы блок кода if выполнялся только тогда, когда число не равно 0, мы можем изменить выражение if следующим образом:

Имя файла: src/main.rs

fn main() {
    let number = 3;

    if number != 0 {
        println!("number was something other than zero");
    }
}

Запуск этого кода выведет number was something other than zero.

Обработка нескольких условий с помощью else if

Можно использовать несколько условий, объединяя if и else в выражении else if. Например:

Имя файла: src/main.rs

fn main() {
    let number = 6;

    if number % 4 == 0 {
        println!("number is divisible by 4");
    } else if number % 3 == 0 {
        println!("number is divisible by 3");
    } else if number % 2 == 0 {
        println!("number is divisible by 2");
    } else {
        println!("number is not divisible by 4, 3, or 2");
    }
}

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

$ cargo run
   Compiling branches v0.1.0 (file:///projects/branches)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.31s
     Running `target/debug/branches`
number is divisible by 3

Когда эта программа выполняется, она по очереди проверяет каждое выражение if и выполняет первое тело, условие которого вычисляется в true. Обратите внимание: хотя 6 делится на 2, мы не видим вывод number is divisible by 2 и не видим текст number is not divisible by 4, 3, or 2 из блока else. Причина в том, что Rust выполняет только блок для первого условия, равного true, и, как только находит его, уже не проверяет остальные.

Использование слишком большого количества выражений else if может загромоздить код, поэтому, если их больше одного, возможно, стоит выполнить рефакторинг. Глава 6 описывает мощную конструкцию ветвления Rust под названием match, которая подходит для таких случаев.

Использование if в инструкции let

Поскольку if — это выражение, мы можем использовать его в правой части инструкции let, чтобы присвоить результат переменной, как в листинге 3-2.

Filename: src/main.rs
fn main() {
    let condition = true;
    let number = if condition { 5 } else { 6 };

    println!("The value of number is: {number}");
}
Listing 3-2: Присваивание результата выражения if переменной

Переменная number будет связана со значением на основе результата выражения if. Запустите этот код, чтобы увидеть, что произойдёт:

$ cargo run
   Compiling branches v0.1.0 (file:///projects/branches)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.30s
     Running `target/debug/branches`
The value of number is: 5

Помните, что блоки кода вычисляются в последнее выражение внутри них, а числа сами по себе тоже являются выражениями. В этом случае значение всего выражения if зависит от того, какой блок кода выполнится. Это означает, что значения, которые потенциально могут быть результатами каждой ветви if, должны иметь один и тот же тип; в листинге 3-2 результатами и ветви if, и ветви else были целые числа i32. Если типы не совпадают, как в следующем примере, мы получим ошибку:

Имя файла: src/main.rs

fn main() {
    let condition = true;

    let number = if condition { 5 } else { "six" };

    println!("The value of number is: {number}");
}

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

$ cargo run
   Compiling branches v0.1.0 (file:///projects/branches)
error[E0308]: `if` and `else` have incompatible types
 --> src/main.rs:4:44
  |
4 |     let number = if condition { 5 } else { "six" };
  |                                 -          ^^^^^ expected integer, found `&str`
  |                                 |
  |                                 expected because of this

For more information about this error, try `rustc --explain E0308`.
error: could not compile `branches` (bin "branches") due to 1 previous error

Выражение в блоке if вычисляется в целое число, а выражение в блоке else вычисляется в строку. Это не сработает, потому что переменные должны иметь один тип, а Rust должен точно знать во время компиляции, какой тип имеет переменная number. Знание типа number позволяет компилятору проверять, что тип допустим везде, где мы используем number. Rust не смог бы этого делать, если бы тип number определялся только во время выполнения; компилятор был бы сложнее и давал бы меньше гарантий о коде, если бы ему приходилось отслеживать несколько гипотетических типов для любой переменной.

Повторение с помощью циклов

Часто бывает полезно выполнить блок кода больше одного раза. Для этой задачи Rust предоставляет несколько видов циклов, которые выполняют код внутри тела цикла до конца, а затем сразу возвращаются к началу. Чтобы поэкспериментировать с циклами, создадим новый проект с именем loops.

В Rust есть три вида циклов: loop, while и for. Попробуем каждый из них.

Повторение кода с помощью loop

Ключевое слово loop говорит Rust выполнять блок кода снова и снова: либо бесконечно, либо до тех пор, пока вы явно не скажете остановиться.

В качестве примера измените файл src/main.rs в каталоге loops так, чтобы он выглядел следующим образом:

Имя файла: src/main.rs

fn main() {
    loop {
        println!("again!");
    }
}

Когда мы запустим эту программу, мы увидим, что again! выводится снова и снова непрерывно, пока мы вручную не остановим программу. Большинство терминалов поддерживают сочетание клавиш ctrl-C, чтобы прервать программу, застрявшую в непрерывном цикле. Попробуйте:

$ cargo run
   Compiling loops v0.1.0 (file:///projects/loops)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.08s
     Running `target/debug/loops`
again!
again!
again!
again!
^Cagain!

Символ ^C показывает место, где вы нажали ctrl-C.

Вы можете увидеть или не увидеть слово again! после ^C — это зависит от того, где именно в цикле находился код, когда получил сигнал прерывания.

К счастью, Rust также предоставляет способ выйти из цикла с помощью кода. Вы можете поместить ключевое слово break внутрь цикла, чтобы сообщить программе, когда нужно прекратить выполнение цикла. Вспомните, что мы делали это в игре «Угадай число» в разделе «Выход после правильного предположения» Главы 2, чтобы выйти из программы, когда пользователь выигрывал игру, угадав правильное число.

В игре «Угадай число» мы также использовали continue, который в цикле сообщает программе пропустить оставшийся код в текущей итерации цикла и перейти к следующей итерации.

Возврат значений из циклов

Один из способов применения loop — повторять операцию, которая, как вы знаете, может завершиться неудачей, например проверку, завершил ли поток свою работу. Также может понадобиться передать результат этой операции из цикла в остальную часть кода. Для этого можно добавить значение, которое вы хотите вернуть, после выражения break, используемого для остановки цикла; это значение будет возвращено из цикла, чтобы вы могли его использовать, как показано здесь:

fn main() {
    let mut counter = 0;

    let result = loop {
        counter += 1;

        if counter == 10 {
            break counter * 2;
        }
    };

    println!("The result is {result}");
}

Перед циклом мы объявляем переменную с именем counter и инициализируем её значением 0. Затем объявляем переменную с именем result, чтобы хранить значение, возвращённое из цикла. На каждой итерации цикла мы добавляем 1 к переменной counter, а затем проверяем, равен ли counter значению 10. Когда это так, мы используем ключевое слово break со значением counter * 2. После цикла мы используем точку с запятой, чтобы завершить инструкцию, которая присваивает значение result. Наконец, мы выводим значение result, которое в этом случае равно 20.

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

Уточнение с помощью меток циклов

Если у вас есть циклы внутри циклов, break и continue применяются к самому внутреннему циклу в этой точке. При желании можно указать для цикла метку цикла, которую затем можно использовать с break или continue, чтобы указать, что эти ключевые слова относятся к помеченному циклу, а не к самому внутреннему. Метки циклов должны начинаться с одинарной кавычки. Вот пример с двумя вложенными циклами:

fn main() {
    let mut count = 0;
    'counting_up: loop {
        println!("count = {count}");
        let mut remaining = 10;

        loop {
            println!("remaining = {remaining}");
            if remaining == 9 {
                break;
            }
            if count == 2 {
                break 'counting_up;
            }
            remaining -= 1;
        }

        count += 1;
    }
    println!("End count = {count}");
}

Внешний цикл имеет метку 'counting_up и будет считать вверх от 0 до 2. Внутренний цикл без метки считает вниз от 10 до 9. Первый break, который не указывает метку, выйдет только из внутреннего цикла. Инструкция break 'counting_up; выйдет из внешнего цикла. Этот код выводит:

$ cargo run
   Compiling loops v0.1.0 (file:///projects/loops)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.58s
     Running `target/debug/loops`
count = 0
remaining = 10
remaining = 9
count = 1
remaining = 10
remaining = 9
count = 2
remaining = 10
End count = 2

Упрощение условных циклов с помощью while

Программе часто нужно проверять условие внутри цикла. Пока условие равно true, цикл выполняется. Когда условие перестаёт быть true, программа вызывает break, останавливая цикл. Такое поведение можно реализовать комбинацией loop, if, else и break; при желании вы можете попробовать сделать это сейчас в программе. Однако этот паттерн настолько распространён, что в Rust для него есть встроенная языковая конструкция — цикл while. В листинге 3-3 мы используем while, чтобы программа выполнила цикл три раза, каждый раз выполняя обратный отсчёт, а затем после цикла вывела сообщение и завершилась.

Filename: src/main.rs
fn main() {
    let mut number = 3;

    while number != 0 {
        println!("{number}!");

        number -= 1;
    }

    println!("LIFTOFF!!!");
}
Listing 3-3: Использование цикла while для выполнения кода, пока условие вычисляется в true

Эта конструкция устраняет большую часть вложенности, которая потребовалась бы при использовании loop, if, else и break, и выглядит понятнее. Пока условие вычисляется в true, код выполняется; иначе происходит выход из цикла.

Перебор коллекции с помощью for

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

Filename: src/main.rs
fn main() {
    let a = [10, 20, 30, 40, 50];
    let mut index = 0;

    while index < 5 {
        println!("the value is: {}", a[index]);

        index += 1;
    }
}
Listing 3-4: Перебор каждого элемента коллекции с помощью цикла while

Здесь код проходит по элементам массива, увеличивая индекс. Он начинает с индекса 0 и затем выполняет цикл, пока не достигнет последнего индекса в массиве (то есть пока index < 5 не перестанет быть true). Запуск этого кода выведет каждый элемент массива:

$ cargo run
   Compiling loops v0.1.0 (file:///projects/loops)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.32s
     Running `target/debug/loops`
the value is: 10
the value is: 20
the value is: 30
the value is: 40
the value is: 50

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

Однако этот подход подвержен ошибкам: мы можем вызвать panic в программе, если значение индекса или проверяемое условие неверны. Например, если вы измените определение массива a, чтобы в нём было четыре элемента, но забудете обновить условие на while index < 4, код вызовет panic. Кроме того, это медленно, потому что компилятор добавляет код времени выполнения для условной проверки того, находится ли индекс в границах массива, на каждой итерации цикла.

В качестве более краткой альтернативы можно использовать цикл for и выполнять некоторый код для каждого элемента коллекции. Цикл for выглядит как код в листинге 3-5.

Filename: src/main.rs
fn main() {
    let a = [10, 20, 30, 40, 50];

    for element in a {
        println!("the value is: {element}");
    }
}
Listing 3-5: Перебор каждого элемента коллекции с помощью цикла for

Когда мы запустим этот код, то увидим тот же вывод, что и в листинге 3-4. Что ещё важнее, теперь мы повысили безопасность кода и устранили вероятность ошибок, которые могли бы возникнуть из-за выхода за конец массива или недостаточного прохода и пропуска некоторых элементов. Машинный код, сгенерированный из циклов for, также может быть эффективнее, потому что индекс не нужно сравнивать с длиной массива на каждой итерации.

Используя цикл for, вам не нужно помнить, что требуется изменить какой-либо другой код при изменении количества значений в массиве, как пришлось бы при подходе из листинга 3-4.

Безопасность и краткость циклов for делают их самой часто используемой конструкцией цикла в Rust. Даже в ситуациях, когда нужно выполнить некоторый код определённое количество раз, как в примере обратного отсчёта с циклом while в листинге 3-3, большинство разработчиков Rust использовали бы цикл for. Для этого можно использовать Range, предоставляемый стандартной библиотекой: он генерирует все числа в последовательности, начиная с одного числа и заканчивая перед другим, не включая его.

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

Имя файла: src/main.rs

fn main() {
    for number in (1..4).rev() {
        println!("{number}!");
    }
    println!("LIFTOFF!!!");
}

Этот код немного приятнее, не правда ли?

Итоги

Вы справились! Это была объёмная глава: вы узнали о переменных, скалярных и составных типах данных, функциях, комментариях, выражениях if и циклах! Чтобы попрактиковаться с концепциями, обсуждёнными в этой главе, попробуйте написать программы, которые делают следующее:

  • Преобразуют температуру между шкалами Фаренгейта и Цельсия.
  • Генерируют n-е число Фибоначчи.
  • Печатают текст рождественской песни «Двенадцать дней Рождества», используя повторения в песне.

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

Понимание владения

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

Что такое владение?

Что такое владение?

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

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

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

Стек и куча

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

И стек, и куча — это части памяти, доступные вашему коду во время выполнения, но устроены они по-разному. Стек хранит значения в порядке их поступления, а удаляет их в обратном порядке. Это называется последним пришёл — первым вышел (last in, first out, LIFO). Представьте стопку тарелок: когда вы добавляете тарелки, вы кладёте их наверх, а когда нужна тарелка, берёте одну сверху. Добавлять или убирать тарелки из середины или снизу было бы не так удобно! Добавление данных называется помещением в стек, а удаление данных — извлечением из стека. Все данные, хранящиеся в стеке, должны иметь известный фиксированный размер. Данные, размер которых во время компиляции неизвестен или может изменяться, должны храниться в куче.

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

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

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

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

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

Правила владения

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

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

Область видимости переменной

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

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

#![allow(unused)]
fn main() {
let s = "hello";
}

Переменная s ссылается на строковый литерал, значение которого жёстко задано в тексте нашей программы. Переменная действительна с момента объявления до конца текущей области видимости. Листинг 4-1 показывает программу с комментариями, отмечающими, где переменная s была бы действительной.

fn main() {
    {                      // s is not valid here, since it's not yet declared
        let s = "hello";   // s is valid from this point forward

        // do stuff with s
    }                      // this scope is now over, and s is no longer valid
}
Listing 4-1: Переменная и область видимости, в которой она действительна

Другими словами, здесь есть два важных момента времени:

  • Когда s входит в область видимости, она становится действительной.
  • Она остаётся действительной, пока не выходит из области видимости.

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

Тип String

Чтобы проиллюстрировать правила владения, нам нужен тип данных сложнее тех, которые мы рассматривали в разделе «Типы данных» Главы 3. Рассмотренные ранее типы имеют известный размер, могут храниться в стеке и извлекаться из стека, когда их область видимости завершается, а также могут быстро и тривиально копироваться для создания нового независимого экземпляра, если другой части кода нужно использовать то же значение в другой области видимости. Но мы хотим посмотреть на данные, которые хранятся в куче, и разобраться, как Rust узнаёт, когда нужно очистить эти данные; тип String для этого отлично подходит.

Мы сосредоточимся на тех частях String, которые связаны с владением. Эти аспекты также применимы к другим сложным типам данных, независимо от того, предоставлены они стандартной библиотекой или созданы вами. Аспекты String, не связанные с владением, мы обсудим в Главе 8.

Мы уже видели строковые литералы, где строковое значение жёстко задано в нашей программе. Строковые литералы удобны, но подходят не для каждой ситуации, когда мы хотим использовать текст. Одна причина в том, что они неизменяемы. Другая — не каждое строковое значение может быть известно во время написания кода: например, что если мы хотим принять пользовательский ввод и сохранить его? Для таких ситуаций в Rust есть тип String. Этот тип управляет данными, выделенными в куче, и поэтому способен хранить объём текста, неизвестный нам во время компиляции. Вы можете создать String из строкового литерала с помощью функции from, например так:

#![allow(unused)]
fn main() {
let s = String::from("hello");
}

Оператор двойного двоеточия :: позволяет поместить именно эту функцию from в пространство имён типа String, а не использовать какое-нибудь имя вроде string_from. Мы подробнее обсудим этот синтаксис в разделе «Методы» Главы 5, а также когда будем говорить о пространствах имён с модулями в разделе «Пути для обращения к элементу в дереве модулей» Главы 7.

Такой вид строки можно изменять:

fn main() {
    let mut s = String::from("hello");

    s.push_str(", world!"); // push_str() appends a literal to a String

    println!("{s}"); // this will print `hello, world!`
}

Так в чём же разница? Почему String можно изменять, а литералы нельзя? Разница в том, как эти два типа работают с памятью.

Память и выделение памяти

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

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

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

Первую часть выполняем мы: когда мы вызываем String::from, его реализация запрашивает необходимую память. Это почти универсально для языков программирования.

Однако вторая часть отличается. В языках со сборщиком мусора (garbage collector, GC) сборщик мусора отслеживает и очищает память, которая больше не используется, и нам не нужно об этом думать. В большинстве языков без GC наша ответственность — определить, когда память больше не используется, и вызвать код для её явного освобождения, так же как мы вызывали код для её запроса. Исторически сделать это правильно было трудной задачей программирования. Если мы забудем, мы будем расходовать память впустую. Если сделаем это слишком рано, у нас появится недействительная переменная. Если сделаем это дважды, это тоже ошибка. Нужно сопоставить ровно один allocate с ровно одним free.

Rust идёт другим путём: память автоматически возвращается, когда переменная, которая ею владеет, выходит из области видимости. Вот версия нашего примера с областью видимости из листинга 4-1, использующая String вместо строкового литерала:

fn main() {
    {
        let s = String::from("hello"); // s is valid from this point forward

        // do stuff with s
    }                                  // this scope is now over, and s is no
                                       // longer valid
}

Есть естественный момент, когда мы можем вернуть аллокатору память, которая нужна нашему String: когда s выходит из области видимости. Когда переменная выходит из области видимости, Rust вызывает для нас специальную функцию. Эта функция называется drop, и именно туда автор String может поместить код для возврата памяти. Rust автоматически вызывает drop при закрывающей фигурной скобке.

Примечание: в C++ этот паттерн освобождения ресурсов в конце времени жизни элемента иногда называется получение ресурса является инициализацией (Resource Acquisition Is Initialization, RAII). Функция drop в Rust будет вам знакома, если вы использовали паттерны RAII.

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

Взаимодействие переменных и данных при перемещении

В Rust несколько переменных могут по-разному взаимодействовать с одними и теми же данными. Листинг 4-2 показывает пример с целым числом.

fn main() {
    let x = 5;
    let y = x;
}
Listing 4-2: Присваивание целочисленного значения переменной x переменной y

Вероятно, мы можем догадаться, что здесь происходит: «Свяжи значение 5 с x; затем сделай копию значения в x и свяжи её с y». Теперь у нас есть две переменные, x и y, и обе равны 5. Именно это и происходит, потому что целые числа — простые значения с известным фиксированным размером, и оба эти значения 5 помещаются в стек.

Теперь посмотрим на версию со String:

fn main() {
    let s1 = String::from("hello");
    let s2 = s1;
}

Это выглядит очень похоже, поэтому можно предположить, что работать это будет так же: то есть вторая строка создаст копию значения из s1 и свяжет её с s2. Но происходит не совсем это.

Посмотрите на рисунок 4-1, чтобы увидеть, что происходит со String внутри. String состоит из трёх частей, показанных слева: указателя на память, где хранится содержимое строки, длины и ёмкости. Эта группа данных хранится в стеке. Справа находится память в куче, которая хранит содержимое.

Две таблицы: первая таблица содержит представление s1 в стеке,
состоящее из длины (5), ёмкости (5) и указателя на первое значение во второй
таблице. Вторая таблица содержит побайтовое представление строковых данных
в куче.

Рисунок 4-1: Представление в памяти значения String, содержащего "hello" и связанного с s1

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

Когда мы присваиваем s1 переменной s2, данные String копируются: мы копируем указатель, длину и ёмкость, находящиеся в стеке. Мы не копируем данные в куче, на которые указывает указатель. Другими словами, представление данных в памяти выглядит как на рисунке 4-2.

Три таблицы: таблицы s1 и s2, представляющие эти строки в стеке
соответственно, при этом обе указывают на одни и те же строковые данные
в куче.

Рисунок 4-2: Представление в памяти переменной s2, которая содержит копию указателя, длины и ёмкости s1

Представление не выглядит как на рисунке 4-3, где показано, как выглядела бы память, если бы Rust также копировал данные в куче. Если бы Rust так делал, операция s2 = s1 могла бы быть очень дорогой с точки зрения производительности во время выполнения, если бы данные в куче были большими.

Четыре таблицы: две таблицы представляют данные в стеке для s1 и s2,
и каждая указывает на собственную копию строковых данных в куче.

Рисунок 4-3: Другой возможный вариант того, что могла бы делать операция s2 = s1, если бы Rust также копировал данные в куче

Ранее мы говорили, что, когда переменная выходит из области видимости, Rust автоматически вызывает функцию drop и очищает память в куче для этой переменной. Но рисунок 4-2 показывает, что оба указателя данных указывают на одно и то же место. Это проблема: когда s2 и s1 выйдут из области видимости, они обе попытаются освободить одну и ту же память. Это известно как ошибка двойного освобождения и является одной из ошибок безопасности памяти, о которых мы упоминали ранее. Двойное освобождение памяти может привести к повреждению памяти, что потенциально может привести к уязвимостям безопасности.

Чтобы обеспечить безопасность памяти, после строки let s2 = s1; Rust считает s1 недействительной. Поэтому Rust не нужно ничего освобождать, когда s1 выходит из области видимости. Посмотрите, что произойдёт, если попытаться использовать s1 после создания s2; это не сработает:

fn main() {
    let s1 = String::from("hello");
    let s2 = s1;

    println!("{s1}, world!");
}

Вы получите такую ошибку, потому что Rust не позволяет использовать недействительное значение:

$ cargo run
   Compiling ownership v0.1.0 (file:///projects/ownership)
error[E0382]: borrow of moved value: `s1`
 --> src/main.rs:5:16
  |
2 |     let s1 = String::from("hello");
  |         -- move occurs because `s1` has type `String`, which does not implement the `Copy` trait
3 |     let s2 = s1;
  |              -- value moved here
4 |
5 |     println!("{s1}, world!");
  |                ^^ value borrowed here after move
  |
  = note: this error originates in the macro `$crate::format_args_nl` which comes from the expansion of the macro `println` (in Nightly builds, run with -Z macro-backtrace for more info)
help: consider cloning the value if the performance cost is acceptable
  |
3 |     let s2 = s1.clone();
  |                ++++++++

For more information about this error, try `rustc --explain E0382`.
error: could not compile `ownership` (bin "ownership") due to 1 previous error

Если при работе с другими языками вы слышали термины поверхностное копирование и глубокое копирование, то копирование указателя, длины и ёмкости без копирования самих данных, вероятно, похоже на поверхностное копирование. Но поскольку Rust также делает первую переменную недействительной, это называется не поверхностным копированием, а перемещением. В этом примере мы сказали бы, что s1 была перемещена в s2. Таким образом, на самом деле происходит то, что показано на рисунке 4-4.

Три таблицы: таблицы s1 и s2, представляющие эти строки в стеке
соответственно, при этом обе указывают на одни и те же строковые данные
в куче. Таблица s1 выделена серым, потому что s1 больше недействительна;
для доступа к данным в куче можно использовать только s2.

Рисунок 4-4: Представление в памяти после того, как s1 стала недействительной

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

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

Область видимости и присваивание

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

fn main() {
    let mut s = String::from("hello");
    s = String::from("ahoy");

    println!("{s}, world!");
}

Сначала мы объявляем переменную s и связываем её со String со значением "hello". Затем сразу создаём новый String со значением "ahoy" и присваиваем его s. В этот момент на исходное значение в куче уже вообще ничто не ссылается. Рисунок 4-5 показывает, как теперь выглядят данные в стеке и куче:

Одна таблица, представляющая строковое значение в стеке, указывающее
на второй фрагмент строковых данных (ahoy) в куче; исходные строковые данные
(hello) выделены серым, потому что к ним больше нельзя обратиться.

Рисунок 4-5: Представление в памяти после того, как исходное значение было полностью заменено

Таким образом, исходная строка немедленно выходит из области видимости. Rust выполнит для неё функцию drop, и её память будет сразу освобождена. Когда в конце мы выведем значение, это будет "ahoy, world!".

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

Если мы действительно хотим глубоко скопировать данные String в куче, а не только данные в стеке, можно использовать распространённый метод clone. Мы обсудим синтаксис методов в Главе 5, но, поскольку методы — распространённая возможность во многих языках программирования, вы, вероятно, уже видели их раньше.

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

fn main() {
    let s1 = String::from("hello");
    let s2 = s1.clone();

    println!("s1 = {s1}, s2 = {s2}");
}

Это работает как надо и явно создаёт поведение, показанное на рисунке 4-3, где данные в куче действительно копируются.

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

Данные только в стеке: Copy

Есть ещё одна тонкость, о которой мы пока не говорили. Этот код с целыми числами, часть которого была показана в листинге 4-2, работает и является допустимым:

fn main() {
    let x = 5;
    let y = x;

    println!("x = {x}, y = {y}");
}

Но этот код, кажется, противоречит тому, что мы только что узнали: у нас нет вызова clone, но x всё ещё действительна и не была перемещена в y.

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

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

Rust не позволит нам пометить тип как Copy, если сам тип или любая его часть реализует трейт Drop. Если типу нужно выполнить что-то особое, когда значение выходит из области видимости, а мы добавим к этому типу аннотацию Copy, то получим ошибку времени компиляции. Чтобы узнать, как добавить аннотацию Copy к своему типу для реализации трейта, смотрите «Автоматически выводимые трейты» в Приложении C.

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

  • Все целочисленные типы, например u32.
  • Логический тип bool со значениями true и false.
  • Все типы с плавающей точкой, например f64.
  • Символьный тип char.
  • Кортежи, если они содержат только типы, которые тоже реализуют Copy. Например, (i32, i32) реализует Copy, а (i32, String) — нет.

Владение и функции

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

Filename: src/main.rs
fn main() {
    let s = String::from("hello");  // s comes into scope

    takes_ownership(s);             // s's value moves into the function...
                                    // ... and so is no longer valid here

    let x = 5;                      // x comes into scope

    makes_copy(x);                  // Because i32 implements the Copy trait,
                                    // x does NOT move into the function,
                                    // so it's okay to use x afterward.

} // Here, x goes out of scope, then s. However, because s's value was moved,
  // nothing special happens.

fn takes_ownership(some_string: String) { // some_string comes into scope
    println!("{some_string}");
} // Here, some_string goes out of scope and `drop` is called. The backing
  // memory is freed.

fn makes_copy(some_integer: i32) { // some_integer comes into scope
    println!("{some_integer}");
} // Here, some_integer goes out of scope. Nothing special happens.
Listing 4-3: Функции с аннотациями владения и области видимости

Если бы мы попытались использовать s после вызова takes_ownership, Rust выдал бы ошибку времени компиляции. Эти статические проверки защищают нас от ошибок. Попробуйте добавить в main код, использующий s и x, чтобы увидеть, где их можно использовать, а где правила владения не позволят это сделать.

Возвращаемые значения и область видимости

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

Filename: src/main.rs
fn main() {
    let s1 = gives_ownership();        // gives_ownership moves its return
                                       // value into s1

    let s2 = String::from("hello");    // s2 comes into scope

    let s3 = takes_and_gives_back(s2); // s2 is moved into
                                       // takes_and_gives_back, which also
                                       // moves its return value into s3
} // Here, s3 goes out of scope and is dropped. s2 was moved, so nothing
  // happens. s1 goes out of scope and is dropped.

fn gives_ownership() -> String {       // gives_ownership will move its
                                       // return value into the function
                                       // that calls it

    let some_string = String::from("yours"); // some_string comes into scope

    some_string                        // some_string is returned and
                                       // moves out to the calling
                                       // function
}

// This function takes a String and returns a String.
fn takes_and_gives_back(a_string: String) -> String {
    // a_string comes into
    // scope

    a_string  // a_string is returned and moves out to the calling function
}
Listing 4-4: Передача владения возвращаемыми значениями

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

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

Rust позволяет возвращать несколько значений с помощью кортежа, как показано в листинге 4-5.

Filename: src/main.rs
fn main() {
    let s1 = String::from("hello");

    let (s2, len) = calculate_length(s1);

    println!("The length of '{s2}' is {len}.");
}

fn calculate_length(s: String) -> (String, usize) {
    let length = s.len(); // len() returns the length of a String

    (s, length)
}
Listing 4-5: Возврат владения параметрами

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

Ссылки и заимствование

Ссылки и заимствование

Проблема кода с кортежем в листинге 4-5 состоит в том, что нам приходится возвращать String вызывающей функции, чтобы мы всё ещё могли использовать String после вызова calculate_length, потому что значение String было перемещено в calculate_length. Вместо этого мы можем предоставить ссылку на значение String. Ссылка похожа на указатель тем, что это адрес, по которому можно перейти для доступа к данным, хранящимся по этому адресу; этими данными владеет какая-то другая переменная. В отличие от указателя, ссылка гарантированно указывает на действительное значение определённого типа на протяжении всей жизни этой ссылки.

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

Filename: src/main.rs
fn main() {
    let s1 = String::from("hello");

    let len = calculate_length(&s1);

    println!("The length of '{s1}' is {len}.");
}

fn calculate_length(s: &String) -> usize {
    s.len()
}

Во-первых, обратите внимание, что весь код с кортежем в объявлении переменной и возвращаемом значении функции исчез. Во-вторых, заметьте, что мы передаём &s1 в calculate_length, а в её определении принимаем &String, а не String. Эти амперсанды обозначают ссылки и позволяют ссылаться на некоторое значение, не забирая владение им. Рисунок 4-6 иллюстрирует эту концепцию.

Три таблицы: таблица для s содержит только указатель на таблицу
для s1. Таблица для s1 содержит данные s1 в стеке и указывает на строковые
данные в куче.

Рисунок 4-6: Схема, на которой &String s указывает на String s1

Примечание: операция, противоположная созданию ссылки с помощью &, — разыменование, которое выполняется оператором разыменования *. Мы увидим несколько способов использования оператора разыменования в Главе 8 и подробно обсудим разыменование в Главе 15.

Рассмотрим этот вызов функции подробнее:

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

    let len = calculate_length(&s1);

    println!("The length of '{s1}' is {len}.");
}

fn calculate_length(s: &String) -> usize {
    s.len()
}

Синтаксис &s1 позволяет создать ссылку, которая ссылается на значение s1, но не владеет им. Поскольку ссылка им не владеет, значение, на которое она указывает, не будет удалено, когда ссылка перестанет использоваться.

Точно так же сигнатура функции использует &, чтобы указать, что тип параметра s — ссылка. Добавим несколько поясняющих аннотаций:

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

    let len = calculate_length(&s1);

    println!("The length of '{s1}' is {len}.");
}

fn calculate_length(s: &String) -> usize { // s is a reference to a String
    s.len()
} // Here, s goes out of scope. But because s does not have ownership of what
  // it refers to, the String is not dropped.

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

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

Что же произойдёт, если мы попытаемся изменить то, что заимствуем? Попробуйте код из листинга 4-6. Спойлер: это не работает!

Filename: src/main.rs
fn main() {
    let s = String::from("hello");

    change(&s);
}

fn change(some_string: &String) {
    some_string.push_str(", world");
}
Listing 4-6: Попытка изменить заимствованное значение

Вот ошибка:

$ cargo run
   Compiling ownership v0.1.0 (file:///projects/ownership)
error[E0596]: cannot borrow `*some_string` as mutable, as it is behind a `&` reference
 --> src/main.rs:8:5
  |
8 |     some_string.push_str(", world");
  |     ^^^^^^^^^^^ `some_string` is a `&` reference, so the data it refers to cannot be borrowed as mutable
  |
help: consider changing this to be a mutable reference
  |
7 | fn change(some_string: &mut String) {
  |                         +++

For more information about this error, try `rustc --explain E0596`.
error: could not compile `ownership` (bin "ownership") due to 1 previous error

Как переменные по умолчанию неизменяемы, так и ссылки по умолчанию неизменяемы. Нам не разрешено изменять то, на что у нас есть ссылка.

Изменяемые ссылки

Мы можем исправить код из листинга 4-6, чтобы разрешить изменять заимствованное значение, внеся всего несколько небольших правок и используя изменяемую ссылку:

Filename: src/main.rs
fn main() {
    let mut s = String::from("hello");

    change(&mut s);
}

fn change(some_string: &mut String) {
    some_string.push_str(", world");
}

Сначала мы меняем s, делая её mut. Затем создаём изменяемую ссылку с помощью &mut s в месте вызова функции change и обновляем сигнатуру функции, чтобы она принимала изменяемую ссылку some_string: &mut String. Это очень ясно показывает, что функция change будет изменять значение, которое она заимствует.

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

Filename: src/main.rs
fn main() {
    let mut s = String::from("hello");

    let r1 = &mut s;
    let r2 = &mut s;

    println!("{r1}, {r2}");
}

Вот ошибка:

$ cargo run
   Compiling ownership v0.1.0 (file:///projects/ownership)
error[E0499]: cannot borrow `s` as mutable more than once at a time
 --> src/main.rs:5:14
  |
4 |     let r1 = &mut s;
  |              ------ first mutable borrow occurs here
5 |     let r2 = &mut s;
  |              ^^^^^^ second mutable borrow occurs here
6 |
7 |     println!("{r1}, {r2}");
  |                -- first borrow later used here

For more information about this error, try `rustc --explain E0499`.
error: could not compile `ownership` (bin "ownership") due to 1 previous error

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

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

  • Два или более указателя одновременно обращаются к одним и тем же данным.
  • Хотя бы один из указателей используется для записи в данные.
  • Не используется механизм для синхронизации доступа к данным.

Гонки данных вызывают неопределённое поведение, и их бывает трудно диагностировать и исправлять, когда вы пытаетесь отследить их во время выполнения; Rust предотвращает эту проблему, отказываясь компилировать код с гонками данных!

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

fn main() {
    let mut s = String::from("hello");

    {
        let r1 = &mut s;
    } // r1 goes out of scope here, so we can make a new reference with no problems.

    let r2 = &mut s;
}

Rust применяет похожее правило для сочетания изменяемых и неизменяемых ссылок. Этот код приводит к ошибке:

fn main() {
    let mut s = String::from("hello");

    let r1 = &s; // no problem
    let r2 = &s; // no problem
    let r3 = &mut s; // BIG PROBLEM

    println!("{r1}, {r2}, and {r3}");
}

Вот ошибка:

$ cargo run
   Compiling ownership v0.1.0 (file:///projects/ownership)
error[E0502]: cannot borrow `s` as mutable because it is also borrowed as immutable
 --> src/main.rs:6:14
  |
4 |     let r1 = &s; // no problem
  |              -- immutable borrow occurs here
5 |     let r2 = &s; // no problem
6 |     let r3 = &mut s; // BIG PROBLEM
  |              ^^^^^^ mutable borrow occurs here
7 |
8 |     println!("{r1}, {r2}, and {r3}");
  |                -- immutable borrow later used here

For more information about this error, try `rustc --explain E0502`.
error: could not compile `ownership` (bin "ownership") due to 1 previous error

Уф! Мы также не можем иметь изменяемую ссылку, пока у нас есть неизменяемая ссылка на то же значение.

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

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

fn main() {
    let mut s = String::from("hello");

    let r1 = &s; // no problem
    let r2 = &s; // no problem
    println!("{r1} and {r2}");
    // Variables r1 and r2 will not be used after this point.

    let r3 = &mut s; // no problem
    println!("{r3}");
}

Области действия неизменяемых ссылок r1 и r2 заканчиваются после println!, где они используются в последний раз, то есть до создания изменяемой ссылки r3. Эти области не пересекаются, поэтому такой код разрешён: компилятор может определить, что ссылка больше не используется в точке до конца области видимости.

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

Висячие ссылки

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

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

Filename: src/main.rs
fn main() {
    let reference_to_nothing = dangle();
}

fn dangle() -> &String {
    let s = String::from("hello");

    &s
}

Вот ошибка:

$ cargo run
   Compiling ownership v0.1.0 (file:///projects/ownership)
error[E0106]: missing lifetime specifier
 --> src/main.rs:5:16
  |
5 | fn dangle() -> &String {
  |                ^ expected named lifetime parameter
  |
  = help: this function's return type contains a borrowed value, but there is no value for it to be borrowed from
help: consider using the `'static` lifetime, but this is uncommon unless you're returning a borrowed value from a `const` or a `static`
  |
5 | fn dangle() -> &'static String {
  |                 +++++++
help: instead, you are more likely to want to return an owned value
  |
5 - fn dangle() -> &String {
5 + fn dangle() -> String {
  |

For more information about this error, try `rustc --explain E0106`.
error: could not compile `ownership` (bin "ownership") due to 1 previous error

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

this function's return type contains a borrowed value, but there is no value
for it to be borrowed from

Давайте подробнее рассмотрим, что именно происходит на каждом этапе нашего кода dangle:

Filename: src/main.rs
fn main() {
    let reference_to_nothing = dangle();
}

fn dangle() -> &String { // dangle returns a reference to a String

    let s = String::from("hello"); // s is a new String

    &s // we return a reference to the String, s
} // Here, s goes out of scope and is dropped, so its memory goes away.
  // Danger!

Поскольку s создаётся внутри dangle, после завершения кода dangle память, связанная с s, будет освобождена. Но мы попытались вернуть ссылку на неё. Это означает, что такая ссылка указывала бы на недействительный String. Это плохо! Rust не позволит нам сделать это.

Решение здесь — вернуть String напрямую:

fn main() {
    let string = no_dangle();
}

fn no_dangle() -> String {
    let s = String::from("hello");

    s
}

Это работает без проблем. Владение перемещается из функции наружу, и ничего не освобождается.

Правила ссылок

Подведём итог тому, что мы обсудили о ссылках:

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

Далее мы рассмотрим другой вид ссылок: срезы.

Тип срезов

Тип срезов

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

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

Примечание: для знакомства со срезами в этом разделе мы предполагаем только ASCII; более подробное обсуждение обработки UTF-8 находится в разделе «Хранение текста в кодировке UTF-8 с помощью строк» Главы 8.

Чтобы понять проблему, которую решают срезы, разберём, как мы написали бы сигнатуру этой функции без использования срезов:

fn first_word(s: &String) -> ?

Функция first_word имеет параметр типа &String. Владение нам не нужно, поэтому это нормально. (В идиоматичном Rust функции не забирают владение своими аргументами, если им это не требуется, и причины этого будут становиться понятнее по мере продвижения.) Но что нам вернуть? У нас на самом деле нет способа говорить о части строки. Однако мы могли бы вернуть индекс конца слова, на который указывает пробел. Попробуем сделать это, как показано в листинге 4-7.

Filename: src/main.rs
fn first_word(s: &String) -> usize {
    let bytes = s.as_bytes();

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

    s.len()
}

fn main() {}
Listing 4-7: Функция first_word, которая возвращает значение байтового индекса внутри параметра String

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

fn first_word(s: &String) -> usize {
    let bytes = s.as_bytes();

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

    s.len()
}

fn main() {}

Затем мы создаём итератор по массиву байтов с помощью метода iter:

fn first_word(s: &String) -> usize {
    let bytes = s.as_bytes();

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

    s.len()
}

fn main() {}

Мы подробнее обсудим итераторы в Главе 13. Пока достаточно знать, что iter — это метод, возвращающий каждый элемент коллекции, а enumerate оборачивает результат iter и вместо этого возвращает каждый элемент как часть кортежа. Первый элемент кортежа, возвращаемого enumerate, — это индекс, а второй — ссылка на элемент. Это немного удобнее, чем вычислять индекс самостоятельно.

Поскольку метод enumerate возвращает кортеж, мы можем использовать шаблоны, чтобы деструктурировать этот кортеж. Подробнее о шаблонах мы поговорим в Главе 6. В цикле for мы указываем шаблон, где i — это индекс в кортеже, а &item — один байт в кортеже. Поскольку из .iter().enumerate() мы получаем ссылку на элемент, в шаблоне используется &.

Внутри цикла for мы ищем байт, представляющий пробел, используя синтаксис байтового литерала. Если мы находим пробел, возвращаем его позицию. Иначе возвращаем длину строки с помощью s.len().

fn first_word(s: &String) -> usize {
    let bytes = s.as_bytes();

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

    s.len()
}

fn main() {}

Теперь у нас есть способ узнать индекс конца первого слова в строке, но есть проблема. Мы возвращаем отдельный usize, но это число имеет смысл только в контексте &String. Другими словами, поскольку это значение отделено от String, нет гарантии, что оно останется действительным в будущем. Рассмотрим программу в листинге 4-8, которая использует функцию first_word из листинга 4-7.

Filename: src/main.rs
fn first_word(s: &String) -> usize {
    let bytes = s.as_bytes();

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

    s.len()
}

fn main() {
    let mut s = String::from("hello world");

    let word = first_word(&s); // word will get the value 5

    s.clear(); // this empties the String, making it equal to ""

    // word still has the value 5 here, but s no longer has any content that we
    // could meaningfully use with the value 5, so word is now totally invalid!
}
Listing 4-8: Сохранение результата вызова функции first_word и последующее изменение содержимого String

Эта программа компилируется без ошибок и компилировалась бы и в том случае, если бы мы использовали word после вызова s.clear(). Поскольку word никак не связан с состоянием s, word всё ещё содержит значение 5. Мы могли бы использовать это значение 5 с переменной s, чтобы попытаться извлечь первое слово, но это было бы ошибкой, потому что содержимое s изменилось после того, как мы сохранили 5 в word.

Необходимость следить за тем, чтобы индекс в word не рассинхронизировался с данными в s, утомительна и подвержена ошибкам! Управление такими индексами становится ещё более хрупким, если мы напишем функцию second_word. Её сигнатура должна была бы выглядеть так:

fn second_word(s: &String) -> (usize, usize) {

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

К счастью, у Rust есть решение этой проблемы: строковые срезы.

Строковые срезы

Строковый срез — это ссылка на непрерывную последовательность элементов String, и выглядит он так:

fn main() {
    let s = String::from("hello world");

    let hello = &s[0..5];
    let world = &s[6..11];
}

Вместо ссылки на весь String, hello является ссылкой на часть String, указанную дополнительным фрагментом [0..5]. Мы создаём срезы с помощью диапазона внутри квадратных скобок, указывая [starting_index..ending_index], где starting_index — первая позиция в срезе, а ending_index — позиция на единицу больше последней позиции в срезе. Внутри структура данных среза хранит начальную позицию и длину среза, которая соответствует ending_index минус starting_index. Поэтому в случае let world = &s[6..11]; world будет срезом, содержащим указатель на байт с индексом 6 в s и значение длины 5.

Рисунок 4-7 показывает это на схеме.

Три таблицы: таблица, представляющая данные s в стеке, указывает
на байт с индексом 0 в таблице строковых данных "hello world" в куче.
Третья таблица представляет данные среза world в стеке, имеет значение длины 5
и указывает на байт 6 таблицы данных в куче.

Рисунок 4-7: Строковый срез, ссылающийся на часть String

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

#![allow(unused)]
fn main() {
let s = String::from("hello");

let slice = &s[0..2];
let slice = &s[..2];
}

По той же причине, если ваш срез включает последний байт String, можно опустить конечное число. Это означает, что эти варианты эквивалентны:

#![allow(unused)]
fn main() {
let s = String::from("hello");

let len = s.len();

let slice = &s[3..len];
let slice = &s[3..];
}

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

#![allow(unused)]
fn main() {
let s = String::from("hello");

let len = s.len();

let slice = &s[0..len];
let slice = &s[..];
}

Примечание: индексы диапазона строкового среза должны попадать на допустимые границы символов UTF-8. Если вы попытаетесь создать строковый срез в середине многобайтового символа, ваша программа завершится с ошибкой.

Учитывая всё это, перепишем first_word, чтобы она возвращала срез. Тип, обозначающий «строковый срез», записывается как &str:

Filename: src/main.rs
fn first_word(s: &String) -> &str {
    let bytes = s.as_bytes();

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

    &s[..]
}

fn main() {}

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

Теперь при вызове first_word мы получаем одно значение, связанное с базовыми данными. Это значение состоит из ссылки на начальную точку среза и количества элементов в срезе.

Возврат среза также подошёл бы для функции second_word:

fn second_word(s: &String) -> &str {

Теперь у нас есть простой API, в котором гораздо труднее ошибиться, потому что компилятор проследит, чтобы ссылки внутрь String оставались действительными. Помните ошибку в программе из листинга 4-8, когда мы получили индекс конца первого слова, а затем очистили строку, так что наш индекс стал недействительным? Тот код был логически неверным, но не показывал немедленных ошибок. Проблемы проявились бы позже, если бы мы продолжили использовать индекс первого слова с очищенной строкой. Срезы делают такую ошибку невозможной и гораздо раньше сообщают нам, что в коде есть проблема. Использование версии first_word со срезом вызовет ошибку времени компиляции:

Filename: src/main.rs
fn first_word(s: &String) -> &str {
    let bytes = s.as_bytes();

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

    &s[..]
}

fn main() {
    let mut s = String::from("hello world");

    let word = first_word(&s);

    s.clear(); // error!

    println!("the first word is: {word}");
}

Вот ошибка компилятора:

$ cargo run
   Compiling ownership v0.1.0 (file:///projects/ownership)
error[E0502]: cannot borrow `s` as mutable because it is also borrowed as immutable
  --> src/main.rs:18:5
   |
16 |     let word = first_word(&s);
   |                           -- immutable borrow occurs here
17 |
18 |     s.clear(); // error!
   |     ^^^^^^^^^ mutable borrow occurs here
19 |
20 |     println!("the first word is: {word}");
   |                                   ---- immutable borrow later used here

For more information about this error, try `rustc --explain E0502`.
error: could not compile `ownership` (bin "ownership") due to 1 previous error

Вспомните из правил заимствования: если у нас есть неизменяемая ссылка на что-то, мы не можем одновременно взять изменяемую ссылку. Поскольку clear нужно усечь String, ей нужна изменяемая ссылка. println! после вызова clear использует ссылку в word, поэтому неизменяемая ссылка должна всё ещё быть активной в этот момент. Rust запрещает изменяемой ссылке в clear и неизменяемой ссылке в word существовать одновременно, и компиляция завершается ошибкой. Rust не только сделал наш API проще в использовании, но и устранил целый класс ошибок во время компиляции!

Строковые литералы как срезы

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

#![allow(unused)]
fn main() {
let s = "Hello, world!";
}

Тип s здесь — &str: это срез, указывающий на конкретное место бинарного файла. Именно поэтому строковые литералы неизменяемы; &str — неизменяемая ссылка.

Строковые срезы как параметры

Знание того, что можно брать срезы литералов и значений String, приводит нас к ещё одному улучшению first_word: её сигнатуре.

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

Более опытный разработчик Rust написал бы сигнатуру, показанную в листинге 4-9, потому что она позволяет использовать одну и ту же функцию и со значениями &String, и со значениями &str.

fn first_word(s: &str) -> &str {
    let bytes = s.as_bytes();

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

    &s[..]
}

fn main() {
    let my_string = String::from("hello world");

    // `first_word` works on slices of `String`s, whether partial or whole.
    let word = first_word(&my_string[0..6]);
    let word = first_word(&my_string[..]);
    // `first_word` also works on references to `String`s, which are equivalent
    // to whole slices of `String`s.
    let word = first_word(&my_string);

    let my_string_literal = "hello world";

    // `first_word` works on slices of string literals, whether partial or
    // whole.
    let word = first_word(&my_string_literal[0..6]);
    let word = first_word(&my_string_literal[..]);

    // Because string literals *are* string slices already,
    // this works too, without the slice syntax!
    let word = first_word(my_string_literal);
}
Listing 4-9: Улучшение функции first_word с помощью строкового среза для типа параметра s

Если у нас есть строковый срез, мы можем передать его напрямую. Если у нас есть String, мы можем передать срез String или ссылку на String. Эта гибкость использует преимущества deref-приведений — возможности, которую мы рассмотрим в разделе «Использование deref-приведений в функциях и методах» Главы 15.

Определение функции, принимающей строковый срез вместо ссылки на String, делает наш API более общим и полезным без потери какой-либо функциональности:

Filename: src/main.rs
fn first_word(s: &str) -> &str {
    let bytes = s.as_bytes();

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

    &s[..]
}

fn main() {
    let my_string = String::from("hello world");

    // `first_word` works on slices of `String`s, whether partial or whole.
    let word = first_word(&my_string[0..6]);
    let word = first_word(&my_string[..]);
    // `first_word` also works on references to `String`s, which are equivalent
    // to whole slices of `String`s.
    let word = first_word(&my_string);

    let my_string_literal = "hello world";

    // `first_word` works on slices of string literals, whether partial or
    // whole.
    let word = first_word(&my_string_literal[0..6]);
    let word = first_word(&my_string_literal[..]);

    // Because string literals *are* string slices already,
    // this works too, without the slice syntax!
    let word = first_word(my_string_literal);
}

Другие срезы

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

#![allow(unused)]
fn main() {
let a = [1, 2, 3, 4, 5];
}

Так же как нам может понадобиться сослаться на часть строки, нам может понадобиться сослаться на часть массива. Мы сделали бы это так:

#![allow(unused)]
fn main() {
let a = [1, 2, 3, 4, 5];

let slice = &a[1..3];

assert_eq!(slice, &[2, 3]);
}

Этот срез имеет тип &[i32]. Он работает так же, как строковые срезы: хранит ссылку на первый элемент и длину. Вы будете использовать такой вид срезов для самых разных коллекций. Мы подробно обсудим эти коллекции, когда будем говорить о векторах в Главе 8.

Итоги

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

Владение влияет на работу многих других частей Rust, поэтому мы ещё будем говорить об этих концепциях на протяжении остальной книги. Перейдём к Главе 5 и посмотрим, как группировать фрагменты данных вместе в struct.

Использование структур для организации связанных данных

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

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

Определение и создание экземпляров структур

Определение и создание экземпляров структур

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

Чтобы определить структуру, мы вводим ключевое слово struct и задаём имя всей структуры. Имя структуры должно описывать смысл частей данных, сгруппированных вместе. Затем внутри фигурных скобок мы определяем имена и типы частей данных, которые называем полями. Например, листинг 5-1 показывает структуру, которая хранит информацию об учётной записи пользователя.

Filename: src/main.rs
struct User {
    active: bool,
    username: String,
    email: String,
    sign_in_count: u64,
}

fn main() {}
Listing 5-1: Определение структуры User

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

Filename: src/main.rs
struct User {
    active: bool,
    username: String,
    email: String,
    sign_in_count: u64,
}

fn main() {
    let user1 = User {
        active: true,
        username: String::from("someusername123"),
        email: String::from("someone@example.com"),
        sign_in_count: 1,
    };
}
Listing 5-2: Создание экземпляра структуры User

Чтобы получить конкретное значение из структуры, мы используем точечную нотацию. Например, чтобы обратиться к адресу электронной почты этого пользователя, мы используем user1.email. Если экземпляр изменяемый, мы можем изменить значение с помощью точечной нотации и присваивания в конкретное поле. Листинг 5-3 показывает, как изменить значение поля email в изменяемом экземпляре User.

Filename: src/main.rs
struct User {
    active: bool,
    username: String,
    email: String,
    sign_in_count: u64,
}

fn main() {
    let mut user1 = User {
        active: true,
        username: String::from("someusername123"),
        email: String::from("someone@example.com"),
        sign_in_count: 1,
    };

    user1.email = String::from("anotheremail@example.com");
}
Listing 5-3: Изменение значения поля email экземпляра User

Обратите внимание, что изменяемым должен быть весь экземпляр; Rust не позволяет помечать изменяемыми только отдельные поля. Как и с любым выражением, мы можем создать новый экземпляр структуры как последнее выражение в теле функции, чтобы неявно вернуть этот новый экземпляр.

Листинг 5-4 показывает функцию build_user, которая возвращает экземпляр User с заданными адресом электронной почты и именем пользователя. Поле active получает значение true, а sign_in_count получает значение 1.

Filename: src/main.rs
struct User {
    active: bool,
    username: String,
    email: String,
    sign_in_count: u64,
}

fn build_user(email: String, username: String) -> User {
    User {
        active: true,
        username: username,
        email: email,
        sign_in_count: 1,
    }
}

fn main() {
    let user1 = build_user(
        String::from("someone@example.com"),
        String::from("someusername123"),
    );
}
Listing 5-4: Функция build_user, которая принимает адрес электронной почты и имя пользователя и возвращает экземпляр User

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

Использование сокращённой инициализации полей

Поскольку имена параметров и имена полей структуры в листинге 5-4 полностью совпадают, мы можем использовать синтаксис сокращённой инициализации полей, чтобы переписать build_user так, чтобы она вела себя точно так же, но без повторения username и email, как показано в листинге 5-5.

Filename: src/main.rs
struct User {
    active: bool,
    username: String,
    email: String,
    sign_in_count: u64,
}

fn build_user(email: String, username: String) -> User {
    User {
        active: true,
        username,
        email,
        sign_in_count: 1,
    }
}

fn main() {
    let user1 = build_user(
        String::from("someone@example.com"),
        String::from("someusername123"),
    );
}
Listing 5-5: Функция build_user, использующая сокращённую инициализацию полей, потому что параметры username и email имеют те же имена, что и поля структуры

Здесь мы создаём новый экземпляр структуры User, у которой есть поле с именем email. Мы хотим установить значение поля email равным значению параметра email функции build_user. Поскольку поле email и параметр email имеют одно и то же имя, нам нужно написать только email, а не email: email.

Создание экземпляров с помощью синтаксиса обновления структуры

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

Сначала в листинге 5-6 мы покажем, как создать новый экземпляр User в user2 обычным способом, без синтаксиса обновления. Мы задаём новое значение для email, но в остальном используем те же значения из user1, который создали в листинге 5-2.

Filename: src/main.rs
struct User {
    active: bool,
    username: String,
    email: String,
    sign_in_count: u64,
}

fn main() {
    // --snip--

    let user1 = User {
        email: String::from("someone@example.com"),
        username: String::from("someusername123"),
        active: true,
        sign_in_count: 1,
    };

    let user2 = User {
        active: user1.active,
        username: user1.username,
        email: String::from("another@example.com"),
        sign_in_count: user1.sign_in_count,
    };
}
Listing 5-6: Создание нового экземпляра User с использованием всех значений из user1, кроме одного

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

Filename: src/main.rs
struct User {
    active: bool,
    username: String,
    email: String,
    sign_in_count: u64,
}

fn main() {
    // --snip--

    let user1 = User {
        email: String::from("someone@example.com"),
        username: String::from("someusername123"),
        active: true,
        sign_in_count: 1,
    };

    let user2 = User {
        email: String::from("another@example.com"),
        ..user1
    };
}
Listing 5-7: Использование синтаксиса обновления структуры, чтобы задать новое значение email для экземпляра User, но использовать остальные значения из user1

Код в листинге 5-7 также создаёт экземпляр в user2, у которого другое значение email, но те же значения полей username, active и sign_in_count, что и у user1. Запись ..user1 должна идти последней, чтобы указать, что любые оставшиеся поля должны получить значения из соответствующих полей user1, но мы можем указать значения для любого числа полей в любом порядке, независимо от порядка полей в определении структуры.

Обратите внимание, что синтаксис обновления структуры использует =, как присваивание; это потому, что он перемещает данные, как мы видели в разделе «Взаимодействие переменных и данных при перемещении». В этом примере мы больше не можем использовать user1 после создания user2, потому что String в поле username из user1 был перемещён в user2. Если бы мы задали для user2 новые значения String и для email, и для username, а значит, использовали бы из user1 только значения active и sign_in_count, тогда user1 оставался бы действительным после создания user2. И active, и sign_in_count имеют типы, реализующие трейт Copy, поэтому применялось бы поведение, которое мы обсуждали в разделе «Данные только в стеке: Copy». В этом примере мы также всё ещё можем использовать user1.email, потому что его значение не было перемещено из user1.

Создание разных типов с помощью кортежных структур

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

Чтобы определить кортежную структуру, начните с ключевого слова struct и имени структуры, после которого идут типы в кортеже. Например, здесь мы определяем и используем две кортежные структуры с именами Color и Point:

Filename: src/main.rs
struct Color(i32, i32, i32);
struct Point(i32, i32, i32);

fn main() {
    let black = Color(0, 0, 0);
    let origin = Point(0, 0, 0);
}

Обратите внимание, что значения black и origin имеют разные типы, потому что они являются экземплярами разных кортежных структур. Каждая структура, которую вы определяете, является собственным типом, даже если поля внутри структуры могут иметь одинаковые типы. Например, функция, принимающая параметр типа Color, не может принять Point в качестве аргумента, даже если оба типа состоят из трёх значений i32. В остальном экземпляры кортежных структур похожи на кортежи: их можно деструктурировать на отдельные части, а для доступа к отдельному значению можно использовать . с последующим индексом. В отличие от кортежей, кортежные структуры требуют указывать имя типа структуры при деструктурировании. Например, мы написали бы let Point(x, y, z) = origin;, чтобы деструктурировать значения точки origin в переменные с именами x, y и z.

Определение unit-like структур

Вы также можете определять структуры, у которых вообще нет полей! Они называются unit-like структурами, потому что ведут себя похоже на () — unit-тип, о котором мы упоминали в разделе «Тип кортежа». Unit-like структуры могут быть полезны, когда нужно реализовать трейт для некоторого типа, но у вас нет данных, которые вы хотите хранить в самом типе. Мы обсудим трейты в Главе 10. Вот пример объявления и создания экземпляра unit-структуры с именем AlwaysEqual:

Filename: src/main.rs
struct AlwaysEqual;

fn main() {
    let subject = AlwaysEqual;
}

Чтобы определить AlwaysEqual, мы используем ключевое слово struct, нужное нам имя, а затем точку с запятой. Фигурные скобки или круглые скобки не нужны! Затем похожим образом мы можем получить экземпляр AlwaysEqual в переменной subject: используя определённое нами имя без фигурных или круглых скобок. Представьте, что позже мы реализуем поведение для этого типа так, что каждый экземпляр AlwaysEqual всегда равен каждому экземпляру любого другого типа, возможно, чтобы иметь известный результат для целей тестирования. Для реализации такого поведения нам не понадобились бы никакие данные! В Главе 10 вы увидите, как определять трейты и реализовывать их для любого типа, включая unit-like структуры.

Владение данными структуры

В определении структуры User в листинге 5-1 мы использовали владеющий тип String, а не тип строкового среза &str. Это осознанный выбор, потому что мы хотим, чтобы каждый экземпляр этой структуры владел всеми своими данными, и чтобы эти данные были действительны настолько же долго, насколько действительна вся структура.

Структуры также могут хранить ссылки на данные, принадлежащие чему-то ещё, но для этого нужно использовать времена жизни — возможность Rust, которую мы обсудим в Главе 10. Времена жизни гарантируют, что данные, на которые ссылается структура, действительны настолько же долго, насколько действительна сама структура. Допустим, вы пытаетесь сохранить ссылку в структуре без указания времён жизни, как в следующем примере в src/main.rs; это не сработает:

Filename: src/main.rs
struct User {
    active: bool,
    username: &str,
    email: &str,
    sign_in_count: u64,
}

fn main() {
    let user1 = User {
        active: true,
        username: "someusername123",
        email: "someone@example.com",
        sign_in_count: 1,
    };
}

Компилятор пожалуется, что ему нужны спецификаторы времён жизни:

$ cargo run
   Compiling structs v0.1.0 (file:///projects/structs)
error[E0106]: missing lifetime specifier
 --> src/main.rs:3:15
  |
3 |     username: &str,
  |               ^ expected named lifetime parameter
  |
help: consider introducing a named lifetime parameter
  |
1 ~ struct User<'a> {
2 |     active: bool,
3 ~     username: &'a str,
  |

error[E0106]: missing lifetime specifier
 --> src/main.rs:4:12
  |
4 |     email: &str,
  |            ^ expected named lifetime parameter
  |
help: consider introducing a named lifetime parameter
  |
1 ~ struct User<'a> {
2 |     active: bool,
3 |     username: &str,
4 ~     email: &'a str,
  |

For more information about this error, try `rustc --explain E0106`.
error: could not compile `structs` (bin "structs") due to 2 previous errors

В Главе 10 мы обсудим, как исправлять такие ошибки, чтобы вы могли хранить ссылки в структурах, а пока будем исправлять подобные ошибки с помощью владеющих типов вроде String вместо ссылок вроде &str.

Пример программы с использованием структур

Пример программы с использованием структур

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

Создадим новый бинарный проект с помощью Cargo под названием rectangles, который будет принимать ширину и высоту прямоугольника, заданные в пикселях, и вычислять его площадь. Листинг 5-8 показывает короткую программу с одним из способов сделать именно это в файле src/main.rs нашего проекта.

Filename: src/main.rs
fn main() {
    let width1 = 30;
    let height1 = 50;

    println!(
        "The area of the rectangle is {} square pixels.",
        area(width1, height1)
    );
}

fn area(width: u32, height: u32) -> u32 {
    width * height
}
Listing 5-8: Вычисление площади прямоугольника, заданного отдельными переменными ширины и высоты

Теперь запустите эту программу с помощью cargo run:

$ cargo run
   Compiling rectangles v0.1.0 (file:///projects/rectangles)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.42s
     Running `target/debug/rectangles`
The area of the rectangle is 1500 square pixels.

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

Проблема этого кода очевидна в сигнатуре area:

fn main() {
    let width1 = 30;
    let height1 = 50;

    println!(
        "The area of the rectangle is {} square pixels.",
        area(width1, height1)
    );
}

fn area(width: u32, height: u32) -> u32 {
    width * height
}

Функция area должна вычислять площадь одного прямоугольника, но написанная нами функция имеет два параметра, и нигде в нашей программе не видно, что эти параметры связаны между собой. Было бы читаемее и удобнее сопровождать код, если бы мы сгруппировали ширину и высоту вместе. Мы уже обсуждали один способ сделать это в разделе «Кортежный тип» главы 3: использовать кортежи.

Рефакторинг с кортежами

Листинг 5-9 показывает другую версию нашей программы, в которой используются кортежи.

Filename: src/main.rs
fn main() {
    let rect1 = (30, 50);

    println!(
        "The area of the rectangle is {} square pixels.",
        area(rect1)
    );
}

fn area(dimensions: (u32, u32)) -> u32 {
    dimensions.0 * dimensions.1
}
Listing 5-9: Указание ширины и высоты прямоугольника с помощью кортежа

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

Если перепутать ширину и высоту, для вычисления площади это не имело бы значения, но если мы захотим нарисовать прямоугольник на экране, значение уже будет! Нам пришлось бы помнить, что width находится в кортеже по индексу 0, а height – по индексу 1. Другому человеку, который будет использовать наш код, было бы еще сложнее разобраться в этом и удерживать это в памяти. Поскольку мы не выразили смысл наших данных в коде, теперь ошибки внести проще.

Рефакторинг со структурами

Мы используем структуры, чтобы добавить смысл, присваивая данным метки. Мы можем превратить используемый кортеж в структуру с именем для целого значения и именами для его частей, как показано в листинге 5-10.

Filename: src/main.rs
struct Rectangle {
    width: u32,
    height: u32,
}

fn main() {
    let rect1 = Rectangle {
        width: 30,
        height: 50,
    };

    println!(
        "The area of the rectangle is {} square pixels.",
        area(&rect1)
    );
}

fn area(rectangle: &Rectangle) -> u32 {
    rectangle.width * rectangle.height
}
Listing 5-10: Определение структуры Rectangle

Здесь мы определили структуру и назвали ее Rectangle. Внутри фигурных скобок мы определили поля width и height, оба типа u32. Затем в main мы создали конкретный экземпляр Rectangle, у которого ширина равна 30, а высота равна 50.

Теперь наша функция area определена с одним параметром, который мы назвали rectangle; его тип – неизменяемое заимствование экземпляра структуры Rectangle. Как упоминалось в главе 4, мы хотим заимствовать структуру, а не получать владение ею. Так main сохраняет владение и может продолжать использовать rect1; именно поэтому мы используем & в сигнатуре функции и в месте вызова функции.

Функция area обращается к полям width и height экземпляра Rectangle (обратите внимание: доступ к полям заимствованного экземпляра структуры не перемещает значения полей, поэтому вы часто видите заимствования структур). Теперь сигнатура функции area говорит ровно то, что мы имеем в виду: вычислить площадь Rectangle, используя его поля width и height. Это показывает, что ширина и высота связаны друг с другом, и дает значениям описательные имена вместо использования индексов кортежа 0 и 1. Это победа для ясности.

Добавление функциональности с помощью производных трейтов

Было бы полезно иметь возможность выводить экземпляр Rectangle во время отладки программы и видеть значения всех его полей. В листинге 5-11 мы пытаемся использовать макрос println! так же, как использовали его в предыдущих главах. Однако это не сработает.

Filename: src/main.rs
struct Rectangle {
    width: u32,
    height: u32,
}

fn main() {
    let rect1 = Rectangle {
        width: 30,
        height: 50,
    };

    println!("rect1 is {rect1}");
}
Listing 5-11: Попытка вывести экземпляр Rectangle

Когда мы компилируем этот код, получаем ошибку с таким основным сообщением:

error[E0277]: `Rectangle` doesn't implement `std::fmt::Display`

Макрос println! умеет выполнять много видов форматирования, и по умолчанию фигурные скобки говорят println! использовать форматирование, известное как Display: вывод, предназначенный для непосредственного потребления конечным пользователем. Примитивные типы, которые мы видели до сих пор, по умолчанию реализуют Display, потому что существует только один способ, которым вы, скорее всего, захотите показать пользователю 1 или любое другое примитивное значение. Но со структурами менее очевидно, как println! должен форматировать вывод, потому что есть больше вариантов отображения: нужны ли запятые или нет? Нужно ли выводить фигурные скобки? Должны ли быть показаны все поля? Из-за этой неоднозначности Rust не пытается угадать, чего мы хотим, и у структур нет предоставленной реализации Display, которую можно было бы использовать с println! и заполнителем {}.

Если продолжить читать ошибки, мы найдем такую полезную заметку:

   |                        |`Rectangle` cannot be formatted with the default formatter
   |                        required by this formatting parameter

Давайте попробуем! Теперь вызов макроса println! будет выглядеть так: println!("rect1 is {rect1:?}");. Помещение спецификатора :? внутрь фигурных скобок говорит println!, что мы хотим использовать формат вывода под названием Debug. Трейт Debug позволяет выводить нашу структуру способом, полезным для разработчиков, чтобы мы могли видеть ее значение во время отладки кода.

Скомпилируйте код с этим изменением. Досадно! Мы все еще получаем ошибку:

error[E0277]: `Rectangle` doesn't implement `Debug`

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

   |                        required by this formatting parameter
   |

Rust действительно включает функциональность для вывода отладочной информации, но нам нужно явно подключить ее, чтобы сделать эту функциональность доступной для нашей структуры. Для этого мы добавляем внешний атрибут #[derive(Debug)] прямо перед определением структуры, как показано в листинге 5-12.

Filename: src/main.rs
#[derive(Debug)]
struct Rectangle {
    width: u32,
    height: u32,
}

fn main() {
    let rect1 = Rectangle {
        width: 30,
        height: 50,
    };

    println!("rect1 is {rect1:?}");
}
Listing 5-12: Добавление атрибута для получения трейта Debug и вывод экземпляра Rectangle с помощью отладочного форматирования

Теперь при запуске программы мы не получим ошибок и увидим следующий вывод:

$ cargo run
   Compiling rectangles v0.1.0 (file:///projects/rectangles)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.48s
     Running `target/debug/rectangles`
rect1 is Rectangle { width: 30, height: 50 }

Отлично! Это не самый красивый вывод, но он показывает значения всех полей для данного экземпляра, что определенно помогло бы во время отладки. Когда у нас есть более крупные структуры, полезно иметь вывод, который немного легче читать; в таких случаях мы можем использовать {:#?} вместо {:?} в строке println!. В этом примере использование стиля {:#?} выведет следующее:

$ cargo run
   Compiling rectangles v0.1.0 (file:///projects/rectangles)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.48s
     Running `target/debug/rectangles`
rect1 is Rectangle {
    width: 30,
    height: 50,
}

Другой способ вывести значение с помощью формата Debug – использовать макрос dbg!, который получает владение выражением (в отличие от println!, который получает ссылку), выводит файл и номер строки, где в вашем коде находится этот вызов макроса dbg!, вместе с результирующим значением этого выражения, а затем возвращает владение значением.

Примечание: вызов макроса dbg! выводит данные в стандартный поток консоли для ошибок (stderr), в отличие от println!, который выводит в стандартный поток консоли для вывода (stdout). Подробнее о stderr и stdout мы поговорим в разделе «Перенаправление ошибок в стандартный поток ошибок» главы 12.

Вот пример, в котором нас интересует значение, присваиваемое полю width, а также значение всей структуры в rect1:

#[derive(Debug)]
struct Rectangle {
    width: u32,
    height: u32,
}

fn main() {
    let scale = 2;
    let rect1 = Rectangle {
        width: dbg!(30 * scale),
        height: 50,
    };

    dbg!(&rect1);
}

Мы можем поместить dbg! вокруг выражения 30 * scale, и поскольку dbg! возвращает владение значением выражения, поле width получит то же значение, как если бы вызова dbg! там не было. Мы не хотим, чтобы dbg! получил владение rect1, поэтому в следующем вызове используем ссылку на rect1. Вот как выглядит вывод этого примера:

$ cargo run
   Compiling rectangles v0.1.0 (file:///projects/rectangles)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.61s
     Running `target/debug/rectangles`
[src/main.rs:10:16] 30 * scale = 60
[src/main.rs:14:5] &rect1 = Rectangle {
    width: 60,
    height: 50,
}

Мы видим, что первая часть вывода пришла из строки 10 файла src/main.rs, где мы отлаживаем выражение 30 * scale, а его результирующее значение равно 60 (форматирование Debug, реализованное для целых чисел, выводит только их значение). Вызов dbg! в строке 14 файла src/main.rs выводит значение &rect1, то есть структуру Rectangle. Этот вывод использует красивое форматирование Debug типа Rectangle. Макрос dbg! может быть действительно полезен, когда вы пытаетесь понять, что делает ваш код!

Помимо трейта Debug, Rust предоставляет нам ряд трейтов для использования с атрибутом derive, которые могут добавлять полезное поведение нашим пользовательским типам. Эти трейты и их поведение перечислены в Приложении C. В главе 10 мы рассмотрим, как реализовать эти трейты с пользовательским поведением, а также как создавать собственные трейты. Также существует много атрибутов помимо derive; дополнительную информацию смотрите в разделе «Attributes» справочника Rust.

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

Методы

Методы

Методы похожи на функции: мы объявляем их с помощью ключевого слова fn и имени, они могут иметь параметры и возвращаемое значение, а также содержат какой-то код, который выполняется, когда метод вызывается из другого места. В отличие от функций, методы определяются в контексте структуры (или перечисления либо трейт-объекта, которые мы рассмотрим соответственно в главе 6 и главе 18), а их первым параметром всегда является self, представляющий экземпляр структуры, для которого вызывается метод.

Синтаксис методов

Давайте изменим функцию area, которая принимает экземпляр Rectangle в качестве параметра, и вместо этого сделаем ее методом area, определенным для структуры Rectangle, как показано в листинге 5-13.

Filename: src/main.rs
#[derive(Debug)]
struct Rectangle {
    width: u32,
    height: u32,
}

impl Rectangle {
    fn area(&self) -> u32 {
        self.width * self.height
    }
}

fn main() {
    let rect1 = Rectangle {
        width: 30,
        height: 50,
    };

    println!(
        "The area of the rectangle is {} square pixels.",
        rect1.area()
    );
}
Listing 5-13: Определение метода area для структуры Rectangle

Чтобы определить функцию в контексте Rectangle, мы начинаем блок реализации impl для Rectangle. Все внутри этого блока impl будет связано с типом Rectangle. Затем мы переносим функцию area внутрь фигурных скобок impl и меняем первый (а в данном случае единственный) параметр на self в сигнатуре и везде в теле функции. В main, где мы вызывали функцию area и передавали rect1 как аргумент, теперь можно использовать синтаксис методов, чтобы вызвать метод area для нашего экземпляра Rectangle. Синтаксис методов записывается после экземпляра: мы добавляем точку, затем имя метода, круглые скобки и любые аргументы.

В сигнатуре area мы используем &self вместо rectangle: &Rectangle. Запись &self на самом деле является сокращением для self: &Self. Внутри блока impl тип Self является псевдонимом для типа, которому принадлежит этот блок impl. Первым параметром методов должен быть параметр с именем self типа Self, поэтому Rust позволяет сократить эту запись до одного имени self на месте первого параметра. Обратите внимание, что нам все равно нужно использовать & перед сокращенной записью self, чтобы указать, что этот метод заимствует экземпляр Self, точно так же как мы делали это в rectangle: &Rectangle. Методы могут получать владение self, неизменяемо заимствовать self, как мы сделали здесь, или изменяемо заимствовать self, как и любой другой параметр.

Мы выбрали здесь &self по той же причине, по которой использовали &Rectangle в версии с функцией: мы не хотим получать владение, а хотим только читать данные из структуры, а не записывать в нее. Если бы мы хотели изменить экземпляр, для которого вызван метод, в рамках работы метода, мы использовали бы &mut self в качестве первого параметра. Метод, который получает владение экземпляром, используя просто self в качестве первого параметра, встречается редко; этот прием обычно используют, когда метод преобразует self во что-то другое и вы хотите запретить вызывающему коду использовать исходный экземпляр после преобразования.

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

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

Filename: src/main.rs
#[derive(Debug)]
struct Rectangle {
    width: u32,
    height: u32,
}

impl Rectangle {
    fn width(&self) -> bool {
        self.width > 0
    }
}

fn main() {
    let rect1 = Rectangle {
        width: 30,
        height: 50,
    };

    if rect1.width() {
        println!("The rectangle has a nonzero width; it is {}", rect1.width);
    }
}

Здесь мы выбираем, что метод width будет возвращать true, если значение в поле width экземпляра больше 0, и false, если значение равно 0: мы можем использовать поле внутри метода с тем же именем для любой цели. В main, когда после rect1.width стоят круглые скобки, Rust понимает, что мы имеем в виду метод width. Когда круглых скобок нет, Rust понимает, что мы имеем в виду поле width.

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

Где оператор ->?

В C и C++ для вызова методов используются два разных оператора: . применяют, если метод вызывается непосредственно для объекта, а -> – если метод вызывается для указателя на объект и сначала нужно разыменовать указатель. Другими словами, если object – это указатель, то object->something() похоже на (*object).something().

В Rust нет аналога оператора ->; вместо этого в Rust есть возможность, которая называется автоматическое взятие ссылки и разыменование. Вызов методов – одно из немногих мест в Rust с таким поведением.

Вот как это работает: когда вы вызываете метод через object.something(), Rust автоматически добавляет &, &mut или *, чтобы object соответствовал сигнатуре метода. Другими словами, следующие записи эквивалентны:

#![allow(unused)]
fn main() {
#[derive(Debug,Copy,Clone)]
struct Point {
    x: f64,
    y: f64,
}

impl Point {
   fn distance(&self, other: &Point) -> f64 {
       let x_squared = f64::powi(other.x - self.x, 2);
       let y_squared = f64::powi(other.y - self.y, 2);

       f64::sqrt(x_squared + y_squared)
   }
}
let p1 = Point { x: 0.0, y: 0.0 };
let p2 = Point { x: 5.0, y: 6.5 };
p1.distance(&p2);
(&p1).distance(&p2);
}

Первый вариант выглядит намного чище. Такое автоматическое взятие ссылки работает потому, что у методов есть ясный получатель – тип self. Зная получателя и имя метода, Rust может однозначно определить, читает ли метод данные (&self), изменяет их (&mut self) или потребляет значение (self). То, что Rust делает заимствование неявным для получателей методов, является важной частью того, что на практике делает владение удобным.

Методы с большим количеством параметров

Давайте потренируемся использовать методы, реализовав второй метод для структуры Rectangle. На этот раз мы хотим, чтобы экземпляр Rectangle принимал другой экземпляр Rectangle и возвращал true, если второй Rectangle может полностью поместиться внутри self (первого Rectangle); в противном случае он должен возвращать false. Иными словами, после определения метода can_hold мы хотим иметь возможность написать программу, показанную в листинге 5-14.

Filename: src/main.rs
fn main() {
    let rect1 = Rectangle {
        width: 30,
        height: 50,
    };
    let rect2 = Rectangle {
        width: 10,
        height: 40,
    };
    let rect3 = Rectangle {
        width: 60,
        height: 45,
    };

    println!("Can rect1 hold rect2? {}", rect1.can_hold(&rect2));
    println!("Can rect1 hold rect3? {}", rect1.can_hold(&rect3));
}
Listing 5-14: Использование еще не написанного метода can_hold

Ожидаемый вывод будет выглядеть следующим образом, потому что оба измерения rect2 меньше измерений rect1, но rect3 шире, чем rect1:

Can rect1 hold rect2? true
Can rect1 hold rect3? false

Мы знаем, что хотим определить метод, поэтому он будет находиться внутри блока impl Rectangle. Метод будет называться can_hold и будет принимать неизменяемое заимствование другого Rectangle в качестве параметра. Мы можем понять, каким будет тип параметра, посмотрев на код, который вызывает метод: rect1.can_hold(&rect2) передает &rect2, то есть неизменяемое заимствование rect2, экземпляра Rectangle. Это имеет смысл, потому что нам нужно только читать rect2 (а не записывать в него, для чего потребовалось бы изменяемое заимствование), и мы хотим, чтобы main сохранил владение rect2, чтобы мы могли снова использовать его после вызова метода can_hold. Возвращаемым значением can_hold будет булево значение, а реализация проверит, больше ли ширина и высота self соответственно ширины и высоты другого Rectangle. Добавим новый метод can_hold в блок impl из листинга 5-13, как показано в листинге 5-15.

Filename: src/main.rs
#[derive(Debug)]
struct Rectangle {
    width: u32,
    height: u32,
}

impl Rectangle {
    fn area(&self) -> u32 {
        self.width * self.height
    }

    fn can_hold(&self, other: &Rectangle) -> bool {
        self.width > other.width && self.height > other.height
    }
}

fn main() {
    let rect1 = Rectangle {
        width: 30,
        height: 50,
    };
    let rect2 = Rectangle {
        width: 10,
        height: 40,
    };
    let rect3 = Rectangle {
        width: 60,
        height: 45,
    };

    println!("Can rect1 hold rect2? {}", rect1.can_hold(&rect2));
    println!("Can rect1 hold rect3? {}", rect1.can_hold(&rect3));
}
Listing 5-15: Реализация метода can_hold для Rectangle, который принимает другой экземпляр Rectangle как параметр

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

Ассоциированные функции

Все функции, определенные внутри блока impl, называются ассоциированными функциями, потому что они связаны с типом, указанным после impl. Мы можем определять ассоциированные функции, у которых первым параметром нет self (и поэтому они не являются методами), потому что для их работы не нужен экземпляр типа. Одну такую функцию мы уже использовали: функцию String::from, определенную для типа String.

Ассоциированные функции, которые не являются методами, часто используются как конструкторы, возвращающие новый экземпляр структуры. Их часто называют new, но new не является особым именем и не встроено в язык. Например, мы могли бы предоставить ассоциированную функцию с именем square, которая принимала бы один параметр размера и использовала его и как ширину, и как высоту, тем самым упрощая создание квадратного Rectangle по сравнению с необходимостью указывать одно и то же значение дважды:

Имя файла: src/main.rs

#[derive(Debug)]
struct Rectangle {
    width: u32,
    height: u32,
}

impl Rectangle {
    fn square(size: u32) -> Self {
        Self {
            width: size,
            height: size,
        }
    }
}

fn main() {
    let sq = Rectangle::square(3);
}

Ключевые слова Self в возвращаемом типе и в теле функции являются псевдонимами типа, который указан после ключевого слова impl; в данном случае это Rectangle.

Чтобы вызвать эту ассоциированную функцию, мы используем синтаксис :: с именем структуры; пример – let sq = Rectangle::square(3);. Эта функция находится в пространстве имен структуры: синтаксис :: используется и для ассоциированных функций, и для пространств имен, создаваемых модулями. Мы обсудим модули в главе 7.

Несколько блоков impl

У каждой структуры может быть несколько блоков impl. Например, листинг 5-15 эквивалентен коду, показанному в листинге 5-16, где каждый метод находится в собственном блоке impl.

#[derive(Debug)]
struct Rectangle {
    width: u32,
    height: u32,
}

impl Rectangle {
    fn area(&self) -> u32 {
        self.width * self.height
    }
}

impl Rectangle {
    fn can_hold(&self, other: &Rectangle) -> bool {
        self.width > other.width && self.height > other.height
    }
}

fn main() {
    let rect1 = Rectangle {
        width: 30,
        height: 50,
    };
    let rect2 = Rectangle {
        width: 10,
        height: 40,
    };
    let rect3 = Rectangle {
        width: 60,
        height: 45,
    };

    println!("Can rect1 hold rect2? {}", rect1.can_hold(&rect2));
    println!("Can rect1 hold rect3? {}", rect1.can_hold(&rect3));
}
Listing 5-16: Переписывание листинга 5-15 с использованием нескольких блоков impl

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

Итоги

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

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

Перечисления и сопоставление с образцом

В этой главе мы рассмотрим перечисления, которые также называют enums. Перечисления позволяют определить тип, перечислив его возможные варианты. Сначала мы определим и используем перечисление, чтобы показать, как enum может кодировать смысл вместе с данными. Затем мы изучим особенно полезное перечисление под названием Option, которое выражает, что значение может быть либо чем-то, либо ничем. После этого мы рассмотрим, как сопоставление с образцом в выражении match позволяет легко выполнять разный код для разных значений перечисления. Наконец, мы разберем, как конструкция if let дает еще одну удобную и краткую идиому для обработки перечислений в вашем коде.

Определение перечисления

Определение перечисления

Если структуры дают способ группировать связанные поля и данные, например Rectangle с его width и height, то перечисления дают способ сказать, что значение является одним вариантом из возможного набора значений. Например, мы можем захотеть сказать, что Rectangle – это одна из возможных фигур, среди которых также есть Circle и Triangle. Для этого Rust позволяет кодировать такие возможности в виде перечисления.

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

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

Мы можем выразить эту идею в коде, определив перечисление IpAddrKind и перечислив возможные виды IP-адреса: V4 и V6. Это варианты перечисления:

enum IpAddrKind {
    V4,
    V6,
}

fn main() {
    let four = IpAddrKind::V4;
    let six = IpAddrKind::V6;

    route(IpAddrKind::V4);
    route(IpAddrKind::V6);
}

fn route(ip_kind: IpAddrKind) {}

Теперь IpAddrKind – это пользовательский тип данных, который мы можем использовать в других местах нашего кода.

Значения перечисления

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

enum IpAddrKind {
    V4,
    V6,
}

fn main() {
    let four = IpAddrKind::V4;
    let six = IpAddrKind::V6;

    route(IpAddrKind::V4);
    route(IpAddrKind::V6);
}

fn route(ip_kind: IpAddrKind) {}

Обратите внимание, что варианты перечисления находятся в пространстве имен его идентификатора, и мы используем двойное двоеточие, чтобы разделить эти две части. Это полезно, потому что теперь оба значения, IpAddrKind::V4 и IpAddrKind::V6, имеют один и тот же тип: IpAddrKind. Затем мы можем, например, определить функцию, которая принимает любой IpAddrKind:

enum IpAddrKind {
    V4,
    V6,
}

fn main() {
    let four = IpAddrKind::V4;
    let six = IpAddrKind::V6;

    route(IpAddrKind::V4);
    route(IpAddrKind::V6);
}

fn route(ip_kind: IpAddrKind) {}

И мы можем вызвать эту функцию с любым из вариантов:

enum IpAddrKind {
    V4,
    V6,
}

fn main() {
    let four = IpAddrKind::V4;
    let six = IpAddrKind::V6;

    route(IpAddrKind::V4);
    route(IpAddrKind::V6);
}

fn route(ip_kind: IpAddrKind) {}

Использование перечислений дает еще больше преимуществ. Если подумать дальше о нашем типе IP-адреса, сейчас у нас нет способа хранить фактические данные IP-адреса; мы знаем только, какого он вида. С учетом того, что вы только что узнали о структурах в главе 5, у вас может возникнуть желание решить эту задачу с помощью структур, как показано в листинге 6-1.

fn main() {
    enum IpAddrKind {
        V4,
        V6,
    }

    struct IpAddr {
        kind: IpAddrKind,
        address: String,
    }

    let home = IpAddr {
        kind: IpAddrKind::V4,
        address: String::from("127.0.0.1"),
    };

    let loopback = IpAddr {
        kind: IpAddrKind::V6,
        address: String::from("::1"),
    };
}
Listing 6-1: Хранение данных IP-адреса и его варианта IpAddrKind с помощью struct

Здесь мы определили структуру IpAddr с двумя полями: полем kind типа IpAddrKind (перечисление, которое мы определили ранее) и полем address типа String. У нас есть два экземпляра этой структуры. Первый – home; в его поле kind хранится значение IpAddrKind::V4, а связанные данные адреса равны 127.0.0.1. Второй экземпляр – loopback. В качестве значения kind у него другой вариант IpAddrKind, V6, и с ним связан адрес ::1. Мы использовали структуру, чтобы объединить значения kind и address, поэтому теперь вариант связан со значением.

Однако представить ту же идею с помощью одного только перечисления можно короче: вместо перечисления внутри структуры мы можем поместить данные прямо в каждый вариант перечисления. Новое определение enum IpAddr говорит, что оба варианта, V4 и V6, будут иметь связанные значения типа String:

fn main() {
    enum IpAddr {
        V4(String),
        V6(String),
    }

    let home = IpAddr::V4(String::from("127.0.0.1"));

    let loopback = IpAddr::V6(String::from("::1"));
}

Мы прикрепляем данные напрямую к каждому варианту перечисления, поэтому дополнительная структура не нужна. Здесь также легче увидеть еще одну деталь работы перечислений: имя каждого варианта enum, который мы определяем, также становится функцией, создающей экземпляр перечисления. То есть IpAddr::V4() – это вызов функции, которая принимает аргумент String и возвращает экземпляр типа IpAddr. Мы автоматически получаем эту функцию-конструктор в результате определения enum.

У использования enum вместо структуры есть еще одно преимущество: каждый вариант может иметь разные типы и разное количество связанных данных. IP-адреса четвертой версии всегда будут иметь четыре числовых компонента со значениями от 0 до 255. Если бы мы захотели хранить адреса V4 как четыре значения u8, но по-прежнему выражать адреса V6 одним значением String, со структурой мы не смогли бы этого сделать. Перечисления легко справляются с таким случаем:

fn main() {
    enum IpAddr {
        V4(u8, u8, u8, u8),
        V6(String),
    }

    let home = IpAddr::V4(127, 0, 0, 1);

    let loopback = IpAddr::V6(String::from("::1"));
}

Мы показали несколько разных способов определить структуры данных для хранения IP-адресов четвертой и шестой версий. Однако, как оказывается, желание хранить IP-адреса и кодировать, какого они вида, настолько распространено, что в стандартной библиотеке уже есть определение, которое мы можем использовать! Посмотрим, как стандартная библиотека определяет IpAddr. В ней есть точно такой enum и такие варианты, какие мы определили и использовали, но данные адреса встроены внутрь вариантов в форме двух разных структур, определенных по-разному для каждого варианта:

#![allow(unused)]
fn main() {
struct Ipv4Addr {
    // --snip--
}

struct Ipv6Addr {
    // --snip--
}

enum IpAddr {
    V4(Ipv4Addr),
    V6(Ipv6Addr),
}
}

Этот код показывает, что внутрь варианта enum можно поместить данные любого вида: например, строки, числовые типы или структуры. Можно даже включить другое перечисление! Кроме того, типы стандартной библиотеки часто не намного сложнее того, что вы могли бы придумать сами.

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

Давайте рассмотрим еще один пример enum в листинге 6-2: у него в вариантах встроено большое разнообразие типов.

enum Message {
    Quit,
    Move { x: i32, y: i32 },
    Write(String),
    ChangeColor(i32, i32, i32),
}

fn main() {}
Listing 6-2: Перечисление Message, каждый вариант которого хранит разные количества и типы значений

У этого enum есть четыре варианта с разными типами:

  • Quit: вообще не имеет связанных с ним данных
  • Move: имеет именованные поля, как у структуры
  • Write: содержит одно значение String
  • ChangeColor: содержит три значения i32

Определение enum с такими вариантами, как в листинге 6-2, похоже на определение разных видов структур, за исключением того, что enum не использует ключевое слово struct, а все варианты сгруппированы вместе под типом Message. Следующие структуры могли бы хранить те же данные, что хранят варианты предыдущего enum:

struct QuitMessage; // unit struct
struct MoveMessage {
    x: i32,
    y: i32,
}
struct WriteMessage(String); // tuple struct
struct ChangeColorMessage(i32, i32, i32); // tuple struct

fn main() {}

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

Между перечислениями и структурами есть еще одно сходство: точно так же, как мы можем определять методы для структур с помощью impl, мы можем определять методы и для перечислений. Вот метод с именем call, который мы могли бы определить для нашего enum Message:

fn main() {
    enum Message {
        Quit,
        Move { x: i32, y: i32 },
        Write(String),
        ChangeColor(i32, i32, i32),
    }

    impl Message {
        fn call(&self) {
            // method body would be defined here
        }
    }

    let m = Message::Write(String::from("hello"));
    m.call();
}

Тело метода использовало бы self, чтобы получить значение, для которого мы вызвали метод. В этом примере мы создали переменную m со значением Message::Write(String::from("hello")), и именно этим значением будет self в теле метода call, когда выполнится m.call().

Давайте рассмотрим еще одно перечисление из стандартной библиотеки, очень распространенное и полезное: Option.

Перечисление Option

В этом разделе мы разберем пример использования Option – еще одного enum, определенного стандартной библиотекой. Тип Option кодирует очень распространенный сценарий, в котором значение может чем-то быть или может быть ничем.

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

Проектирование языка программирования часто рассматривают через призму того, какие возможности в него включают, но возможности, которые исключают, тоже важны. В Rust нет возможности null, которая есть во многих других языках. Null – это значение, означающее отсутствие значения. В языках с null переменные всегда могут находиться в одном из двух состояний: null или не-null.

В своей презентации 2009 года «Null References: The Billion Dollar Mistake» Тони Хоар, изобретатель null, сказал об этом так:

Я называю это своей ошибкой на миллиард долларов. В то время я проектировал первую всеобъемлющую систему типов для ссылок в объектно-ориентированном языке. Моей целью было гарантировать, что любое использование ссылок будет абсолютно безопасным, а проверка будет автоматически выполняться компилятором. Но я не смог устоять перед искушением добавить null-ссылку просто потому, что ее было так легко реализовать. Это привело к бесчисленным ошибкам, уязвимостям и сбоям систем, которые, вероятно, причинили боль и ущерб на миллиард долларов за последние сорок лет.

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

Однако идея, которую пытается выразить null, все равно полезна: null – это значение, которое сейчас по какой-то причине недопустимо или отсутствует.

Проблема на самом деле не в самой идее, а в конкретной реализации. Поэтому в Rust нет null, но есть enum, который может кодировать идею наличия или отсутствия значения. Этот enum называется Option<T>, и он определен стандартной библиотекой следующим образом:

#![allow(unused)]
fn main() {
enum Option<T> {
    None,
    Some(T),
}
}

Перечисление Option<T> настолько полезно, что даже включено в прелюдию; вам не нужно явно вводить его в область видимости. Его варианты тоже включены в прелюдию: вы можете использовать Some и None напрямую, без префикса Option::. Option<T> при этом остается обычным enum, а Some(T) и None по-прежнему являются вариантами типа Option<T>.

Синтаксис <T> – это возможность Rust, о которой мы еще не говорили. Это обобщенный параметр типа, и обобщения мы подробнее рассмотрим в главе 10. Пока вам достаточно знать, что <T> означает: вариант Some enum Option может хранить одно значение любого типа, а каждый конкретный тип, используемый вместо T, делает весь тип Option<T> другим типом. Вот несколько примеров использования значений Option для хранения числовых типов и символьных типов:

fn main() {
    let some_number = Some(5);
    let some_char = Some('e');

    let absent_number: Option<i32> = None;
}

Тип some_numberOption<i32>. Тип some_charOption<char>, и это другой тип. Rust может вывести эти типы, потому что мы указали значение внутри варианта Some. Для absent_number Rust требует, чтобы мы указали общий тип Option: компилятор не может вывести тип, который будет хранить соответствующий вариант Some, глядя только на значение None. Здесь мы говорим Rust, что хотим, чтобы absent_number имел тип Option<i32>.

Когда у нас есть значение Some, мы знаем, что значение присутствует и хранится внутри Some. Когда у нас есть значение None, в некотором смысле оно означает то же самое, что и null: у нас нет допустимого значения. Так почему же наличие Option<T> лучше, чем наличие null?

Коротко говоря, потому что Option<T> и T (где T может быть любым типом) – это разные типы, и компилятор не позволит нам использовать значение Option<T> так, как будто оно точно является допустимым значением. Например, этот код не скомпилируется, потому что пытается прибавить i8 к Option<i8>:

fn main() {
    let x: i8 = 5;
    let y: Option<i8> = Some(5);

    let sum = x + y;
}

Если мы запустим этот код, получим сообщение об ошибке, похожее на следующее:

$ cargo run
   Compiling enums v0.1.0 (file:///projects/enums)
error[E0277]: cannot add `Option<i8>` to `i8`
 --> src/main.rs:5:17
  |
5 |     let sum = x + y;
  |                 ^ no implementation for `i8 + Option<i8>`
  |
  = help: the trait `Add<Option<i8>>` is not implemented for `i8`
  = help: the following other types implement trait `Add<Rhs>`:
            `&i8` implements `Add<i8>`
            `&i8` implements `Add`
            `i8` implements `Add<&i8>`
            `i8` implements `Add`

For more information about this error, try `rustc --explain E0277`.
error: could not compile `enums` (bin "enums") due to 1 previous error

Сурово! По сути, это сообщение об ошибке означает, что Rust не понимает, как сложить i8 и Option<i8>, потому что это разные типы. Когда в Rust у нас есть значение такого типа, как i8, компилятор гарантирует, что у нас всегда есть допустимое значение. Мы можем уверенно продолжать работу, не проверяя значение на null перед его использованием. Только когда у нас есть Option<i8> (или Option с любым другим типом значения, с которым мы работаем), нам нужно думать о возможном отсутствии значения, и компилятор проследит, чтобы мы обработали этот случай перед использованием значения.

Иными словами, нужно преобразовать Option<T> в T, прежде чем можно будет выполнять с ним операции для T. В общем случае это помогает поймать одну из самых распространенных проблем с null: предположение, что что-то не является null, хотя на самом деле является.

Устранение риска ошибочно предположить, что значение не равно null, помогает быть увереннее в коде. Чтобы иметь значение, которое потенциально может быть null, нужно явно согласиться на это, сделав тип этого значения Option<T>. Затем, когда вы используете это значение, от вас требуется явно обработать случай, когда значение отсутствует. Везде, где значение имеет тип, отличный от Option<T>, вы можете безопасно предполагать, что значение не является null. Это было осознанным проектным решением Rust: ограничить распространенность null и повысить безопасность кода на Rust.

Так как же получить значение T из варианта Some, когда у вас есть значение типа Option<T>, чтобы затем использовать это значение? У enum Option<T> есть большое количество методов, полезных в самых разных ситуациях; вы можете изучить их в его документации. Знакомство с методами Option<T> будет чрезвычайно полезно на вашем пути в Rust.

В целом, чтобы использовать значение Option<T>, вам нужен код, который обработает каждый вариант. Вам нужен один код, который выполнится только при наличии значения Some(T), и этому коду разрешено использовать внутреннее значение T. Вам нужен другой код, который выполнится только при значении None, и у этого кода нет доступного значения T. Выражение match – это конструкция управления потоком выполнения, которая при использовании с enum делает именно это: она выполняет разный код в зависимости от того, какой вариант enum у нее есть, и этот код может использовать данные внутри совпавшего значения.

Конструкция управления потоком match

Конструкция управления потоком match

В Rust есть чрезвычайно мощная конструкция управления потоком под названием match, которая позволяет сравнить значение с рядом образцов, а затем выполнить код в зависимости от того, какой образец совпал. Образцы могут состоять из литеральных значений, имен переменных, подстановочных знаков и многих других элементов; глава 19 охватывает все виды образцов и то, что они делают. Сила match заключается в выразительности образцов и в том, что компилятор подтверждает: обработаны все возможные случаи.

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

Раз уж речь зашла о монетах, давайте используем их как пример для match! Мы можем написать функцию, которая принимает неизвестную монету США и, подобно счетной машине, определяет, что это за монета, и возвращает ее стоимость в центах, как показано в листинге 6-3.

enum Coin {
    Penny,
    Nickel,
    Dime,
    Quarter,
}

fn value_in_cents(coin: Coin) -> u8 {
    match coin {
        Coin::Penny => 1,
        Coin::Nickel => 5,
        Coin::Dime => 10,
        Coin::Quarter => 25,
    }
}

fn main() {}
Listing 6-3: Перечисление и выражение match, в котором варианты enum используются как образцы

Давайте разберем match в функции value_in_cents. Сначала мы пишем ключевое слово match, за которым следует выражение; в данном случае это значение coin. Это очень похоже на условное выражение, используемое с if, но есть большое отличие: с if условие должно вычисляться в булево значение, а здесь оно может быть любого типа. Тип coin в этом примере – enum Coin, который мы определили в первой строке.

Далее идут ветви match. У ветви есть две части: образец и некоторый код. У первой ветви здесь образцом является значение Coin::Penny, а затем идет оператор =>, который отделяет образец от кода, который нужно выполнить. В данном случае код – это просто значение 1. Каждая ветвь отделяется от следующей запятой.

Когда выражение match выполняется, оно по порядку сравнивает результирующее значение с образцом каждой ветви. Если образец соответствует значению, выполняется код, связанный с этим образцом. Если этот образец не соответствует значению, выполнение продолжается со следующей ветви, почти как в машине для сортировки монет. У нас может быть столько ветвей, сколько нужно: в листинге 6-3 у нашего match четыре ветви.

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

Обычно мы не используем фигурные скобки, если код ветви match короткий, как в листинге 6-3, где каждая ветвь просто возвращает значение. Если вы хотите выполнить несколько строк кода в ветви match, нужно использовать фигурные скобки, и запятая после ветви тогда становится необязательной. Например, следующий код выводит “Lucky penny!” каждый раз, когда метод вызывается с Coin::Penny, но все равно возвращает последнее значение блока, 1:

enum Coin {
    Penny,
    Nickel,
    Dime,
    Quarter,
}

fn value_in_cents(coin: Coin) -> u8 {
    match coin {
        Coin::Penny => {
            println!("Lucky penny!");
            1
        }
        Coin::Nickel => 5,
        Coin::Dime => 10,
        Coin::Quarter => 25,
    }
}

fn main() {}

Образцы, которые связываются со значениями

Еще одна полезная возможность ветвей match заключается в том, что они могут связываться с частями значений, которые соответствуют образцу. Именно так мы можем извлекать значения из вариантов enum.

В качестве примера изменим один из вариантов нашего enum так, чтобы он хранил данные внутри себя. С 1999 по 2008 год США выпускали 25-центовые монеты с разными изображениями для каждого из 50 штатов на одной стороне. Никакие другие монеты не получали изображений штатов, поэтому только у 25-центовых монет есть это дополнительное значение. Мы можем добавить эту информацию в наш enum, изменив вариант Quarter так, чтобы он включал значение UsState, хранящееся внутри него, как сделано в листинге 6-4.

#[derive(Debug)] // so we can inspect the state in a minute
enum UsState {
    Alabama,
    Alaska,
    // --snip--
}

enum Coin {
    Penny,
    Nickel,
    Dime,
    Quarter(UsState),
}

fn main() {}
Listing 6-4: Перечисление Coin, в котором вариант Quarter также хранит значение UsState

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

В выражении match для этого кода мы добавляем переменную state в образец, который соответствует значениям варианта Coin::Quarter. Когда совпадает Coin::Quarter, переменная state связывается со значением штата этой 25-центовой монеты. Затем мы можем использовать state в коде этой ветви, вот так:

#[derive(Debug)]
enum UsState {
    Alabama,
    Alaska,
    // --snip--
}

enum Coin {
    Penny,
    Nickel,
    Dime,
    Quarter(UsState),
}

fn value_in_cents(coin: Coin) -> u8 {
    match coin {
        Coin::Penny => 1,
        Coin::Nickel => 5,
        Coin::Dime => 10,
        Coin::Quarter(state) => {
            println!("State quarter from {state:?}!");
            25
        }
    }
}

fn main() {
    value_in_cents(Coin::Quarter(UsState::Alaska));
}

Если бы мы вызвали value_in_cents(Coin::Quarter(UsState::Alaska)), значение coin было бы Coin::Quarter(UsState::Alaska). Когда мы сравниваем это значение с каждой из ветвей match, ни одна из них не совпадает, пока мы не дойдем до Coin::Quarter(state). В этот момент привязка state получит значение UsState::Alaska. Затем мы можем использовать эту привязку в выражении println!, тем самым извлекая внутреннее значение штата из варианта enum Coin для Quarter.

Образец match для Option<T>

В предыдущем разделе мы хотели получить внутреннее значение T из случая Some при использовании Option<T>; мы также можем обрабатывать Option<T> с помощью match, как мы делали с enum Coin! Вместо сравнения монет мы будем сравнивать варианты Option<T>, но принцип работы выражения match остается тем же.

Допустим, мы хотим написать функцию, которая принимает Option<i32> и, если внутри есть значение, прибавляет к нему 1. Если внутри нет значения, функция должна вернуть значение None и не пытаться выполнять никаких операций.

Благодаря match эту функцию очень легко написать, и она будет выглядеть как в листинге 6-5.

fn main() {
    fn plus_one(x: Option<i32>) -> Option<i32> {
        match x {
            None => None,
            Some(i) => Some(i + 1),
        }
    }

    let five = Some(5);
    let six = plus_one(five);
    let none = plus_one(None);
}
Listing 6-5: Функция, которая использует выражение match для Option<i32>

Давайте подробнее рассмотрим первое выполнение plus_one. Когда мы вызываем plus_one(five), переменная x в теле plus_one будет иметь значение Some(5). Затем мы сравниваем его с каждой ветвью match:

fn main() {
    fn plus_one(x: Option<i32>) -> Option<i32> {
        match x {
            None => None,
            Some(i) => Some(i + 1),
        }
    }

    let five = Some(5);
    let six = plus_one(five);
    let none = plus_one(None);
}

Значение Some(5) не соответствует образцу None, поэтому мы переходим к следующей ветви:

fn main() {
    fn plus_one(x: Option<i32>) -> Option<i32> {
        match x {
            None => None,
            Some(i) => Some(i + 1),
        }
    }

    let five = Some(5);
    let six = plus_one(five);
    let none = plus_one(None);
}

Соответствует ли Some(5) образцу Some(i)? Да! У нас тот же вариант. Переменная i связывается со значением, содержащимся в Some, поэтому i получает значение 5. Затем выполняется код в ветви match: мы прибавляем 1 к значению i и создаем новое значение Some, внутри которого находится наш результат 6.

Теперь рассмотрим второй вызов plus_one в листинге 6-5, где x равно None. Мы входим в match и сравниваем с первой ветвью:

fn main() {
    fn plus_one(x: Option<i32>) -> Option<i32> {
        match x {
            None => None,
            Some(i) => Some(i + 1),
        }
    }

    let five = Some(5);
    let six = plus_one(five);
    let none = plus_one(None);
}

Совпадает! Значения, к которому можно что-то прибавить, нет, поэтому программа останавливается и возвращает значение None в правой части =>. Поскольку первая ветвь совпала, остальные ветви не сравниваются.

Объединение match и enum полезно во многих ситуациях. Вы часто будете видеть этот шаблон в коде Rust: сопоставить enum с образцами, связать переменную с данными внутри, а затем выполнить код на их основе. Поначалу это немного непривычно, но когда вы привыкнете, вам захочется иметь такую возможность во всех языках. Пользователям она стабильно нравится.

Сопоставления являются исчерпывающими

Есть еще один аспект match, который нужно обсудить: образцы ветвей должны покрывать все возможности. Рассмотрим эту версию нашей функции plus_one, в которой есть ошибка и которая не скомпилируется:

fn main() {
    fn plus_one(x: Option<i32>) -> Option<i32> {
        match x {
            Some(i) => Some(i + 1),
        }
    }

    let five = Some(5);
    let six = plus_one(five);
    let none = plus_one(None);
}

Мы не обработали случай None, поэтому этот код приведет к ошибке. К счастью, Rust умеет такую ошибку обнаруживать. Если мы попробуем скомпилировать этот код, получим такую ошибку:

$ cargo run
   Compiling enums v0.1.0 (file:///projects/enums)
error[E0004]: non-exhaustive patterns: `None` not covered
 --> src/main.rs:3:15
  |
3 |         match x {
  |               ^ pattern `None` not covered
  |
note: `Option<i32>` defined here
 --> /rustc/1159e78c4747b02ef996e55082b704c09b970588/library/core/src/option.rs:593:1
 ::: /rustc/1159e78c4747b02ef996e55082b704c09b970588/library/core/src/option.rs:597:5
  |
  = note: not covered
  = note: the matched value is of type `Option<i32>`
help: ensure that all possible cases are being handled by adding a match arm with a wildcard pattern or an explicit pattern as shown
  |
4 ~             Some(i) => Some(i + 1),
5 ~             None => todo!(),
  |

For more information about this error, try `rustc --explain E0004`.
error: could not compile `enums` (bin "enums") due to 1 previous error

Rust знает, что мы не покрыли все возможные случаи, и даже знает, какой образец мы забыли! Сопоставления в Rust исчерпывающие: мы должны исчерпать все до последней возможности, чтобы код был допустимым. Особенно в случае Option<T>, когда Rust не дает нам забыть явно обработать случай None, он защищает нас от предположения, что у нас есть значение, когда у нас может быть null, тем самым делая невозможной ошибку на миллиард долларов, обсуждавшуюся ранее.

Всеохватывающие образцы и заполнитель _

Используя enum, мы также можем выполнять особые действия для нескольких конкретных значений, а для всех остальных значений выполнять одно действие по умолчанию. Представьте, что мы реализуем игру, где, если при броске кости выпадает 3, ваш игрок не двигается, а вместо этого получает красивую новую шляпу. Если выпадает 7, ваш игрок теряет красивую шляпу. Для всех остальных значений игрок перемещается на это число клеток по игровому полю. Вот match, который реализует эту логику; результат броска кости задан прямо в коде, а вся остальная логика представлена функциями без тел, потому что их фактическая реализация выходит за рамки этого примера:

fn main() {
    let dice_roll = 9;
    match dice_roll {
        3 => add_fancy_hat(),
        7 => remove_fancy_hat(),
        other => move_player(other),
    }

    fn add_fancy_hat() {}
    fn remove_fancy_hat() {}
    fn move_player(num_spaces: u8) {}
}

Для первых двух ветвей образцами являются литеральные значения 3 и 7. Для последней ветви, которая покрывает все остальные возможные значения, образцом является переменная, которой мы выбрали имя other. Код, выполняемый для ветви other, использует эту переменную, передавая ее в функцию move_player.

Этот код компилируется, хотя мы не перечислили все возможные значения, которые может иметь u8, потому что последний образец совпадет со всеми значениями, которые не были явно перечислены. Этот всеохватывающий образец удовлетворяет требованию, что match должен быть исчерпывающим. Обратите внимание, что всеохватывающую ветвь нужно ставить последней, потому что образцы проверяются по порядку. Если бы мы поставили всеохватывающую ветвь раньше, остальные ветви никогда бы не выполнялись, поэтому Rust предупредит нас, если мы добавим ветви после всеохватывающей!

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

Изменим правила игры: теперь, если выпадает что угодно, кроме 3 или 7, нужно бросить кость еще раз. Нам больше не нужно использовать всеохватывающее значение, поэтому мы можем изменить код, чтобы использовать _ вместо переменной с именем other:

fn main() {
    let dice_roll = 9;
    match dice_roll {
        3 => add_fancy_hat(),
        7 => remove_fancy_hat(),
        _ => reroll(),
    }

    fn add_fancy_hat() {}
    fn remove_fancy_hat() {}
    fn reroll() {}
}

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

Наконец, изменим правила игры еще раз: если выпадает что угодно, кроме 3 или 7, в ваш ход больше ничего не происходит. Мы можем выразить это, используя единичное значение (пустой кортежный тип, который мы упоминали в разделе «Кортежный тип») как код, связанный с ветвью _:

fn main() {
    let dice_roll = 9;
    match dice_roll {
        3 => add_fancy_hat(),
        7 => remove_fancy_hat(),
        _ => (),
    }

    fn add_fancy_hat() {}
    fn remove_fancy_hat() {}
}

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

О сопоставлении с образцом есть еще много материала, который мы рассмотрим в главе 19. А пока мы перейдем к синтаксису if let, который может быть полезен в ситуациях, когда выражение match получается немного многословным.

Компактное управление потоком с if let и let...else

Краткое управление потоком с if let и let...else

Синтаксис if let позволяет объединить if и let в менее многословный способ обработки значений, которые соответствуют одному образцу, с игнорированием остальных. Рассмотрим программу в листинге 6-6, которая сопоставляет значение Option<u8> в переменной config_max, но должна выполнить код только в том случае, если значение является вариантом Some.

fn main() {
    let config_max = Some(3u8);
    match config_max {
        Some(max) => println!("The maximum is configured to be {max}"),
        _ => (),
    }
}
Listing 6-6: match, которому важно выполнить код только тогда, когда значение равно Some

Если значение равно Some, мы выводим значение внутри варианта Some, связывая его с переменной max в образце. Мы не хотим ничего делать со значением None. Чтобы удовлетворить выражение match, после обработки только одного варианта нам приходится добавить _ => (), что является досадным шаблонным кодом.

Вместо этого мы могли бы записать то же самое короче с помощью if let. Следующий код ведет себя так же, как match в листинге 6-6:

fn main() {
    let config_max = Some(3u8);
    if let Some(max) = config_max {
        println!("The maximum is configured to be {max}");
    }
}

Синтаксис if let принимает образец и выражение, разделенные знаком равенства. Он работает так же, как match, где выражение передается в match, а образец является его первой ветвью. В этом случае образец – Some(max), и max связывается со значением внутри Some. Затем мы можем использовать max в теле блока if let так же, как использовали max в соответствующей ветви match. Код в блоке if let выполняется только если значение соответствует образцу.

Использование if let означает, что нужно меньше печатать, делать меньше отступов и писать меньше шаблонного кода. Однако при этом вы теряете исчерпывающую проверку, которую навязывает match и которая гарантирует, что вы не забыли обработать ни один случай. Выбор между match и if let зависит от того, что именно вы делаете в конкретной ситуации, и от того, является ли краткость приемлемым обменом на потерю исчерпывающей проверки.

Иными словами, if let можно рассматривать как синтаксический сахар для match, который выполняет код, когда значение соответствует одному образцу, а затем игнорирует все остальные значения.

Мы можем использовать else вместе с if let. Блок кода, связанный с else, соответствует блоку кода, который был бы связан со случаем _ в выражении match, эквивалентном сочетанию if let и else. Вспомните определение enum Coin из листинга 6-4, где вариант Quarter также хранил значение UsState. Если бы мы хотели подсчитывать все монеты, не являющиеся 25-центовыми, и одновременно объявлять штат для 25-центовых монет, мы могли бы сделать это с помощью выражения match, вот так:

#[derive(Debug)]
enum UsState {
    Alabama,
    Alaska,
    // --snip--
}

enum Coin {
    Penny,
    Nickel,
    Dime,
    Quarter(UsState),
}

fn main() {
    let coin = Coin::Penny;
    let mut count = 0;
    match coin {
        Coin::Quarter(state) => println!("State quarter from {state:?}!"),
        _ => count += 1,
    }
}

Или мы могли бы использовать выражение if let и else, вот так:

#[derive(Debug)]
enum UsState {
    Alabama,
    Alaska,
    // --snip--
}

enum Coin {
    Penny,
    Nickel,
    Dime,
    Quarter(UsState),
}

fn main() {
    let coin = Coin::Penny;
    let mut count = 0;
    if let Coin::Quarter(state) = coin {
        println!("State quarter from {state:?}!");
    } else {
        count += 1;
    }
}

Оставаться на «счастливом пути» с let...else

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

#[derive(Debug)] // so we can inspect the state in a minute
enum UsState {
    Alabama,
    Alaska,
    // --snip--
}

impl UsState {
    fn existed_in(&self, year: u16) -> bool {
        match self {
            UsState::Alabama => year >= 1819,
            UsState::Alaska => year >= 1959,
            // -- snip --
        }
    }
}

enum Coin {
    Penny,
    Nickel,
    Dime,
    Quarter(UsState),
}

fn describe_state_quarter(coin: Coin) -> Option<String> {
    if let Coin::Quarter(state) = coin {
        if state.existed_in(1900) {
            Some(format!("{state:?} is pretty old, for America!"))
        } else {
            Some(format!("{state:?} is relatively new."))
        }
    } else {
        None
    }
}

fn main() {
    if let Some(desc) = describe_state_quarter(Coin::Quarter(UsState::Alaska)) {
        println!("{desc}");
    }
}

Затем мы могли бы использовать if let, чтобы сопоставить тип монеты и ввести переменную state внутри тела условия, как в листинге 6-7.

#[derive(Debug)] // so we can inspect the state in a minute
enum UsState {
    Alabama,
    Alaska,
    // --snip--
}

impl UsState {
    fn existed_in(&self, year: u16) -> bool {
        match self {
            UsState::Alabama => year >= 1819,
            UsState::Alaska => year >= 1959,
            // -- snip --
        }
    }
}

enum Coin {
    Penny,
    Nickel,
    Dime,
    Quarter(UsState),
}

fn describe_state_quarter(coin: Coin) -> Option<String> {
    if let Coin::Quarter(state) = coin {
        if state.existed_in(1900) {
            Some(format!("{state:?} is pretty old, for America!"))
        } else {
            Some(format!("{state:?} is relatively new."))
        }
    } else {
        None
    }
}

fn main() {
    if let Some(desc) = describe_state_quarter(Coin::Quarter(UsState::Alaska)) {
        println!("{desc}");
    }
}
Listing 6-7: Проверка, существовал ли штат в 1900 году, с помощью условий, вложенных внутрь if let

Это решает задачу, но переносит работу в тело инструкции if let, и если работа, которую нужно выполнить, становится сложнее, может быть трудно понять, как именно соотносятся ветви верхнего уровня. Мы также могли бы воспользоваться тем, что выражения создают значение: либо получить state из if let, либо выполнить ранний возврат, как в листинге 6-8. (Нечто похожее можно сделать и с match.)

#[derive(Debug)] // so we can inspect the state in a minute
enum UsState {
    Alabama,
    Alaska,
    // --snip--
}

impl UsState {
    fn existed_in(&self, year: u16) -> bool {
        match self {
            UsState::Alabama => year >= 1819,
            UsState::Alaska => year >= 1959,
            // -- snip --
        }
    }
}

enum Coin {
    Penny,
    Nickel,
    Dime,
    Quarter(UsState),
}

fn describe_state_quarter(coin: Coin) -> Option<String> {
    let state = if let Coin::Quarter(state) = coin {
        state
    } else {
        return None;
    };

    if state.existed_in(1900) {
        Some(format!("{state:?} is pretty old, for America!"))
    } else {
        Some(format!("{state:?} is relatively new."))
    }
}

fn main() {
    if let Some(desc) = describe_state_quarter(Coin::Quarter(UsState::Alaska)) {
        println!("{desc}");
    }
}
Listing 6-8: Использование if let, чтобы получить значение или выполнить ранний возврат

Но и за этим следить по-своему немного неудобно! Одна ветвь if let создает значение, а другая полностью возвращает управление из функции.

Чтобы этот распространенный шаблон было приятнее выражать, в Rust есть let...else. Синтаксис let...else принимает образец слева и выражение справа, очень похоже на if let, но у него нет ветви if, есть только ветвь else. Если образец совпадает, он связывает значение из образца во внешней области видимости. Если образец не совпадает, программа переходит в ветвь else, которая должна вернуть управление из функции.

В листинге 6-9 показано, как выглядит листинг 6-8 при использовании let...else вместо if let.

#[derive(Debug)] // so we can inspect the state in a minute
enum UsState {
    Alabama,
    Alaska,
    // --snip--
}

impl UsState {
    fn existed_in(&self, year: u16) -> bool {
        match self {
            UsState::Alabama => year >= 1819,
            UsState::Alaska => year >= 1959,
            // -- snip --
        }
    }
}

enum Coin {
    Penny,
    Nickel,
    Dime,
    Quarter(UsState),
}

fn describe_state_quarter(coin: Coin) -> Option<String> {
    let Coin::Quarter(state) = coin else {
        return None;
    };

    if state.existed_in(1900) {
        Some(format!("{state:?} is pretty old, for America!"))
    } else {
        Some(format!("{state:?} is relatively new."))
    }
}

fn main() {
    if let Some(desc) = describe_state_quarter(Coin::Quarter(UsState::Alaska)) {
        println!("{desc}");
    }
}
Listing 6-9: Использование let...else, чтобы сделать поток выполнения функции понятнее

Обратите внимание, что так основной текст функции остается на «счастливом пути»: без двух ветвей с существенно разным потоком управления, как это было с if let.

Если у вас возникла ситуация, в которой логика программы слишком многословна для выражения с помощью match, помните, что if let и let...else тоже есть в вашем наборе инструментов Rust.

Итоги

Теперь мы рассмотрели, как использовать enum для создания пользовательских типов, которые могут быть одним из набора перечисленных значений. Мы показали, как тип Option<T> из стандартной библиотеки помогает использовать систему типов для предотвращения ошибок. Когда значения enum содержат данные внутри себя, можно использовать match или if let, чтобы извлечь и использовать эти значения, в зависимости от того, сколько случаев нужно обработать.

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

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

Пакеты, крейты и модули

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

Программы, которые мы писали до сих пор, находились в одном модуле в одном файле. По мере роста проекта следует организовывать код, разделяя его сначала на несколько модулей, а затем на несколько файлов. Пакет может содержать несколько бинарных крейтов и необязательно один библиотечный крейт. По мере роста пакета можно выносить его части в отдельные крейты, которые становятся внешними зависимостями. Эта глава охватывает все эти приемы. Для очень больших проектов, состоящих из набора взаимосвязанных пакетов, которые развиваются вместе, Cargo предоставляет рабочие пространства, которые мы рассмотрим в разделе «Рабочие пространства Cargo» главы 14.

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

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

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

  • Пакеты: возможность Cargo, которая позволяет собирать, тестировать и распространять крейты
  • Крейты: дерево модулей, которое создает библиотеку или исполняемый файл
  • Модули и use: позволяют управлять организацией, областью видимости и приватностью путей
  • Пути: способ назвать элемент, например структуру, функцию или модуль

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

Пакеты и крейты

Пакеты и крейты

Первые части системы модулей, которые мы рассмотрим, – это пакеты и крейты.

Крейт – это наименьший объем кода, который компилятор Rust рассматривает за один раз. Даже если вы запускаете rustc, а не cargo, и передаете один файл исходного кода (как мы делали еще в разделе «Основы программы на Rust» главы 1), компилятор считает этот файл крейтом. Крейты могут содержать модули, а модули могут быть определены в других файлах, которые компилируются вместе с крейтом, как мы увидим в следующих разделах.

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

Библиотечные крейты не имеют функции main и не компилируются в исполняемый файл. Вместо этого они определяют функциональность, предназначенную для совместного использования несколькими проектами. Например, крейт rand, который мы использовали в главе 2, предоставляет функциональность для генерации случайных чисел. В большинстве случаев, когда разработчики Rust говорят «крейт», они имеют в виду библиотечный крейт и используют слово «крейт» взаимозаменяемо с общим программистским термином «библиотека».

Корень крейта – это исходный файл, с которого компилятор Rust начинает работу и который образует корневой модуль вашего крейта (мы подробно объясним модули в разделе «Управление областью видимости и приватностью с помощью модулей»).

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

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

Давайте пройдемся по тому, что происходит, когда мы создаем пакет. Сначала мы вводим команду cargo new my-project:

$ cargo new my-project
     Created binary (application) `my-project` package
$ ls my-project
Cargo.toml
src
$ ls my-project/src
main.rs

После запуска cargo new my-project мы используем ls, чтобы увидеть, что создал Cargo. В каталоге my-project находится файл Cargo.toml, так что у нас есть пакет. Также есть каталог src, который содержит main.rs. Откройте Cargo.toml в текстовом редакторе и обратите внимание, что там нет упоминания src/main.rs. Cargo следует соглашению, по которому src/main.rs является корнем бинарного крейта с тем же именем, что и пакет. Аналогично Cargo знает: если каталог пакета содержит src/lib.rs, пакет содержит библиотечный крейт с тем же именем, что и пакет, а src/lib.rs является его корнем крейта. Cargo передает файлы корней крейтов в rustc, чтобы собрать библиотеку или бинарный файл.

Здесь у нас есть пакет, который содержит только src/main.rs, то есть он содержит только бинарный крейт с именем my-project. Если пакет содержит src/main.rs и src/lib.rs, в нем два крейта: бинарный и библиотечный, оба с тем же именем, что и пакет. Пакет может иметь несколько бинарных крейтов, если поместить файлы в каталог src/bin: каждый файл будет отдельным бинарным крейтом.

Управление областью видимости и приватностью с помощью модулей

Управление областью видимости и приватностью с помощью модулей

В этом разделе мы поговорим о модулях и других частях системы модулей, а именно о путях, которые позволяют называть элементы; о ключевом слове use, которое вводит путь в область видимости; и о ключевом слове pub, которое делает элементы публичными. Мы также обсудим ключевое слово as, внешние пакеты и оператор glob.

Шпаргалка по модулям

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

  • Начинайте с корня крейта: при компиляции крейта компилятор сначала ищет код для компиляции в файле корня крейта (обычно src/lib.rs для библиотечного крейта и src/main.rs для бинарного крейта).
  • Объявление модулей: в файле корня крейта можно объявлять новые модули; допустим, вы объявляете модуль “garden” с помощью mod garden;. Компилятор будет искать код модуля в следующих местах:
    • Встроенным образом, внутри фигурных скобок, которые заменяют точку с запятой после mod garden
    • В файле src/garden.rs
    • В файле src/garden/mod.rs
  • Объявление подмодулей: в любом файле, кроме корня крейта, можно объявлять подмодули. Например, вы можете объявить mod vegetables; в src/garden.rs. Компилятор будет искать код подмодуля внутри каталога, названного по имени родительского модуля, в следующих местах:
    • Встроенным образом, сразу после mod vegetables, внутри фигурных скобок вместо точки с запятой
    • В файле src/garden/vegetables.rs
    • В файле src/garden/vegetables/mod.rs
  • Пути к коду в модулях: как только модуль становится частью вашего крейта, вы можете обращаться к коду в этом модуле из любого другого места того же крейта, если это позволяют правила приватности, используя путь к коду. Например, тип Asparagus в модуле vegetables, вложенном в garden, можно найти по пути crate::garden::vegetables::Asparagus.
  • Приватное и публичное: код внутри модуля по умолчанию приватен для его родительских модулей. Чтобы сделать модуль публичным, объявите его с помощью pub mod вместо mod. Чтобы элементы внутри публичного модуля тоже стали публичными, используйте pub перед их объявлениями.
  • Ключевое слово use: внутри области видимости ключевое слово use создает сокращения для элементов, чтобы уменьшить повторение длинных путей. В любой области видимости, которая может обратиться к crate::garden::vegetables::Asparagus, можно создать сокращение с помощью use crate::garden::vegetables::Asparagus;, и с этого момента в этой области видимости достаточно писать Asparagus, чтобы использовать этот тип.

Здесь мы создаем бинарный крейт с именем backyard, который иллюстрирует эти правила. Каталог крейта, также названный backyard, содержит такие файлы и каталоги:

backyard
├── Cargo.lock
├── Cargo.toml
└── src
    ├── garden
    │   └── vegetables.rs
    ├── garden.rs
    └── main.rs

Файл корня крейта в этом случае – src/main.rs, и он содержит:

Filename: src/main.rs
use crate::garden::vegetables::Asparagus;

pub mod garden;

fn main() {
    let plant = Asparagus {};
    println!("I'm growing {plant:?}!");
}

Строка pub mod garden; сообщает компилятору включить код, который он найдет в src/garden.rs, а именно:

Filename: src/garden.rs
pub mod vegetables;

Здесь pub mod vegetables; означает, что код из src/garden/vegetables.rs тоже включается. Этот код:

#[derive(Debug)]
pub struct Asparagus {}

Теперь перейдем к деталям этих правил и покажем их в действии!

Группирование связанного кода в модулях

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

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

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

Чтобы структурировать наш крейт таким образом, мы можем организовать его функции во вложенные модули. Создайте новую библиотеку с именем restaurant, выполнив cargo new restaurant --lib. Затем введите код из листинга 7-1 в src/lib.rs, чтобы определить несколько модулей и сигнатур функций; этот код представляет часть, относящуюся к залу.

Filename: src/lib.rs
mod front_of_house {
    mod hosting {
        fn add_to_waitlist() {}

        fn seat_at_table() {}
    }

    mod serving {
        fn take_order() {}

        fn serve_order() {}

        fn take_payment() {}
    }
}
Listing 7-1: Модуль front_of_house, содержащий другие модули, которые затем содержат функции

Мы определяем модуль с помощью ключевого слова mod, за которым следует имя модуля (в этом случае front_of_house). Затем тело модуля помещается внутрь фигурных скобок. Внутри модулей можно размещать другие модули, как в этом случае с модулями hosting и serving. Модули также могут содержать определения других элементов, например структур, enum, констант, трейтов и, как в листинге 7-1, функций.

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

Ранее мы упоминали, что src/main.rs и src/lib.rs называются корнями крейта. Причина такого названия в том, что содержимое любого из этих двух файлов образует модуль с именем crate в корне модульной структуры крейта, известной как дерево модулей.

Листинг 7-2 показывает дерево модулей для структуры из листинга 7-1.

crate
 └── front_of_house
     ├── hosting
     │   ├── add_to_waitlist
     │   └── seat_at_table
     └── serving
         ├── take_order
         ├── serve_order
         └── take_payment
Listing 7-2: Дерево модулей для кода из листинга 7-1

Это дерево показывает, как некоторые модули вложены в другие модули; например, hosting вложен в front_of_house. Дерево также показывает, что некоторые модули являются соседними, то есть они определены в одном и том же модуле; hosting и serving – соседние модули, определенные внутри front_of_house. Если модуль A содержится внутри модуля B, мы говорим, что модуль A является дочерним модулем B, а модуль B является родительским модулем A. Обратите внимание, что все дерево модулей находится под корнем в неявном модуле с именем crate.

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

Пути для обращения к элементу в дереве модулей

Пути для обращения к элементу в дереве модулей

Чтобы показать Rust, где найти элемент в дереве модулей, мы используем путь так же, как используем путь при навигации по файловой системе. Чтобы вызвать функцию, нам нужно знать ее путь.

Путь может иметь две формы:

  • Абсолютный путь – это полный путь, начинающийся от корня крейта; для кода из внешнего крейта абсолютный путь начинается с имени крейта, а для кода из текущего крейта он начинается с литерала crate.
  • Относительный путь начинается от текущего модуля и использует self, super или идентификатор в текущем модуле.

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

Вернемся к листингу 7-1. Допустим, мы хотим вызвать функцию add_to_waitlist. Это то же самое, что спросить: какой путь у функции add_to_waitlist? Листинг 7-3 содержит листинг 7-1, из которого удалена часть модулей и функций.

Мы покажем два способа вызвать функцию add_to_waitlist из новой функции eat_at_restaurant, определенной в корне крейта. Эти пути правильные, но остается еще одна проблема, из-за которой этот пример пока не скомпилируется в таком виде. Скоро мы объясним почему.

Функция eat_at_restaurant является частью публичного API нашего библиотечного крейта, поэтому мы помечаем ее ключевым словом pub. В разделе «Открытие путей с помощью ключевого слова pub» мы подробнее рассмотрим pub.

Filename: src/lib.rs
mod front_of_house {
    mod hosting {
        fn add_to_waitlist() {}
    }
}

pub fn eat_at_restaurant() {
    // Absolute path
    crate::front_of_house::hosting::add_to_waitlist();

    // Relative path
    front_of_house::hosting::add_to_waitlist();
}
Listing 7-3: Вызов функции add_to_waitlist с использованием абсолютного и относительного путей

При первом вызове функции add_to_waitlist в eat_at_restaurant мы используем абсолютный путь. Функция add_to_waitlist определена в том же крейте, что и eat_at_restaurant, а значит, мы можем использовать ключевое слово crate, чтобы начать абсолютный путь. Затем мы указываем каждый последующий модуль, пока не доберемся до add_to_waitlist. Можно представить файловую систему с такой же структурой: чтобы запустить программу add_to_waitlist, мы указали бы путь /front_of_house/hosting/add_to_waitlist; использование имени crate для начала от корня крейта похоже на использование / для начала от корня файловой системы в командной оболочке.

При втором вызове add_to_waitlist в eat_at_restaurant мы используем относительный путь. Путь начинается с front_of_house, имени модуля, определенного на том же уровне дерева модулей, что и eat_at_restaurant. Эквивалентом в файловой системе здесь был бы путь front_of_house/hosting/add_to_waitlist. Начало с имени модуля означает, что путь относительный.

Выбор между относительным и абсолютным путем – это решение, которое вы будете принимать в зависимости от проекта; оно зависит от того, насколько вероятно, что вы будете перемещать код определения элемента отдельно от кода, который использует этот элемент, или вместе с ним. Например, если бы мы переместили модуль front_of_house и функцию eat_at_restaurant в модуль с именем customer_experience, нам пришлось бы обновить абсолютный путь к add_to_waitlist, но относительный путь остался бы действительным. Однако если бы мы отдельно переместили функцию eat_at_restaurant в модуль с именем dining, абсолютный путь к вызову add_to_waitlist остался бы тем же, а относительный путь пришлось бы обновить. В целом мы предпочитаем указывать абсолютные пути, потому что чаще хотим перемещать определения кода и вызовы элементов независимо друг от друга.

Попробуем скомпилировать листинг 7-3 и выяснить, почему он пока не компилируется! Ошибки, которые мы получим, показаны в листинге 7-4.

$ cargo build
   Compiling restaurant v0.1.0 (file:///projects/restaurant)
error[E0603]: module `hosting` is private
 --> src/lib.rs:9:28
  |
9 |     crate::front_of_house::hosting::add_to_waitlist();
  |                            ^^^^^^^  --------------- function `add_to_waitlist` is not publicly re-exported
  |                            |
  |                            private module
  |
note: the module `hosting` is defined here
 --> src/lib.rs:2:5
  |
2 |     mod hosting {
  |     ^^^^^^^^^^^

error[E0603]: module `hosting` is private
  --> src/lib.rs:12:21
   |
12 |     front_of_house::hosting::add_to_waitlist();
   |                     ^^^^^^^  --------------- function `add_to_waitlist` is not publicly re-exported
   |                     |
   |                     private module
   |
note: the module `hosting` is defined here
  --> src/lib.rs:2:5
   |
 2 |     mod hosting {
   |     ^^^^^^^^^^^

For more information about this error, try `rustc --explain E0603`.
error: could not compile `restaurant` (lib) due to 2 previous errors
Listing 7-4: Ошибки компилятора при сборке кода из листинга 7-3

Сообщения об ошибках говорят, что модуль hosting приватный. Иными словами, у нас правильные пути к модулю hosting и функции add_to_waitlist, но Rust не позволит использовать их, потому что у него нет доступа к приватным частям. В Rust все элементы (функции, методы, структуры, enum, модули и константы) по умолчанию приватны для родительских модулей. Если вы хотите сделать элемент, например функцию или структуру, приватным, вы помещаете его в модуль.

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

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

Открытие путей с помощью ключевого слова pub

Вернемся к ошибке из листинга 7-4, которая сообщила, что модуль hosting приватный. Мы хотим, чтобы функция eat_at_restaurant в родительском модуле имела доступ к функции add_to_waitlist в дочернем модуле, поэтому помечаем модуль hosting ключевым словом pub, как показано в листинге 7-5.

Filename: src/lib.rs
mod front_of_house {
    pub mod hosting {
        fn add_to_waitlist() {}
    }
}

// -- snip --
pub fn eat_at_restaurant() {
    // Absolute path
    crate::front_of_house::hosting::add_to_waitlist();

    // Relative path
    front_of_house::hosting::add_to_waitlist();
}
Listing 7-5: Объявление модуля hosting как pub, чтобы использовать его из eat_at_restaurant

К сожалению, код в листинге 7-5 все равно приводит к ошибкам компилятора, как показано в листинге 7-6.

$ cargo build
   Compiling restaurant v0.1.0 (file:///projects/restaurant)
error[E0603]: function `add_to_waitlist` is private
  --> src/lib.rs:10:37
   |
10 |     crate::front_of_house::hosting::add_to_waitlist();
   |                                     ^^^^^^^^^^^^^^^ private function
   |
note: the function `add_to_waitlist` is defined here
  --> src/lib.rs:3:9
   |
 3 |         fn add_to_waitlist() {}
   |         ^^^^^^^^^^^^^^^^^^^^

error[E0603]: function `add_to_waitlist` is private
  --> src/lib.rs:13:30
   |
13 |     front_of_house::hosting::add_to_waitlist();
   |                              ^^^^^^^^^^^^^^^ private function
   |
note: the function `add_to_waitlist` is defined here
  --> src/lib.rs:3:9
   |
 3 |         fn add_to_waitlist() {}
   |         ^^^^^^^^^^^^^^^^^^^^

For more information about this error, try `rustc --explain E0603`.
error: could not compile `restaurant` (lib) due to 2 previous errors
Listing 7-6: Ошибки компилятора при сборке кода из листинга 7-5

Что произошло? Добавление ключевого слова pub перед mod hosting делает модуль публичным. С этим изменением, если мы можем получить доступ к front_of_house, мы можем получить доступ к hosting. Но содержимое hosting все еще приватно; публичность модуля не делает его содержимое публичным. Ключевое слово pub у модуля только позволяет коду в модулях-предках ссылаться на него, но не получать доступ к его внутреннему коду. Поскольку модули являются контейнерами, от одной только публичности модуля мало пользы; нужно пойти дальше и решить сделать один или несколько элементов внутри модуля тоже публичными.

Ошибки в листинге 7-6 говорят, что функция add_to_waitlist приватная. Правила приватности применяются к структурам, enum, функциям и методам так же, как и к модулям.

Сделаем функцию add_to_waitlist тоже публичной, добавив ключевое слово pub перед ее определением, как в листинге 7-7.

Filename: src/lib.rs
mod front_of_house {
    pub mod hosting {
        pub fn add_to_waitlist() {}
    }
}

// -- snip --
pub fn eat_at_restaurant() {
    // Absolute path
    crate::front_of_house::hosting::add_to_waitlist();

    // Relative path
    front_of_house::hosting::add_to_waitlist();
}
Listing 7-7: Добавление ключевого слова pub к mod hosting и fn add_to_waitlist позволяет вызвать функцию из eat_at_restaurant.

Теперь код скомпилируется! Чтобы понять, почему добавление ключевого слова pub позволяет использовать эти пути в eat_at_restaurant с точки зрения правил приватности, рассмотрим абсолютный и относительный пути.

В абсолютном пути мы начинаем с crate, корня дерева модулей нашего крейта. Модуль front_of_house определен в корне крейта. Хотя front_of_house не публичный, функция eat_at_restaurant определена в том же модуле, что и front_of_house (то есть eat_at_restaurant и front_of_house являются соседними элементами), поэтому мы можем ссылаться на front_of_house из eat_at_restaurant. Далее идет модуль hosting, помеченный pub. Мы можем получить доступ к родительскому модулю hosting, поэтому можем получить доступ к hosting. Наконец, функция add_to_waitlist помечена pub, и мы можем получить доступ к ее родительскому модулю, поэтому этот вызов функции работает!

В относительном пути логика такая же, как и в абсолютном пути, за исключением первого шага: вместо начала от корня крейта путь начинается с front_of_house. Модуль front_of_house определен внутри того же модуля, что и eat_at_restaurant, поэтому относительный путь, начинающийся от модуля, в котором определена функция eat_at_restaurant, работает. Затем, поскольку hosting и add_to_waitlist помечены pub, остальная часть пути работает, и этот вызов функции допустим!

Если вы планируете распространять свой библиотечный крейт, чтобы другие проекты могли использовать ваш код, публичный API является вашим контрактом с пользователями крейта и определяет, как они могут взаимодействовать с вашим кодом. В управлении изменениями публичного API есть много нюансов, которые помогают людям проще зависеть от вашего крейта. Эти вопросы выходят за рамки этой книги; если вам интересна эта тема, смотрите Рекомендации по API Rust.

Лучшие практики для пакетов с бинарным и библиотечным крейтом

Мы упоминали, что пакет может содержать как корень бинарного крейта src/main.rs, так и корень библиотечного крейта src/lib.rs, и оба крейта по умолчанию будут иметь имя пакета. Обычно пакеты с такой схемой, содержащие и библиотечный, и бинарный крейт, имеют в бинарном крейте ровно столько кода, сколько нужно для запуска исполняемого файла, который вызывает код, определенный в библиотечном крейте. Это позволяет другим проектам извлечь пользу из большей части функциональности, предоставляемой пакетом, потому что код библиотечного крейта можно использовать совместно.

Дерево модулей должно быть определено в src/lib.rs. Затем любые публичные элементы можно использовать в бинарном крейте, начиная пути с имени пакета. Бинарный крейт становится пользователем библиотечного крейта точно так же, как полностью внешний крейт использовал бы библиотечный крейт: он может использовать только публичный API. Это помогает проектировать хороший API: вы не только автор, но и клиент!

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

Начало относительных путей с super

Мы можем строить относительные пути, которые начинаются в родительском модуле, а не в текущем модуле или корне крейта, используя super в начале пути. Это похоже на начало пути файловой системы с синтаксиса .., который означает переход к родительскому каталогу. Использование super позволяет ссылаться на элемент, который, как мы знаем, находится в родительском модуле; это может упростить перестройку дерева модулей, когда модуль тесно связан с родителем, но родитель когда-нибудь может быть перемещен в другое место дерева модулей.

Рассмотрим код в листинге 7-8, который моделирует ситуацию, когда повар исправляет неправильный заказ и лично выносит его клиенту. Функция fix_incorrect_order, определенная в модуле back_of_house, вызывает функцию deliver_order, определенную в родительском модуле, указывая путь к deliver_order, начинающийся с super.

Filename: src/lib.rs
fn deliver_order() {}

mod back_of_house {
    fn fix_incorrect_order() {
        cook_order();
        super::deliver_order();
    }

    fn cook_order() {}
}
Listing 7-8: Вызов функции с использованием относительного пути, начинающегося с super

Функция fix_incorrect_order находится в модуле back_of_house, поэтому мы можем использовать super, чтобы перейти к родительскому модулю back_of_house, которым в этом случае является crate, корень. Оттуда мы ищем deliver_order и находим его. Успех! Мы считаем, что модуль back_of_house и функция deliver_order, скорее всего, останутся в таком же отношении друг к другу и будут перемещаться вместе, если мы решим реорганизовать дерево модулей крейта. Поэтому мы использовали super, чтобы в будущем было меньше мест, где нужно обновлять код, если этот код переместится в другой модуль.

Как сделать структуры и enum публичными

Мы также можем использовать pub, чтобы обозначить структуры и enum как публичные, но в использовании pub со структурами и enum есть несколько дополнительных деталей. Если мы используем pub перед определением структуры, мы делаем структуру публичной, но поля структуры по-прежнему будут приватными. Каждое поле можно сделать публичным или оставить приватным отдельно. В листинге 7-9 мы определили публичную структуру back_of_house::Breakfast с публичным полем toast и приватным полем seasonal_fruit. Это моделирует случай в ресторане, где клиент может выбрать тип хлеба, который идет с едой, но повар решает, какой фрукт подается с блюдом, исходя из сезона и наличия на складе. Доступные фрукты быстро меняются, поэтому клиенты не могут выбрать фрукт и даже увидеть, какой фрукт они получат.

Filename: src/lib.rs
mod back_of_house {
    pub struct Breakfast {
        pub toast: String,
        seasonal_fruit: String,
    }

    impl Breakfast {
        pub fn summer(toast: &str) -> Breakfast {
            Breakfast {
                toast: String::from(toast),
                seasonal_fruit: String::from("peaches"),
            }
        }
    }
}

pub fn eat_at_restaurant() {
    // Order a breakfast in the summer with Rye toast.
    let mut meal = back_of_house::Breakfast::summer("Rye");
    // Change our mind about what bread we'd like.
    meal.toast = String::from("Wheat");
    println!("I'd like {} toast please", meal.toast);

    // The next line won't compile if we uncomment it; we're not allowed
    // to see or modify the seasonal fruit that comes with the meal.
    // meal.seasonal_fruit = String::from("blueberries");
}
Listing 7-9: Структура с некоторыми публичными и некоторыми приватными полями

Поскольку поле toast в структуре back_of_house::Breakfast публичное, в eat_at_restaurant мы можем записывать и читать поле toast, используя точечную нотацию. Обратите внимание, что мы не можем использовать поле seasonal_fruit в eat_at_restaurant, потому что seasonal_fruit приватное. Попробуйте раскомментировать строку, изменяющую значение поля seasonal_fruit, чтобы увидеть, какую ошибку вы получите!

Также обратите внимание: поскольку у back_of_house::Breakfast есть приватное поле, структура должна предоставить публичную ассоциированную функцию, которая создает экземпляр Breakfast (здесь мы назвали ее summer). Если бы у Breakfast не было такой функции, мы не смогли бы создать экземпляр Breakfast в eat_at_restaurant, потому что не могли бы задать значение приватного поля seasonal_fruit в eat_at_restaurant.

В отличие от этого, если мы делаем enum публичным, все его варианты тоже становятся публичными. Нам нужно поставить pub только перед ключевым словом enum, как показано в листинге 7-10.

Filename: src/lib.rs
mod back_of_house {
    pub enum Appetizer {
        Soup,
        Salad,
    }
}

pub fn eat_at_restaurant() {
    let order1 = back_of_house::Appetizer::Soup;
    let order2 = back_of_house::Appetizer::Salad;
}
Listing 7-10: Обозначение enum как публичного делает все его варианты публичными.

Поскольку мы сделали enum Appetizer публичным, мы можем использовать варианты Soup и Salad в eat_at_restaurant.

Enum не очень полезны, если их варианты не публичны; было бы утомительно помечать все варианты enum с помощью pub в каждом случае, поэтому по умолчанию варианты enum являются публичными. Структуры часто полезны и без публичных полей, поэтому поля структур следуют общему правилу: все приватно по умолчанию, если не помечено pub.

Есть еще одна ситуация, связанная с pub, которую мы не рассмотрели, и это последняя возможность системы модулей: ключевое слово use. Сначала мы рассмотрим use отдельно, а затем покажем, как сочетать pub и use.

Добавление путей в область видимости с помощью ключевого слова use

Введение путей в область видимости с помощью ключевого слова use

Необходимость каждый раз полностью записывать пути для вызова функций может казаться неудобной и повторяющейся. В листинге 7-7, независимо от того, выбрали ли мы абсолютный или относительный путь к функции add_to_waitlist, каждый раз при вызове add_to_waitlist нам также приходилось указывать front_of_house и hosting. К счастью, есть способ упростить этот процесс: мы можем один раз создать сокращение для пути с помощью ключевого слова use, а затем использовать более короткое имя во всей остальной области видимости.

В листинге 7-11 мы вводим модуль crate::front_of_house::hosting в область видимости функции eat_at_restaurant, чтобы для вызова функции add_to_waitlist в eat_at_restaurant нам нужно было указывать только hosting::add_to_waitlist.

Filename: src/lib.rs
mod front_of_house {
    pub mod hosting {
        pub fn add_to_waitlist() {}
    }
}

use crate::front_of_house::hosting;

pub fn eat_at_restaurant() {
    hosting::add_to_waitlist();
}
Listing 7-11: Введение модуля в область видимости с помощью use

Добавление use и пути в область видимости похоже на создание символической ссылки в файловой системе. Добавляя use crate::front_of_house::hosting в корень крейта, мы делаем hosting допустимым именем в этой области видимости, как если бы модуль hosting был определен в корне крейта. Пути, введенные в область видимости с помощью use, также проверяются на приватность, как и любые другие пути.

Обратите внимание, что use создает сокращение только для той конкретной области видимости, в которой он находится. Листинг 7-12 перемещает функцию eat_at_restaurant в новый дочерний модуль с именем customer, который является областью видимости, отличной от инструкции use, поэтому тело функции не скомпилируется.

Filename: src/lib.rs
mod front_of_house {
    pub mod hosting {
        pub fn add_to_waitlist() {}
    }
}

use crate::front_of_house::hosting;

mod customer {
    pub fn eat_at_restaurant() {
        hosting::add_to_waitlist();
    }
}
Listing 7-12: Инструкция use применяется только в той области видимости, где она находится.

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

$ cargo build
   Compiling restaurant v0.1.0 (file:///projects/restaurant)
error[E0433]: failed to resolve: use of unresolved module or unlinked crate `hosting`
  --> src/lib.rs:11:9
   |
11 |         hosting::add_to_waitlist();
   |         ^^^^^^^ use of unresolved module or unlinked crate `hosting`
   |
   = help: if you wanted to use a crate named `hosting`, use `cargo add hosting` to add it to your `Cargo.toml`
help: consider importing this module through its public re-export
   |
10 +     use crate::hosting;
   |

warning: unused import: `crate::front_of_house::hosting`
 --> src/lib.rs:7:5
  |
7 | use crate::front_of_house::hosting;
  |     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  |
  = note: `#[warn(unused_imports)]` on by default

For more information about this error, try `rustc --explain E0433`.
warning: `restaurant` (lib) generated 1 warning
error: could not compile `restaurant` (lib) due to 1 previous error; 1 warning emitted

Обратите внимание, что также есть предупреждение: use больше не используется в своей области видимости! Чтобы исправить эту проблему, переместите use также внутрь модуля customer или обращайтесь к сокращению в родительском модуле через super::hosting внутри дочернего модуля customer.

Создание идиоматичных путей use

В листинге 7-11 вы могли задаться вопросом, почему мы указали use crate::front_of_house::hosting, а затем вызвали hosting::add_to_waitlist в eat_at_restaurant, вместо того чтобы указать путь use до самой функции add_to_waitlist и получить тот же результат, как в листинге 7-13.

Filename: src/lib.rs
mod front_of_house {
    pub mod hosting {
        pub fn add_to_waitlist() {}
    }
}

use crate::front_of_house::hosting::add_to_waitlist;

pub fn eat_at_restaurant() {
    add_to_waitlist();
}
Listing 7-13: Введение функции add_to_waitlist в область видимости с помощью use, что не является идиоматичным

Хотя листинг 7-11 и листинг 7-13 выполняют одну и ту же задачу, листинг 7-11 является идиоматичным способом ввести функцию в область видимости с помощью use. Введение родительского модуля функции в область видимости с помощью use означает, что при вызове функции нужно указывать родительский модуль. Указание родительского модуля при вызове функции ясно показывает, что функция не определена локально, но при этом все еще сокращает повторение полного пути. В коде листинга 7-13 неясно, где определена add_to_waitlist.

С другой стороны, при введении структур, enum и других элементов в область видимости с помощью use идиоматично указывать полный путь. Листинг 7-14 показывает идиоматичный способ ввести структуру HashMap из стандартной библиотеки в область видимости бинарного крейта.

Filename: src/main.rs
use std::collections::HashMap;

fn main() {
    let mut map = HashMap::new();
    map.insert(1, 2);
}
Listing 7-14: Идиоматичное введение HashMap в область видимости

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

Исключение из этой идиомы возникает, если мы вводим в область видимости два элемента с одним и тем же именем с помощью инструкций use, потому что Rust не разрешает этого. Листинг 7-15 показывает, как ввести в область видимости два типа Result, у которых одинаковое имя, но разные родительские модули, и как к ним обращаться.

Filename: src/lib.rs
use std::fmt;
use std::io;

fn function1() -> fmt::Result {
    // --snip--
    Ok(())
}

fn function2() -> io::Result<()> {
    // --snip--
    Ok(())
}
Listing 7-15: Введение двух типов с одинаковым именем в одну область видимости требует использования их родительских модулей.

Как видите, использование родительских модулей различает два типа Result. Если бы вместо этого мы указали use std::fmt::Result и use std::io::Result, в одной области видимости оказалось бы два типа Result, и Rust не знал бы, какой из них мы имеем в виду при использовании Result.

Предоставление новых имен с помощью ключевого слова as

Есть еще одно решение проблемы введения двух типов с одинаковым именем в одну область видимости с помощью use: после пути можно указать as и новое локальное имя, или псевдоним, для типа. Листинг 7-16 показывает другой способ записать код из листинга 7-15, переименовав один из двух типов Result с помощью as.

Filename: src/lib.rs
use std::fmt::Result;
use std::io::Result as IoResult;

fn function1() -> Result {
    // --snip--
    Ok(())
}

fn function2() -> IoResult<()> {
    // --snip--
    Ok(())
}
Listing 7-16: Переименование типа при введении его в область видимости с помощью ключевого слова as

Во второй инструкции use мы выбрали новое имя IoResult для типа std::io::Result, которое не будет конфликтовать с Result из std::fmt, также введенным в область видимости. Листинги 7-15 и 7-16 считаются идиоматичными, поэтому выбор за вами!

Повторный экспорт имен с помощью pub use

Когда мы вводим имя в область видимости с помощью ключевого слова use, это имя является приватным для области видимости, в которую мы его импортировали. Чтобы код вне этой области видимости мог обращаться к этому имени так, как если бы оно было определено в этой области, можно объединить pub и use. Этот прием называется повторным экспортом, потому что мы вводим элемент в область видимости и одновременно делаем его доступным для другого кода, чтобы тот мог вводить этот элемент в свою область видимости.

Листинг 7-17 показывает код из листинга 7-11, где use в корневом модуле заменен на pub use.

Filename: src/lib.rs
mod front_of_house {
    pub mod hosting {
        pub fn add_to_waitlist() {}
    }
}

pub use crate::front_of_house::hosting;

pub fn eat_at_restaurant() {
    hosting::add_to_waitlist();
}
Listing 7-17: Предоставление имени любому коду для использования из новой области видимости с помощью pub use

До этого изменения внешний код должен был бы вызывать функцию add_to_waitlist по пути restaurant::front_of_house::hosting::add_to_waitlist(), что также требовало бы пометить модуль front_of_house как pub. Теперь, когда этот pub use повторно экспортировал модуль hosting из корневого модуля, внешний код может вместо этого использовать путь restaurant::hosting::add_to_waitlist().

Повторный экспорт полезен, когда внутренняя структура вашего кода отличается от того, как программисты, вызывающие ваш код, воспринимали бы предметную область. Например, в этой ресторанной метафоре люди, управляющие рестораном, мыслят категориями «зал» и «служебная часть». Но клиенты, посещающие ресторан, скорее всего, не будут думать о частях ресторана в этих терминах. С помощью pub use мы можем писать код с одной структурой, но предоставлять внешнему коду другую структуру. Это делает нашу библиотеку хорошо организованной и для программистов, работающих над библиотекой, и для программистов, вызывающих библиотеку. Мы рассмотрим еще один пример pub use и то, как он влияет на документацию крейта, в разделе «Экспорт удобного публичного API» главы 14.

Использование внешних пакетов

В главе 2 мы программировали проект игры в угадывание, который использовал внешний пакет rand для получения случайных чисел. Чтобы использовать rand в нашем проекте, мы добавили эту строку в Cargo.toml:

Filename: Cargo.toml
rand = "0.8.5"

Добавление rand как зависимости в Cargo.toml говорит Cargo загрузить пакет rand и любые его зависимости с crates.io и сделать rand доступным нашему проекту.

Затем, чтобы ввести определения rand в область видимости нашего пакета, мы добавили строку use, начинающуюся с имени крейта, rand, и перечислили элементы, которые хотели ввести в область видимости. Вспомните, что в разделе «Генерация случайного числа» главы 2 мы ввели трейт Rng в область видимости и вызвали функцию rand::thread_rng:

use std::io;

use rand::Rng;

fn main() {
    println!("Guess the number!");

    let secret_number = rand::thread_rng().gen_range(1..=100);

    println!("The secret number is: {secret_number}");

    println!("Please input your guess.");

    let mut guess = String::new();

    io::stdin()
        .read_line(&mut guess)
        .expect("Failed to read line");

    println!("You guessed: {guess}");
}

Участники сообщества Rust сделали множество пакетов доступными на crates.io, и подключение любого из них к вашему пакету включает те же шаги: перечислить их в файле Cargo.toml вашего пакета и использовать use, чтобы ввести элементы из их крейтов в область видимости.

Обратите внимание, что стандартная библиотека std также является крейтом, внешним по отношению к нашему пакету. Поскольку стандартная библиотека поставляется вместе с языком Rust, нам не нужно менять Cargo.toml, чтобы включить std. Но нам нужно обращаться к ней с помощью use, чтобы ввести элементы оттуда в область видимости нашего пакета. Например, для HashMap мы использовали бы такую строку:

#![allow(unused)]
fn main() {
use std::collections::HashMap;
}

Это абсолютный путь, начинающийся с std, имени крейта стандартной библиотеки.

Использование вложенных путей для очистки списков use

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

Filename: src/main.rs
use rand::Rng;
// --snip--
use std::cmp::Ordering;
use std::io;
// --snip--

fn main() {
    println!("Guess the number!");

    let secret_number = rand::thread_rng().gen_range(1..=100);

    println!("The secret number is: {secret_number}");

    println!("Please input your guess.");

    let mut guess = String::new();

    io::stdin()
        .read_line(&mut guess)
        .expect("Failed to read line");

    println!("You guessed: {guess}");

    match guess.cmp(&secret_number) {
        Ordering::Less => println!("Too small!"),
        Ordering::Greater => println!("Too big!"),
        Ordering::Equal => println!("You win!"),
    }
}

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

Filename: src/main.rs
use rand::Rng;
// --snip--
use std::{cmp::Ordering, io};
// --snip--

fn main() {
    println!("Guess the number!");

    let secret_number = rand::thread_rng().gen_range(1..=100);

    println!("The secret number is: {secret_number}");

    println!("Please input your guess.");

    let mut guess = String::new();

    io::stdin()
        .read_line(&mut guess)
        .expect("Failed to read line");

    let guess: u32 = guess.trim().parse().expect("Please type a number!");

    println!("You guessed: {guess}");

    match guess.cmp(&secret_number) {
        Ordering::Less => println!("Too small!"),
        Ordering::Greater => println!("Too big!"),
        Ordering::Equal => println!("You win!"),
    }
}
Listing 7-18: Указание вложенного пути для введения нескольких элементов с одинаковым префиксом в область видимости

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

Мы можем использовать вложенный путь на любом уровне пути, что полезно при объединении двух инструкций use, у которых есть общая часть пути. Например, листинг 7-19 показывает две инструкции use: одна вводит std::io в область видимости, а другая вводит std::io::Write в область видимости.

Filename: src/lib.rs
use std::io;
use std::io::Write;
Listing 7-19: Две инструкции use, где одна является частью пути другой

Общая часть этих двух путей – std::io, и это полный первый путь. Чтобы объединить эти два пути в одну инструкцию use, мы можем использовать self во вложенном пути, как показано в листинге 7-20.

Filename: src/lib.rs
use std::io::{self, Write};
Listing 7-20: Объединение путей из листинга 7-19 в одну инструкцию use

Эта строка вводит std::io и std::io::Write в область видимости.

Импорт элементов с помощью glob-оператора

Если мы хотим ввести в область видимости все публичные элементы, определенные в пути, мы можем указать этот путь, за которым следует glob-оператор *:

#![allow(unused)]
fn main() {
use std::collections::*;
}

Эта инструкция use вводит все публичные элементы, определенные в std::collections, в текущую область видимости. Будьте осторожны при использовании glob-оператора! Glob может затруднить понимание того, какие имена находятся в области видимости и где было определено имя, используемое в вашей программе. Кроме того, если зависимость изменит свои определения, изменится и то, что вы импортировали; например, это может привести к ошибкам компилятора при обновлении зависимости, если зависимость добавит определение с тем же именем, что и ваше определение в той же области видимости.

Glob-оператор часто используется при тестировании, чтобы ввести все, что тестируется, в модуль tests; мы поговорим об этом в разделе «Как писать тесты» главы 11. Glob-оператор также иногда используется как часть шаблона прелюдии: дополнительную информацию об этом шаблоне смотрите в документации стандартной библиотеки.

Разделение модулей на отдельные файлы

Разделение модулей по разным файлам

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

Например, начнем с кода из листинга 7-17, где было несколько ресторанных модулей. Мы вынесем модули в файлы вместо того, чтобы определять все модули в файле корня крейта. В этом случае файл корня крейта – src/lib.rs, но эта процедура также работает с бинарными крейтами, у которых файл корня крейта – src/main.rs.

Сначала мы вынесем модуль front_of_house в собственный файл. Удалите код внутри фигурных скобок модуля front_of_house, оставив только объявление mod front_of_house;, чтобы src/lib.rs содержал код, показанный в листинге 7-21. Обратите внимание, что это не скомпилируется, пока мы не создадим файл src/front_of_house.rs из листинга 7-22.

Filename: src/lib.rs
mod front_of_house;

pub use crate::front_of_house::hosting;

pub fn eat_at_restaurant() {
    hosting::add_to_waitlist();
}
Listing 7-21: Объявление модуля front_of_house, тело которого будет находиться в src/front_of_house.rs

Затем поместите код, который был внутри фигурных скобок, в новый файл с именем src/front_of_house.rs, как показано в листинге 7-22. Компилятор знает, что нужно искать в этом файле, потому что он встретил объявление модуля с именем front_of_house в корне крейта.

Filename: src/front_of_house.rs
pub mod hosting {
    pub fn add_to_waitlist() {}
}
Listing 7-22: Определения внутри модуля front_of_house в src/front_of_house.rs

Обратите внимание, что файл нужно загрузить с помощью объявления mod только один раз в дереве модулей. После того как компилятор узнает, что файл является частью проекта (и узнает, где в дереве модулей находится код, из-за того, где вы поместили инструкцию mod), другие файлы проекта должны обращаться к коду загруженного файла по пути к месту, где он был объявлен, как описано в разделе «Пути для обращения к элементу в дереве модулей». Иными словами, mod – это не операция «include», которую вы могли видеть в других языках программирования.

Далее мы вынесем модуль hosting в собственный файл. Процесс немного отличается, потому что hosting – дочерний модуль front_of_house, а не корневого модуля. Мы поместим файл для hosting в новый каталог, который будет назван по его предкам в дереве модулей, в данном случае src/front_of_house.

Чтобы начать перенос hosting, изменим src/front_of_house.rs так, чтобы он содержал только объявление модуля hosting:

Filename: src/front_of_house.rs
pub mod hosting;

Затем мы создаем каталог src/front_of_house и файл hosting.rs, который будет содержать определения, сделанные в модуле hosting:

Filename: src/front_of_house/hosting.rs
pub fn add_to_waitlist() {}

Если бы вместо этого мы поместили hosting.rs в каталог src, компилятор ожидал бы, что код hosting.rs находится в модуле hosting, объявленном в корне крейта, а не объявлен как дочерний модуль front_of_house. Правила компилятора о том, какие файлы проверять для кода каких модулей, означают, что каталоги и файлы ближе соответствуют дереву модулей.

Альтернативные пути к файлам

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

  • src/front_of_house.rs (то, что мы рассмотрели)
  • src/front_of_house/mod.rs (старый стиль, путь все еще поддерживается)

Для модуля с именем hosting, который является подмодулем front_of_house, компилятор будет искать код модуля в:

  • src/front_of_house/hosting.rs (то, что мы рассмотрели)
  • src/front_of_house/hosting/mod.rs (старый стиль, путь все еще поддерживается)

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

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

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

Обратите внимание, что инструкция pub use crate::front_of_house::hosting в src/lib.rs также не изменилась, и use никак не влияет на то, какие файлы компилируются как часть крейта. Ключевое слово mod объявляет модули, а Rust ищет в файле с тем же именем, что и модуль, код, который входит в этот модуль.

Итоги

Rust позволяет разделить пакет на несколько крейтов, а крейт – на модули, так что вы можете обращаться к элементам, определенным в одном модуле, из другого модуля. Это можно делать, указывая абсолютные или относительные пути. Эти пути можно вводить в область видимости с помощью инструкции use, чтобы использовать более короткий путь при многократном обращении к элементу в этой области видимости. Код модулей по умолчанию приватен, но определения можно сделать публичными, добавив ключевое слово pub.

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

Распространенные коллекции

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

  • Вектор позволяет хранить переменное количество значений рядом друг с другом.
  • Строка – это коллекция символов. Мы уже упоминали тип String раньше, но в этой главе поговорим о нем подробно.
  • Хеш-карта позволяет связать значение с определенным ключом. Это конкретная реализация более общей структуры данных, называемой map.

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

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

Хранение списков значений с помощью векторов

Хранение списков значений с помощью векторов

Первый тип коллекции, который мы рассмотрим, – это Vec<T>, также известный как вектор. Векторы позволяют хранить более одного значения в одной структуре данных, размещая все значения рядом друг с другом в памяти. Векторы могут хранить значения только одного и того же типа. Они полезны, когда у вас есть список элементов, например строки текста в файле или цены товаров в корзине покупок.

Создание нового вектора

Чтобы создать новый пустой вектор, мы вызываем функцию Vec::new, как показано в листинге 8-1.

fn main() {
    let v: Vec<i32> = Vec::new();
}
Listing 8-1: Создание нового пустого вектора для хранения значений типа i32

Обратите внимание, что здесь мы добавили аннотацию типа. Поскольку мы не вставляем в этот вектор никаких значений, Rust не знает, элементы какого типа мы собираемся хранить. Это важный момент. Векторы реализованы с помощью обобщений; в главе 10 мы рассмотрим, как использовать обобщения с вашими собственными типами. Пока достаточно знать, что тип Vec<T>, предоставляемый стандартной библиотекой, может хранить любой тип. Когда мы создаем вектор для хранения конкретного типа, можно указать этот тип в угловых скобках. В листинге 8-1 мы сообщили Rust, что Vec<T> в переменной v будет хранить элементы типа i32.

Чаще вы будете создавать Vec<T> с начальными значениями, и Rust сам выведет тип значений, которые вы хотите хранить, поэтому такая аннотация типа нужна редко. Rust удобно предоставляет макрос vec!, который создает новый вектор, содержащий переданные ему значения. Листинг 8-2 создает новый Vec<i32>, который хранит значения 1, 2 и 3. Целочисленный тип – i32, потому что это целочисленный тип по умолчанию, как мы обсуждали в разделе «Типы данных» главы 3.

fn main() {
    let v = vec![1, 2, 3];
}
Listing 8-2: Создание нового вектора, содержащего значения

Поскольку мы задали начальные значения i32, Rust может вывести, что тип vVec<i32>, и аннотация типа не нужна. Далее посмотрим, как изменять вектор.

Обновление вектора

Чтобы создать вектор, а затем добавить в него элементы, можно использовать метод push, как показано в листинге 8-3.

fn main() {
    let mut v = Vec::new();

    v.push(5);
    v.push(6);
    v.push(7);
    v.push(8);
}
Listing 8-3: Использование метода push для добавления значений в вектор

Как и с любой переменной, если мы хотим иметь возможность изменять ее значение, нужно сделать ее изменяемой с помощью ключевого слова mut, как обсуждалось в главе 3. Все числа, которые мы помещаем внутрь, имеют тип i32, и Rust выводит это из данных, поэтому аннотация Vec<i32> нам не нужна.

Чтение элементов векторов

Есть два способа сослаться на значение, хранящееся в векторе: через индексацию или с помощью метода get. В следующих примерах мы аннотировали типы значений, возвращаемых этими функциями, для дополнительной ясности.

Листинг 8-4 показывает оба способа доступа к значению в векторе: с помощью синтаксиса индексации и метода get.

fn main() {
    let v = vec![1, 2, 3, 4, 5];

    let third: &i32 = &v[2];
    println!("The third element is {third}");

    let third: Option<&i32> = v.get(2);
    match third {
        Some(third) => println!("The third element is {third}"),
        None => println!("There is no third element."),
    }
}
Listing 8-4: Использование синтаксиса индексации и метода get для доступа к элементу в векторе

Обратите внимание на несколько деталей. Мы используем значение индекса 2, чтобы получить третий элемент, потому что векторы индексируются числами, начиная с нуля. Использование & и [] дает нам ссылку на элемент по этому индексу. Когда мы используем метод get и передаем индекс как аргумент, мы получаем Option<&T>, с которым можно работать через match.

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

fn main() {
    let v = vec![1, 2, 3, 4, 5];

    let does_not_exist = &v[100];
    let does_not_exist = v.get(100);
}
Listing 8-5: Попытка получить элемент с индексом 100 в векторе, содержащем пять элементов

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

Когда методу get передают индекс, находящийся за пределами вектора, он возвращает None без паники. Этот способ стоит использовать, если доступ к элементу за пределами диапазона вектора может время от времени происходить при нормальных обстоятельствах. Тогда в вашем коде будет логика для обработки Some(&element) или None, как обсуждалось в главе 6. Например, индекс может поступать от человека, вводящего число. Если он случайно введет слишком большое число и программа получит значение None, вы можете сообщить пользователю, сколько элементов находится в текущем векторе, и дать ему еще одну возможность ввести допустимое значение. Это было бы удобнее для пользователя, чем аварийное завершение программы из-за опечатки!

Когда у программы есть допустимая ссылка, проверщик заимствований применяет правила владения и заимствования (рассмотренные в главе 4), чтобы убедиться, что эта ссылка и любые другие ссылки на содержимое вектора остаются действительными. Вспомните правило, которое говорит, что нельзя иметь изменяемые и неизменяемые ссылки в одной области видимости. Это правило применяется в листинге 8-6, где мы удерживаем неизменяемую ссылку на первый элемент вектора и пытаемся добавить элемент в конец. Эта программа не будет работать, если мы также попытаемся позже обратиться к этому элементу в функции.

fn main() {
    let mut v = vec![1, 2, 3, 4, 5];

    let first = &v[0];

    v.push(6);

    println!("The first element is: {first}");
}
Listing 8-6: Попытка добавить элемент в вектор при удержании ссылки на элемент

Компиляция этого кода приведет к такой ошибке:

$ cargo run
   Compiling collections v0.1.0 (file:///projects/collections)
error[E0502]: cannot borrow `v` as mutable because it is also borrowed as immutable
 --> src/main.rs:6:5
  |
4 |     let first = &v[0];
  |                  - immutable borrow occurs here
5 |
6 |     v.push(6);
  |     ^^^^^^^^^ mutable borrow occurs here
7 |
8 |     println!("The first element is: {first}");
  |                                      ----- immutable borrow later used here

For more information about this error, try `rustc --explain E0502`.
error: could not compile `collections` (bin "collections") due to 1 previous error

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

Примечание: подробнее о деталях реализации типа Vec<T> смотрите в «Rustonomicon».

Итерация по значениям в векторе

Чтобы получить доступ к каждому элементу вектора по очереди, мы должны итерироваться по всем элементам, а не использовать индексы для доступа к одному элементу за раз. Листинг 8-7 показывает, как использовать цикл for, чтобы получить неизменяемые ссылки на каждый элемент в векторе значений i32 и вывести их.

fn main() {
    let v = vec![100, 32, 57];
    for i in &v {
        println!("{i}");
    }
}
Listing 8-7: Вывод каждого элемента вектора через итерацию по элементам с помощью цикла for

Мы также можем итерироваться по изменяемым ссылкам на каждый элемент в изменяемом векторе, чтобы изменить все элементы. Цикл for в листинге 8-8 добавит 50 к каждому элементу.

fn main() {
    let mut v = vec![100, 32, 57];
    for i in &mut v {
        *i += 50;
    }
}
Listing 8-8: Итерация по изменяемым ссылкам на элементы в векторе

Чтобы изменить значение, на которое указывает изменяемая ссылка, нужно использовать оператор разыменования *, чтобы получить значение в i, прежде чем применять оператор +=. Подробнее об операторе разыменования мы поговорим в разделе «Переход по ссылке к значению» главы 15.

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

Использование enum для хранения нескольких типов

Векторы могут хранить только значения одного и того же типа. Это может быть неудобно; определенно существуют случаи, когда нужно хранить список элементов разных типов. К счастью, варианты enum определены внутри одного и того же типа enum, поэтому, когда нам нужен один тип для представления элементов разных типов, можно определить и использовать enum!

Например, допустим, мы хотим получить значения из строки электронной таблицы, где некоторые столбцы строки содержат целые числа, некоторые – числа с плавающей точкой, а некоторые – строки. Мы можем определить enum, варианты которого будут хранить разные типы значений, и все варианты enum будут считаться одним и тем же типом: типом этого enum. Затем мы можем создать вектор для хранения этого enum и тем самым в конечном итоге хранить разные типы. Мы продемонстрировали это в листинге 8-9.

fn main() {
    enum SpreadsheetCell {
        Int(i32),
        Float(f64),
        Text(String),
    }

    let row = vec![
        SpreadsheetCell::Int(3),
        SpreadsheetCell::Text(String::from("blue")),
        SpreadsheetCell::Float(10.12),
    ];
}
Listing 8-9: Определение enum для хранения значений разных типов в одном векторе

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

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

Теперь, когда мы обсудили некоторые самые распространенные способы использования векторов, обязательно просмотрите документацию API по всем многочисленным полезным методам, определенным для Vec<T> стандартной библиотекой. Например, помимо push, метод pop удаляет и возвращает последний элемент.

Освобождение вектора освобождает его элементы

Как и любая другая struct, вектор освобождается, когда выходит из области видимости, как отмечено в листинге 8-10.

fn main() {
    {
        let v = vec![1, 2, 3, 4];

        // do stuff with v
    } // <- v goes out of scope and is freed here
}
Listing 8-10: Показано, где вектор и его элементы освобождаются

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

Перейдем к следующему типу коллекции: String!

Хранение текста в кодировке UTF-8 с помощью строк

Хранение текста в кодировке UTF-8 с помощью строк

Мы говорили о строках в главе 4, но теперь рассмотрим их подробнее. Новички в Rust часто застревают на строках из-за сочетания трех причин: склонности Rust показывать возможные ошибки, того факта, что строки являются более сложной структурой данных, чем многие программисты привыкли считать, и UTF-8. Эти факторы соединяются так, что при переходе с других языков программирования строки могут казаться трудными.

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

Определение строк

Сначала определим, что мы имеем в виду под термином строка. В самом языке Rust есть только один строковый тип: строковый срез str, который обычно встречается в заимствованной форме, &str. В главе 4 мы говорили о строковых срезах, которые являются ссылками на данные строки в кодировке UTF-8, хранящиеся где-то еще. Например, строковые литералы хранятся в бинарном файле программы и поэтому являются строковыми срезами.

Тип String, предоставляемый стандартной библиотекой Rust, а не встроенный в сам язык, является строковым типом с владением, кодировкой UTF-8, изменяемым и способным расти. Когда разработчики Rust говорят о «строках» в Rust, они могут иметь в виду как тип String, так и тип строкового среза &str, а не только один из этих типов. Хотя этот раздел в основном посвящен String, оба типа широко используются в стандартной библиотеке Rust, и как String, так и строковые срезы кодируются в UTF-8.

Создание новой строки

Многие операции, доступные для Vec<T>, доступны и для String, потому что String фактически реализован как обертка над вектором байтов с некоторыми дополнительными гарантиями, ограничениями и возможностями. Пример функции, которая работает одинаково с Vec<T> и String, – функция new для создания экземпляра, показанная в листинге 8-11.

fn main() {
    let mut s = String::new();
}
Listing 8-11: Создание новой пустой String

Эта строка создает новую пустую строку с именем s, в которую затем можно загрузить данные. Часто у нас уже есть начальные данные, с которых мы хотим начать строку. Для этого мы используем метод to_string, доступный для любого типа, реализующего трейт Display, как это делают строковые литералы. Листинг 8-12 показывает два примера.

fn main() {
    let data = "initial contents";

    let s = data.to_string();

    // The method also works on a literal directly:
    let s = "initial contents".to_string();
}
Listing 8-12: Использование метода to_string для создания String из строкового литерала

Этот код создает строку, содержащую initial contents.

Мы также можем использовать функцию String::from, чтобы создать String из строкового литерала. Код в листинге 8-13 эквивалентен коду из листинга 8-12, который использует to_string.

fn main() {
    let s = String::from("initial contents");
}
Listing 8-13: Использование функции String::from для создания String из строкового литерала

Поскольку строки используются для множества задач, для них доступно много разных обобщенных API, что дает нам немало вариантов. Некоторые из них могут казаться избыточными, но у каждого есть свое место! В этом случае String::from и to_string делают одно и то же, поэтому выбор между ними – вопрос стиля и читаемости.

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

fn main() {
    let hello = String::from("السلام عليكم");
    let hello = String::from("Dobrý den");
    let hello = String::from("Hello");
    let hello = String::from("שלום");
    let hello = String::from("नमस्ते");
    let hello = String::from("こんにちは");
    let hello = String::from("안녕하세요");
    let hello = String::from("你好");
    let hello = String::from("Olá");
    let hello = String::from("Здравствуйте");
    let hello = String::from("Hola");
}
Listing 8-14: Хранение приветствий на разных языках в строках

Все эти значения являются допустимыми значениями String.

Обновление строки

String может увеличиваться в размере, а ее содержимое может изменяться, как и содержимое Vec<T>, если добавить в нее больше данных. Кроме того, для конкатенации значений String можно удобно использовать оператор + или макрос format!.

Добавление с помощью push_str или push

Мы можем увеличить String, используя метод push_str для добавления строкового среза, как показано в листинге 8-15.

fn main() {
    let mut s = String::from("foo");
    s.push_str("bar");
}
Listing 8-15: Добавление строкового среза к String с помощью метода push_str

После этих двух строк s будет содержать foobar. Метод push_str принимает строковый срез, потому что мы не обязательно хотим получать владение параметром. Например, в коде листинга 8-16 мы хотим иметь возможность использовать s2 после добавления ее содержимого к s1.

fn main() {
    let mut s1 = String::from("foo");
    let s2 = "bar";
    s1.push_str(s2);
    println!("s2 is {s2}");
}
Listing 8-16: Использование строкового среза после добавления его содержимого к String

Если бы метод push_str получал владение s2, мы не смогли бы вывести ее значение в последней строке. Однако этот код работает так, как мы ожидаем!

Метод push принимает один символ как параметр и добавляет его к String. Листинг 8-17 добавляет букву l к String с помощью метода push.

fn main() {
    let mut s = String::from("lo");
    s.push('l');
}
Listing 8-17: Добавление одного символа к значению String с помощью push

В результате s будет содержать lol.

Конкатенация с помощью + или format!

Часто требуется объединить две существующие строки. Один способ сделать это – использовать оператор +, как показано в листинге 8-18.

fn main() {
    let s1 = String::from("Hello, ");
    let s2 = String::from("world!");
    let s3 = s1 + &s2; // note s1 has been moved here and can no longer be used
}
Listing 8-18: Использование оператора + для объединения двух значений String в новое значение String

Строка s3 будет содержать Hello, world!. Причина, по которой s1 больше не действительна после сложения, и причина, по которой мы использовали ссылку на s2, связаны с сигнатурой метода, вызываемого при использовании оператора +. Оператор + использует метод add, сигнатура которого выглядит примерно так:

fn add(self, s: &str) -> String {

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

Во-первых, у s2 есть &, что означает: мы добавляем ссылку на вторую строку к первой строке. Это связано с параметром s в функции add: мы можем добавить к String только строковый срез; мы не можем сложить два значения String напрямую. Но подождите: тип &s2&String, а не &str, как указано во втором параметре add. Так почему листинг 8-18 компилируется?

Причина, по которой мы можем использовать &s2 в вызове add, заключается в том, что компилятор может привести аргумент &String к &str. Когда мы вызываем метод add, Rust использует принудительное разыменовывающее приведение (deref coercion), которое здесь превращает &s2 в &s2[..]. Мы подробнее обсудим deref coercion в главе 15. Поскольку add не получает владение параметром s, s2 останется действительной String после этой операции.

Во-вторых, из сигнатуры видно, что add получает владение self, потому что у self нет &. Это означает, что s1 в листинге 8-18 будет перемещена в вызов add и после этого больше не будет действительной. Поэтому, хотя let s3 = s1 + &s2; выглядит так, будто копирует обе строки и создает новую, на самом деле эта инструкция получает владение s1, добавляет копию содержимого s2, а затем возвращает владение результатом. Иными словами, выглядит так, будто создается много копий, но это не так; реализация эффективнее копирования.

Если нужно конкатенировать несколько строк, поведение оператора + становится громоздким:

fn main() {
    let s1 = String::from("tic");
    let s2 = String::from("tac");
    let s3 = String::from("toe");

    let s = s1 + "-" + &s2 + "-" + &s3;
}

В этот момент s будет равно tic-tac-toe. Со всеми символами + и " трудно понять, что происходит. Для более сложного объединения строк вместо этого можно использовать макрос format!:

fn main() {
    let s1 = String::from("tic");
    let s2 = String::from("tac");
    let s3 = String::from("toe");

    let s = format!("{s1}-{s2}-{s3}");
}

Этот код тоже устанавливает s в tic-tac-toe. Макрос format! работает как println!, но вместо вывода результата на экран возвращает String с содержимым. Версия кода с format! намного легче читается, а код, сгенерированный макросом format!, использует ссылки, поэтому этот вызов не получает владение ни одним из своих параметров.

Индексация строк

Во многих других языках программирования доступ к отдельным символам строки по индексу является допустимой и распространенной операцией. Однако если вы попытаетесь получить доступ к частям String с помощью синтаксиса индексации в Rust, получите ошибку. Рассмотрим недопустимый код в листинге 8-19.

fn main() {
    let s1 = String::from("hi");
    let h = s1[0];
}
Listing 8-19: Попытка использовать синтаксис индексации с String

Этот код приведет к следующей ошибке:

$ cargo run
   Compiling collections v0.1.0 (file:///projects/collections)
error[E0277]: the type `str` cannot be indexed by `{integer}`
 --> src/main.rs:3:16
  |
3 |     let h = s1[0];
  |                ^ string indices are ranges of `usize`
  |
  = help: the trait `SliceIndex<str>` is not implemented for `{integer}`
  = note: you can use `.chars().nth()` or `.bytes().nth()`
          for more information, see chapter 8 in The Book: <https://doc.rust-lang.org/book/ch08-02-strings.html#indexing-into-strings>
  = help: the following other types implement trait `SliceIndex<T>`:
            `usize` implements `SliceIndex<ByteStr>`
            `usize` implements `SliceIndex<[T]>`
  = note: required for `String` to implement `Index<{integer}>`

For more information about this error, try `rustc --explain E0277`.
error: could not compile `collections` (bin "collections") due to 1 previous error

Ошибка говорит сама за себя: строки Rust не поддерживают индексацию. Но почему нет? Чтобы ответить на этот вопрос, нужно обсудить, как Rust хранит строки в памяти.

Внутреннее представление

String – это обертка над Vec<u8>. Посмотрим на некоторые наши корректно закодированные примеры строк UTF-8 из листинга 8-14. Сначала этот:

fn main() {
    let hello = String::from("السلام عليكم");
    let hello = String::from("Dobrý den");
    let hello = String::from("Hello");
    let hello = String::from("שלום");
    let hello = String::from("नमस्ते");
    let hello = String::from("こんにちは");
    let hello = String::from("안녕하세요");
    let hello = String::from("你好");
    let hello = String::from("Olá");
    let hello = String::from("Здравствуйте");
    let hello = String::from("Hola");
}

В этом случае len будет равно 4, что означает: вектор, хранящий строку "Hola", имеет длину 4 байта. Каждая из этих букв занимает 1 байт при кодировании в UTF-8. Однако следующая строка может вас удивить (обратите внимание, что эта строка начинается с заглавной кириллической буквы Зе, а не с цифры 3):

fn main() {
    let hello = String::from("السلام عليكم");
    let hello = String::from("Dobrý den");
    let hello = String::from("Hello");
    let hello = String::from("שלום");
    let hello = String::from("नमस्ते");
    let hello = String::from("こんにちは");
    let hello = String::from("안녕하세요");
    let hello = String::from("你好");
    let hello = String::from("Olá");
    let hello = String::from("Здравствуйте");
    let hello = String::from("Hola");
}

Если бы вас спросили, какова длина строки, вы могли бы сказать 12. На самом деле ответ Rust – 24: это количество байтов, необходимое для кодирования «Здравствуйте» в UTF-8, потому что каждое скалярное значение Unicode в этой строке занимает 2 байта памяти. Поэтому индекс в байтах строки не всегда соответствует допустимому скалярному значению Unicode. Для демонстрации рассмотрим этот недопустимый код Rust:

let hello = "Здравствуйте";
let answer = &hello[0];

Вы уже знаете, что answer не будет З, первой буквой. При кодировании в UTF-8 первый байт З равен 208, а второй – 151, поэтому может показаться, что answer на самом деле должен быть 208, но 208 сам по себе не является допустимым символом. Возврат 208, скорее всего, не то, чего пользователь ожидал бы, попросив первую букву этой строки; однако это единственные данные, которые Rust имеет по байтовому индексу 0. Пользователи обычно не хотят получать байтовое значение, даже если строка содержит только латинские буквы: если бы &"hi"[0] был допустимым кодом и возвращал байтовое значение, он вернул бы 104, а не h.

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

Байты, скалярные значения и графемные кластеры

Еще один момент о UTF-8: с точки зрения Rust есть фактически три важных способа смотреть на строки – как на байты, скалярные значения и графемные кластеры (самое близкое к тому, что мы назвали бы буквами).

Если мы посмотрим на слово на хинди “नमस्ते”, записанное письмом деванагари, оно хранится как вектор значений u8, который выглядит так:

[224, 164, 168, 224, 164, 174, 224, 164, 184, 224, 165, 141, 224, 164, 164,
224, 165, 135]

Это 18 байтов, и именно так компьютеры в конечном счете хранят эти данные. Если смотреть на них как на скалярные значения Unicode, то есть как на то, чем является тип Rust char, эти байты выглядят так:

['न', 'म', 'स', '्', 'त', 'े']

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

["न", "म", "स्", "ते"]

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

Последняя причина, по которой Rust не позволяет индексировать String, чтобы получить символ, заключается в том, что операции индексации, как ожидается, всегда должны выполняться за постоянное время (O(1)). Но с String гарантировать такую производительность невозможно, потому что Rust пришлось бы проходить по содержимому от начала до индекса, чтобы определить, сколько там допустимых символов.

Срезы строк

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

Вместо индексации с помощью [] и одного числа можно использовать [] с диапазоном, чтобы создать строковый срез, содержащий определенные байты:

#![allow(unused)]
fn main() {
let hello = "Здравствуйте";

let s = &hello[0..4];
}

Здесь s будет &str, содержащим первые 4 байта строки. Ранее мы упоминали, что каждый из этих символов занимает 2 байта, а значит s будет Зд.

Если бы мы попытались взять срез только части байтов символа, например &hello[0..1], Rust запаниковал бы во время выполнения так же, как при обращении к недопустимому индексу в векторе:

$ cargo run
   Compiling collections v0.1.0 (file:///projects/collections)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.43s
     Running `target/debug/collections`

thread 'main' panicked at src/main.rs:4:19:
byte index 1 is not a char boundary; it is inside 'З' (bytes 0..2) of `Здравствуйте`
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

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

Итерация по строкам

Лучший способ работать с частями строк – явно указать, нужны вам символы или байты. Для отдельных скалярных значений Unicode используйте метод chars. Вызов chars для “Зд” разделяет строку и возвращает два значения типа char, и по результату можно итерироваться, чтобы получить доступ к каждому элементу:

#![allow(unused)]
fn main() {
for c in "Зд".chars() {
    println!("{c}");
}
}

Этот код выведет следующее:

З
д

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

#![allow(unused)]
fn main() {
for b in "Зд".bytes() {
    println!("{b}");
}
}

Этот код выведет 4 байта, из которых состоит эта строка:

208
151
208
180

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

Получение графемных кластеров из строк, как в случае письма деванагари, – сложная задача, поэтому эта функциональность не предоставляется стандартной библиотекой. Если вам нужна такая функциональность, на crates.io доступны крейты.

Работа со сложностью строк

Подводя итог, строки сложны. Разные языки программирования делают разные выборы в том, как представить эту сложность программисту. Rust выбрал поведение по умолчанию, при котором данные String обрабатываются корректно во всех программах на Rust. Это означает, что программистам нужно заранее внимательнее относиться к обработке данных UTF-8. Такой компромисс раскрывает больше сложности строк, чем видно в других языках программирования, но избавляет вас от необходимости позже в жизненном цикле разработки обрабатывать ошибки, связанные с не-ASCII-символами.

Хорошая новость в том, что стандартная библиотека предлагает много функциональности, построенной на типах String и &str, чтобы помочь правильно обрабатывать такие сложные ситуации. Обязательно посмотрите документацию на полезные методы вроде contains для поиска в строке и replace для замены частей строки другой строкой.

Переключимся на что-то немного менее сложное: хеш-карты!

Хранение ключей со связанными значениями в HashMap

Хранение ключей со связанными значениями в хеш-картах

Последняя из наших распространенных коллекций – хеш-карта. Тип HashMap<K, V> хранит соответствие ключей типа K значениям типа V с помощью хеш-функции, которая определяет, как размещать эти ключи и значения в памяти. Многие языки программирования поддерживают такую структуру данных, но часто используют для нее другое имя, например hash, map, object, hash table, dictionary или associative array, если назвать лишь несколько вариантов.

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

В этом разделе мы рассмотрим базовый API хеш-карт, но в функциях, определенных стандартной библиотекой для HashMap<K, V>, скрыто гораздо больше полезных возможностей. Как всегда, за дополнительной информацией обращайтесь к документации стандартной библиотеки.

Создание новой хеш-карты

Один способ создать пустую хеш-карту – использовать new и добавлять элементы с помощью insert. В листинге 8-20 мы отслеживаем счет двух команд, имена которых Blue и Yellow. Команда Blue начинает с 10 очков, а команда Yellow – с 50.

fn main() {
    use std::collections::HashMap;

    let mut scores = HashMap::new();

    scores.insert(String::from("Blue"), 10);
    scores.insert(String::from("Yellow"), 50);
}
Listing 8-20: Создание новой хеш-карты и вставка нескольких ключей и значений

Обратите внимание, что сначала нужно подключить HashMap из части стандартной библиотеки, посвященной коллекциям, с помощью use. Из трех распространенных коллекций эта используется реже всего, поэтому она не входит в набор возможностей, автоматически вводимых в область видимости прелюдией. Хеш-карты также имеют меньшую поддержку со стороны стандартной библиотеки; например, нет встроенного макроса для их создания.

Как и векторы, хеш-карты хранят свои данные в куче. У этой HashMap ключи имеют тип String, а значения – тип i32. Как и векторы, хеш-карты однородны: все ключи должны иметь один и тот же тип, и все значения должны иметь один и тот же тип.

Доступ к значениям в хеш-карте

Мы можем получить значение из хеш-карты, передав его ключ в метод get, как показано в листинге 8-21.

fn main() {
    use std::collections::HashMap;

    let mut scores = HashMap::new();

    scores.insert(String::from("Blue"), 10);
    scores.insert(String::from("Yellow"), 50);

    let team_name = String::from("Blue");
    let score = scores.get(&team_name).copied().unwrap_or(0);
}
Listing 8-21: Получение счета команды Blue, сохраненного в хеш-карте

Здесь score получит значение, связанное с командой Blue, и результатом будет 10. Метод get возвращает Option<&V>; если в хеш-карте нет значения для такого ключа, get вернет None. Эта программа обрабатывает Option, вызывая copied, чтобы получить Option<i32> вместо Option<&i32>, а затем unwrap_or, чтобы установить score в ноль, если в scores нет записи для ключа.

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

fn main() {
    use std::collections::HashMap;

    let mut scores = HashMap::new();

    scores.insert(String::from("Blue"), 10);
    scores.insert(String::from("Yellow"), 50);

    for (key, value) in &scores {
        println!("{key}: {value}");
    }
}

Этот код выведет каждую пару в произвольном порядке:

Yellow: 50
Blue: 10

Управление владением в хеш-картах

Для типов, реализующих трейт Copy, таких как i32, значения копируются в хеш-карту. Для значений с владением, таких как String, значения будут перемещены, и хеш-карта станет владельцем этих значений, как показано в листинге 8-22.

fn main() {
    use std::collections::HashMap;

    let field_name = String::from("Favorite color");
    let field_value = String::from("Blue");

    let mut map = HashMap::new();
    map.insert(field_name, field_value);
    // field_name and field_value are invalid at this point, try using them and
    // see what compiler error you get!
}
Listing 8-22: Показано, что ключи и значения принадлежат хеш-карте после вставки

Мы не сможем использовать переменные field_name и field_value после того, как они будут перемещены в хеш-карту вызовом insert.

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

Обновление хеш-карты

Хотя количество пар ключ-значение может расти, каждый уникальный ключ может иметь только одно связанное с ним значение в каждый момент времени (но не наоборот: например, и команда Blue, и команда Yellow могли бы иметь значение 10, сохраненное в хеш-карте scores).

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

Перезапись значения

Если мы вставим ключ и значение в хеш-карту, а затем вставим тот же ключ с другим значением, значение, связанное с этим ключом, будет заменено. Хотя код в листинге 8-23 вызывает insert дважды, хеш-карта будет содержать только одну пару ключ-значение, потому что оба раза мы вставляем значение для ключа команды Blue.

fn main() {
    use std::collections::HashMap;

    let mut scores = HashMap::new();

    scores.insert(String::from("Blue"), 10);
    scores.insert(String::from("Blue"), 25);

    println!("{scores:?}");
}
Listing 8-23: Замена значения, сохраненного с конкретным ключом

Этот код выведет {"Blue": 25}. Исходное значение 10 было перезаписано.

Добавление ключа и значения только если ключ отсутствует

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

У хеш-карт есть специальный API для этого, называемый entry, который принимает ключ, который вы хотите проверить, как параметр. Возвращаемое значение метода entry – enum под названием Entry, представляющий значение, которое может существовать или не существовать. Допустим, мы хотим проверить, связано ли значение с ключом команды Yellow. Если нет, мы хотим вставить значение 50; то же самое сделаем для команды Blue. При использовании API entry код выглядит как в листинге 8-24.

fn main() {
    use std::collections::HashMap;

    let mut scores = HashMap::new();
    scores.insert(String::from("Blue"), 10);

    scores.entry(String::from("Yellow")).or_insert(50);
    scores.entry(String::from("Blue")).or_insert(50);

    println!("{scores:?}");
}
Listing 8-24: Использование метода entry, чтобы вставить значение только если у ключа еще нет значения

Метод or_insert для Entry определен так, что он возвращает изменяемую ссылку на значение для соответствующего ключа Entry, если этот ключ существует; если нет, он вставляет параметр как новое значение для этого ключа и возвращает изменяемую ссылку на новое значение. Этот прием гораздо чище, чем самостоятельное написание такой логики, и, кроме того, лучше взаимодействует с проверщиком заимствований.

Запуск кода из листинга 8-24 выведет {"Yellow": 50, "Blue": 10}. Первый вызов entry вставит ключ команды Yellow со значением 50, потому что у команды Yellow еще нет значения. Второй вызов entry не изменит хеш-карту, потому что у команды Blue уже есть значение 10.

Обновление значения на основе старого значения

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

fn main() {
    use std::collections::HashMap;

    let text = "hello world wonderful world";

    let mut map = HashMap::new();

    for word in text.split_whitespace() {
        let count = map.entry(word).or_insert(0);
        *count += 1;
    }

    println!("{map:?}");
}
Listing 8-25: Подсчет вхождений слов с помощью хеш-карты, которая хранит слова и счетчики

Этот код выведет {"world": 2, "hello": 1, "wonderful": 1}. Вы можете увидеть те же пары ключ-значение, выведенные в другом порядке: вспомните из раздела «Доступ к значениям в хеш-карте», что итерация по хеш-карте происходит в произвольном порядке.

Метод split_whitespace возвращает итератор по подсрезам значения text, разделенным пробельными символами. Метод or_insert возвращает изменяемую ссылку (&mut V) на значение для указанного ключа. Здесь мы сохраняем эту изменяемую ссылку в переменной count, поэтому, чтобы присвоить значение тому, на что она указывает, сначала нужно разыменовать count с помощью звездочки (*). Изменяемая ссылка выходит из области видимости в конце цикла for, поэтому все эти изменения безопасны и разрешены правилами заимствования.

Хеш-функции

По умолчанию HashMap использует хеш-функцию под названием SipHash, которая может обеспечивать устойчивость к атакам отказа в обслуживании (DoS), связанным с хеш-таблицами1. Это не самый быстрый доступный алгоритм хеширования, но компромисс в пользу лучшей безопасности, который сопровождается снижением производительности, того стоит. Если вы профилируете свой код и обнаружите, что хеш-функция по умолчанию слишком медленная для ваших целей, можно переключиться на другую функцию, указав другой хешер. Хешер – это тип, реализующий трейт BuildHasher. Мы поговорим о трейтах и о том, как их реализовывать, в главе 10. Вам не обязательно реализовывать собственный хешер с нуля; на crates.io есть библиотеки, опубликованные другими пользователями Rust, которые предоставляют хешеры с реализациями многих распространенных алгоритмов хеширования.

Итоги

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

  1. Дан список целых чисел. Используйте вектор и верните медиану (после сортировки это значение в средней позиции) и моду (значение, которое встречается чаще всего; здесь пригодится хеш-карта) списка.
  2. Преобразуйте строки в Pig Latin. Первая согласная каждого слова переносится в конец слова, и добавляется ay, так что first становится irst-fay. К словам, начинающимся с гласной, вместо этого добавляется hay в конец (apple становится apple-hay). Помните о деталях кодировки UTF-8!
  3. Используя хеш-карту и векторы, создайте текстовый интерфейс, позволяющий пользователю добавлять имена сотрудников в отдел компании; например, “Add Sally to Engineering” или “Add Amir to Sales”. Затем позвольте пользователю получить список всех людей в отделе или всех людей в компании по отделам, отсортированный по алфавиту.

Документация API стандартной библиотеки описывает методы, которые есть у векторов, строк и хеш-карт и которые будут полезны для этих упражнений!

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


  1. https://en.wikipedia.org/wiki/SipHash

Обработка ошибок

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

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

Большинство языков не различают эти два вида ошибок и обрабатывают оба одинаково, используя такие механизмы, как исключения. В Rust исключений нет. Вместо этого в нем есть тип Result<T, E> для восстанавливаемых ошибок и макрос panic!, который останавливает выполнение, когда программа сталкивается с невосстанавливаемой ошибкой. В этой главе мы сначала рассмотрим вызов panic!, а затем поговорим о возврате значений Result<T, E>. Кроме того, мы изучим соображения, которые помогают решить, пытаться ли восстановиться после ошибки или остановить выполнение.

Неустранимые ошибки с panic!

Невосстанавливаемые ошибки с panic!

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

Раскручивание стека или аварийное завершение в ответ на панику

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

Память, которую использовала программа, затем должна будет очистить операционная система. Если в вашем проекте нужно сделать итоговый бинарный файл как можно меньше, можно переключиться с раскручивания стека на аварийное завершение при панике, добавив panic = 'abort' в соответствующие разделы [profile] файла Cargo.toml. Например, если вы хотите аварийно завершать программу при панике в режиме release, добавьте следующее:

[profile.release]
panic = 'abort'

Попробуем вызвать panic! в простой программе:

Filename: src/main.rs
fn main() {
    panic!("crash and burn");
}

Когда вы запустите программу, то увидите примерно следующее:

$ cargo run
   Compiling panic v0.1.0 (file:///projects/panic)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.25s
     Running `target/debug/panic`

thread 'main' panicked at src/main.rs:2:5:
crash and burn
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

Вызов panic! приводит к сообщению об ошибке, содержащемуся в последних двух строках. Первая строка показывает наше сообщение паники и место в исходном коде, где произошла паника: src/main.rs:2:5 означает вторую строку, пятый символ файла src/main.rs.

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

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

Filename: src/main.rs
fn main() {
    let v = vec![1, 2, 3];

    v[99];
}
Listing 9-1: Попытка обратиться к элементу за концом вектора, которая приведет к вызову panic!

Здесь мы пытаемся обратиться к 100-му элементу нашего вектора (он находится по индексу 99, потому что индексация начинается с нуля), но вектор содержит только три элемента. В этой ситуации Rust запаникует. Использование [] предполагает возврат элемента, но если передать недопустимый индекс, здесь нет элемента, который Rust мог бы корректно вернуть.

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

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

$ cargo run
   Compiling panic v0.1.0 (file:///projects/panic)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.27s
     Running `target/debug/panic`

thread 'main' panicked at src/main.rs:4:6:
index out of bounds: the len is 3 but the index is 99
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

Эта ошибка указывает на строку 4 файла main.rs, где мы пытаемся обратиться к индексу 99 вектора v.

Строка note: сообщает, что можно установить переменную окружения RUST_BACKTRACE, чтобы получить обратную трассировку того, что именно произошло и привело к ошибке. Обратная трассировка – это список всех функций, которые были вызваны, чтобы дойти до этой точки. Обратные трассировки в Rust работают так же, как и в других языках: главное при чтении трассировки – начать сверху и читать до тех пор, пока вы не увидите файлы, которые писали сами. Это и есть место, где возникла проблема. Строки выше этого места – это код, который был вызван вашим кодом; строки ниже – код, который вызвал ваш код. Эти строки до и после могут включать код ядра Rust, код стандартной библиотеки или крейты, которые вы используете. Попробуем получить обратную трассировку, установив переменную окружения RUST_BACKTRACE в любое значение, кроме 0. Листинг 9-2 показывает вывод, похожий на тот, который вы увидите.

$ RUST_BACKTRACE=1 cargo run
thread 'main' panicked at src/main.rs:4:6:
index out of bounds: the len is 3 but the index is 99
stack backtrace:
   0: rust_begin_unwind
             at /rustc/4d91de4e48198da2e33413efdcd9cd2cc0c46688/library/std/src/panicking.rs:692:5
   1: core::panicking::panic_fmt
             at /rustc/4d91de4e48198da2e33413efdcd9cd2cc0c46688/library/core/src/panicking.rs:75:14
   2: core::panicking::panic_bounds_check
             at /rustc/4d91de4e48198da2e33413efdcd9cd2cc0c46688/library/core/src/panicking.rs:273:5
   3: <usize as core::slice::index::SliceIndex<[T]>>::index
             at file:///home/.rustup/toolchains/1.85/lib/rustlib/src/rust/library/core/src/slice/index.rs:274:10
   4: core::slice::index::<impl core::ops::index::Index<I> for [T]>::index
             at file:///home/.rustup/toolchains/1.85/lib/rustlib/src/rust/library/core/src/slice/index.rs:16:9
   5: <alloc::vec::Vec<T,A> as core::ops::index::Index<I>>::index
             at file:///home/.rustup/toolchains/1.85/lib/rustlib/src/rust/library/alloc/src/vec/mod.rs:3361:9
   6: panic::main
             at ./src/main.rs:4:6
   7: core::ops::function::FnOnce::call_once
             at file:///home/.rustup/toolchains/1.85/lib/rustlib/src/rust/library/core/src/ops/function.rs:250:5
note: Some details are omitted, run with `RUST_BACKTRACE=full` for a verbose backtrace.
Listing 9-2: Обратная трассировка, созданная вызовом panic! и показанная, когда установлена переменная окружения RUST_BACKTRACE

Вывода получилось много! Точный вывод может отличаться в зависимости от вашей операционной системы и версии Rust. Чтобы получить обратные трассировки с такой информацией, должны быть включены отладочные символы. Отладочные символы включены по умолчанию при использовании cargo build или cargo run без флага --release, как в нашем случае.

В выводе листинга 9-2 строка 6 обратной трассировки указывает на строку в нашем проекте, которая вызывает проблему: строку 4 файла src/main.rs. Если мы не хотим, чтобы наша программа паниковала, расследование следует начинать с места, на которое указывает первая строка с упоминанием файла, написанного нами. В листинге 9-1 мы намеренно написали код, который запаникует, и способ исправить панику – не запрашивать элемент за пределами диапазона индексов вектора. Когда ваш код будет паниковать в будущем, вам нужно будет выяснить, какое действие выполняет код, с какими значениями это вызывает панику и что код должен делать вместо этого.

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

Устранимые ошибки с Result

Восстанавливаемые ошибки с Result

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

Вспомните из раздела «Обработка потенциальной ошибки с Result» главы 2, что enum Result определен с двумя вариантами, Ok и Err, следующим образом:

#![allow(unused)]
fn main() {
enum Result<T, E> {
    Ok(T),
    Err(E),
}
}

T и E – это обобщенные параметры типов: подробнее об обобщениях мы поговорим в главе 10. Сейчас вам нужно знать, что T представляет тип значения, которое будет возвращено в случае успеха внутри варианта Ok, а E представляет тип ошибки, которая будет возвращена в случае неудачи внутри варианта Err. Поскольку у Result есть эти обобщенные параметры типов, мы можем использовать тип Result и определенные для него функции во многих разных ситуациях, где возвращаемые значения успеха и ошибки могут отличаться.

Вызовем функцию, которая возвращает значение Result, потому что эта функция может завершиться неудачно. В листинге 9-3 мы пытаемся открыть файл.

Filename: src/main.rs
use std::fs::File;

fn main() {
    let greeting_file_result = File::open("hello.txt");
}
Listing 9-3: Открытие файла

Возвращаемый тип File::openResult<T, E>. Обобщенный параметр T в реализации File::open заменен типом значения успеха, std::fs::File, то есть дескриптором файла. Тип E, используемый в значении ошибки, – std::io::Error. Такой возвращаемый тип означает, что вызов File::open может завершиться успешно и вернуть дескриптор файла, из которого можно читать или в который можно писать. Вызов функции также может завершиться неудачно: например, файл может не существовать, или у нас может не быть прав доступа к нему. Функции File::open нужен способ сообщить нам, завершилась ли она успешно или неудачно, и одновременно дать нам либо дескриптор файла, либо информацию об ошибке. Именно эту информацию и передает enum Result.

В случае успеха File::open значение в переменной greeting_file_result будет экземпляром Ok, содержащим дескриптор файла. В случае неудачи значение в greeting_file_result будет экземпляром Err, содержащим больше информации о том, какая ошибка произошла.

Нам нужно дополнить код из листинга 9-3, чтобы выполнять разные действия в зависимости от значения, которое возвращает File::open. Листинг 9-4 показывает один способ обработать Result с помощью базового инструмента – выражения match, которое мы обсуждали в главе 6.

Filename: src/main.rs
use std::fs::File;

fn main() {
    let greeting_file_result = File::open("hello.txt");

    let greeting_file = match greeting_file_result {
        Ok(file) => file,
        Err(error) => panic!("Problem opening the file: {error:?}"),
    };
}
Listing 9-4: Использование выражения match для обработки вариантов Result, которые могут быть возвращены

Обратите внимание: как и enum Option, enum Result и его варианты уже введены в область видимости прелюдией, поэтому нам не нужно указывать Result:: перед вариантами Ok и Err в ветвях match.

Когда результат равен Ok, этот код вернет внутреннее значение file из варианта Ok, а затем мы присвоим этот дескриптор файла переменной greeting_file. После match мы сможем использовать дескриптор файла для чтения или записи.

Другая ветвь match обрабатывает случай, когда от File::open мы получаем значение Err. В этом примере мы решили вызвать макрос panic!. Если в нашем текущем каталоге нет файла с именем hello.txt и мы запустим этот код, то увидим следующий вывод от макроса panic!:

$ cargo run
   Compiling error-handling v0.1.0 (file:///projects/error-handling)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.73s
     Running `target/debug/error-handling`

thread 'main' panicked at src/main.rs:8:23:
Problem opening the file: Os { code: 2, kind: NotFound, message: "No such file or directory" }
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

Как обычно, этот вывод точно сообщает нам, что пошло не так.

Сопоставление разных ошибок

Код в листинге 9-4 вызовет panic! независимо от того, почему File::open завершилась неудачно. Однако мы хотим выполнять разные действия для разных причин неудачи. Если File::open завершилась неудачно из-за того, что файл не существует, мы хотим создать файл и вернуть дескриптор нового файла. Если File::open завершилась неудачно по любой другой причине – например, потому что у нас нет прав на открытие файла, – мы все равно хотим, чтобы код вызвал panic! так же, как в листинге 9-4. Для этого мы добавим внутреннее выражение match, показанное в листинге 9-5.

Filename: src/main.rs
use std::fs::File;
use std::io::ErrorKind;

fn main() {
    let greeting_file_result = File::open("hello.txt");

    let greeting_file = match greeting_file_result {
        Ok(file) => file,
        Err(error) => match error.kind() {
            ErrorKind::NotFound => match File::create("hello.txt") {
                Ok(fc) => fc,
                Err(e) => panic!("Problem creating the file: {e:?}"),
            },
            _ => {
                panic!("Problem opening the file: {error:?}");
            }
        },
    };
}
Listing 9-5: Обработка разных видов ошибок разными способами

Тип значения, которое File::open возвращает внутри варианта Err, – io::Error, структура из стандартной библиотеки. У этой структуры есть метод kind, который можно вызвать, чтобы получить значение io::ErrorKind. Enum io::ErrorKind предоставляется стандартной библиотекой и содержит варианты, представляющие разные виды ошибок, которые могут возникнуть при операции ввода-вывода. Нужный нам вариант – ErrorKind::NotFound, который указывает, что файла, который мы пытаемся открыть, еще не существует. Поэтому мы сопоставляем greeting_file_result, но также используем внутренний match для error.kind().

Условие, которое мы хотим проверить во внутреннем match, состоит в том, является ли значение, возвращенное error.kind(), вариантом NotFound enum ErrorKind. Если это так, мы пытаемся создать файл с помощью File::create. Однако, поскольку File::create тоже может завершиться неудачно, нам нужна вторая ветвь во внутреннем выражении match. Когда файл нельзя создать, печатается другое сообщение об ошибке. Вторая ветвь внешнего match остается прежней, поэтому программа паникует при любой ошибке, кроме ошибки отсутствия файла.

Альтернативы использованию match с Result<T, E>

Здесь много match! Выражение match очень полезно, но при этом довольно низкоуровневое. В главе 13 вы узнаете о замыканиях, которые используются со многими методами, определенными для Result<T, E>. Эти методы могут быть более лаконичными, чем использование match, когда вы обрабатываете значения Result<T, E> в своем коде.

Например, вот еще один способ записать ту же логику, что и в листинге 9-5, на этот раз с использованием замыканий и метода unwrap_or_else:

use std::fs::File;
use std::io::ErrorKind;

fn main() {
    let greeting_file = File::open("hello.txt").unwrap_or_else(|error| {
        if error.kind() == ErrorKind::NotFound {
            File::create("hello.txt").unwrap_or_else(|error| {
                panic!("Problem creating the file: {error:?}");
            })
        } else {
            panic!("Problem opening the file: {error:?}");
        }
    });
}

Хотя этот код ведет себя так же, как код из листинга 9-5, в нем нет ни одного выражения match, и его проще читать. Вернитесь к этому примеру после прочтения главы 13 и найдите метод unwrap_or_else в документации стандартной библиотеки. Многие подобные методы могут привести в порядок огромные вложенные выражения match, когда вы работаете с ошибками.

Краткие способы вызвать панику при ошибке

Использование match работает достаточно хорошо, но может быть немного многословным и не всегда хорошо передает намерение. Для типа Result<T, E> определено множество вспомогательных методов, выполняющих разные, более конкретные задачи. Метод unwrap – это сокращенный метод, реализованный так же, как выражение match, которое мы написали в листинге 9-4. Если значение Result является вариантом Ok, unwrap вернет значение внутри Ok. Если Result является вариантом Err, unwrap вызовет за нас макрос panic!. Вот пример unwrap в действии:

Filename: src/main.rs
use std::fs::File;

fn main() {
    let greeting_file = File::open("hello.txt").unwrap();
}

Если мы запустим этот код без файла hello.txt, то увидим сообщение об ошибке от вызова panic!, который делает метод unwrap:

thread 'main' panicked at src/main.rs:4:49:
called `Result::unwrap()` on an `Err` value: Os { code: 2, kind: NotFound, message: "No such file or directory" }

Аналогично, метод expect позволяет нам также выбрать сообщение об ошибке для panic!. Использование expect вместо unwrap и хорошие сообщения об ошибках могут передать ваше намерение и упростить поиск источника паники. Синтаксис expect выглядит так:

Filename: src/main.rs
use std::fs::File;

fn main() {
    let greeting_file = File::open("hello.txt")
        .expect("hello.txt should be included in this project");
}

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

thread 'main' panicked at src/main.rs:5:10:
hello.txt should be included in this project: Os { code: 2, kind: NotFound, message: "No such file or directory" }

В коде промышленного качества большинство Rust-разработчиков выбирают expect, а не unwrap, и дают больше контекста о том, почему операция, как ожидается, должна всегда завершаться успешно. Тогда, если ваши предположения когда-нибудь окажутся неверными, у вас будет больше информации для отладки.

Распространение ошибок

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

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

Filename: src/main.rs
#![allow(unused)]
fn main() {
use std::fs::File;
use std::io::{self, Read};

fn read_username_from_file() -> Result<String, io::Error> {
    let username_file_result = File::open("hello.txt");

    let mut username_file = match username_file_result {
        Ok(file) => file,
        Err(e) => return Err(e),
    };

    let mut username = String::new();

    match username_file.read_to_string(&mut username) {
        Ok(_) => Ok(username),
        Err(e) => Err(e),
    }
}
}
Listing 9-6: Функция, которая возвращает ошибки вызывающему коду с помощью match

Эту функцию можно написать гораздо короче, но мы начнем с подробной ручной версии, чтобы изучить обработку ошибок; в конце мы покажем более короткий способ. Сначала посмотрим на возвращаемый тип функции: Result<String, io::Error>. Это означает, что функция возвращает значение типа Result<T, E>, где обобщенный параметр T заменен конкретным типом String, а обобщенный тип E заменен конкретным типом io::Error.

Если эта функция успешно выполнится без каких-либо проблем, код, вызывающий эту функцию, получит значение Ok, содержащее String, – имя пользователя, которое функция прочитала из файла. Если функция столкнется с проблемами, вызывающий код получит значение Err, содержащее экземпляр io::Error с дополнительной информацией о том, какие проблемы произошли. Мы выбрали io::Error в качестве возвращаемого типа этой функции, потому что именно этот тип значения ошибки возвращают обе операции, которые мы вызываем в теле функции и которые могут завершиться неудачно: функция File::open и метод read_to_string.

Тело функции начинается с вызова функции File::open. Затем мы обрабатываем значение Result с помощью match, похожего на match из листинга 9-4. Если File::open завершается успешно, дескриптор файла в переменной шаблона file становится значением изменяемой переменной username_file, и функция продолжает выполнение. В случае Err вместо вызова panic! мы используем ключевое слово return, чтобы досрочно выйти из функции целиком и передать значение ошибки из File::open, теперь находящееся в переменной шаблона e, обратно вызывающему коду как значение ошибки этой функции.

Итак, если у нас есть дескриптор файла в username_file, функция затем создает новую String в переменной username и вызывает метод read_to_string для дескриптора файла в username_file, чтобы прочитать содержимое файла в username. Метод read_to_string тоже возвращает Result, потому что он может завершиться неудачно, даже если File::open завершилась успешно. Поэтому нам нужен еще один match, чтобы обработать этот Result: если read_to_string завершается успешно, наша функция тоже завершилась успешно, и мы возвращаем имя пользователя из файла, которое теперь находится в username, обернув его в Ok. Если read_to_string завершается неудачно, мы возвращаем значение ошибки так же, как вернули значение ошибки в match, который обрабатывал возвращаемое значение File::open. Однако здесь не нужно явно писать return, потому что это последнее выражение в функции.

Код, который вызывает эту функцию, затем обработает либо значение Ok, содержащее имя пользователя, либо значение Err, содержащее io::Error. Именно вызывающий код решает, что делать с этими значениями. Если вызывающий код получит значение Err, он может вызвать panic! и аварийно завершить программу, использовать имя пользователя по умолчанию или, например, найти имя пользователя где-то еще, а не в файле. У нас недостаточно информации о том, что на самом деле пытается сделать вызывающий код, поэтому мы распространяем всю информацию об успехе или ошибке вверх, чтобы она была обработана подходящим образом.

Этот шаблон распространения ошибок настолько распространен в Rust, что Rust предоставляет оператор вопросительного знака ?, чтобы упростить его.

Сокращение для распространения ошибок: оператор ?

Листинг 9-7 показывает реализацию read_username_from_file, которая имеет ту же функциональность, что и в листинге 9-6, но использует оператор ?.

Filename: src/main.rs
#![allow(unused)]
fn main() {
use std::fs::File;
use std::io::{self, Read};

fn read_username_from_file() -> Result<String, io::Error> {
    let mut username_file = File::open("hello.txt")?;
    let mut username = String::new();
    username_file.read_to_string(&mut username)?;
    Ok(username)
}
}
Listing 9-7: Функция, которая возвращает ошибки вызывающему коду с помощью оператора ?

Оператор ?, поставленный после значения Result, определен так, что работает почти так же, как выражения match, которые мы написали для обработки значений Result в листинге 9-6. Если значение Result равно Ok, значение внутри Ok будет возвращено из этого выражения, и программа продолжит выполнение. Если значение равно Err, Err будет возвращен из всей функции так, как если бы мы использовали ключевое слово return, чтобы значение ошибки было распространено в вызывающий код.

Между тем, что делает выражение match из листинга 9-6, и тем, что делает оператор ?, есть различие: значения ошибок, к которым применяется оператор ?, проходят через функцию from, определенную в трейте From стандартной библиотеки и используемую для преобразования значений из одного типа в другой. Когда оператор ? вызывает функцию from, полученный тип ошибки преобразуется в тип ошибки, указанный в возвращаемом типе текущей функции. Это полезно, когда функция возвращает один тип ошибки, представляющий все способы, которыми функция может завершиться неудачно, даже если разные части могут завершаться неудачно по разным причинам.

Например, мы могли бы изменить функцию read_username_from_file из листинга 9-7 так, чтобы она возвращала собственный тип ошибки с именем OurError, который мы определим. Если мы также определим impl From<io::Error> for OurError, чтобы создавать экземпляр OurError из io::Error, то вызовы оператора ? в теле read_username_from_file будут вызывать from и преобразовывать типы ошибок без необходимости добавлять в функцию какой-либо дополнительный код.

В контексте листинга 9-7 оператор ? в конце вызова File::open вернет значение внутри Ok в переменную username_file. Если произойдет ошибка, оператор ? досрочно выйдет из всей функции и отдаст любое значение Err вызывающему коду. То же самое относится к оператору ? в конце вызова read_to_string.

Оператор ? устраняет большое количество шаблонного кода и делает реализацию этой функции проще. Мы могли бы сократить этот код еще сильнее, сразу связав вызовы методов после ?, как показано в листинге 9-8.

Filename: src/main.rs
#![allow(unused)]
fn main() {
use std::fs::File;
use std::io::{self, Read};

fn read_username_from_file() -> Result<String, io::Error> {
    let mut username = String::new();

    File::open("hello.txt")?.read_to_string(&mut username)?;

    Ok(username)
}
}
Listing 9-8: Связывание вызовов методов после оператора ?

Мы перенесли создание новой String в username в начало функции; эта часть не изменилась. Вместо создания переменной username_file мы связали вызов read_to_string напрямую с результатом File::open("hello.txt")?. У нас по-прежнему есть ? в конце вызова read_to_string, и мы по-прежнему возвращаем значение Ok, содержащее username, когда и File::open, и read_to_string завершаются успешно, вместо того чтобы возвращать ошибки. Функциональность снова та же, что в листингах 9-6 и 9-7; это просто другой, более эргономичный способ записать ее.

Листинг 9-9 показывает, как сделать это еще короче с помощью fs::read_to_string.

Filename: src/main.rs
#![allow(unused)]
fn main() {
use std::fs;
use std::io;

fn read_username_from_file() -> Result<String, io::Error> {
    fs::read_to_string("hello.txt")
}
}
Listing 9-9: Использование fs::read_to_string вместо открытия и последующего чтения файла

Чтение файла в строку – довольно распространенная операция, поэтому стандартная библиотека предоставляет удобную функцию fs::read_to_string, которая открывает файл, создает новую String, читает содержимое файла, помещает содержимое в эту String и возвращает ее. Конечно, использование fs::read_to_string не дало бы нам возможности объяснить всю обработку ошибок, поэтому сначала мы сделали это более длинным способом.

Где можно использовать оператор ?

Оператор ? можно использовать только в функциях, возвращаемый тип которых совместим со значением, к которому применяется ?. Причина в том, что оператор ? определен так, чтобы выполнять досрочный возврат значения из функции, так же как выражение match, которое мы определили в листинге 9-6. В листинге 9-6 match использовал значение Result, а ветвь досрочного возврата возвращала значение Err(e). Возвращаемый тип функции должен быть Result, чтобы быть совместимым с этим return.

В листинге 9-10 посмотрим, какую ошибку мы получим, если используем оператор ? в функции main с возвращаемым типом, несовместимым с типом значения, к которому мы применяем ?.

Filename: src/main.rs
use std::fs::File;

fn main() {
    let greeting_file = File::open("hello.txt")?;
}
Listing 9-10: Попытка использовать ? в функции main, которая возвращает (), не скомпилируется.

Этот код открывает файл, что может завершиться неудачно. Оператор ? следует за значением Result, возвращенным File::open, но у этой функции main возвращаемый тип (), а не Result. Когда мы компилируем этот код, получаем следующее сообщение об ошибке:

$ cargo run
   Compiling error-handling v0.1.0 (file:///projects/error-handling)
error[E0277]: the `?` operator can only be used in a function that returns `Result` or `Option` (or another type that implements `FromResidual`)
 --> src/main.rs:4:48
  |
3 | fn main() {
  | --------- this function should return `Result` or `Option` to accept `?`
4 |     let greeting_file = File::open("hello.txt")?;
  |                                                ^ cannot use the `?` operator in a function that returns `()`
  |
help: consider adding return type
  |
3 ~ fn main() -> Result<(), Box<dyn std::error::Error>> {
4 |     let greeting_file = File::open("hello.txt")?;
5 +     Ok(())
  |

For more information about this error, try `rustc --explain E0277`.
error: could not compile `error-handling` (bin "error-handling") due to 1 previous error

Эта ошибка указывает, что использовать оператор ? разрешено только в функции, которая возвращает Result, Option или другой тип, реализующий FromResidual.

Чтобы исправить ошибку, у вас есть два варианта. Первый – изменить возвращаемый тип функции так, чтобы он был совместим со значением, к которому вы применяете оператор ?, если этому ничего не препятствует. Второй – использовать match или один из методов Result<T, E>, чтобы обработать Result<T, E> подходящим образом.

Сообщение об ошибке также упоминало, что ? можно использовать и со значениями Option<T>. Как и при использовании ? с Result, использовать ? с Option можно только в функции, которая возвращает Option. Поведение оператора ? при вызове для Option<T> похоже на его поведение при вызове для Result<T, E>: если значение равно None, None будет досрочно возвращено из функции в этой точке. Если значение равно Some, значение внутри Some становится итоговым значением выражения, и функция продолжает выполнение. В листинге 9-11 приведен пример функции, которая находит последний символ первой строки в заданном тексте.

fn last_char_of_first_line(text: &str) -> Option<char> {
    text.lines().next()?.chars().last()
}

fn main() {
    assert_eq!(
        last_char_of_first_line("Hello, world\nHow are you today?"),
        Some('d')
    );

    assert_eq!(last_char_of_first_line(""), None);
    assert_eq!(last_char_of_first_line("\nhi"), None);
}
Listing 9-11: Использование оператора ? для значения Option<T>

Эта функция возвращает Option<char>, потому что символ может существовать, а может и отсутствовать. Этот код принимает строковый срез text в качестве аргумента и вызывает для него метод lines, который возвращает итератор по строкам в строке. Поскольку эта функция хочет проверить первую строку, она вызывает next у итератора, чтобы получить первое значение из итератора. Если text – пустая строка, этот вызов next вернет None; в таком случае мы используем ?, чтобы остановиться и вернуть None из last_char_of_first_line. Если text не является пустой строкой, next вернет значение Some, содержащее строковый срез первой строки в text.

Оператор ? извлекает строковый срез, и мы можем вызвать chars для этого строкового среза, чтобы получить итератор по его символам. Нас интересует последний символ в этой первой строке, поэтому мы вызываем last, чтобы вернуть последний элемент итератора. Это Option, потому что первая строка может быть пустой: например, если text начинается с пустой строки, но содержит символы в других строках, как в "\nhi". Однако если в первой строке есть последний символ, он будет возвращен в варианте Some. Оператор ? в середине дает нам лаконичный способ выразить эту логику, позволяя реализовать функцию в одну строку. Если бы мы не могли использовать оператор ? с Option, нам пришлось бы реализовать эту логику с помощью большего числа вызовов методов или выражения match.

Обратите внимание: оператор ? можно использовать с Result в функции, которая возвращает Result, и можно использовать оператор ? с Option в функции, которая возвращает Option, но нельзя смешивать эти случаи. Оператор ? не будет автоматически преобразовывать Result в Option или наоборот; в таких случаях можно использовать методы вроде ok для Result или ok_or для Option, чтобы выполнить преобразование явно.

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

К счастью, main также может возвращать Result<(), E>. В листинге 9-12 показан код из листинга 9-10, но мы изменили возвращаемый тип main на Result<(), Box<dyn Error>> и добавили возвращаемое значение Ok(()) в конец. Теперь этот код скомпилируется.

Filename: src/main.rs
use std::error::Error;
use std::fs::File;

fn main() -> Result<(), Box<dyn Error>> {
    let greeting_file = File::open("hello.txt")?;

    Ok(())
}
Listing 9-12: Изменение main так, чтобы она возвращала Result<(), E>, позволяет использовать оператор ? для значений Result.

Тип Box<dyn Error> – это трейт-объект, о котором мы поговорим в разделе «Использование трейт-объектов для абстракции над общим поведением» главы 18. Пока что можно читать Box<dyn Error> как «любой вид ошибки». Использовать ? для значения Result в функции main с типом ошибки Box<dyn Error> разрешено, потому что это позволяет досрочно вернуть любое значение Err. Хотя тело этой функции main всегда будет возвращать только ошибки типа std::io::Error, при указании Box<dyn Error> эта сигнатура останется корректной, даже если в тело main позже будет добавлен код, возвращающий другие ошибки.

Когда функция main возвращает Result<(), E>, исполняемый файл завершится со значением 0, если main вернет Ok(()), и завершится с ненулевым значением, если main вернет значение Err. Исполняемые файлы, написанные на C, возвращают целые числа при завершении: программы, завершившиеся успешно, возвращают целое число 0, а программы, завершившиеся с ошибкой, возвращают какое-то целое число, отличное от 0. Rust также возвращает целые числа из исполняемых файлов, чтобы быть совместимым с этим соглашением.

Функция main может возвращать любые типы, реализующие трейт std::process::Termination, который содержит функцию report, возвращающую ExitCode. Обратитесь к документации стандартной библиотеки за дополнительной информацией о реализации трейта Termination для собственных типов.

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

panic! или не panic!

Использовать panic! или не использовать panic!

Итак, как решить, когда следует вызывать panic!, а когда следует возвращать Result? Когда код паникует, восстановиться уже невозможно. Вы можете вызвать panic! в любой ошибочной ситуации, независимо от того, есть ли возможный способ восстановиться или нет, но тогда вы принимаете решение за вызывающий код о том, что ситуация невосстанавливаемая. Когда вы выбираете возврат значения Result, вы даете вызывающему коду варианты. Вызывающий код может попытаться восстановиться способом, подходящим для его ситуации, или решить, что значение Err в этом случае невосстанавливаемо, и вызвать panic!, превратив вашу восстанавливаемую ошибку в невосстанавливаемую. Поэтому возврат Result – хороший выбор по умолчанию при определении функции, которая может завершиться неудачно.

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

Примеры, прототипный код и тесты

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

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

Если вызов метода завершается неудачно в тесте, вы хотите, чтобы весь тест провалился, даже если этот метод не является проверяемой функциональностью. Поскольку именно panic! помечает тест как неуспешный, вызов unwrap или expect – ровно то, что должно произойти.

Когда у вас больше информации, чем у компилятора

Также уместно вызвать expect, когда у вас есть другая логика, гарантирующая, что Result будет содержать значение Ok, но эта логика не является чем-то, что понимает компилятор. У вас все равно будет значение Result, которое нужно обработать: любая вызываемая операция в общем случае все еще может завершиться неудачно, хотя в вашей конкретной ситуации это логически невозможно. Если с помощью ручной проверки кода вы можете убедиться, что варианта Err никогда не будет, вполне допустимо вызвать expect и в тексте аргумента задокументировать причину, по которой, как вы считаете, варианта Err никогда не будет. Вот пример:

fn main() {
    use std::net::IpAddr;

    let home: IpAddr = "127.0.0.1"
        .parse()
        .expect("Hardcoded IP address should be valid");
}

Мы создаем экземпляр IpAddr, разбирая жестко заданную строку. Мы видим, что 127.0.0.1 – допустимый IP-адрес, поэтому здесь допустимо использовать expect. Однако наличие жестко заданной допустимой строки не меняет возвращаемый тип метода parse: мы все равно получаем значение Result, и компилятор все равно заставит нас обработать Result так, как если бы вариант Err был возможен, потому что компилятор недостаточно умен, чтобы увидеть, что эта строка всегда является допустимым IP-адресом. Если бы строка IP-адреса поступала от пользователя, а не была жестко задана в программе, и поэтому действительно могла привести к неудаче, мы определенно захотели бы вместо этого обработать Result более надежным способом. Упоминание предположения о том, что этот IP-адрес жестко задан, подскажет нам заменить expect на более качественный код обработки ошибок, если в будущем нам понадобится получать IP-адрес из другого источника.

Рекомендации по обработке ошибок

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

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

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

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

Когда ваш код выполняет операцию, которая может подвергнуть пользователя риску, если ее вызвать с недопустимыми значениями, ваш код должен сначала проверить, что значения допустимы, и запаниковать, если они недопустимы. Это нужно в основном по причинам безопасности: попытка работать с недопустимыми данными может открыть ваш код для уязвимостей. Это главная причина, по которой стандартная библиотека вызовет panic!, если вы попытаетесь обратиться к памяти за допустимыми границами: попытка доступа к памяти, которая не принадлежит текущей структуре данных, является распространенной проблемой безопасности. У функций часто есть контракты: их поведение гарантируется только если входные данные удовлетворяют определенным требованиям. Паника при нарушении контракта имеет смысл, потому что нарушение контракта всегда указывает на ошибку со стороны вызывающего кода, и это не тот вид ошибки, который вызывающий код должен явно обрабатывать. Фактически у вызывающего кода нет разумного способа восстановиться; исправить код должны вызывающие программисты. Контракты функции, особенно когда нарушение приведет к панике, должны быть объяснены в API-документации этой функции.

Однако большое количество проверок ошибок во всех ваших функциях было бы многословным и раздражающим. К счастью, можно использовать систему типов Rust (и, следовательно, проверку типов, выполняемую компилятором), чтобы сделать многие проверки за вас. Если у функции есть параметр определенного типа, вы можете продолжать выполнять логику своего кода, зная, что компилятор уже убедился в наличии допустимого значения. Например, если у вас есть тип, а не Option, ваша программа ожидает получить что-то, а не ничего. Тогда коду не нужно обрабатывать два случая для вариантов Some и None: у него будет только один случай – значение точно есть. Код, который пытается передать в вашу функцию ничего, даже не скомпилируется, поэтому вашей функции не нужно проверять этот случай во время выполнения. Другой пример – использование беззнакового целочисленного типа, такого как u32, который гарантирует, что параметр никогда не будет отрицательным.

Пользовательские типы для валидации

Сделаем еще один шаг в идее использования системы типов Rust, чтобы обеспечить наличие допустимого значения, и рассмотрим создание пользовательского типа для валидации. Вспомните игру в угадывание из главы 2, где наш код просил пользователя угадать число от 1 до 100. Мы никогда не проверяли, что предположение пользователя находится между этими числами, прежде чем сравнивать его с нашим секретным числом; мы проверяли только, что предположение положительное. В этом случае последствия были не слишком серьезными: наш вывод «Слишком много» или «Слишком мало» все равно был бы корректным. Но полезным улучшением было бы направлять пользователя к допустимым предположениям и иметь разное поведение, когда пользователь угадывает число вне диапазона и когда пользователь вводит, например, буквы.

Один способ сделать это – разобрать предположение как i32, а не только как u32, чтобы допустить потенциально отрицательные числа, а затем добавить проверку, что число находится в диапазоне, вот так:

Filename: src/main.rs
use rand::Rng;
use std::cmp::Ordering;
use std::io;

fn main() {
    println!("Guess the number!");

    let secret_number = rand::thread_rng().gen_range(1..=100);

    loop {
        // --snip--

        println!("Please input your guess.");

        let mut guess = String::new();

        io::stdin()
            .read_line(&mut guess)
            .expect("Failed to read line");

        let guess: i32 = match guess.trim().parse() {
            Ok(num) => num,
            Err(_) => continue,
        };

        if guess < 1 || guess > 100 {
            println!("The secret number will be between 1 and 100.");
            continue;
        }

        match guess.cmp(&secret_number) {
            // --snip--
            Ordering::Less => println!("Too small!"),
            Ordering::Greater => println!("Too big!"),
            Ordering::Equal => {
                println!("You win!");
                break;
            }
        }
    }
}

Выражение if проверяет, находится ли наше значение вне диапазона, сообщает пользователю о проблеме и вызывает continue, чтобы начать следующую итерацию цикла и запросить другое предположение. После выражения if мы можем продолжить сравнения между guess и секретным числом, зная, что guess находится между 1 и 100.

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

Вместо этого мы можем создать новый тип в отдельном модуле и поместить валидации в функцию, которая создает экземпляр типа, а не повторять проверки повсюду. Тогда функции смогут безопасно использовать новый тип в своих сигнатурах и уверенно работать с полученными значениями. Листинг 9-13 показывает один способ определить тип Guess, который создаст экземпляр Guess только если функция new получит значение между 1 и 100.

Filename: src/guessing_game.rs
#![allow(unused)]
fn main() {
pub struct Guess {
    value: i32,
}

impl Guess {
    pub fn new(value: i32) -> Guess {
        if value < 1 || value > 100 {
            panic!("Guess value must be between 1 and 100, got {value}.");
        }

        Guess { value }
    }

    pub fn value(&self) -> i32 {
        self.value
    }
}
}
Listing 9-13: Тип Guess, который продолжит работу только со значениями между 1 и 100

Обратите внимание, что этот код в src/guessing_game.rs зависит от добавления объявления модуля mod guessing_game; в src/lib.rs, которое здесь не показано. В файле этого нового модуля мы определяем структуру с именем Guess, у которой есть поле с именем value, содержащее i32. Именно здесь будет храниться число.

Затем мы реализуем для Guess связанную функцию с именем new, которая создает экземпляры значений Guess. Функция new определена с одним параметром value типа i32 и возвращает Guess. Код в теле функции new проверяет value, чтобы убедиться, что оно находится между 1 и 100. Если value не проходит эту проверку, мы вызываем panic!, что предупредит программиста, пишущего вызывающий код, о наличии ошибки, которую нужно исправить, потому что создание Guess со значением value вне этого диапазона нарушило бы контракт, на который полагается Guess::new. Условия, при которых Guess::new может паниковать, следует обсудить в его публичной API-документации; в главе 14 мы рассмотрим соглашения документации, указывающие на возможность panic! в API-документации, которую вы создаете. Если value проходит проверку, мы создаем новый Guess, установив его поле value равным параметру value, и возвращаем Guess.

Затем мы реализуем метод с именем value, который заимствует self, не имеет других параметров и возвращает i32. Такой метод иногда называют геттером, потому что его цель – получить некоторые данные из полей и вернуть их. Этот публичный метод необходим, потому что поле value структуры Guess приватное. Важно, чтобы поле value было приватным, чтобы код, использующий структуру Guess, не мог устанавливать value напрямую: код вне модуля guessing_game обязан использовать функцию Guess::new для создания экземпляра Guess, тем самым гарантируя, что не существует способа получить Guess со значением value, которое не было проверено условиями в функции Guess::new.

Функция, параметром которой являются только числа между 1 и 100 или которая возвращает только такие числа, затем могла бы объявить в своей сигнатуре, что она принимает или возвращает Guess, а не i32, и ей не потребовались бы дополнительные проверки в теле.

Итоги

Возможности Rust для обработки ошибок предназначены для того, чтобы помогать писать более надежный код. Макрос panic! сигнализирует, что ваша программа находится в состоянии, с которым не может справиться, и позволяет сказать процессу остановиться, вместо того чтобы пытаться продолжать работу с недопустимыми или некорректными значениями. Enum Result использует систему типов Rust, чтобы указать, что операции могут завершиться неудачно таким образом, от которого ваш код может восстановиться. Вы можете использовать Result, чтобы сообщить коду, вызывающему ваш код, что ему тоже нужно обработать потенциальный успех или неудачу. Использование panic! и Result в подходящих ситуациях сделает ваш код надежнее перед лицом неизбежных проблем.

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

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

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

Функции могут принимать параметры некоторого обобщенного типа вместо конкретного типа, такого как i32 или String, подобно тому как они принимают параметры с неизвестными значениями, чтобы выполнять один и тот же код для нескольких конкретных значений. Фактически мы уже использовали обобщения в главе 6 с Option<T>, в главе 8 с Vec<T> и HashMap<K, V>, а в главе 9 с Result<T, E>. В этой главе вы узнаете, как определять собственные типы, функции и методы с обобщениями!

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

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

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

Устранение дублирования путем выделения функции

Обобщения позволяют заменить конкретные типы заполнителем, представляющим несколько типов, чтобы устранить дублирование кода. Прежде чем погружаться в синтаксис обобщений, сначала посмотрим, как устранить дублирование способом, который не связан с обобщенными типами: выделим функцию, заменяющую конкретные значения заполнителем, представляющим несколько значений. Затем применим тот же прием, чтобы выделить обобщенную функцию! Рассматривая, как распознавать дублирующийся код, который можно вынести в функцию, вы начнете распознавать и дублирующийся код, где можно использовать обобщения.

Начнем с короткой программы в листинге 10-1, которая находит наибольшее число в списке.

Filename: src/main.rs
fn main() {
    let number_list = vec![34, 50, 25, 100, 65];

    let mut largest = &number_list[0];

    for number in &number_list {
        if number > largest {
            largest = number;
        }
    }

    println!("The largest number is {largest}");
    assert_eq!(*largest, 100);
}
Listing 10-1: Поиск наибольшего числа в списке чисел

Мы сохраняем список целых чисел в переменной number_list и помещаем ссылку на первое число в списке в переменную с именем largest. Затем мы перебираем все числа в списке, и если текущее число больше числа, сохраненного в largest, заменяем ссылку в этой переменной. Однако если текущее число меньше или равно наибольшему числу, встреченному на данный момент, переменная не изменяется, и код переходит к следующему числу в списке. После рассмотрения всех чисел в списке largest должна ссылаться на наибольшее число, которым в этом случае является 100.

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

Filename: src/main.rs
fn main() {
    let number_list = vec![34, 50, 25, 100, 65];

    let mut largest = &number_list[0];

    for number in &number_list {
        if number > largest {
            largest = number;
        }
    }

    println!("The largest number is {largest}");

    let number_list = vec![102, 34, 6000, 89, 54, 2, 43, 8];

    let mut largest = &number_list[0];

    for number in &number_list {
        if number > largest {
            largest = number;
        }
    }

    println!("The largest number is {largest}");
}
Listing 10-2: Код для поиска наибольшего числа в двух списках чисел

Хотя этот код работает, дублирование кода утомительно и чревато ошибками. Кроме того, когда мы хотим изменить код, нам нужно не забыть обновить его в нескольких местах.

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

В листинге 10-3 мы выделяем код, который находит наибольшее число, в функцию с именем largest. Затем мы вызываем эту функцию, чтобы найти наибольшее число в двух списках из листинга 10-2. В будущем мы также могли бы использовать эту функцию для любого другого имеющегося у нас списка значений i32.

Filename: src/main.rs
fn largest(list: &[i32]) -> &i32 {
    let mut largest = &list[0];

    for item in list {
        if item > largest {
            largest = item;
        }
    }

    largest
}

fn main() {
    let number_list = vec![34, 50, 25, 100, 65];

    let result = largest(&number_list);
    println!("The largest number is {result}");
    assert_eq!(*result, 100);

    let number_list = vec![102, 34, 6000, 89, 54, 2, 43, 8];

    let result = largest(&number_list);
    println!("The largest number is {result}");
    assert_eq!(*result, 6000);
}
Listing 10-3: Абстрагированный код для поиска наибольшего числа в двух списках

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

Итак, вот шаги, которые мы выполнили, чтобы изменить код из листинга 10-2 в код из листинга 10-3:

  1. Определить дублирующийся код.
  2. Выделить дублирующийся код в тело функции и указать входные и возвращаемые значения этого кода в сигнатуре функции.
  3. Обновить два экземпляра дублирующегося кода так, чтобы вместо него они вызывали функцию.

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

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

Обобщённые типы данных

Обобщенные типы данных

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

В определениях функций

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

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

Filename: src/main.rs
fn largest_i32(list: &[i32]) -> &i32 {
    let mut largest = &list[0];

    for item in list {
        if item > largest {
            largest = item;
        }
    }

    largest
}

fn largest_char(list: &[char]) -> &char {
    let mut largest = &list[0];

    for item in list {
        if item > largest {
            largest = item;
        }
    }

    largest
}

fn main() {
    let number_list = vec![34, 50, 25, 100, 65];

    let result = largest_i32(&number_list);
    println!("The largest number is {result}");
    assert_eq!(*result, 100);

    let char_list = vec!['y', 'm', 'a', 'q'];

    let result = largest_char(&char_list);
    println!("The largest char is {result}");
    assert_eq!(*result, 'y');
}
Listing 10-4: Две функции, которые отличаются только именами и типами в своих сигнатурах

Функция largest_i32 – это функция, которую мы выделили в листинге 10-3 и которая находит наибольшее значение i32 в срезе. Функция largest_char находит наибольшее значение char в срезе. Тела функций содержат один и тот же код, поэтому устраним дублирование, введя параметр обобщенного типа в единственной функции.

Чтобы параметризовать типы в новой единственной функции, нужно дать имя параметру типа, так же как мы даем имена параметрам значений функции. В качестве имени параметра типа можно использовать любой идентификатор. Но мы будем использовать T, потому что по соглашению имена параметров типов в Rust короткие, часто всего из одной буквы, а соглашение Rust об именовании типов – UpperCamelCase. T, сокращение от type, является выбором по умолчанию для большинства Rust-программистов.

Когда мы используем параметр в теле функции, нужно объявить имя параметра в сигнатуре, чтобы компилятор знал, что означает это имя. Аналогично, когда мы используем имя параметра типа в сигнатуре функции, нужно объявить это имя параметра типа до его использования. Чтобы определить обобщенную функцию largest, мы помещаем объявления имен типов в угловые скобки, <>, между именем функции и списком параметров, вот так:

fn largest<T>(list: &[T]) -> &T {

Мы читаем это определение так: «Функция largest является обобщенной по некоторому типу T». У этой функции есть один параметр с именем list, срез значений типа T. Функция largest вернет ссылку на значение того же типа T.

Листинг 10-5 показывает объединенное определение функции largest, в сигнатуре которой используется обобщенный тип данных. Листинг также показывает, как можно вызвать функцию либо со срезом значений i32, либо со значениями char. Обратите внимание, что этот код пока не скомпилируется.

Filename: src/main.rs
fn largest<T>(list: &[T]) -> &T {
    let mut largest = &list[0];

    for item in list {
        if item > largest {
            largest = item;
        }
    }

    largest
}

fn main() {
    let number_list = vec![34, 50, 25, 100, 65];

    let result = largest(&number_list);
    println!("The largest number is {result}");

    let char_list = vec!['y', 'm', 'a', 'q'];

    let result = largest(&char_list);
    println!("The largest char is {result}");
}
Listing 10-5: Функция largest, использующая параметры обобщенного типа; этот код пока не компилируется

Если мы скомпилируем этот код прямо сейчас, получим такую ошибку:

$ cargo run
   Compiling chapter10 v0.1.0 (file:///projects/chapter10)
error[E0369]: binary operation `>` cannot be applied to type `&T`
 --> src/main.rs:5:17
  |
5 |         if item > largest {
  |            ---- ^ ------- &T
  |            |
  |            &T
  |
help: consider restricting type parameter `T` with trait `PartialOrd`
  |
1 | fn largest<T: std::cmp::PartialOrd>(list: &[T]) -> &T {
  |             ++++++++++++++++++++++

For more information about this error, try `rustc --explain E0369`.
error: could not compile `chapter10` (bin "chapter10") due to 1 previous error

Текст помощи упоминает std::cmp::PartialOrd, который является трейтом, а о трейтах мы поговорим в следующем разделе. Пока достаточно знать, что эта ошибка говорит: тело largest не будет работать для всех возможных типов, которыми может быть T. Поскольку мы хотим сравнивать значения типа T в теле функции, можно использовать только типы, значения которых можно упорядочивать. Чтобы включить сравнения, стандартная библиотека предоставляет трейт std::cmp::PartialOrd, который можно реализовать для типов (подробнее об этом трейте см. в приложении C). Чтобы исправить листинг 10-5, можно последовать предложению текста помощи и ограничить типы, допустимые для T, только теми, которые реализуют PartialOrd. Тогда листинг скомпилируется, потому что стандартная библиотека реализует PartialOrd и для i32, и для char.

В определениях структур

Мы также можем определять структуры так, чтобы они использовали параметр обобщенного типа в одном или нескольких полях, применяя синтаксис <>. Листинг 10-6 определяет структуру Point<T> для хранения значений координат x и y любого типа.

Filename: src/main.rs
struct Point<T> {
    x: T,
    y: T,
}

fn main() {
    let integer = Point { x: 5, y: 10 };
    let float = Point { x: 1.0, y: 4.0 };
}
Listing 10-6: Структура Point<T>, которая хранит значения x и y типа T

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

Обратите внимание: поскольку мы использовали только один обобщенный тип для определения Point<T>, это определение говорит, что структура Point<T> является обобщенной по некоторому типу T, а поля x и y имеют оба этот же самый тип, каким бы он ни был. Если мы создадим экземпляр Point<T> со значениями разных типов, как в листинге 10-7, наш код не скомпилируется.

Filename: src/main.rs
struct Point<T> {
    x: T,
    y: T,
}

fn main() {
    let wont_work = Point { x: 5, y: 4.0 };
}
Listing 10-7: Поля x и y должны быть одного типа, потому что оба имеют один и тот же обобщенный тип данных T.

В этом примере, когда мы присваиваем целочисленное значение 5 полю x, мы сообщаем компилятору, что обобщенный тип T для этого экземпляра Point<T> будет целочисленным. Затем, когда мы указываем 4.0 для y, которое определено как имеющее тот же тип, что и x, мы получим ошибку несоответствия типов вроде этой:

$ cargo run
   Compiling chapter10 v0.1.0 (file:///projects/chapter10)
error[E0308]: mismatched types
 --> src/main.rs:7:38
  |
7 |     let wont_work = Point { x: 5, y: 4.0 };
  |                                      ^^^ expected integer, found floating-point number

For more information about this error, try `rustc --explain E0308`.
error: could not compile `chapter10` (bin "chapter10") due to 1 previous error

Чтобы определить структуру Point, где x и y оба являются обобщенными, но могут иметь разные типы, можно использовать несколько параметров обобщенных типов. Например, в листинге 10-8 мы изменяем определение Point так, чтобы оно было обобщенным по типам T и U, где x имеет тип T, а y – тип U.

Filename: src/main.rs
struct Point<T, U> {
    x: T,
    y: U,
}

fn main() {
    let both_integer = Point { x: 5, y: 10 };
    let both_float = Point { x: 1.0, y: 4.0 };
    let integer_and_float = Point { x: 5, y: 4.0 };
}
Listing 10-8: Point<T, U>, обобщенная по двум типам, чтобы x и y могли быть значениями разных типов

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

В определениях enum

Как и со структурами, мы можем определять enum так, чтобы их варианты хранили обобщенные типы данных. Давайте еще раз посмотрим на enum Option<T>, предоставляемый стандартной библиотекой, который мы использовали в главе 6:

#![allow(unused)]
fn main() {
enum Option<T> {
    Some(T),
    None,
}
}

Теперь это определение должно быть понятнее. Как видите, enum Option<T> является обобщенным по типу T и имеет два варианта: Some, который хранит одно значение типа T, и вариант None, который не хранит никакого значения. Используя enum Option<T>, мы можем выразить абстрактное понятие необязательного значения, и поскольку Option<T> является обобщенным, мы можем использовать эту абстракцию независимо от типа необязательного значения.

Enum также могут использовать несколько обобщенных типов. Определение enum Result, которое мы использовали в главе 9, является одним из примеров:

#![allow(unused)]
fn main() {
enum Result<T, E> {
    Ok(T),
    Err(E),
}
}

Enum Result является обобщенным по двум типам, T и E, и имеет два варианта: Ok, который хранит значение типа T, и Err, который хранит значение типа E. Это определение делает удобным использование enum Result везде, где есть операция, которая может завершиться успешно (вернуть значение некоторого типа T) или завершиться неудачно (вернуть ошибку некоторого типа E). Фактически именно это мы использовали для открытия файла в листинге 9-3: T был заменен типом std::fs::File, когда файл успешно открывался, а E – типом std::io::Error, когда при открытии файла возникали проблемы.

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

В определениях методов

Мы можем реализовывать методы для структур и enum (как делали в главе 5) и также использовать обобщенные типы в их определениях. Листинг 10-9 показывает структуру Point<T>, которую мы определили в листинге 10-6, с реализованным для нее методом с именем x.

Filename: src/main.rs
struct Point<T> {
    x: T,
    y: T,
}

impl<T> Point<T> {
    fn x(&self) -> &T {
        &self.x
    }
}

fn main() {
    let p = Point { x: 5, y: 10 };

    println!("p.x = {}", p.x());
}
Listing 10-9: Реализация метода с именем x для структуры Point<T>, который возвращает ссылку на поле x типа T

Здесь мы определили метод с именем x для Point<T>, который возвращает ссылку на данные в поле x.

Обратите внимание, что нужно объявить T сразу после impl, чтобы мы могли использовать T для указания, что реализуем методы для типа Point<T>. Объявляя T как обобщенный тип после impl, Rust может определить, что тип в угловых скобках в Point является обобщенным типом, а не конкретным типом. Мы могли бы выбрать другое имя для этого обобщенного параметра, отличное от обобщенного параметра, объявленного в определении структуры, но использовать то же имя принято по соглашению. Если вы пишете метод внутри impl, который объявляет обобщенный тип, этот метод будет определен для любого экземпляра типа, независимо от того, какой конкретный тип в итоге подставится вместо обобщенного типа.

При определении методов для типа мы также можем задавать ограничения на обобщенные типы. Например, можно реализовать методы только для экземпляров Point<f32>, а не для экземпляров Point<T> с любым обобщенным типом. В листинге 10-10 мы используем конкретный тип f32, то есть не объявляем никаких типов после impl.

Filename: src/main.rs
struct Point<T> {
    x: T,
    y: T,
}

impl<T> Point<T> {
    fn x(&self) -> &T {
        &self.x
    }
}

impl Point<f32> {
    fn distance_from_origin(&self) -> f32 {
        (self.x.powi(2) + self.y.powi(2)).sqrt()
    }
}

fn main() {
    let p = Point { x: 5, y: 10 };

    println!("p.x = {}", p.x());
}
Listing 10-10: Блок impl, который применяется только к структуре с определенным конкретным типом для параметра обобщенного типа T

Этот код означает, что тип Point<f32> будет иметь метод distance_from_origin; другие экземпляры Point<T>, где T не является типом f32, не будут иметь определенного метода. Метод измеряет, как далеко наша точка находится от точки с координатами (0.0, 0.0), и использует математические операции, доступные только для типов с плавающей точкой.

Параметры обобщенных типов в определении структуры не всегда совпадают с теми, которые используются в сигнатурах методов этой же структуры. Листинг 10-11 использует обобщенные типы X1 и Y1 для структуры Point и X2 и Y2 для сигнатуры метода mixup, чтобы сделать пример понятнее. Метод создает новый экземпляр Point со значением x из self типа Point (типа X1) и значением y из переданного Point (типа Y2).

Filename: src/main.rs
struct Point<X1, Y1> {
    x: X1,
    y: Y1,
}

impl<X1, Y1> Point<X1, Y1> {
    fn mixup<X2, Y2>(self, other: Point<X2, Y2>) -> Point<X1, Y2> {
        Point {
            x: self.x,
            y: other.y,
        }
    }
}

fn main() {
    let p1 = Point { x: 5, y: 10.4 };
    let p2 = Point { x: "Hello", y: 'c' };

    let p3 = p1.mixup(p2);

    println!("p3.x = {}, p3.y = {}", p3.x, p3.y);
}
Listing 10-11: Метод, который использует обобщенные типы, отличающиеся от типов в определении его структуры

В main мы определили Point, у которого x имеет тип i32 (со значением 5), а y имеет тип f64 (со значением 10.4). Переменная p2 – это структура Point, у которой x является строковым срезом (со значением "Hello"), а y имеет тип char (со значением c). Вызов mixup для p1 с аргументом p2 дает нам p3, у которого x будет иметь тип i32, потому что x пришел из p1. У переменной p3 y будет иметь тип char, потому что y пришел из p2. Вызов макроса println! напечатает p3.x = 5, p3.y = c.

Цель этого примера – показать ситуацию, в которой некоторые обобщенные параметры объявляются с impl, а некоторые – с определением метода. Здесь обобщенные параметры X1 и Y1 объявлены после impl, потому что они относятся к определению структуры. Обобщенные параметры X2 и Y2 объявлены после fn mixup, потому что они относятся только к методу.

Производительность кода, использующего обобщения

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

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

Посмотрим, как это работает, используя обобщенный enum Option<T> из стандартной библиотеки:

#![allow(unused)]
fn main() {
let integer = Some(5);
let float = Some(5.0);
}

Когда Rust компилирует этот код, он выполняет мономорфизацию. Во время этого процесса компилятор читает значения, использованные в экземплярах Option<T>, и определяет две разновидности Option<T>: одна – i32, другая – f64. Соответственно, он разворачивает обобщенное определение Option<T> в два определения, специализированные для i32 и f64, тем самым заменяя обобщенное определение конкретными.

Мономорфизированная версия кода выглядит примерно так (для иллюстрации компилятор использует имена, отличные от тех, что используем здесь мы):

Filename: src/main.rs
enum Option_i32 {
    Some(i32),
    None,
}

enum Option_f64 {
    Some(f64),
    None,
}

fn main() {
    let integer = Option_i32::Some(5);
    let float = Option_f64::Some(5.0);
}

Обобщенный Option<T> заменяется конкретными определениями, созданными компилятором. Поскольку Rust компилирует обобщенный код в код, где в каждом экземпляре указан конкретный тип, мы не платим никаких затрат времени выполнения за использование обобщений. Когда код выполняется, он работает так же, как если бы мы вручную продублировали каждое определение. Процесс мономорфизации делает обобщения Rust чрезвычайно эффективными во время выполнения.

Определение общего поведения с помощью трейтов

Определение общего поведения с помощью трейтов

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

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

Определение трейта

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

Например, допустим, у нас есть несколько структур, которые хранят разные виды и объемы текста: структура NewsArticle, хранящая новостную историю, отправленную из определенного места, и SocialPost, который может иметь не более 280 символов вместе с метаданными, указывающими, был ли это новый пост, репост или ответ на другой пост.

Мы хотим создать библиотечный крейт медиаагрегатора с именем aggregator, который может отображать краткие сводки данных, хранящихся в экземпляре NewsArticle или SocialPost. Для этого нам нужна сводка от каждого типа, и мы будем запрашивать ее вызовом метода summarize для экземпляра. Листинг 10-12 показывает определение публичного трейта Summary, выражающего это поведение.

Filename: src/lib.rs
pub trait Summary {
    fn summarize(&self) -> String;
}
Listing 10-12: Трейт Summary, состоящий из поведения, предоставляемого методом summarize

Здесь мы объявляем трейт с помощью ключевого слова trait, а затем указываем имя трейта, в данном случае Summary. Мы также объявляем трейт как pub, чтобы крейты, зависящие от этого крейта, тоже могли использовать этот трейт, как мы увидим в нескольких примерах. Внутри фигурных скобок мы объявляем сигнатуры методов, описывающие поведение типов, реализующих этот трейт; в данном случае это fn summarize(&self) -> String.

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

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

Реализация трейта для типа

Теперь, когда мы определили желаемые сигнатуры методов трейта Summary, мы можем реализовать его для типов в нашем медиаагрегаторе. Листинг 10-13 показывает реализацию трейта Summary для структуры NewsArticle, которая использует заголовок, автора и место, чтобы создать возвращаемое значение summarize. Для структуры SocialPost мы определяем summarize как имя пользователя, за которым следует весь текст поста, предполагая, что содержимое поста уже ограничено 280 символами.

Filename: src/lib.rs
pub trait Summary {
    fn summarize(&self) -> String;
}

pub struct NewsArticle {
    pub headline: String,
    pub location: String,
    pub author: String,
    pub content: String,
}

impl Summary for NewsArticle {
    fn summarize(&self) -> String {
        format!("{}, by {} ({})", self.headline, self.author, self.location)
    }
}

pub struct SocialPost {
    pub username: String,
    pub content: String,
    pub reply: bool,
    pub repost: bool,
}

impl Summary for SocialPost {
    fn summarize(&self) -> String {
        format!("{}: {}", self.username, self.content)
    }
}
Listing 10-13: Реализация трейта Summary для типов NewsArticle и SocialPost

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

Теперь, когда библиотека реализовала трейт Summary для NewsArticle и SocialPost, пользователи крейта могут вызывать методы трейта для экземпляров NewsArticle и SocialPost так же, как мы вызываем обычные методы. Единственное отличие в том, что пользователь должен ввести в область видимости и трейт, и типы. Вот пример того, как бинарный крейт мог бы использовать наш библиотечный крейт aggregator:

use aggregator::{SocialPost, Summary};

fn main() {
    let post = SocialPost {
        username: String::from("horse_ebooks"),
        content: String::from(
            "of course, as you probably already know, people",
        ),
        reply: false,
        repost: false,
    };

    println!("1 new post: {}", post.summarize());
}

Этот код печатает 1 new post: horse_ebooks: of course, as you probably already know, people.

Другие крейты, зависящие от крейта aggregator, также могут ввести трейт Summary в область видимости, чтобы реализовать Summary для собственных типов. Важно отметить одно ограничение: мы можем реализовать трейт для типа только если трейт, тип или они оба являются локальными для нашего крейта. Например, мы можем реализовать трейты стандартной библиотеки вроде Display для пользовательского типа вроде SocialPost как часть функциональности нашего крейта aggregator, потому что тип SocialPost локален для нашего крейта aggregator. Мы также можем реализовать Summary для Vec<T> в нашем крейте aggregator, потому что трейт Summary локален для нашего крейта aggregator.

Но мы не можем реализовывать внешние трейты для внешних типов. Например, мы не можем реализовать трейт Display для Vec<T> внутри нашего крейта aggregator, потому что и Display, и Vec<T> определены в стандартной библиотеке и не являются локальными для нашего крейта aggregator. Это ограничение является частью свойства, называемого согласованностью (coherence), а точнее правилом сиротства (orphan rule), названным так потому, что родительский тип отсутствует. Это правило гарантирует, что чужой код не сможет сломать ваш код и наоборот. Без этого правила два крейта могли бы реализовать один и тот же трейт для одного и того же типа, и Rust не знал бы, какую реализацию использовать.

Использование реализаций по умолчанию

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

В листинге 10-14 мы задаем строку по умолчанию для метода summarize трейта Summary, вместо того чтобы только определить сигнатуру метода, как мы делали в листинге 10-12.

Filename: src/lib.rs
pub trait Summary {
    fn summarize(&self) -> String {
        String::from("(Read more...)")
    }
}

pub struct NewsArticle {
    pub headline: String,
    pub location: String,
    pub author: String,
    pub content: String,
}

impl Summary for NewsArticle {}

pub struct SocialPost {
    pub username: String,
    pub content: String,
    pub reply: bool,
    pub repost: bool,
}

impl Summary for SocialPost {
    fn summarize(&self) -> String {
        format!("{}: {}", self.username, self.content)
    }
}
Listing 10-14: Определение трейта Summary с реализацией метода summarize по умолчанию

Чтобы использовать реализацию по умолчанию для краткого описания экземпляров NewsArticle, мы указываем пустой блок impl Summary for NewsArticle {}.

Даже несмотря на то, что мы больше не определяем метод summarize для NewsArticle напрямую, мы предоставили реализацию по умолчанию и указали, что NewsArticle реализует трейт Summary. В результате мы все еще можем вызвать метод summarize для экземпляра NewsArticle, вот так:

use aggregator::{self, NewsArticle, Summary};

fn main() {
    let article = NewsArticle {
        headline: String::from("Penguins win the Stanley Cup Championship!"),
        location: String::from("Pittsburgh, PA, USA"),
        author: String::from("Iceburgh"),
        content: String::from(
            "The Pittsburgh Penguins once again are the best \
             hockey team in the NHL.",
        ),
    };

    println!("New article available! {}", article.summarize());
}

Этот код печатает New article available! (Read more...).

Создание реализации по умолчанию не требует ничего менять в реализации Summary для SocialPost из листинга 10-13. Причина в том, что синтаксис переопределения реализации по умолчанию такой же, как синтаксис реализации метода трейта, у которого нет реализации по умолчанию.

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

pub trait Summary {
    fn summarize_author(&self) -> String;

    fn summarize(&self) -> String {
        format!("(Read more from {}...)", self.summarize_author())
    }
}

pub struct SocialPost {
    pub username: String,
    pub content: String,
    pub reply: bool,
    pub repost: bool,
}

impl Summary for SocialPost {
    fn summarize_author(&self) -> String {
        format!("@{}", self.username)
    }
}

Чтобы использовать эту версию Summary, нам нужно определить только summarize_author, когда мы реализуем трейт для типа:

pub trait Summary {
    fn summarize_author(&self) -> String;

    fn summarize(&self) -> String {
        format!("(Read more from {}...)", self.summarize_author())
    }
}

pub struct SocialPost {
    pub username: String,
    pub content: String,
    pub reply: bool,
    pub repost: bool,
}

impl Summary for SocialPost {
    fn summarize_author(&self) -> String {
        format!("@{}", self.username)
    }
}

После определения summarize_author мы можем вызвать summarize для экземпляров структуры SocialPost, и реализация summarize по умолчанию вызовет предоставленное нами определение summarize_author. Поскольку мы реализовали summarize_author, трейт Summary дал нам поведение метода summarize без необходимости писать дополнительный код. Вот как это выглядит:

use aggregator::{self, SocialPost, Summary};

fn main() {
    let post = SocialPost {
        username: String::from("horse_ebooks"),
        content: String::from(
            "of course, as you probably already know, people",
        ),
        reply: false,
        repost: false,
    };

    println!("1 new post: {}", post.summarize());
}

Этот код печатает 1 new post: (Read more from @horse_ebooks...).

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

Использование трейтов как параметров

Теперь, когда вы знаете, как определять и реализовывать трейты, мы можем рассмотреть, как использовать трейты для определения функций, принимающих много разных типов. Мы используем трейт Summary, реализованный для типов NewsArticle и SocialPost в листинге 10-13, чтобы определить функцию notify, вызывающую метод summarize для своего параметра item, который имеет некоторый тип, реализующий трейт Summary. Для этого мы используем синтаксис impl Trait, вот так:

pub trait Summary {
    fn summarize(&self) -> String;
}

pub struct NewsArticle {
    pub headline: String,
    pub location: String,
    pub author: String,
    pub content: String,
}

impl Summary for NewsArticle {
    fn summarize(&self) -> String {
        format!("{}, by {} ({})", self.headline, self.author, self.location)
    }
}

pub struct SocialPost {
    pub username: String,
    pub content: String,
    pub reply: bool,
    pub repost: bool,
}

impl Summary for SocialPost {
    fn summarize(&self) -> String {
        format!("{}: {}", self.username, self.content)
    }
}

pub fn notify(item: &impl Summary) {
    println!("Breaking news! {}", item.summarize());
}

Вместо конкретного типа для параметра item мы указываем ключевое слово impl и имя трейта. Этот параметр принимает любой тип, который реализует указанный трейт. В теле notify мы можем вызывать для item любые методы, предоставляемые трейтом Summary, например summarize. Мы можем вызвать notify и передать любой экземпляр NewsArticle или SocialPost. Код, который вызывает функцию с любым другим типом, например String или i32, не скомпилируется, потому что эти типы не реализуют Summary.

Синтаксис ограничений трейтов

Синтаксис impl Trait работает для простых случаев, но на самом деле это синтаксический сахар для более длинной формы, известной как ограничение трейта; она выглядит так:

pub fn notify<T: Summary>(item: &T) {
    println!("Breaking news! {}", item.summarize());
}

Эта более длинная форма эквивалентна примеру из предыдущего раздела, но более многословна. Мы помещаем ограничения трейтов вместе с объявлением параметра обобщенного типа после двоеточия и внутри угловых скобок.

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

pub fn notify(item1: &impl Summary, item2: &impl Summary) {

Использование impl Trait подходит, если мы хотим, чтобы эта функция разрешала item1 и item2 иметь разные типы (при условии, что оба типа реализуют Summary). Однако если мы хотим заставить оба параметра иметь один и тот же тип, нужно использовать ограничение трейта, вот так:

pub fn notify<T: Summary>(item1: &T, item2: &T) {

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

Несколько ограничений трейтов с синтаксисом +

Мы также можем указать более одного ограничения трейта. Допустим, мы хотим, чтобы notify использовала для item форматирование для отображения, а также summarize: в определении notify мы указываем, что item должен реализовывать и Display, и Summary. Это можно сделать с помощью синтаксиса +:

pub fn notify(item: &(impl Summary + Display)) {

Синтаксис + также допустим с ограничениями трейтов для обобщенных типов:

pub fn notify<T: Summary + Display>(item: &T) {

Когда указаны эти два ограничения трейтов, тело notify может вызывать summarize и использовать {} для форматирования item.

Более понятные ограничения трейтов с предложениями where

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

fn some_function<T: Display + Clone, U: Clone + Debug>(t: &T, u: &U) -> i32 {

можно использовать предложение where, вот так:

fn some_function<T, U>(t: &T, u: &U) -> i32
where
    T: Display + Clone,
    U: Clone + Debug,
{
    unimplemented!()
}

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

Возврат типов, реализующих трейты

Мы также можем использовать синтаксис impl Trait в позиции возвращаемого типа, чтобы вернуть значение некоторого типа, реализующего трейт, как показано здесь:

pub trait Summary {
    fn summarize(&self) -> String;
}

pub struct NewsArticle {
    pub headline: String,
    pub location: String,
    pub author: String,
    pub content: String,
}

impl Summary for NewsArticle {
    fn summarize(&self) -> String {
        format!("{}, by {} ({})", self.headline, self.author, self.location)
    }
}

pub struct SocialPost {
    pub username: String,
    pub content: String,
    pub reply: bool,
    pub repost: bool,
}

impl Summary for SocialPost {
    fn summarize(&self) -> String {
        format!("{}: {}", self.username, self.content)
    }
}

fn returns_summarizable() -> impl Summary {
    SocialPost {
        username: String::from("horse_ebooks"),
        content: String::from(
            "of course, as you probably already know, people",
        ),
        reply: false,
        repost: false,
    }
}

Используя impl Summary для возвращаемого типа, мы указываем, что функция returns_summarizable возвращает некоторый тип, реализующий трейт Summary, не называя конкретный тип. В этом случае returns_summarizable возвращает SocialPost, но коду, вызывающему эту функцию, не нужно это знать.

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

Однако использовать impl Trait можно только если вы возвращаете один единственный тип. Например, этот код, возвращающий либо NewsArticle, либо SocialPost с возвращаемым типом, указанным как impl Summary, не будет работать:

pub trait Summary {
    fn summarize(&self) -> String;
}

pub struct NewsArticle {
    pub headline: String,
    pub location: String,
    pub author: String,
    pub content: String,
}

impl Summary for NewsArticle {
    fn summarize(&self) -> String {
        format!("{}, by {} ({})", self.headline, self.author, self.location)
    }
}

pub struct SocialPost {
    pub username: String,
    pub content: String,
    pub reply: bool,
    pub repost: bool,
}

impl Summary for SocialPost {
    fn summarize(&self) -> String {
        format!("{}: {}", self.username, self.content)
    }
}

fn returns_summarizable(switch: bool) -> impl Summary {
    if switch {
        NewsArticle {
            headline: String::from(
                "Penguins win the Stanley Cup Championship!",
            ),
            location: String::from("Pittsburgh, PA, USA"),
            author: String::from("Iceburgh"),
            content: String::from(
                "The Pittsburgh Penguins once again are the best \
                 hockey team in the NHL.",
            ),
        }
    } else {
        SocialPost {
            username: String::from("horse_ebooks"),
            content: String::from(
                "of course, as you probably already know, people",
            ),
            reply: false,
            repost: false,
        }
    }
}

Возвращать либо NewsArticle, либо SocialPost нельзя из-за ограничений того, как синтаксис impl Trait реализован в компиляторе. Мы рассмотрим, как написать функцию с таким поведением, в разделе «Использование трейт-объектов для абстракции над общим поведением» главы 18.

Использование ограничений трейтов для условной реализации методов

Используя ограничение трейта с блоком impl, который применяет параметры обобщенных типов, мы можем условно реализовывать методы для типов, реализующих указанные трейты. Например, тип Pair<T> в листинге 10-15 всегда реализует функцию new, возвращающую новый экземпляр Pair<T> (вспомните из раздела «Синтаксис методов» главы 5, что Self – это псевдоним типа для типа блока impl, которым в этом случае является Pair<T>). Но в следующем блоке impl Pair<T> реализует метод cmp_display только если его внутренний тип T реализует трейт PartialOrd, который включает сравнение, и трейт Display, который включает печать.

Filename: src/lib.rs
use std::fmt::Display;

struct Pair<T> {
    x: T,
    y: T,
}

impl<T> Pair<T> {
    fn new(x: T, y: T) -> Self {
        Self { x, y }
    }
}

impl<T: Display + PartialOrd> Pair<T> {
    fn cmp_display(&self) {
        if self.x >= self.y {
            println!("The largest member is x = {}", self.x);
        } else {
            println!("The largest member is y = {}", self.y);
        }
    }
}
Listing 10-15: Условная реализация методов для обобщенного типа в зависимости от ограничений трейтов

Мы также можем условно реализовать трейт для любого типа, реализующего другой трейт. Реализации трейта для любого типа, удовлетворяющего ограничениям трейтов, называются blanket-реализациями и широко используются в стандартной библиотеке Rust. Например, стандартная библиотека реализует трейт ToString для любого типа, реализующего трейт Display. Блок impl в стандартной библиотеке выглядит примерно так:

impl<T: Display> ToString for T {
    // --snip--
}

Поскольку в стандартной библиотеке есть такая blanket-реализация, мы можем вызывать метод to_string, определенный трейтом ToString, для любого типа, реализующего трейт Display. Например, мы можем превратить целые числа в соответствующие значения String вот так, потому что целые числа реализуют Display:

#![allow(unused)]
fn main() {
let s = 3.to_string();
}

Blanket-реализации появляются в документации для трейта в разделе “Implementors”.

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

Проверка ссылок с помощью времён жизни

Проверка ссылок с помощью времен жизни

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

Одна деталь, которую мы не обсуждали в разделе «Ссылки и заимствование» главы 4, состоит в том, что у каждой ссылки в Rust есть время жизни, то есть область видимости, в которой эта ссылка действительна. Большую часть времени времена жизни неявны и выводятся, так же как большую часть времени выводятся типы. Мы обязаны аннотировать типы только тогда, когда возможны несколько типов. Похожим образом мы должны аннотировать времена жизни, когда времена жизни ссылок могут быть связаны несколькими разными способами. Rust требует аннотировать эти связи с помощью обобщенных параметров времен жизни, чтобы гарантировать, что реальные ссылки, используемые во время выполнения, точно будут действительны.

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

Висячие ссылки

Главная цель времен жизни – предотвращать висячие ссылки, которые, если бы их разрешили, заставляли бы программу ссылаться на данные, отличные от тех, на которые она должна ссылаться. Рассмотрим программу в листинге 10-16, где есть внешняя и внутренняя области видимости.

fn main() {
    let r;

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

    println!("r: {r}");
}
Listing 10-16: Попытка использовать ссылку, значение которой вышло из области видимости

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

Во внешней области видимости объявляется переменная с именем r без начального значения, а во внутренней области видимости объявляется переменная с именем x и начальным значением 5. Внутри внутренней области видимости мы пытаемся установить значение r как ссылку на x. Затем внутренняя область видимости завершается, и мы пытаемся напечатать значение в r. Этот код не скомпилируется, потому что значение, на которое ссылается r, выходит из области видимости до того, как мы пытаемся его использовать. Вот сообщение об ошибке:

$ cargo run
   Compiling chapter10 v0.1.0 (file:///projects/chapter10)
error[E0597]: `x` does not live long enough
 --> src/main.rs:6:13
  |
5 |         let x = 5;
  |             - binding `x` declared here
6 |         r = &x;
  |             ^^ borrowed value does not live long enough
7 |     }
  |     - `x` dropped here while still borrowed
8 |
9 |     println!("r: {r}");
  |                   - borrow later used here

For more information about this error, try `rustc --explain E0597`.
error: could not compile `chapter10` (bin "chapter10") due to 1 previous error

Сообщение об ошибке говорит, что переменная x «живет недостаточно долго». Причина в том, что x выйдет из области видимости, когда внутренняя область закончится в строке 7. Но r все еще действительна для внешней области видимости; поскольку ее область видимости больше, мы говорим, что она «живет дольше». Если бы Rust разрешил этому коду работать, r ссылалась бы на память, освобожденную после выхода x из области видимости, и все, что мы попытались бы сделать с r, работало бы некорректно. Так как же Rust определяет, что этот код недопустим? Он использует проверку заимствований.

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

В компиляторе Rust есть проверка заимствований (borrow checker), которая сравнивает области видимости, чтобы определить, все ли заимствования действительны. Листинг 10-17 показывает тот же код, что и листинг 10-16, но с аннотациями, показывающими времена жизни переменных.

fn main() {
    let r;                // ---------+-- 'a
                          //          |
    {                     //          |
        let x = 5;        // -+-- 'b  |
        r = &x;           //  |       |
    }                     // -+       |
                          //          |
    println!("r: {r}");   //          |
}                         // ---------+
Listing 10-17: Аннотации времен жизни r и x, названных соответственно 'a и 'b

Здесь мы аннотировали время жизни r как 'a, а время жизни x как 'b. Как видите, внутренний блок 'b намного меньше внешнего блока времени жизни 'a. Во время компиляции Rust сравнивает размеры двух времен жизни и видит, что у r время жизни 'a, но она ссылается на память со временем жизни 'b. Программа отклоняется, потому что 'b короче, чем 'a: объект ссылки живет не так долго, как сама ссылка.

Листинг 10-18 исправляет код так, что в нем нет висячей ссылки, и он компилируется без ошибок.

fn main() {
    let x = 5;            // ----------+-- 'b
                          //           |
    let r = &x;           // --+-- 'a  |
                          //   |       |
    println!("r: {r}");   //   |       |
                          // --+       |
}                         // ----------+
Listing 10-18: Действительная ссылка, потому что данные имеют более длинное время жизни, чем ссылка

Здесь у x время жизни 'b, которое в этом случае больше, чем 'a. Это означает, что r может ссылаться на x, потому что Rust знает: ссылка в r всегда будет действительна, пока x остается действительной.

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

Обобщенные времена жизни в функциях

Мы напишем функцию, которая возвращает более длинный из двух строковых срезов. Эта функция будет принимать два строковых среза и возвращать один строковый срез. После реализации функции longest код в листинге 10-19 должен напечатать The longest string is abcd.

Filename: src/main.rs
fn main() {
    let string1 = String::from("abcd");
    let string2 = "xyz";

    let result = longest(string1.as_str(), string2);
    println!("The longest string is {result}");
}
Listing 10-19: Функция main, которая вызывает функцию longest, чтобы найти более длинный из двух строковых срезов

Обратите внимание, что мы хотим, чтобы функция принимала строковые срезы, то есть ссылки, а не строки, потому что не хотим, чтобы функция longest забирала владение своими параметрами. См. раздел «Строковые срезы как параметры» главы 4, где подробнее обсуждается, почему параметры, используемые в листинге 10-19, именно такие.

Если мы попытаемся реализовать функцию longest, как показано в листинге 10-20, она не скомпилируется.

Filename: src/main.rs
fn main() {
    let string1 = String::from("abcd");
    let string2 = "xyz";

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

fn longest(x: &str, y: &str) -> &str {
    if x.len() > y.len() { x } else { y }
}
Listing 10-20: Реализация функции longest, которая возвращает более длинный из двух строковых срезов, но пока не компилируется

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

$ cargo run
   Compiling chapter10 v0.1.0 (file:///projects/chapter10)
error[E0106]: missing lifetime specifier
 --> src/main.rs:9:33
  |
9 | fn longest(x: &str, y: &str) -> &str {
  |               ----     ----     ^ expected named lifetime parameter
  |
  = help: this function's return type contains a borrowed value, but the signature does not say whether it is borrowed from `x` or `y`
help: consider introducing a named lifetime parameter
  |
9 | fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
  |           ++++     ++          ++          ++

For more information about this error, try `rustc --explain E0106`.
error: could not compile `chapter10` (bin "chapter10") due to 1 previous error

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

Когда мы определяем эту функцию, мы не знаем конкретных значений, которые будут переданы в нее, поэтому не знаем, выполнится ветка if или ветка else. Мы также не знаем конкретных времен жизни ссылок, которые будут переданы, поэтому не можем посмотреть на области видимости, как делали в листингах 10-17 и 10-18, чтобы определить, всегда ли возвращаемая ссылка будет действительна. Проверка заимствований тоже не может этого определить, потому что не знает, как времена жизни x и y связаны со временем жизни возвращаемого значения. Чтобы исправить эту ошибку, мы добавим обобщенные параметры времен жизни, которые определяют связь между ссылками, чтобы проверка заимствований могла выполнить свой анализ.

Синтаксис аннотаций времен жизни

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

У аннотаций времен жизни немного необычный синтаксис: имена параметров времен жизни должны начинаться с апострофа (') и обычно состоят только из строчных букв и очень короткие, как имена обобщенных типов. Большинство людей используют имя 'a для первой аннотации времени жизни. Мы помещаем аннотации параметров времен жизни после & у ссылки, отделяя аннотацию от типа ссылки пробелом.

Вот несколько примеров: ссылка на i32 без параметра времени жизни, ссылка на i32 с параметром времени жизни с именем 'a и изменяемая ссылка на i32, которая также имеет время жизни 'a:

&i32        // ссылка
&'a i32     // ссылка с явным временем жизни
&'a mut i32 // изменяемая ссылка с явным временем жизни

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

В сигнатурах функций

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

Мы хотим, чтобы сигнатура выражала следующее ограничение: возвращаемая ссылка будет действительна настолько долго, насколько действительны оба параметра. Это связь между временами жизни параметров и возвращаемого значения. Мы назовем время жизни 'a, а затем добавим его к каждой ссылке, как показано в листинге 10-21.

Filename: src/main.rs
fn main() {
    let string1 = String::from("abcd");
    let string2 = "xyz";

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

fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
    if x.len() > y.len() { x } else { y }
}
Listing 10-21: Определение функции longest, указывающее, что все ссылки в сигнатуре должны иметь одно и то же время жизни 'a

Этот код должен скомпилироваться и дать нужный нам результат, когда мы используем его с функцией main из листинга 10-19.

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

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

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

Когда мы передаем конкретные ссылки в longest, конкретное время жизни, подставляемое вместо 'a, является частью области видимости x, которая пересекается с областью видимости y. Другими словами, обобщенное время жизни 'a получит конкретное время жизни, равное меньшему из времен жизни x и y. Поскольку мы аннотировали возвращаемую ссылку тем же параметром времени жизни 'a, возвращаемая ссылка также будет действительна в течение меньшего из времен жизни x и y.

Посмотрим, как аннотации времен жизни ограничивают функцию longest, передав ей ссылки с разными конкретными временами жизни. Листинг 10-22 – простой пример.

Filename: src/main.rs
fn main() {
    let string1 = String::from("long string is long");

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

fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
    if x.len() > y.len() { x } else { y }
}
Listing 10-22: Использование функции longest со ссылками на значения String, имеющие разные конкретные времена жизни

В этом примере string1 действительна до конца внешней области видимости, string2 действительна до конца внутренней области видимости, а result ссылается на что-то, что действительно до конца внутренней области видимости. Запустите этот код, и вы увидите, что проверка заимствований его одобряет: он скомпилируется и напечатает The longest string is long string is long.

Теперь попробуем пример, который показывает, что время жизни ссылки в result должно быть меньшим временем жизни из двух аргументов. Мы переместим объявление переменной result за пределы внутренней области видимости, но оставим присваивание значения переменной result внутри области видимости с string2. Затем переместим println!, использующий result, за пределы внутренней области видимости, после того как внутренняя область завершилась. Код в листинге 10-23 не скомпилируется.

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

fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
    if x.len() > y.len() { x } else { y }
}
Listing 10-23: Попытка использовать result после выхода string2 из области видимости

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

$ cargo run
   Compiling chapter10 v0.1.0 (file:///projects/chapter10)
error[E0597]: `string2` does not live long enough
 --> src/main.rs:6:44
  |
5 |         let string2 = String::from("xyz");
  |             ------- binding `string2` declared here
6 |         result = longest(string1.as_str(), string2.as_str());
  |                                            ^^^^^^^ borrowed value does not live long enough
7 |     }
  |     - `string2` dropped here while still borrowed
8 |     println!("The longest string is {result}");
  |                                      ------ borrow later used here

For more information about this error, try `rustc --explain E0597`.
error: could not compile `chapter10` (bin "chapter10") due to 1 previous error

Ошибка показывает, что для действительности result в инструкции println! переменная string2 должна была бы быть действительной до конца внешней области видимости. Rust знает это, потому что мы аннотировали времена жизни параметров функции и возвращаемых значений одним и тем же параметром времени жизни 'a.

Как люди, мы можем посмотреть на этот код и увидеть, что string1 длиннее, чем string2, а значит result будет содержать ссылку на string1. Поскольку string1 еще не вышла из области видимости, ссылка на string1 все еще будет действительна для инструкции println!. Однако компилятор не может увидеть, что в этом случае ссылка действительна. Мы сообщили Rust, что время жизни ссылки, возвращаемой функцией longest, совпадает с меньшим из времен жизни переданных ссылок. Поэтому проверка заимствований запрещает код из листинга 10-23 как потенциально содержащий недействительную ссылку.

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

Связи

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

Filename: src/main.rs
fn main() {
    let string1 = String::from("abcd");
    let string2 = "efghijklmnopqrstuvwxyz";

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

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

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

Когда функция возвращает ссылку, параметр времени жизни возвращаемого типа должен совпадать с параметром времени жизни одного из параметров. Если возвращаемая ссылка не ссылается на один из параметров, она должна ссылаться на значение, созданное внутри этой функции. Однако это была бы висячая ссылка, потому что значение выйдет из области видимости в конце функции. Рассмотрим попытку реализации функции longest, которая не скомпилируется:

Filename: src/main.rs
fn main() {
    let string1 = String::from("abcd");
    let string2 = "xyz";

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

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

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

$ cargo run
   Compiling chapter10 v0.1.0 (file:///projects/chapter10)
error[E0515]: cannot return value referencing local variable `result`
  --> src/main.rs:11:5
   |
11 |     result.as_str()
   |     ------^^^^^^^^^
   |     |
   |     returns a value referencing data owned by the current function
   |     `result` is borrowed here

For more information about this error, try `rustc --explain E0515`.
error: could not compile `chapter10` (bin "chapter10") due to 1 previous error

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

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

В определениях структур

До сих пор все определенные нами структуры содержали владеющие типы. Мы можем определять структуры, хранящие ссылки, но в таком случае нужно добавить аннотацию времени жизни к каждой ссылке в определении структуры. В листинге 10-24 есть структура с именем ImportantExcerpt, которая хранит строковый срез.

Filename: src/main.rs
struct ImportantExcerpt<'a> {
    part: &'a str,
}

fn main() {
    let novel = String::from("Call me Ishmael. Some years ago...");
    let first_sentence = novel.split('.').next().unwrap();
    let i = ImportantExcerpt {
        part: first_sentence,
    };
}
Listing 10-24: Структура, которая хранит ссылку и требует аннотации времени жизни

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

Функция main здесь создает экземпляр структуры ImportantExcerpt, который хранит ссылку на первое предложение из String, принадлежащей переменной novel. Данные в novel существуют до создания экземпляра ImportantExcerpt. Кроме того, novel не выходит из области видимости до тех пор, пока ImportantExcerpt не выйдет из области видимости, поэтому ссылка в экземпляре ImportantExcerpt действительна.

Опущение времен жизни

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

Filename: src/lib.rs
fn first_word(s: &str) -> &str {
    let bytes = s.as_bytes();

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

    &s[..]
}

fn main() {
    let my_string = String::from("hello world");

    // first_word works on slices of `String`s
    let word = first_word(&my_string[..]);

    let my_string_literal = "hello world";

    // first_word works on slices of string literals
    let word = first_word(&my_string_literal[..]);

    // Because string literals *are* string slices already,
    // this works too, without the slice syntax!
    let word = first_word(my_string_literal);
}
Listing 10-25: Функция, которую мы определили в листинге 4-9 и которая скомпилировалась без аннотаций времен жизни, хотя параметр и возвращаемый тип являются ссылками

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

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

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

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

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

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

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

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

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

Второе правило: если есть ровно один входной параметр времени жизни, это время жизни назначается всем выходным параметрам времени жизни: fn foo<'a>(x: &'a i32) -> &'a i32.

Третье правило: если есть несколько входных параметров времен жизни, но один из них – &self или &mut self, потому что это метод, время жизни self назначается всем выходным параметрам времен жизни. Это третье правило делает методы намного приятнее читать и писать, потому что требуется меньше символов.

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

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

Затем компилятор применяет первое правило, которое указывает, что каждый параметр получает собственное время жизни. Как обычно, назовем его 'a, и теперь сигнатура выглядит так:

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

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

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

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

Посмотрим на другой пример, на этот раз с функцией longest, у которой не было параметров времен жизни, когда мы начали работать с ней в листинге 10-20:

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

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

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

Вы видите, что второе правило не применяется, потому что входных времен жизни больше одного. Третье правило тоже не применяется, потому что longest – это функция, а не метод, поэтому ни один из параметров не является self. После применения всех трех правил мы все еще не выяснили, какое время жизни у возвращаемого типа. Именно поэтому мы получили ошибку при попытке скомпилировать код из листинга 10-20: компилятор прошел по правилам опущения времен жизни, но все еще не смог определить все времена жизни ссылок в сигнатуре.

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

В определениях методов

Когда мы реализуем методы для структуры со временами жизни, мы используем тот же синтаксис, что и для параметров обобщенных типов, как показано в листинге 10-11. Где объявлять и использовать параметры времен жизни, зависит от того, связаны ли они с полями структуры или с параметрами и возвращаемыми значениями методов.

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

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

Сначала используем метод с именем level, единственным параметром которого является ссылка на self, а возвращаемым значением – i32, который не является ссылкой ни на что:

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

impl<'a> ImportantExcerpt<'a> {
    fn level(&self) -> i32 {
        3
    }
}

impl<'a> ImportantExcerpt<'a> {
    fn announce_and_return_part(&self, announcement: &str) -> &str {
        println!("Attention please: {announcement}");
        self.part
    }
}

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

Объявление параметра времени жизни после impl и его использование после имени типа обязательны, но благодаря первому правилу опущения мы не обязаны аннотировать время жизни ссылки на self.

Вот пример, где применяется третье правило опущения времен жизни:

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

impl<'a> ImportantExcerpt<'a> {
    fn level(&self) -> i32 {
        3
    }
}

impl<'a> ImportantExcerpt<'a> {
    fn announce_and_return_part(&self, announcement: &str) -> &str {
        println!("Attention please: {announcement}");
        self.part
    }
}

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

Есть два входных времени жизни, поэтому Rust применяет первое правило опущения времен жизни и дает собственные времена жизни и &self, и announcement. Затем, поскольку один из параметров – &self, возвращаемый тип получает время жизни &self, и все времена жизни оказываются учтены.

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

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

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

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

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

Параметры обобщенных типов, ограничения трейтов и времена жизни вместе

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

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

    let result = longest_with_an_announcement(
        string1.as_str(),
        string2,
        "Today is someone's birthday!",
    );
    println!("The longest string is {result}");
}

use std::fmt::Display;

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

Это функция longest из листинга 10-21, которая возвращает более длинный из двух строковых срезов. Но теперь у нее есть дополнительный параметр с именем ann обобщенного типа T, вместо которого можно подставить любой тип, реализующий трейт Display, как указано предложением where. Этот дополнительный параметр будет напечатан с помощью {}, поэтому ограничение трейта Display необходимо. Поскольку времена жизни являются разновидностью обобщений, объявления параметра времени жизни 'a и параметра обобщенного типа T находятся в одном списке внутри угловых скобок после имени функции.

Итоги

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

Хотите верьте, хотите нет, но по темам, которые мы обсуждали в этой главе, можно узнать еще многое: глава 18 обсуждает трейт-объекты, еще один способ использовать трейты. Также есть более сложные сценарии с аннотациями времен жизни, которые понадобятся только в очень продвинутых случаях; о них следует прочитать в Rust Reference. Но дальше вы узнаете, как писать тесты в Rust, чтобы убедиться, что ваш код работает так, как должен.

Написание автоматизированных тестов

В своем эссе 1972 года «Скромный программист» Эдсгер В. Дейкстра сказал, что «тестирование программы может быть очень эффективным способом показать наличие ошибок, но совершенно непригодно для доказательства их отсутствия». Это не значит, что мы не должны стараться тестировать как можно больше!

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

Допустим, мы пишем функцию add_two, которая прибавляет 2 к любому переданному ей числу. Сигнатура этой функции принимает целое число как параметр и возвращает целое число как результат. Когда мы реализуем и компилируем эту функцию, Rust выполняет все проверки типов и проверки заимствований, которые вы уже изучили, чтобы гарантировать, например, что мы не передаем в эту функцию значение String или недействительную ссылку. Но Rust не может проверить, что эта функция сделает ровно то, что мы задумали: вернет параметр плюс 2, а не, скажем, параметр плюс 10 или параметр минус 50! Именно здесь нужны тесты.

Мы можем написать тесты, которые утверждают, например, что при передаче 3 в функцию add_two возвращаемое значение равно 5. Мы можем запускать эти тесты каждый раз, когда меняем код, чтобы убедиться, что существующее правильное поведение не изменилось.

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

Как писать тесты

Как писать тесты

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

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

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

Структура тестовых функций

В самом простом виде тест в Rust – это функция, помеченная атрибутом test. Атрибуты – это метаданные о частях кода Rust; один пример – атрибут derive, который мы использовали со структурами в главе 5. Чтобы превратить функцию в тестовую, добавьте #[test] в строке перед fn. Когда вы запускаете тесты командой cargo test, Rust собирает бинарный файл запуска тестов, который выполняет помеченные функции и сообщает, прошла или провалилась каждая тестовая функция.

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

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

Создадим новый библиотечный проект с именем adder, который будет складывать два числа:

$ cargo new adder --lib
     Created library `adder` project
$ cd adder

Содержимое файла src/lib.rs в вашей библиотеке adder должно выглядеть как в листинге 11-1.

Filename: src/lib.rs
pub fn add(left: u64, right: u64) -> u64 {
    left + right
}

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

    #[test]
    fn it_works() {
        let result = add(2, 2);
        assert_eq!(result, 4);
    }
}
Listing 11-1: Код, автоматически сгенерированный cargo new

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

Пока сосредоточимся только на функции it_works. Обратите внимание на аннотацию #[test]: этот атрибут указывает, что функция является тестовой, и запускатель тестов знает, что ее нужно рассматривать как тест. В модуле tests у нас также могут быть функции, не являющиеся тестами, чтобы помогать подготавливать общие сценарии или выполнять общие операции, поэтому всегда нужно указывать, какие функции являются тестами.

Тело примерной функции использует макрос assert_eq!, чтобы проверить, что result, содержащий результат вызова add с 2 и 2, равен 4. Это утверждение служит примером формата типичного теста. Запустим его и убедимся, что тест проходит.

Команда cargo test запускает все тесты в нашем проекте, как показано в листинге 11-2.

$ cargo test
   Compiling adder v0.1.0 (file:///projects/adder)
    Finished `test` profile [unoptimized + debuginfo] target(s) in 0.57s
     Running unittests src/lib.rs (target/debug/deps/adder-01ad14159ff659ab)

running 1 test
test tests::it_works ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

   Doc-tests adder

running 0 tests

test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

Listing 11-2: Вывод запуска автоматически сгенерированного теста

Cargo скомпилировал и запустил тест. Мы видим строку running 1 test. Следующая строка показывает имя сгенерированной тестовой функции, tests::it_works, и то, что результат запуска этого теста – ok. Общая сводка test result: ok. означает, что все тесты прошли, а часть 1 passed; 0 failed суммирует количество прошедших и провалившихся тестов.

Тест можно пометить как игнорируемый, чтобы в конкретном запуске он не выполнялся; мы рассмотрим это позже в этой главе, в разделе «Игнорирование тестов, если они не запрошены явно». Поскольку здесь мы этого не делали, сводка показывает 0 ignored. Команде cargo test также можно передать аргумент, чтобы запускались только тесты, имя которых совпадает со строкой; это называется фильтрацией, и мы рассмотрим ее в разделе «Запуск подмножества тестов по имени». Здесь мы не фильтровали запускаемые тесты, поэтому в конце сводки показано 0 filtered out.

Статистика 0 measured относится к benchmark-тестам, измеряющим производительность. На момент написания benchmark-тесты доступны только в nightly Rust. Подробнее см. документацию по benchmark-тестам.

Следующая часть вывода тестов, начинающаяся с Doc-tests adder, относится к результатам тестов документации. У нас пока нет тестов документации, но Rust может компилировать любые примеры кода, встречающиеся в API-документации. Эта возможность помогает поддерживать документацию и код в согласованном состоянии! О том, как писать тесты документации, мы поговорим в разделе «Комментарии документации как тесты» главы 14. Пока мы проигнорируем вывод Doc-tests.

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

Файл: src/lib.rs

pub fn add(left: u64, right: u64) -> u64 {
    left + right
}

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

    #[test]
    fn exploration() {
        let result = add(2, 2);
        assert_eq!(result, 4);
    }
}

Затем снова запустим cargo test. Теперь в выводе будет показано exploration вместо it_works:

$ cargo test
   Compiling adder v0.1.0 (file:///projects/adder)
    Finished `test` profile [unoptimized + debuginfo] target(s) in 0.59s
     Running unittests src/lib.rs (target/debug/deps/adder-92948b65e88960b4)

running 1 test
test tests::exploration ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

   Doc-tests adder

running 0 tests

test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

Теперь добавим еще один тест, но на этот раз сделаем тест, который проваливается! Тесты проваливаются, когда что-то в тестовой функции паникует. Каждый тест запускается в новом потоке, и когда главный поток видит, что поток теста завершился аварийно, тест помечается как провалившийся. В главе 9 мы говорили, что самый простой способ вызвать панику – вызвать макрос panic!. Введите новый тест как функцию с именем another, чтобы ваш файл src/lib.rs выглядел как в листинге 11-3.

Filename: src/lib.rs
pub fn add(left: u64, right: u64) -> u64 {
    left + right
}

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

    #[test]
    fn exploration() {
        let result = add(2, 2);
        assert_eq!(result, 4);
    }

    #[test]
    fn another() {
        panic!("Make this test fail");
    }
}
Listing 11-3: Добавление второго теста, который провалится, потому что мы вызываем макрос panic!

Снова запустите тесты с помощью cargo test. Вывод должен выглядеть как в листинге 11-4: он показывает, что тест exploration прошел, а another провалился.

$ cargo test
   Compiling adder v0.1.0 (file:///projects/adder)
    Finished `test` profile [unoptimized + debuginfo] target(s) in 0.72s
     Running unittests src/lib.rs (target/debug/deps/adder-92948b65e88960b4)

running 2 tests
test tests::another ... FAILED
test tests::exploration ... ok

failures:

---- tests::another stdout ----

thread 'tests::another' panicked at src/lib.rs:17:9:
Make this test fail
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace


failures:
    tests::another

test result: FAILED. 1 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

error: test failed, to rerun pass `--lib`
Listing 11-4: Результаты тестов, когда один тест проходит, а один проваливается

Вместо ok строка test tests::another показывает FAILED. Между отдельными результатами и сводкой появляются два новых раздела: первый показывает подробную причину каждого сбоя теста. В этом случае мы получаем подробности о том, что tests::another провалился, потому что запаниковал с сообщением Make this test fail в строке 17 файла src/lib.rs. Следующий раздел перечисляет только имена всех провалившихся тестов, что полезно, когда тестов много и подробного вывода об ошибках тоже много. Мы можем использовать имя провалившегося теста, чтобы запустить только его и проще отладить; подробнее о способах запуска тестов мы поговорим в разделе «Управление запуском тестов».

В конце выводится строка сводки: в целом результат наших тестов – FAILED. Один тест прошел, и один тест провалился.

Теперь, когда вы увидели, как выглядят результаты тестов в разных сценариях, рассмотрим несколько полезных в тестах макросов, кроме panic!.

Проверка результатов с помощью assert!

Макрос assert!, предоставляемый стандартной библиотекой, полезен, когда нужно убедиться, что некоторое условие в тесте вычисляется в true. Мы передаем макросу assert! аргумент, который вычисляется в логическое значение. Если значение равно true, ничего не происходит и тест проходит. Если значение равно false, макрос assert! вызывает panic!, чтобы тест провалился. Использование макроса assert! помогает проверить, что наш код работает так, как мы задумали.

В главе 5, в листинге 5-15, мы использовали структуру Rectangle и метод can_hold; они повторены здесь, в листинге 11-5. Поместим этот код в файл src/lib.rs, а затем напишем для него тесты с помощью макроса assert!.

Filename: src/lib.rs
#[derive(Debug)]
struct Rectangle {
    width: u32,
    height: u32,
}

impl Rectangle {
    fn can_hold(&self, other: &Rectangle) -> bool {
        self.width > other.width && self.height > other.height
    }
}
Listing 11-5: Структура Rectangle и ее метод can_hold из главы 5

Метод can_hold возвращает логическое значение, а значит он идеально подходит для макроса assert!. В листинге 11-6 мы пишем тест, который проверяет метод can_hold: создаем экземпляр Rectangle с шириной 8 и высотой 7 и утверждаем, что он может вместить другой экземпляр Rectangle с шириной 5 и высотой 1.

Filename: src/lib.rs
#[derive(Debug)]
struct Rectangle {
    width: u32,
    height: u32,
}

impl Rectangle {
    fn can_hold(&self, other: &Rectangle) -> bool {
        self.width > other.width && self.height > other.height
    }
}

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

    #[test]
    fn larger_can_hold_smaller() {
        let larger = Rectangle {
            width: 8,
            height: 7,
        };
        let smaller = Rectangle {
            width: 5,
            height: 1,
        };

        assert!(larger.can_hold(&smaller));
    }
}
Listing 11-6: Тест для can_hold, проверяющий, действительно ли больший прямоугольник может вместить меньший

Обратите внимание на строку use super::*; внутри модуля tests. Модуль tests – обычный модуль, который подчиняется привычным правилам видимости, рассмотренным в главе 7 в разделе «Пути для ссылки на элемент в дереве модулей». Поскольку модуль tests является внутренним модулем, нам нужно ввести тестируемый код из внешнего модуля в область видимости внутреннего модуля. Здесь мы используем glob-импорт, поэтому все, что мы определим во внешнем модуле, будет доступно этому модулю tests.

Мы назвали тест larger_can_hold_smaller и создали два нужных экземпляра Rectangle. Затем вызвали макрос assert! и передали ему результат вызова larger.can_hold(&smaller). Это выражение должно вернуть true, поэтому наш тест должен пройти. Проверим!

$ cargo test
   Compiling rectangle v0.1.0 (file:///projects/rectangle)
    Finished `test` profile [unoptimized + debuginfo] target(s) in 0.66s
     Running unittests src/lib.rs (target/debug/deps/rectangle-6584c4561e48942e)

running 1 test
test tests::larger_can_hold_smaller ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

   Doc-tests rectangle

running 0 tests

test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

Он действительно проходит! Добавим еще один тест, на этот раз утверждая, что меньший прямоугольник не может вместить больший:

Файл: src/lib.rs

#[derive(Debug)]
struct Rectangle {
    width: u32,
    height: u32,
}

impl Rectangle {
    fn can_hold(&self, other: &Rectangle) -> bool {
        self.width > other.width && self.height > other.height
    }
}

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

    #[test]
    fn larger_can_hold_smaller() {
        // --snip--
        let larger = Rectangle {
            width: 8,
            height: 7,
        };
        let smaller = Rectangle {
            width: 5,
            height: 1,
        };

        assert!(larger.can_hold(&smaller));
    }

    #[test]
    fn smaller_cannot_hold_larger() {
        let larger = Rectangle {
            width: 8,
            height: 7,
        };
        let smaller = Rectangle {
            width: 5,
            height: 1,
        };

        assert!(!smaller.can_hold(&larger));
    }
}

Поскольку правильный результат функции can_hold в этом случае – false, нужно отрицать этот результат перед передачей в макрос assert!. В результате наш тест пройдет, если can_hold вернет false:

$ cargo test
   Compiling rectangle v0.1.0 (file:///projects/rectangle)
    Finished `test` profile [unoptimized + debuginfo] target(s) in 0.66s
     Running unittests src/lib.rs (target/debug/deps/rectangle-6584c4561e48942e)

running 2 tests
test tests::larger_can_hold_smaller ... ok
test tests::smaller_cannot_hold_larger ... ok

test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

   Doc-tests rectangle

running 0 tests

test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

Два теста проходят! Теперь посмотрим, что произойдет с результатами тестов, если мы внесем ошибку в код. Изменим реализацию метода can_hold, заменив знак «больше» (>) на знак «меньше» (<) при сравнении ширины:

#[derive(Debug)]
struct Rectangle {
    width: u32,
    height: u32,
}

// --snip--
impl Rectangle {
    fn can_hold(&self, other: &Rectangle) -> bool {
        self.width < other.width && self.height > other.height
    }
}

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

    #[test]
    fn larger_can_hold_smaller() {
        let larger = Rectangle {
            width: 8,
            height: 7,
        };
        let smaller = Rectangle {
            width: 5,
            height: 1,
        };

        assert!(larger.can_hold(&smaller));
    }

    #[test]
    fn smaller_cannot_hold_larger() {
        let larger = Rectangle {
            width: 8,
            height: 7,
        };
        let smaller = Rectangle {
            width: 5,
            height: 1,
        };

        assert!(!smaller.can_hold(&larger));
    }
}

Теперь запуск тестов дает следующий результат:

$ cargo test
   Compiling rectangle v0.1.0 (file:///projects/rectangle)
    Finished `test` profile [unoptimized + debuginfo] target(s) in 0.66s
     Running unittests src/lib.rs (target/debug/deps/rectangle-6584c4561e48942e)

running 2 tests
test tests::larger_can_hold_smaller ... FAILED
test tests::smaller_cannot_hold_larger ... ok

failures:

---- tests::larger_can_hold_smaller stdout ----

thread 'tests::larger_can_hold_smaller' panicked at src/lib.rs:28:9:
assertion failed: larger.can_hold(&smaller)
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace


failures:
    tests::larger_can_hold_smaller

test result: FAILED. 1 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

error: test failed, to rerun pass `--lib`

Наши тесты поймали ошибку! Поскольку larger.width равна 8, а smaller.width равна 5, сравнение ширины в can_hold теперь возвращает false: 8 не меньше 5.

Проверка равенства с помощью assert_eq! и assert_ne!

Распространенный способ проверить функциональность – сравнить результат тестируемого кода со значением, которое вы ожидаете получить. Это можно сделать с помощью макроса assert!, передав ему выражение с оператором ==. Однако такая проверка встречается настолько часто, что стандартная библиотека предоставляет пару макросов – assert_eq! и assert_ne!, – чтобы выполнять ее удобнее. Эти макросы сравнивают два аргумента на равенство или неравенство соответственно. Если проверка провалится, они также напечатают оба значения, благодаря чему проще увидеть, почему тест не прошел; в отличие от них, макрос assert! только сообщает, что получил значение false для выражения ==, не печатая значения, которые привели к false.

В листинге 11-7 мы пишем функцию с именем add_two, которая прибавляет 2 к своему параметру, а затем тестируем ее с помощью макроса assert_eq!.

Filename: src/lib.rs
pub fn add_two(a: u64) -> u64 {
    a + 2
}

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

    #[test]
    fn it_adds_two() {
        let result = add_two(2);
        assert_eq!(result, 4);
    }
}
Listing 11-7: Тестирование функции add_two с помощью макроса assert_eq!

Проверим, что тест проходит!

$ cargo test
   Compiling adder v0.1.0 (file:///projects/adder)
    Finished `test` profile [unoptimized + debuginfo] target(s) in 0.58s
     Running unittests src/lib.rs (target/debug/deps/adder-92948b65e88960b4)

running 1 test
test tests::it_adds_two ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

   Doc-tests adder

running 0 tests

test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

Мы создаем переменную с именем result, которая хранит результат вызова add_two(2). Затем передаем result и 4 как аргументы макросу assert_eq!. Строка вывода для этого теста – test tests::it_adds_two ... ok, и текст ok указывает, что тест прошел!

Внесем ошибку в код, чтобы увидеть, как выглядит assert_eq!, когда тест не проходит. Измените реализацию функции add_two, чтобы она вместо этого прибавляла 3:

pub fn add_two(a: u64) -> u64 {
    a + 3
}

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

    #[test]
    fn it_adds_two() {
        let result = add_two(2);
        assert_eq!(result, 4);
    }
}

Снова запустите тесты:

$ cargo test
   Compiling adder v0.1.0 (file:///projects/adder)
    Finished `test` profile [unoptimized + debuginfo] target(s) in 0.61s
     Running unittests src/lib.rs (target/debug/deps/adder-92948b65e88960b4)

running 1 test
test tests::it_adds_two ... FAILED

failures:

---- tests::it_adds_two stdout ----

thread 'tests::it_adds_two' panicked at src/lib.rs:12:9:
assertion `left == right` failed
  left: 5
 right: 4
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace


failures:
    tests::it_adds_two

test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

error: test failed, to rerun pass `--lib`

Наш тест поймал ошибку! Тест tests::it_adds_two провалился, и сообщение говорит, что провалившаяся проверка была left == right, а также показывает значения left и right. Это сообщение помогает начать отладку: аргумент left, где был результат вызова add_two(2), равен 5, а аргумент right равен 4. Можно представить, насколько это особенно полезно, когда идет много тестов.

Обратите внимание, что в некоторых языках и тестовых фреймворках параметры функций проверки равенства называются expected и actual, и порядок указания аргументов важен. Однако в Rust они называются left и right, и порядок, в котором мы указываем ожидаемое значение и значение, произведенное кодом, не важен. Мы могли бы записать утверждение в этом тесте как assert_eq!(4, result), что привело бы к тому же сообщению об ошибке, где показано assertion `left == right` failed.

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

Под капотом макросы assert_eq! и assert_ne! используют операторы == и != соответственно. Когда утверждения проваливаются, эти макросы печатают свои аргументы с помощью отладочного форматирования, а значит сравниваемые значения должны реализовывать трейты PartialEq и Debug. Все примитивные типы и большинство типов стандартной библиотеки реализуют эти трейты. Для структур и enum, которые вы определяете сами, нужно реализовать PartialEq, чтобы утверждать равенство таких типов. Также нужно реализовать Debug, чтобы печатать значения при провале утверждения. Поскольку оба трейта являются выводимыми, как упоминалось в листинге 5-12 главы 5, обычно достаточно добавить аннотацию #[derive(PartialEq, Debug)] к определению структуры или enum. Подробнее об этих и других выводимых трейтах см. приложение C, «Выводимые трейты».

Добавление собственных сообщений об ошибках

К сообщениям о сбое можно добавить собственное сообщение как необязательные аргументы макросов assert!, assert_eq! и assert_ne!. Любые аргументы, указанные после обязательных, передаются макросу format! (он обсуждался в разделе «Конкатенация с помощью + или format!» главы 8), поэтому можно передать строку формата с заполнителями {} и значения для этих заполнителей. Собственные сообщения полезны для документирования смысла проверки; когда тест проваливается, вам будет проще понять, в чем проблема с кодом.

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

Файл: src/lib.rs

pub fn greeting(name: &str) -> String {
    format!("Hello {name}!")
}

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

    #[test]
    fn greeting_contains_name() {
        let result = greeting("Carol");
        assert!(result.contains("Carol"));
    }
}

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

Теперь внесем ошибку в этот код, изменив greeting так, чтобы она исключала name, и посмотрим, как выглядит сообщение о провале теста по умолчанию:

pub fn greeting(name: &str) -> String {
    String::from("Hello!")
}

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

    #[test]
    fn greeting_contains_name() {
        let result = greeting("Carol");
        assert!(result.contains("Carol"));
    }
}

Запуск этого теста дает следующий результат:

$ cargo test
   Compiling greeter v0.1.0 (file:///projects/greeter)
    Finished `test` profile [unoptimized + debuginfo] target(s) in 0.91s
     Running unittests src/lib.rs (target/debug/deps/greeter-170b942eb5bf5e3a)

running 1 test
test tests::greeting_contains_name ... FAILED

failures:

---- tests::greeting_contains_name stdout ----

thread 'tests::greeting_contains_name' panicked at src/lib.rs:12:9:
assertion failed: result.contains("Carol")
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace


failures:
    tests::greeting_contains_name

test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

error: test failed, to rerun pass `--lib`

Этот результат только показывает, что проверка провалилась, и указывает строку, на которой она находится. Более полезное сообщение о сбое напечатало бы значение, возвращенное функцией greeting. Добавим собственное сообщение об ошибке, составленное из строки формата с заполнителем, заполненным фактическим значением, которое мы получили от функции greeting:

pub fn greeting(name: &str) -> String {
    String::from("Hello!")
}

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

    #[test]
    fn greeting_contains_name() {
        let result = greeting("Carol");
        assert!(
            result.contains("Carol"),
            "Greeting did not contain name, value was `{result}`"
        );
    }
}

Теперь при запуске теста мы получим более информативное сообщение об ошибке:

$ cargo test
   Compiling greeter v0.1.0 (file:///projects/greeter)
    Finished `test` profile [unoptimized + debuginfo] target(s) in 0.93s
     Running unittests src/lib.rs (target/debug/deps/greeter-170b942eb5bf5e3a)

running 1 test
test tests::greeting_contains_name ... FAILED

failures:

---- tests::greeting_contains_name stdout ----

thread 'tests::greeting_contains_name' panicked at src/lib.rs:12:9:
Greeting did not contain name, value was `Hello!`
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace


failures:
    tests::greeting_contains_name

test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

error: test failed, to rerun pass `--lib`

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

Проверка паник с помощью should_panic

Помимо проверки возвращаемых значений важно проверять, что наш код обрабатывает ошибочные условия так, как мы ожидаем. Например, рассмотрим тип Guess, который мы создали в главе 9, листинге 9-13. Другой код, использующий Guess, полагается на гарантию, что экземпляры Guess будут содержать только значения от 1 до 100. Мы можем написать тест, который гарантирует, что попытка создать экземпляр Guess со значением вне этого диапазона вызовет панику.

Мы делаем это, добавляя атрибут should_panic к тестовой функции. Тест проходит, если код внутри функции паникует; тест проваливается, если код внутри функции не паникует.

Листинг 11-8 показывает тест, который проверяет, что ошибочные условия Guess::new возникают тогда, когда мы ожидаем.

Filename: src/lib.rs
pub struct Guess {
    value: i32,
}

impl Guess {
    pub fn new(value: i32) -> Guess {
        if value < 1 || value > 100 {
            panic!("Guess value must be between 1 and 100, got {value}.");
        }

        Guess { value }
    }
}

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

    #[test]
    #[should_panic]
    fn greater_than_100() {
        Guess::new(200);
    }
}
Listing 11-8: Проверка того, что условие вызовет panic!

Мы помещаем атрибут #[should_panic] после атрибута #[test] и перед тестовой функцией, к которой он применяется. Посмотрим на результат, когда этот тест проходит:

$ cargo test
   Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
    Finished `test` profile [unoptimized + debuginfo] target(s) in 0.58s
     Running unittests src/lib.rs (target/debug/deps/guessing_game-57d70c3acb738f4d)

running 1 test
test tests::greater_than_100 - should panic ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

   Doc-tests guessing_game

running 0 tests

test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

Выглядит хорошо! Теперь внесем ошибку в код, удалив условие, по которому функция new должна паниковать, если значение больше 100:

pub struct Guess {
    value: i32,
}

// --snip--
impl Guess {
    pub fn new(value: i32) -> Guess {
        if value < 1 {
            panic!("Guess value must be between 1 and 100, got {value}.");
        }

        Guess { value }
    }
}

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

    #[test]
    #[should_panic]
    fn greater_than_100() {
        Guess::new(200);
    }
}

Когда мы запустим тест из листинга 11-8, он провалится:

$ cargo test
   Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
    Finished `test` profile [unoptimized + debuginfo] target(s) in 0.62s
     Running unittests src/lib.rs (target/debug/deps/guessing_game-57d70c3acb738f4d)

running 1 test
test tests::greater_than_100 - should panic ... FAILED

failures:

---- tests::greater_than_100 stdout ----
note: test did not panic as expected at src/lib.rs:21:8

failures:
    tests::greater_than_100

test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

error: test failed, to rerun pass `--lib`

В этом случае мы не получаем особенно полезного сообщения, но когда смотрим на тестовую функцию, видим, что она аннотирована #[should_panic]. Полученный сбой означает, что код в тестовой функции не вызвал панику.

Тесты, использующие should_panic, могут быть неточными. Тест с should_panic пройдет даже если он паникует по причине, отличной от той, которую мы ожидали. Чтобы сделать тесты с should_panic точнее, можно добавить к атрибуту should_panic необязательный параметр expected. Тестовая обвязка проверит, что сообщение о сбое содержит предоставленный текст. Например, рассмотрим измененный код для Guess в листинге 11-9, где функция new паникует с разными сообщениями в зависимости от того, слишком мало значение или слишком велико.

Filename: src/lib.rs
pub struct Guess {
    value: i32,
}

// --snip--

impl Guess {
    pub fn new(value: i32) -> Guess {
        if value < 1 {
            panic!(
                "Guess value must be greater than or equal to 1, got {value}."
            );
        } else if value > 100 {
            panic!(
                "Guess value must be less than or equal to 100, got {value}."
            );
        }

        Guess { value }
    }
}

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

    #[test]
    #[should_panic(expected = "less than or equal to 100")]
    fn greater_than_100() {
        Guess::new(200);
    }
}
Listing 11-9: Проверка panic! с сообщением паники, содержащим указанную подстроку

Этот тест пройдет, потому что значение, которое мы поместили в параметр expected атрибута should_panic, является подстрокой сообщения, с которым паникует функция Guess::new. Мы могли бы указать все ожидаемое сообщение паники целиком; в этом случае это было бы Guess value must be less than or equal to 100, got 200. Что именно указывать, зависит от того, какая часть сообщения паники уникальна или динамична и насколько точным вы хотите сделать тест. В этом случае подстроки сообщения паники достаточно, чтобы убедиться, что код в тестовой функции выполняет ветку else if value > 100.

Чтобы увидеть, что происходит, когда тест should_panic с сообщением expected проваливается, снова внесем ошибку в код, поменяв местами тела блоков if value < 1 и else if value > 100:

pub struct Guess {
    value: i32,
}

impl Guess {
    pub fn new(value: i32) -> Guess {
        if value < 1 {
            panic!(
                "Guess value must be less than or equal to 100, got {value}."
            );
        } else if value > 100 {
            panic!(
                "Guess value must be greater than or equal to 1, got {value}."
            );
        }

        Guess { value }
    }
}

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

    #[test]
    #[should_panic(expected = "less than or equal to 100")]
    fn greater_than_100() {
        Guess::new(200);
    }
}

На этот раз при запуске тест should_panic провалится:

$ cargo test
   Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
    Finished `test` profile [unoptimized + debuginfo] target(s) in 0.66s
     Running unittests src/lib.rs (target/debug/deps/guessing_game-57d70c3acb738f4d)

running 1 test
test tests::greater_than_100 - should panic ... FAILED

failures:

---- tests::greater_than_100 stdout ----

thread 'tests::greater_than_100' panicked at src/lib.rs:12:13:
Guess value must be greater than or equal to 1, got 200.
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
note: panic did not contain expected string
      panic message: "Guess value must be greater than or equal to 1, got 200."
 expected substring: "less than or equal to 100"

failures:
    tests::greater_than_100

test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

error: test failed, to rerun pass `--lib`

Сообщение о провале указывает, что этот тест действительно запаниковал, как мы и ожидали, но сообщение паники не содержало ожидаемую строку less than or equal to 100. Сообщение паники, которое мы получили в этом случае, было Guess value must be greater than or equal to 1, got 200. Теперь можно начать выяснять, где находится ошибка!

Использование Result<T, E> в тестах

До сих пор все наши тесты паниковали при провале. Мы также можем писать тесты, которые используют Result<T, E>! Вот тест из листинга 11-1, переписанный для использования Result<T, E> и возврата Err вместо паники:

pub fn add(left: u64, right: u64) -> u64 {
    left + right
}

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

    #[test]
    fn it_works() -> Result<(), String> {
        let result = add(2, 2);

        if result == 4 {
            Ok(())
        } else {
            Err(String::from("two plus two does not equal four"))
        }
    }
}

Теперь функция it_works имеет возвращаемый тип Result<(), String>. В теле функции вместо вызова макроса assert_eq! мы возвращаем Ok(()), когда тест проходит, и Err со String внутри, когда тест проваливается.

Написание тестов, возвращающих Result<T, E>, позволяет использовать оператор вопросительного знака в теле тестов. Это бывает удобным способом писать тесты, которые должны провалиться, если любая операция внутри них возвращает вариант Err.

Нельзя использовать аннотацию #[should_panic] для тестов, которые используют Result<T, E>. Чтобы утверждать, что операция возвращает вариант Err, не используйте оператор вопросительного знака для значения Result<T, E>. Вместо этого используйте assert!(value.is_err()).

Теперь, когда вы знаете несколько способов писать тесты, посмотрим, что происходит при запуске тестов, и изучим разные параметры, которые можно использовать с cargo test.

Управление запуском тестов

Управление запуском тестов

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

Некоторые параметры командной строки предназначены для cargo test, а некоторые – для получившегося тестового бинарного файла. Чтобы разделить эти два вида аргументов, сначала перечисляют аргументы для cargo test, затем ставят разделитель --, а после него указывают аргументы для тестового бинарного файла. Команда cargo test --help показывает параметры, которые можно использовать с cargo test, а команда cargo test -- --help показывает параметры, которые можно использовать после разделителя. Эти параметры также описаны в разделе «Tests» книги The rustc Book.

Параллельный или последовательный запуск тестов

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

Например, допустим, каждый из ваших тестов запускает код, который создает на диске файл с именем test-output.txt и записывает в него какие-то данные. Затем каждый тест читает данные из этого файла и проверяет, что файл содержит конкретное значение, причем в каждом тесте это значение разное. Поскольку тесты выполняются одновременно, один тест может перезаписать файл в промежутке между тем, как другой тест записывает файл и читает его. Второй тест тогда провалится не потому, что код неверен, а потому что тесты помешали друг другу при параллельном выполнении. Одно решение – убедиться, что каждый тест пишет в отдельный файл; другое – запускать тесты по одному.

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

$ cargo test -- --test-threads=1

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

Отображение вывода функций

По умолчанию, если тест проходит, тестовая библиотека Rust перехватывает все, что было напечатано в стандартный вывод. Например, если мы вызываем println! в тесте и тест проходит, мы не увидим вывод println! в терминале; мы увидим только строку, которая сообщает, что тест прошел. Если тест провалится, мы увидим все, что было напечатано в стандартный вывод, вместе с остальным сообщением о сбое.

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

Filename: src/lib.rs
fn prints_and_returns_10(a: i32) -> i32 {
    println!("I got the value {a}");
    10
}

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

    #[test]
    fn this_test_will_pass() {
        let value = prints_and_returns_10(4);
        assert_eq!(value, 10);
    }

    #[test]
    fn this_test_will_fail() {
        let value = prints_and_returns_10(8);
        assert_eq!(value, 5);
    }
}
Listing 11-10: Тесты для функции, вызывающей println!

Когда мы запускаем эти тесты с помощью cargo test, видим следующий вывод:

$ cargo test
   Compiling silly-function v0.1.0 (file:///projects/silly-function)
    Finished `test` profile [unoptimized + debuginfo] target(s) in 0.58s
     Running unittests src/lib.rs (target/debug/deps/silly_function-160869f38cff9166)

running 2 tests
test tests::this_test_will_fail ... FAILED
test tests::this_test_will_pass ... ok

failures:

---- tests::this_test_will_fail stdout ----
I got the value 8

thread 'tests::this_test_will_fail' panicked at src/lib.rs:19:9:
assertion `left == right` failed
  left: 10
 right: 5
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace


failures:
    tests::this_test_will_fail

test result: FAILED. 1 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

error: test failed, to rerun pass `--lib`

Обратите внимание: нигде в этом выводе мы не видим I got the value 4, которое печатается при выполнении проходящего теста. Этот вывод был перехвачен. Вывод провалившегося теста, I got the value 8, появляется в разделе сводки тестового вывода, где также показана причина сбоя теста.

Если мы хотим видеть напечатанные значения и для проходящих тестов, можно сказать Rust также показывать вывод успешных тестов с помощью --show-output:

$ cargo test -- --show-output

Когда мы снова запускаем тесты из листинга 11-10 с флагом --show-output, мы видим такой вывод:

$ cargo test -- --show-output
   Compiling silly-function v0.1.0 (file:///projects/silly-function)
    Finished `test` profile [unoptimized + debuginfo] target(s) in 0.60s
     Running unittests src/lib.rs (target/debug/deps/silly_function-160869f38cff9166)

running 2 tests
test tests::this_test_will_fail ... FAILED
test tests::this_test_will_pass ... ok

successes:

---- tests::this_test_will_pass stdout ----
I got the value 4


successes:
    tests::this_test_will_pass

failures:

---- tests::this_test_will_fail stdout ----
I got the value 8

thread 'tests::this_test_will_fail' panicked at src/lib.rs:19:9:
assertion `left == right` failed
  left: 10
 right: 5
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace


failures:
    tests::this_test_will_fail

test result: FAILED. 1 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

error: test failed, to rerun pass `--lib`

Запуск подмножества тестов по имени

Запуск полного набора тестов иногда может занимать много времени. Если вы работаете над кодом в конкретной области, возможно, вам захочется запускать только тесты, относящиеся к этому коду. Вы можете выбрать, какие тесты запустить, передав cargo test имя или имена нужных тестов как аргумент.

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

Filename: src/lib.rs
pub fn add_two(a: u64) -> u64 {
    a + 2
}

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

    #[test]
    fn add_two_and_two() {
        let result = add_two(2);
        assert_eq!(result, 4);
    }

    #[test]
    fn add_three_and_two() {
        let result = add_two(3);
        assert_eq!(result, 5);
    }

    #[test]
    fn one_hundred() {
        let result = add_two(100);
        assert_eq!(result, 102);
    }
}
Listing 11-11: Три теста с тремя разными именами

Если запустить тесты без аргументов, как мы видели ранее, все тесты будут запущены параллельно:

$ cargo test
   Compiling adder v0.1.0 (file:///projects/adder)
    Finished `test` profile [unoptimized + debuginfo] target(s) in 0.62s
     Running unittests src/lib.rs (target/debug/deps/adder-92948b65e88960b4)

running 3 tests
test tests::add_three_and_two ... ok
test tests::add_two_and_two ... ok
test tests::one_hundred ... ok

test result: ok. 3 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

   Doc-tests adder

running 0 tests

test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

Запуск отдельных тестов

Мы можем передать имя любой тестовой функции в cargo test, чтобы запустить только этот тест:

$ cargo test one_hundred
   Compiling adder v0.1.0 (file:///projects/adder)
    Finished `test` profile [unoptimized + debuginfo] target(s) in 0.69s
     Running unittests src/lib.rs (target/debug/deps/adder-92948b65e88960b4)

running 1 test
test tests::one_hundred ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 2 filtered out; finished in 0.00s

Запустился только тест с именем one_hundred; два других теста не совпали с этим именем. Тестовый вывод сообщает, что у нас были и другие тесты, которые не запускались, показывая в конце 2 filtered out.

Указать имена нескольких тестов таким способом нельзя: будет использовано только первое значение, переданное cargo test. Но есть способ запустить несколько тестов.

Фильтрация для запуска нескольких тестов

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

$ cargo test add
   Compiling adder v0.1.0 (file:///projects/adder)
    Finished `test` profile [unoptimized + debuginfo] target(s) in 0.61s
     Running unittests src/lib.rs (target/debug/deps/adder-92948b65e88960b4)

running 2 tests
test tests::add_three_and_two ... ok
test tests::add_two_and_two ... ok

test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 1 filtered out; finished in 0.00s

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

Игнорирование тестов, если они не запрошены явно

Иногда несколько конкретных тестов могут выполняться очень долго, поэтому вы можете исключить их из большинства запусков cargo test. Вместо того чтобы перечислять в аргументах все тесты, которые вы хотите запустить, можно пометить долгие тесты атрибутом ignore, чтобы исключить их, как показано здесь:

Файл: src/lib.rs

pub fn add(left: u64, right: u64) -> u64 {
    left + right
}

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

    #[test]
    fn it_works() {
        let result = add(2, 2);
        assert_eq!(result, 4);
    }

    #[test]
    #[ignore]
    fn expensive_test() {
        // code that takes an hour to run
    }
}

После #[test] мы добавляем строку #[ignore] к тесту, который хотим исключить. Теперь, когда мы запускаем тесты, it_works выполняется, а expensive_test – нет:

$ cargo test
   Compiling adder v0.1.0 (file:///projects/adder)
    Finished `test` profile [unoptimized + debuginfo] target(s) in 0.60s
     Running unittests src/lib.rs (target/debug/deps/adder-92948b65e88960b4)

running 2 tests
test tests::expensive_test ... ignored
test tests::it_works ... ok

test result: ok. 1 passed; 0 failed; 1 ignored; 0 measured; 0 filtered out; finished in 0.00s

   Doc-tests adder

running 0 tests

test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

Функция expensive_test указана как ignored. Если мы хотим запустить только игнорируемые тесты, можно использовать cargo test -- --ignored:

$ cargo test -- --ignored
   Compiling adder v0.1.0 (file:///projects/adder)
    Finished `test` profile [unoptimized + debuginfo] target(s) in 0.61s
     Running unittests src/lib.rs (target/debug/deps/adder-92948b65e88960b4)

running 1 test
test tests::expensive_test ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 1 filtered out; finished in 0.00s

   Doc-tests adder

running 0 tests

test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

Управляя тем, какие тесты запускаются, вы можете быстрее получать результаты cargo test. Когда наступит момент проверить результаты тестов ignored и у вас будет время дождаться их завершения, можно запустить команду cargo test -- --ignored. Если вы хотите запустить все тесты, независимо от того, игнорируются они или нет, можно выполнить команду cargo test -- --include-ignored.

Организация тестов

Организация тестов

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

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

Модульные тесты

Цель модульных тестов – проверять каждую единицу кода в изоляции от остального кода, чтобы быстро определить, где код работает ожидаемо, а где нет. Модульные тесты размещают в каталоге src, в каждом файле рядом с кодом, который они тестируют. По соглашению в каждом файле создают модуль с именем tests, содержащий тестовые функции, и аннотируют этот модуль с помощью cfg(test).

Модуль tests и #[cfg(test)]

Аннотация #[cfg(test)] у модуля tests сообщает Rust, что тестовый код нужно компилировать и запускать только при выполнении cargo test, а не при выполнении cargo build. Это экономит время компиляции, когда нужно только собрать библиотеку, и уменьшает размер полученного скомпилированного артефакта, потому что тесты в него не включаются. Вы увидите, что интеграционные тесты находятся в другом каталоге, поэтому им не нужна аннотация #[cfg(test)]. Однако модульные тесты находятся в тех же файлах, что и код, поэтому вы будете использовать #[cfg(test)], чтобы указать, что они не должны включаться в скомпилированный результат.

Вспомните: когда в первом разделе этой главы мы сгенерировали новый проект adder, Cargo сгенерировал для нас такой код:

Файл: src/lib.rs

pub fn add(left: u64, right: u64) -> u64 {
    left + right
}

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

    #[test]
    fn it_works() {
        let result = add(2, 2);
        assert_eq!(result, 4);
    }
}

В автоматически сгенерированном модуле tests атрибут cfg означает configuration и сообщает Rust, что следующий элемент должен включаться только при заданном параметре конфигурации. В этом случае параметр конфигурации – test, который Rust предоставляет для компиляции и запуска тестов. Благодаря атрибуту cfg Cargo компилирует наш тестовый код только если мы явно запускаем тесты командой cargo test. Это касается и всех вспомогательных функций, которые могут находиться внутри этого модуля, а не только функций, аннотированных #[test].

Тестирование приватных функций

В сообществе тестирования идут споры о том, следует ли напрямую тестировать приватные функции, а некоторые языки затрудняют или вообще запрещают тестирование приватных функций. Независимо от того, какой идеологии тестирования вы придерживаетесь, правила приватности Rust позволяют тестировать приватные функции. Рассмотрим код в листинге 11-12 с приватной функцией internal_adder.

Filename: src/lib.rs
pub fn add_two(a: u64) -> u64 {
    internal_adder(a, 2)
}

fn internal_adder(left: u64, right: u64) -> u64 {
    left + right
}

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

    #[test]
    fn internal() {
        let result = internal_adder(2, 2);
        assert_eq!(result, 4);
    }
}
Listing 11-12: Тестирование приватной функции

Обратите внимание, что функция internal_adder не помечена как pub. Тесты – это обычный код Rust, а модуль tests – просто еще один модуль. Как мы обсуждали в разделе «Пути для ссылки на элемент в дереве модулей», элементы в дочерних модулях могут использовать элементы своих родительских модулей. В этом тесте мы вводим все элементы, принадлежащие родителю модуля tests, в область видимости с помощью use super::*, и затем тест может вызвать internal_adder. Если вы считаете, что приватные функции не следует тестировать, Rust ни к чему вас не принуждает.

Интеграционные тесты

В Rust интеграционные тесты полностью внешние по отношению к вашей библиотеке. Они используют библиотеку так же, как любой другой код, а значит могут вызывать только функции, входящие в публичный API библиотеки. Их цель – проверить, правильно ли многие части библиотеки работают вместе. Единицы кода, которые корректно работают сами по себе, могут иметь проблемы при интеграции, поэтому покрытие интегрированного кода тестами тоже важно. Чтобы создать интеграционные тесты, сначала нужен каталог tests.

Каталог tests

Мы создаем каталог tests на верхнем уровне каталога проекта, рядом с src. Cargo знает, что файлы интеграционных тестов нужно искать в этом каталоге. Затем мы можем создать сколько угодно тестовых файлов, и Cargo скомпилирует каждый из них как отдельный крейт.

Создадим интеграционный тест. Оставив код из листинга 11-12 в файле src/lib.rs, создайте каталог tests и новый файл с именем tests/integration_test.rs. Структура каталогов должна выглядеть так:

adder
├── Cargo.lock
├── Cargo.toml
├── src
│   └── lib.rs
└── tests
    └── integration_test.rs

Введите код из листинга 11-13 в файл tests/integration_test.rs.

Filename: tests/integration_test.rs
use adder::add_two;

#[test]
fn it_adds_two() {
    let result = add_two(2);
    assert_eq!(result, 4);
}
Listing 11-13: Интеграционный тест функции из крейта adder

Каждый файл в каталоге tests является отдельным крейтом, поэтому нам нужно вводить нашу библиотеку в область видимости каждого тестового крейта. По этой причине в начало кода мы добавляем use adder::add_two;, что не требовалось в модульных тестах.

Нам не нужно аннотировать какой-либо код в tests/integration_test.rs с помощью #[cfg(test)]. Cargo особым образом обрабатывает каталог tests и компилирует файлы в нем только при выполнении cargo test. Теперь запустите cargo test:

$ cargo test
   Compiling adder v0.1.0 (file:///projects/adder)
    Finished `test` profile [unoptimized + debuginfo] target(s) in 1.31s
     Running unittests src/lib.rs (target/debug/deps/adder-1082c4b063a8fbe6)

running 1 test
test tests::internal ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

     Running tests/integration_test.rs (target/debug/deps/integration_test-1082c4b063a8fbe6)

running 1 test
test it_adds_two ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

   Doc-tests adder

running 0 tests

test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

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

Первый раздел для модульных тестов такой же, как мы уже видели: одна строка для каждого модульного теста (один из них называется internal, мы добавили его в листинге 11-12), а затем строка сводки для модульных тестов.

Раздел интеграционных тестов начинается со строки Running tests/integration_test.rs. Далее идет строка для каждой тестовой функции в этом интеграционном тесте и строка сводки результатов интеграционного теста прямо перед началом раздела Doc-tests adder.

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

Мы по-прежнему можем запустить конкретную функцию интеграционного теста, указав имя тестовой функции как аргумент cargo test. Чтобы запустить все тесты в конкретном файле интеграционных тестов, используйте аргумент --test команды cargo test, а после него укажите имя файла:

$ cargo test --test integration_test
   Compiling adder v0.1.0 (file:///projects/adder)
    Finished `test` profile [unoptimized + debuginfo] target(s) in 0.64s
     Running tests/integration_test.rs (target/debug/deps/integration_test-82e7799c1bc62298)

running 1 test
test it_adds_two ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

Эта команда запускает только тесты из файла tests/integration_test.rs.

Подмодули в интеграционных тестах

По мере добавления интеграционных тестов вы можете захотеть создать больше файлов в каталоге tests, чтобы лучше их организовать; например, можно группировать тестовые функции по проверяемой функциональности. Как упоминалось ранее, каждый файл в каталоге tests компилируется как отдельный крейт. Это полезно для создания отдельных областей видимости, которые точнее имитируют то, как конечные пользователи будут использовать ваш крейт. Однако это означает, что файлы в каталоге tests ведут себя иначе, чем файлы в src, о чем вы узнали в главе 7 при обсуждении разделения кода на модули и файлы.

Различие в поведении файлов каталога tests заметнее всего, когда у вас есть набор вспомогательных функций для использования в нескольких файлах интеграционных тестов, и вы пытаетесь следовать шагам из раздела «Разделение модулей на разные файлы» главы 7, чтобы вынести их в общий модуль. Например, если мы создадим tests/common.rs и поместим в него функцию с именем setup, то сможем добавить в setup код, который хотим вызывать из нескольких тестовых функций в нескольких тестовых файлах:

Файл: tests/common.rs

pub fn setup() {
    // setup code specific to your library's tests would go here
}

Когда мы снова запустим тесты, увидим в тестовом выводе новый раздел для файла common.rs, хотя этот файл не содержит тестовых функций, и мы нигде не вызывали функцию setup:

$ cargo test
   Compiling adder v0.1.0 (file:///projects/adder)
    Finished `test` profile [unoptimized + debuginfo] target(s) in 0.89s
     Running unittests src/lib.rs (target/debug/deps/adder-92948b65e88960b4)

running 1 test
test tests::internal ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

     Running tests/common.rs (target/debug/deps/common-92948b65e88960b4)

running 0 tests

test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

     Running tests/integration_test.rs (target/debug/deps/integration_test-92948b65e88960b4)

running 1 test
test it_adds_two ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

   Doc-tests adder

running 0 tests

test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

Появление common в результатах тестов со строкой running 0 tests – не то, чего мы хотели. Мы просто хотели поделиться кодом с другими файлами интеграционных тестов. Чтобы common не появлялся в тестовом выводе, вместо tests/common.rs создадим tests/common/mod.rs. Теперь каталог проекта выглядит так:

├── Cargo.lock
├── Cargo.toml
├── src
│   └── lib.rs
└── tests
    ├── common
    │   └── mod.rs
    └── integration_test.rs

Это старое соглашение об именовании, которое Rust тоже понимает и о котором мы упоминали в разделе «Альтернативные пути к файлам» главы 7. Такое имя файла сообщает Rust, что модуль common не следует рассматривать как файл интеграционного теста. Когда мы перенесем код функции setup в tests/common/mod.rs и удалим файл tests/common.rs, раздел в тестовом выводе больше не появится. Файлы в подкаталогах каталога tests не компилируются как отдельные крейты и не получают разделов в тестовом выводе.

После создания tests/common/mod.rs мы можем использовать его как модуль из любого файла интеграционных тестов. Вот пример вызова функции setup из теста it_adds_two в tests/integration_test.rs:

Файл: tests/integration_test.rs

use adder::add_two;

mod common;

#[test]
fn it_adds_two() {
    common::setup();

    let result = add_two(2);
    assert_eq!(result, 4);
}

Обратите внимание, что объявление mod common; такое же, как объявление модуля, которое мы показывали в листинге 7-21. Затем в тестовой функции мы можем вызвать функцию common::setup().

Интеграционные тесты для бинарных крейтов

Если наш проект является бинарным крейтом, который содержит только файл src/main.rs и не имеет файла src/lib.rs, мы не можем создать интеграционные тесты в каталоге tests и ввести функции, определенные в файле src/main.rs, в область видимости с помощью инструкции use. Только библиотечные крейты предоставляют функции, которые могут использовать другие крейты; бинарные крейты предназначены для самостоятельного запуска.

Это одна из причин, по которой проекты Rust, предоставляющие бинарный файл, имеют простой файл src/main.rs, вызывающий логику, которая находится в файле src/lib.rs. При такой структуре интеграционные тесты могут тестировать библиотечный крейт с помощью use, чтобы сделать важную функциональность доступной. Если важная функциональность работает, небольшое количество кода в файле src/main.rs тоже будет работать, и это небольшое количество кода не нужно тестировать.

Итоги

Возможности тестирования Rust дают способ указать, как должен работать код, чтобы он продолжал работать ожидаемо даже после изменений. Модульные тесты проверяют разные части библиотеки по отдельности и могут тестировать приватные детали реализации. Интеграционные тесты проверяют, что многие части библиотеки правильно работают вместе, и используют публичный API библиотеки, проверяя код так же, как его будет использовать внешний код. Хотя система типов Rust и правила владения помогают предотвратить некоторые виды ошибок, тесты все равно важны для уменьшения логических ошибок, связанных с ожидаемым поведением вашего кода.

Объединим знания, полученные в этой и предыдущих главах, и поработаем над проектом!

I/O-проект: создание программы командной строки

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

Скорость Rust, безопасность, сборка в единый бинарный файл и кроссплатформенная поддержка делают его отличным языком для создания инструментов командной строки. Поэтому в этом проекте мы сделаем собственную версию классического поискового инструмента командной строки grep; его название происходит от английского globally search a regular expression and print. В самом простом случае grep ищет указанную строку в указанном файле. Для этого grep принимает в аргументах путь к файлу и строку. Затем он читает файл, находит в нем строки, содержащие переданную строку, и печатает эти строки.

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

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

Наш проект grep объединит несколько концепций, которые вы уже изучили:

  • Организация кода (глава 7)
  • Использование векторов и строк (глава 8)
  • Обработка ошибок (глава 9)
  • Использование трейтов и времен жизни там, где это уместно (глава 10)
  • Написание тестов (глава 11)

Мы также кратко познакомимся с замыканиями, итераторами и трейт-объектами, которые подробно рассматриваются в главе 13 и главе 18.

Получение аргументов командной строки

Прием аргументов командной строки

Создадим новый проект, как обычно, с помощью cargo new. Назовем его minigrep, чтобы отличать от инструмента grep, который уже может быть установлен в вашей системе:

$ cargo new minigrep
     Created binary (application) `minigrep` project
$ cd minigrep

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

$ cargo run -- searchstring example-filename.txt

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

Чтение значений аргументов

Чтобы minigrep мог читать значения аргументов командной строки, которые мы ему передаем, понадобится функция std::env::args из стандартной библиотеки Rust. Эта функция возвращает итератор по аргументам командной строки, переданным minigrep. Полностью итераторы мы рассмотрим в главе 13. Сейчас нужно знать о них только две вещи: итераторы создают последовательность значений, а метод collect можно вызвать для итератора, чтобы превратить его в коллекцию, например в вектор, содержащий все элементы, которые выдает итератор.

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

Filename: src/main.rs
use std::env;

fn main() {
    let args: Vec<String> = env::args().collect();
    dbg!(args);
}
Listing 12-1: Сбор аргументов командной строки в вектор и их печать

Сначала мы вводим модуль std::env в область видимости с помощью инструкции use, чтобы пользоваться его функцией args. Обратите внимание, что функция std::env::args вложена в два уровня модулей. Как мы обсуждали в главе 7, если нужная функция вложена больше чем в один модуль, мы предпочитаем вводить в область видимости родительский модуль, а не саму функцию. Так нам проще использовать другие функции из std::env. Кроме того, это менее двусмысленно, чем добавить use std::env::args, а затем вызывать функцию просто как args, потому что args легко принять за функцию, определенную в текущем модуле.

Функция args и недопустимый Unicode

Обратите внимание, что std::env::args запаникует, если какой-либо аргумент содержит недопустимый Unicode. Если вашей программе нужно принимать аргументы, содержащие недопустимый Unicode, используйте вместо нее std::env::args_os. Эта функция возвращает итератор, который выдает значения OsString вместо значений String. Здесь мы выбрали std::env::args для простоты, потому что значения OsString различаются в зависимости от платформы и работать с ними сложнее, чем со значениями String.

В первой строке main мы вызываем env::args и сразу используем collect, чтобы превратить итератор в вектор со всеми значениями, которые он выдает. Функция collect умеет создавать разные виды коллекций, поэтому мы явно аннотируем тип args, чтобы указать, что нам нужен вектор строк. Хотя в Rust очень редко приходится аннотировать типы, collect – одна из функций, где это часто нужно делать: Rust не может сам вывести, какую именно коллекцию вы хотите получить.

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

$ cargo run
   Compiling minigrep v0.1.0 (file:///projects/minigrep)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.61s
     Running `target/debug/minigrep`
[src/main.rs:5:5] args = [
    "target/debug/minigrep",
]
$ cargo run -- needle haystack
   Compiling minigrep v0.1.0 (file:///projects/minigrep)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 1.57s
     Running `target/debug/minigrep needle haystack`
[src/main.rs:5:5] args = [
    "target/debug/minigrep",
    "needle",
    "haystack",
]

Обратите внимание, что первое значение в векторе – "target/debug/minigrep", то есть имя нашего бинарного файла. Это совпадает с поведением списка аргументов в C и позволяет программам использовать имя, под которым они были запущены. Часто удобно иметь доступ к имени программы, если нужно напечатать его в сообщениях или изменить поведение программы в зависимости от того, через какой псевдоним командной строки она была вызвана. Но для целей этой главы мы проигнорируем это значение и сохраним только два нужных нам аргумента.

Сохранение значений аргументов в переменные

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

Filename: src/main.rs
use std::env;

fn main() {
    let args: Vec<String> = env::args().collect();

    let query = &args[1];
    let file_path = &args[2];

    println!("Searching for {query}");
    println!("In file {file_path}");
}
Listing 12-2: Создание переменных для хранения аргумента запроса и аргумента пути к файлу

Как мы увидели при печати вектора, имя программы занимает первое значение в векторе, args[0], поэтому аргументы начинаются с индекса 1. Первый аргумент, который принимает minigrep, – строка, которую мы ищем, поэтому мы помещаем ссылку на первый аргумент в переменную query. Второй аргумент будет путем до файла, поэтому мы помещаем ссылку на второй аргумент в переменную file_path.

Мы временно печатаем значения этих переменных, чтобы убедиться, что код работает так, как мы ожидаем. Снова запустим программу с аргументами test и sample.txt:

$ cargo run -- test sample.txt
   Compiling minigrep v0.1.0 (file:///projects/minigrep)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.0s
     Running `target/debug/minigrep test sample.txt`
Searching for test
In file sample.txt

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

Чтение файла

Чтение файла

Теперь добавим возможность читать файл, указанный в аргументе file_path. Сначала нам нужен пример файла, на котором можно это проверить: используем файл с небольшим объемом текста на нескольких строках и с несколькими повторяющимися словами. В листинге 12-3 приведено стихотворение Эмили Дикинсон, которое хорошо подойдет! Создайте файл с именем poem.txt в корне проекта и введите в него стихотворение “I’m Nobody! Who are you?”

Filename: poem.txt
I'm nobody! Who are you?
Are you nobody, too?
Then there's a pair of us - don't tell!
They'd banish us, you know.

How dreary to be somebody!
How public, like a frog
To tell your name the livelong day
To an admiring bog!
Listing 12-3: Стихотворение Эмили Дикинсон хорошо подходит как тестовый пример.

Когда текст будет на месте, отредактируйте src/main.rs и добавьте код для чтения файла, как показано в листинге 12-4.

Filename: src/main.rs
use std::env;
use std::fs;

fn main() {
    // --snip--
    let args: Vec<String> = env::args().collect();

    let query = &args[1];
    let file_path = &args[2];

    println!("Searching for {query}");
    println!("In file {file_path}");

    let contents = fs::read_to_string(file_path)
        .expect("Should have been able to read the file");

    println!("With text:\n{contents}");
}
Listing 12-4: Чтение содержимого файла, указанного вторым аргументом

Сначала мы вводим в область видимости нужную часть стандартной библиотеки с помощью инструкции use: для работы с файлами нам нужен std::fs.

В main новое выражение fs::read_to_string принимает file_path, открывает этот файл и возвращает значение типа std::io::Result<String>, содержащее содержимое файла.

После этого мы снова добавляем временный вызов println!, который печатает значение contents после чтения файла, чтобы проверить, что программа пока работает правильно.

Запустим этот код с любой строкой в качестве первого аргумента командной строки (потому что часть с поиском мы еще не реализовали) и файлом poem.txt в качестве второго аргумента:

$ cargo run -- the poem.txt
   Compiling minigrep v0.1.0 (file:///projects/minigrep)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.0s
     Running `target/debug/minigrep the poem.txt`
Searching for the
In file poem.txt
With text:
I'm nobody! Who are you?
Are you nobody, too?
Then there's a pair of us - don't tell!
They'd banish us, you know.

How dreary to be somebody!
How public, like a frog
To tell your name the livelong day
To an admiring bog!

Отлично! Код прочитал, а затем напечатал содержимое файла. Но у него есть несколько недостатков. Сейчас функция main отвечает за несколько вещей: обычно функции понятнее и проще поддерживать, если каждая функция отвечает только за одну идею. Другая проблема в том, что мы обрабатываем ошибки не так хорошо, как могли бы. Программа пока маленькая, поэтому эти недостатки не создают большой проблемы, но по мере роста программы их будет труднее исправить чисто. Хорошая практика – начинать рефакторинг на ранних этапах разработки, потому что меньшие объемы кода рефакторить намного проще. Этим мы займемся дальше.

Рефакторинг для улучшения модульности и обработки ошибок

Рефакторинг для улучшения модульности и обработки ошибок

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

Эта проблема также связана со второй: хотя query и file_path являются переменными конфигурации нашей программы, такие переменные, как contents, используются для выполнения логики программы. Чем длиннее становится main, тем больше переменных нам придется вводить в область видимости; чем больше переменных находится в области видимости, тем сложнее отслеживать назначение каждой из них. Лучше сгруппировать переменные конфигурации в одну структуру, чтобы их назначение было понятно.

Третья проблема в том, что мы использовали expect, чтобы напечатать сообщение об ошибке, когда чтение файла завершается неудачей, но это сообщение просто выводит Should have been able to read the file. Чтение файла может не удаться по разным причинам: например, файл может отсутствовать или у нас может не быть разрешения на его открытие. Сейчас, независимо от ситуации, мы напечатали бы одно и то же сообщение об ошибке для всего, а значит, не дали бы пользователю никакой полезной информации!

В-четвертых, мы используем expect для обработки ошибки, и если пользователь запустит нашу программу, не указав достаточно аргументов, он получит от Rust ошибку index out of bounds, которая не объясняет проблему достаточно ясно. Лучше, чтобы весь код обработки ошибок находился в одном месте: тогда будущим сопровождающим придется обращаться только к одному участку кода, если логику обработки ошибок понадобится изменить. Если весь код обработки ошибок будет в одном месте, это также поможет нам печатать сообщения, понятные конечным пользователям.

Решим эти четыре проблемы с помощью рефакторинга проекта.

Разделение ответственностей в бинарных проектах

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

  • Разделить программу на файл main.rs и файл lib.rs и переместить логику программы в lib.rs.
  • Пока логика разбора аргументов командной строки остается небольшой, она может оставаться в функции main.
  • Когда логика разбора аргументов командной строки начинает усложняться, вынести ее из функции main в другие функции или типы.

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

  • Вызов логики разбора командной строки со значениями аргументов
  • Настройка любой другой конфигурации
  • Вызов функции run в lib.rs
  • Обработка ошибки, если run вернет ошибку

Этот шаблон связан с разделением ответственностей: main.rs отвечает за запуск программы, а lib.rs отвечает за всю логику выполняемой задачи. Поскольку функцию main нельзя тестировать напрямую, такая структура позволяет тестировать всю логику программы, переместив ее из функции main. Код, оставшийся в функции main, будет достаточно маленьким, чтобы проверить его правильность чтением. Переработаем нашу программу, следуя этому процессу.

Извлечение парсера аргументов

Мы вынесем функциональность разбора аргументов в функцию, которую будет вызывать main. В листинге 12-5 показано новое начало функции main, которое вызывает новую функцию parse_config; ее мы определим в src/main.rs.

Filename: src/main.rs
use std::env;
use std::fs;

fn main() {
    let args: Vec<String> = env::args().collect();

    let (query, file_path) = parse_config(&args);

    // --snip--

    println!("Searching for {query}");
    println!("In file {file_path}");

    let contents = fs::read_to_string(file_path)
        .expect("Should have been able to read the file");

    println!("With text:\n{contents}");
}

fn parse_config(args: &[String]) -> (&str, &str) {
    let query = &args[1];
    let file_path = &args[2];

    (query, file_path)
}
Listing 12-5: Извлечение функции parse_config из main

Мы по-прежнему собираем аргументы командной строки в вектор, но вместо того чтобы присваивать значение аргумента с индексом 1 переменной query, а значение аргумента с индексом 2 переменной file_path внутри функции main, мы передаем весь вектор функции parse_config. Затем функция parse_config содержит логику, определяющую, какой аргумент в какую переменную попадает, и передает значения обратно в main. Мы все еще создаем переменные query и file_path в main, но main больше не отвечает за то, как аргументы командной строки сопоставляются с переменными.

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

Группировка значений конфигурации

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

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

В листинге 12-6 показаны улучшения функции parse_config.

Filename: src/main.rs
use std::env;
use std::fs;

fn main() {
    let args: Vec<String> = env::args().collect();

    let config = parse_config(&args);

    println!("Searching for {}", config.query);
    println!("In file {}", config.file_path);

    let contents = fs::read_to_string(config.file_path)
        .expect("Should have been able to read the file");

    // --snip--

    println!("With text:\n{contents}");
}

struct Config {
    query: String,
    file_path: String,
}

fn parse_config(args: &[String]) -> Config {
    let query = args[1].clone();
    let file_path = args[2].clone();

    Config { query, file_path }
}
Listing 12-6: Рефакторинг parse_config, чтобы она возвращала экземпляр структуры Config

Мы добавили структуру с именем Config, определенную с полями query и file_path. Сигнатура parse_config теперь показывает, что она возвращает значение Config. В теле parse_config, где раньше мы возвращали строковые срезы, ссылающиеся на значения String в args, теперь мы определяем Config так, чтобы она содержала собственные значения String. Переменная args в main является владельцем значений аргументов и только позволяет функции parse_config заимствовать их; это значит, что мы нарушили бы правила заимствования Rust, если бы Config попыталась забрать владение значениями из args.

Есть несколько способов управлять данными String; самый простой, хотя и несколько неэффективный путь, – вызвать метод clone для этих значений. Это сделает полную копию данных, которой будет владеть экземпляр Config, что требует больше времени и памяти, чем хранение ссылки на строковые данные. Однако клонирование данных также делает наш код очень простым, потому что нам не нужно управлять временами жизни ссылок; в этой ситуации отказаться от небольшой части производительности ради простоты – разумный компромисс.

Компромиссы использования clone

Многие разработчики Rust склонны избегать использования clone для исправления проблем владения из-за его стоимости во время выполнения. В главе 13 вы узнаете, как использовать более эффективные методы в таких ситуациях. Но сейчас нормально скопировать несколько строк, чтобы продолжать двигаться вперед, потому что вы сделаете эти копии только один раз, а путь к файлу и строка запроса очень малы. Лучше иметь рабочую программу, которая немного неэффективна, чем пытаться гипероптимизировать код при первом проходе. Когда вы станете опытнее в Rust, будет проще сразу начинать с самого эффективного решения, но пока совершенно допустимо вызвать clone.

Мы обновили main: теперь она помещает экземпляр Config, возвращенный parse_config, в переменную с именем config. Также мы обновили код, который раньше использовал отдельные переменные query и file_path, чтобы теперь он использовал поля структуры Config.

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

Создание конструктора для Config

До сих пор мы извлекли из main логику, отвечающую за разбор аргументов командной строки, и поместили ее в функцию parse_config. Это помогло нам увидеть, что значения query и file_path связаны между собой и что эта связь должна быть выражена в нашем коде. Затем мы добавили структуру Config, чтобы назвать связанное назначение query и file_path и чтобы функция parse_config могла возвращать имена значений как имена полей структуры.

Итак, теперь, когда назначение функции parse_config – создать экземпляр Config, мы можем превратить parse_config из обычной функции в функцию с именем new, связанную со структурой Config. Это сделает код более идиоматичным. Мы можем создавать экземпляры типов из стандартной библиотеки, например String, вызывая String::new. Точно так же, превратив parse_config в функцию new, связанную с Config, мы сможем создавать экземпляры Config, вызывая Config::new. В листинге 12-7 показаны изменения, которые нужно внести.

Filename: src/main.rs
use std::env;
use std::fs;

fn main() {
    let args: Vec<String> = env::args().collect();

    let config = Config::new(&args);

    println!("Searching for {}", config.query);
    println!("In file {}", config.file_path);

    let contents = fs::read_to_string(config.file_path)
        .expect("Should have been able to read the file");

    println!("With text:\n{contents}");

    // --snip--
}

// --snip--

struct Config {
    query: String,
    file_path: String,
}

impl Config {
    fn new(args: &[String]) -> Config {
        let query = args[1].clone();
        let file_path = args[2].clone();

        Config { query, file_path }
    }
}
Listing 12-7: Превращение parse_config в Config::new

Мы обновили main: там, где раньше вызывалась parse_config, теперь вызывается Config::new. Мы изменили имя parse_config на new и переместили ее в блок impl, который связывает функцию new с Config. Попробуйте снова скомпилировать этот код, чтобы убедиться, что он работает.

Исправление обработки ошибок

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

$ cargo run
   Compiling minigrep v0.1.0 (file:///projects/minigrep)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.0s
     Running `target/debug/minigrep`

thread 'main' panicked at src/main.rs:27:21:
index out of bounds: the len is 1 but the index is 1
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

Строка index out of bounds: the len is 1 but the index is 1 – это сообщение об ошибке, предназначенное для программистов. Оно не поможет конечным пользователям понять, что им нужно сделать иначе. Исправим это.

Улучшение сообщения об ошибке

В листинге 12-8 мы добавляем в функцию new проверку, которая удостоверится, что срез достаточно длинный, прежде чем обращаться к индексам 1 и 2. Если срез недостаточно длинный, программа паникует и выводит более понятное сообщение об ошибке.

Filename: src/main.rs
use std::env;
use std::fs;

fn main() {
    let args: Vec<String> = env::args().collect();

    let config = Config::new(&args);

    println!("Searching for {}", config.query);
    println!("In file {}", config.file_path);

    let contents = fs::read_to_string(config.file_path)
        .expect("Should have been able to read the file");

    println!("With text:\n{contents}");
}

struct Config {
    query: String,
    file_path: String,
}

impl Config {
    // --snip--
    fn new(args: &[String]) -> Config {
        if args.len() < 3 {
            panic!("not enough arguments");
        }
        // --snip--

        let query = args[1].clone();
        let file_path = args[2].clone();

        Config { query, file_path }
    }
}
Listing 12-8: Добавление проверки количества аргументов

Этот код похож на функцию Guess::new, которую мы написали в листинге 9-13, где мы вызывали panic!, когда аргумент value выходил за диапазон допустимых значений. Здесь, вместо проверки диапазона значений, мы проверяем, что длина args не меньше 3, и оставшаяся часть функции может работать, исходя из предположения, что это условие выполнено. Если в args меньше трех элементов, это условие будет true, и мы вызываем макрос panic!, чтобы немедленно завершить программу.

Добавив эти несколько строк кода в new, снова запустим программу без аргументов и посмотрим, как теперь выглядит ошибка:

$ cargo run
   Compiling minigrep v0.1.0 (file:///projects/minigrep)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.0s
     Running `target/debug/minigrep`

thread 'main' panicked at src/main.rs:26:13:
not enough arguments
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

Этот вывод лучше: теперь у нас есть разумное сообщение об ошибке. Однако в нем также есть лишняя информация, которую мы не хотим показывать пользователям. Возможно, прием, который мы использовали в листинге 9-13, не лучший для этого случая: вызов panic! больше подходит для проблемы в программе, чем для проблемы использования, как обсуждалось в главе 9. Вместо этого мы используем другой прием, о котором вы узнали в главе 9, – возврат Result, который указывает либо на успех, либо на ошибку.

Возврат Result вместо вызова panic!

Вместо этого мы можем вернуть значение Result, которое в случае успеха будет содержать экземпляр Config, а в случае ошибки будет описывать проблему. Мы также изменим имя функции с new на build, потому что многие программисты ожидают, что функции new никогда не завершаются ошибкой. Когда Config::build взаимодействует с main, мы можем использовать тип Result, чтобы подать сигнал о возникшей проблеме. Затем мы можем изменить main так, чтобы она превращала вариант Err в более практичную ошибку для пользователей, без окружающего текста про thread 'main' и RUST_BACKTRACE, который возникает при вызове panic!.

В листинге 12-9 показаны изменения, которые нужно внести в возвращаемое значение функции, теперь называемой Config::build, и в тело функции, чтобы она возвращала Result. Обратите внимание, что этот код не скомпилируется, пока мы не обновим также main; это мы сделаем в следующем листинге.

Filename: src/main.rs
use std::env;
use std::fs;

fn main() {
    let args: Vec<String> = env::args().collect();

    let config = Config::new(&args);

    println!("Searching for {}", config.query);
    println!("In file {}", config.file_path);

    let contents = fs::read_to_string(config.file_path)
        .expect("Should have been able to read the file");

    println!("With text:\n{contents}");
}

struct Config {
    query: String,
    file_path: String,
}

impl Config {
    fn build(args: &[String]) -> Result<Config, &'static str> {
        if args.len() < 3 {
            return Err("not enough arguments");
        }

        let query = args[1].clone();
        let file_path = args[2].clone();

        Ok(Config { query, file_path })
    }
}
Listing 12-9: Возврат Result из Config::build

Наша функция build возвращает Result с экземпляром Config в случае успеха и строковым литералом в случае ошибки. Наши значения ошибок всегда будут строковыми литералами с временем жизни 'static.

Мы внесли два изменения в тело функции: вместо вызова panic!, когда пользователь не передает достаточно аргументов, мы теперь возвращаем значение Err, а возвращаемое значение Config обернули в Ok. Эти изменения приводят функцию в соответствие с ее новой сигнатурой типа.

Возврат значения Err из Config::build позволяет функции main обработать значение Result, возвращенное функцией build, и чище завершить процесс в случае ошибки.

Вызов Config::build и обработка ошибок

Чтобы обработать случай ошибки и напечатать удобное для пользователя сообщение, нам нужно обновить main, чтобы она обрабатывала Result, возвращаемый Config::build, как показано в листинге 12-10. Мы также уберем ответственность за завершение инструмента командной строки с ненулевым кодом ошибки у panic! и вместо этого реализуем ее вручную. Ненулевой статус завершения – это соглашение, которое сообщает процессу, вызвавшему нашу программу, что программа завершилась с состоянием ошибки.

Filename: src/main.rs
use std::env;
use std::fs;
use std::process;

fn main() {
    let args: Vec<String> = env::args().collect();

    let config = Config::build(&args).unwrap_or_else(|err| {
        println!("Problem parsing arguments: {err}");
        process::exit(1);
    });

    // --snip--

    println!("Searching for {}", config.query);
    println!("In file {}", config.file_path);

    let contents = fs::read_to_string(config.file_path)
        .expect("Should have been able to read the file");

    println!("With text:\n{contents}");
}

struct Config {
    query: String,
    file_path: String,
}

impl Config {
    fn build(args: &[String]) -> Result<Config, &'static str> {
        if args.len() < 3 {
            return Err("not enough arguments");
        }

        let query = args[1].clone();
        let file_path = args[2].clone();

        Ok(Config { query, file_path })
    }
}
Listing 12-10: Завершение с кодом ошибки, если построить Config не удалось

В этом листинге мы использовали метод, который еще не рассматривали подробно: unwrap_or_else, определенный стандартной библиотекой для Result<T, E>. Использование unwrap_or_else позволяет нам определить собственную обработку ошибок без panic!. Если Result содержит значение Ok, поведение этого метода похоже на unwrap: он возвращает внутреннее значение, обернутое в Ok. Однако если значение является Err, этот метод вызывает код в замыкании: это анонимная функция, которую мы определяем и передаем как аргумент в unwrap_or_else. Замыкания мы подробнее рассмотрим в главе 13. Сейчас вам достаточно знать, что unwrap_or_else передаст внутреннее значение Err, которым в этом случае является статическая строка "not enough arguments", добавленная нами в листинге 12-9, в наше замыкание через аргумент err, указанный между вертикальными чертами. Код в замыкании затем сможет использовать значение err при выполнении.

Мы добавили новую строку use, чтобы ввести process из стандартной библиотеки в область видимости. Код в замыкании, который будет выполнен в случае ошибки, состоит всего из двух строк: мы печатаем значение err, а затем вызываем process::exit. Функция process::exit немедленно остановит программу и вернет число, переданное как код статуса завершения. Это похоже на обработку на основе panic!, которую мы использовали в листинге 12-8, но теперь мы больше не получаем весь лишний вывод. Попробуем:

$ cargo run
   Compiling minigrep v0.1.0 (file:///projects/minigrep)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.48s
     Running `target/debug/minigrep`
Problem parsing arguments: not enough arguments

Отлично! Этот вывод гораздо дружелюбнее для наших пользователей.

Извлечение логики из main

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

В листинге 12-11 показано небольшое последовательное улучшение: извлечение функции run.

Filename: src/main.rs
use std::env;
use std::fs;
use std::process;

fn main() {
    // --snip--

    let args: Vec<String> = env::args().collect();

    let config = Config::build(&args).unwrap_or_else(|err| {
        println!("Problem parsing arguments: {err}");
        process::exit(1);
    });

    println!("Searching for {}", config.query);
    println!("In file {}", config.file_path);

    run(config);
}

fn run(config: Config) {
    let contents = fs::read_to_string(config.file_path)
        .expect("Should have been able to read the file");

    println!("With text:\n{contents}");
}

// --snip--

struct Config {
    query: String,
    file_path: String,
}

impl Config {
    fn build(args: &[String]) -> Result<Config, &'static str> {
        if args.len() < 3 {
            return Err("not enough arguments");
        }

        let query = args[1].clone();
        let file_path = args[2].clone();

        Ok(Config { query, file_path })
    }
}
Listing 12-11: Извлечение функции run, содержащей остальную логику программы

Функция run теперь содержит всю оставшуюся логику из main, начиная с чтения файла. Функция run принимает экземпляр Config как аргумент.

Возврат ошибок из run

Теперь, когда оставшаяся логика программы отделена в функцию run, мы можем улучшить обработку ошибок так же, как сделали это с Config::build в листинге 12-9. Вместо того чтобы позволять программе паниковать из-за вызова expect, функция run будет возвращать Result<T, E>, когда что-то пойдет не так. Это позволит нам дальше объединять логику обработки ошибок в main и делать это удобным для пользователя способом. В листинге 12-12 показаны изменения, которые нужно внести в сигнатуру и тело run.

Filename: src/main.rs
use std::env;
use std::fs;
use std::process;
use std::error::Error;

// --snip--


fn main() {
    let args: Vec<String> = env::args().collect();

    let config = Config::build(&args).unwrap_or_else(|err| {
        println!("Problem parsing arguments: {err}");
        process::exit(1);
    });

    println!("Searching for {}", config.query);
    println!("In file {}", config.file_path);

    run(config);
}

fn run(config: Config) -> Result<(), Box<dyn Error>> {
    let contents = fs::read_to_string(config.file_path)?;

    println!("With text:\n{contents}");

    Ok(())
}

struct Config {
    query: String,
    file_path: String,
}

impl Config {
    fn build(args: &[String]) -> Result<Config, &'static str> {
        if args.len() < 3 {
            return Err("not enough arguments");
        }

        let query = args[1].clone();
        let file_path = args[2].clone();

        Ok(Config { query, file_path })
    }
}
Listing 12-12: Изменение функции run, чтобы она возвращала Result

Здесь мы сделали три важных изменения. Во-первых, мы изменили возвращаемый тип функции run на Result<(), Box<dyn Error>>. Раньше эта функция возвращала единичный тип, (), и мы сохраняем его как значение, возвращаемое в случае Ok.

Для типа ошибки мы использовали трейт-объект Box<dyn Error> (и ввели std::error::Error в область видимости с помощью инструкции use в начале). Мы рассмотрим трейт-объекты в главе 18. Пока просто знайте, что Box<dyn Error> означает: функция вернет тип, реализующий трейт Error, но нам не нужно указывать, каким конкретным типом будет возвращаемое значение. Это дает нам гибкость возвращать значения ошибок, которые могут быть разных типов в разных случаях ошибки. Ключевое слово dyn – сокращение от dynamic.

Во-вторых, мы удалили вызов expect в пользу оператора ?, о котором говорили в главе 9. Вместо того чтобы вызывать panic! при ошибке, ? вернет значение ошибки из текущей функции, чтобы его обработал вызывающий код.

В-третьих, функция run теперь возвращает значение Ok в случае успеха. В сигнатуре мы объявили успешный тип функции run как (), поэтому нам нужно обернуть значение единичного типа в Ok. Синтаксис Ok(()) сначала может выглядеть немного странно. Но использование () таким образом – идиоматичный способ показать, что мы вызываем run только ради побочных эффектов; она не возвращает значение, которое нам нужно.

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

$ cargo run -- the poem.txt
   Compiling minigrep v0.1.0 (file:///projects/minigrep)
warning: unused `Result` that must be used
  --> src/main.rs:19:5
   |
19 |     run(config);
   |     ^^^^^^^^^^^
   |
   = note: this `Result` may be an `Err` variant, which should be handled
   = note: `#[warn(unused_must_use)]` on by default
help: use `let _ = ...` to ignore the resulting value
   |
19 |     let _ = run(config);
   |     +++++++

warning: `minigrep` (bin "minigrep") generated 1 warning
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.71s
     Running `target/debug/minigrep the poem.txt`
Searching for the
In file poem.txt
With text:
I'm nobody! Who are you?
Are you nobody, too?
Then there's a pair of us - don't tell!
They'd banish us, you know.

How dreary to be somebody!
How public, like a frog
To tell your name the livelong day
To an admiring bog!

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

Обработка ошибок, возвращенных из run, в main

Мы проверим наличие ошибок и обработаем их приемом, похожим на тот, который использовали с Config::build в листинге 12-10, но с небольшим отличием:

Имя файла: src/main.rs

use std::env;
use std::error::Error;
use std::fs;
use std::process;

fn main() {
    // --snip--

    let args: Vec<String> = env::args().collect();

    let config = Config::build(&args).unwrap_or_else(|err| {
        println!("Problem parsing arguments: {err}");
        process::exit(1);
    });

    println!("Searching for {}", config.query);
    println!("In file {}", config.file_path);

    if let Err(e) = run(config) {
        println!("Application error: {e}");
        process::exit(1);
    }
}

fn run(config: Config) -> Result<(), Box<dyn Error>> {
    let contents = fs::read_to_string(config.file_path)?;

    println!("With text:\n{contents}");

    Ok(())
}

struct Config {
    query: String,
    file_path: String,
}

impl Config {
    fn build(args: &[String]) -> Result<Config, &'static str> {
        if args.len() < 3 {
            return Err("not enough arguments");
        }

        let query = args[1].clone();
        let file_path = args[2].clone();

        Ok(Config { query, file_path })
    }
}

Мы используем if let, а не unwrap_or_else, чтобы проверить, возвращает ли run значение Err, и вызвать process::exit(1), если это так. Функция run не возвращает значение, которое мы хотели бы unwrap таким же образом, как Config::build возвращает экземпляр Config. Поскольку в случае успеха run возвращает (), нас интересует только обнаружение ошибки, поэтому нам не нужен unwrap_or_else, чтобы вернуть распакованное значение, которым все равно было бы только ().

Тела if let и функций unwrap_or_else в обоих случаях одинаковы: мы печатаем ошибку и завершаем программу.

Разделение кода на библиотечный крейт

Наш проект minigrep пока выглядит хорошо! Теперь мы разделим файл src/main.rs и поместим часть кода в файл src/lib.rs. Так мы сможем тестировать код и получим файл src/main.rs с меньшим количеством обязанностей.

Определим код, отвечающий за поиск текста, в src/lib.rs, а не в src/main.rs. Это позволит нам (или кому угодно еще, кто использует нашу библиотеку minigrep) вызывать функцию поиска из большего числа контекстов, чем только из бинарного файла minigrep.

Сначала определим сигнатуру функции search в src/lib.rs, как показано в листинге 12-13, с телом, которое вызывает макрос unimplemented!. Мы объясним сигнатуру подробнее, когда заполним реализацию.

Filename: src/lib.rs
pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
    unimplemented!();
}
Listing 12-13: Определение функции search в src/lib.rs

Мы использовали ключевое слово pub в определении функции, чтобы обозначить search как часть публичного API нашего библиотечного крейта. Теперь у нас есть библиотечный крейт, который мы можем использовать из бинарного крейта и который можем тестировать!

Теперь нужно ввести код, определенный в src/lib.rs, в область видимости бинарного крейта в src/main.rs и вызвать его, как показано в листинге 12-14.

Filename: src/main.rs
use std::env;
use std::error::Error;
use std::fs;
use std::process;

// --snip--
use minigrep::search;

fn main() {
    // --snip--
    let args: Vec<String> = env::args().collect();

    let config = Config::build(&args).unwrap_or_else(|err| {
        println!("Problem parsing arguments: {err}");
        process::exit(1);
    });

    if let Err(e) = run(config) {
        println!("Application error: {e}");
        process::exit(1);
    }
}

// --snip--


struct Config {
    query: String,
    file_path: String,
}

impl Config {
    fn build(args: &[String]) -> Result<Config, &'static str> {
        if args.len() < 3 {
            return Err("not enough arguments");
        }

        let query = args[1].clone();
        let file_path = args[2].clone();

        Ok(Config { query, file_path })
    }
}

fn run(config: Config) -> Result<(), Box<dyn Error>> {
    let contents = fs::read_to_string(config.file_path)?;

    for line in search(&config.query, &contents) {
        println!("{line}");
    }

    Ok(())
}
Listing 12-14: Использование функции search библиотечного крейта minigrep в src/main.rs

Мы добавляем строку use minigrep::search, чтобы ввести функцию search из библиотечного крейта в область видимости бинарного крейта. Затем в функции run, вместо того чтобы печатать содержимое файла, мы вызываем функцию search и передаем ей значения config.query и contents как аргументы. После этого run использует цикл for, чтобы напечатать каждую строку, возвращенную из search и совпавшую с запросом. Сейчас также подходящее время удалить вызовы println! в функции main, которые показывали запрос и путь к файлу, чтобы наша программа печатала только результаты поиска (если ошибок не произошло).

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

Уф! Это был большой объем работы, но мы подготовили основу для дальнейшего успеха. Теперь обрабатывать ошибки намного проще, а код стал более модульным. Почти вся наша дальнейшая работа будет выполняться в src/lib.rs.

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

Добавление функциональности с помощью разработки через тестирование (TDD)

Добавление функциональности с помощью разработки через тестирование

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

В этом разделе мы добавим логику поиска в программу minigrep, используя процесс разработки через тестирование (test-driven development, TDD), который состоит из следующих шагов:

  1. Напишите тест, который завершается неудачей, и запустите его, чтобы убедиться, что он завершается неудачей именно по ожидаемой причине.
  2. Напишите или измените ровно столько кода, сколько нужно, чтобы новый тест прошел.
  3. Проведите рефакторинг только что добавленного или измененного кода и убедитесь, что тесты продолжают проходить.
  4. Повторите, начиная с шага 1!

Хотя TDD – всего лишь один из многих способов писать программы, он может помогать направлять проектирование кода. Если писать тест до кода, который должен заставить этот тест пройти, это помогает поддерживать высокое покрытие тестами на протяжении всего процесса.

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

Написание падающего теста

В src/lib.rs мы добавим модуль tests с тестовой функцией, как делали в главе 11. Тестовая функция задает поведение, которое мы хотим получить от функции search: она будет принимать запрос и текст для поиска и возвращать только те строки из текста, которые содержат запрос. В листинге 12-15 показан этот тест.

Filename: src/lib.rs
pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
    unimplemented!();
}

// --snip--

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

    #[test]
    fn one_result() {
        let query = "duct";
        let contents = "\
Rust:
safe, fast, productive.
Pick three.";

        assert_eq!(vec!["safe, fast, productive."], search(query, contents));
    }
}
Listing 12-15: Создание падающего теста для функции search, описывающего функциональность, которую мы хотим иметь

Этот тест ищет строку "duct". Текст, в котором мы ищем, состоит из трех строк, и только одна из них содержит "duct" (обратите внимание: обратная косая черта после открывающей двойной кавычки говорит Rust не вставлять символ новой строки в начало содержимого этого строкового литерала). Мы утверждаем, что значение, возвращаемое функцией search, содержит только ту строку, которую мы ожидаем.

Если мы запустим этот тест сейчас, он завершится неудачей, потому что макрос unimplemented! паникует с сообщением “not implemented”. Следуя принципам TDD, мы сделаем небольшой шаг: добавим ровно столько кода, чтобы вызов функции в тесте больше не приводил к панике. Для этого определим функцию search так, чтобы она всегда возвращала пустой вектор, как показано в листинге 12-16. После этого тест должен скомпилироваться и завершиться неудачей, потому что пустой вектор не совпадает с вектором, содержащим строку "safe, fast, productive.".

Filename: src/lib.rs
pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
    vec![]
}

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

    #[test]
    fn one_result() {
        let query = "duct";
        let contents = "\
Rust:
safe, fast, productive.
Pick three.";

        assert_eq!(vec!["safe, fast, productive."], search(query, contents));
    }
}
Listing 12-16: Определение минимальной части функции search, чтобы ее вызов не приводил к панике

Теперь обсудим, почему в сигнатуре search нужно явно определить время жизни 'a и использовать его с аргументом contents и возвращаемым значением. Вспомните из главы 10, что параметры времен жизни указывают, время жизни какого аргумента связано со временем жизни возвращаемого значения. В этом случае мы указываем, что возвращаемый вектор должен содержать строковые срезы, которые ссылаются на срезы аргумента contents (а не аргумента query).

Другими словами, мы говорим Rust, что данные, возвращаемые функцией search, будут жить так же долго, как данные, переданные в функцию search через аргумент contents. Это важно! Данные, на которые ссылается срез, должны быть действительными, чтобы ссылка была действительной; если компилятор решит, что мы создаем строковые срезы из query, а не из contents, он выполнит проверки безопасности неправильно.

Если мы забудем аннотации времен жизни и попробуем скомпилировать эту функцию, то получим такую ошибку:

$ cargo build
   Compiling minigrep v0.1.0 (file:///projects/minigrep)
error[E0106]: missing lifetime specifier
 --> src/lib.rs:1:51
  |
1 | pub fn search(query: &str, contents: &str) -> Vec<&str> {
  |                      ----            ----         ^ expected named lifetime parameter
  |
  = help: this function's return type contains a borrowed value, but the signature does not say whether it is borrowed from `query` or `contents`
help: consider introducing a named lifetime parameter
  |
1 | pub fn search<'a>(query: &'a str, contents: &'a str) -> Vec<&'a str> {
  |              ++++         ++                 ++              ++

For more information about this error, try `rustc --explain E0106`.
error: could not compile `minigrep` (lib) due to 1 previous error

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

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

Написание кода, чтобы тест прошел

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

  1. Пройти по каждой строке содержимого.
  2. Проверить, содержит ли строка нашу строку запроса.
  3. Если содержит, добавить ее в список возвращаемых значений.
  4. Если не содержит, ничего не делать.
  5. Вернуть список совпавших результатов.

Разберем каждый шаг, начав с обхода строк.

Обход строк с помощью метода lines

В Rust есть удобный метод для построчного обхода строк, который так и называется – lines. Он работает так, как показано в листинге 12-17. Обратите внимание, что этот код пока не скомпилируется.

Filename: src/lib.rs
pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
    for line in contents.lines() {
        // do something with line
    }
}

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

    #[test]
    fn one_result() {
        let query = "duct";
        let contents = "\
Rust:
safe, fast, productive.
Pick three.";

        assert_eq!(vec!["safe, fast, productive."], search(query, contents));
    }
}
Listing 12-17: Обход каждой строки в contents

Метод lines возвращает итератор. Мы подробно поговорим об итераторах в главе 13. Но вспомните, что вы уже видели такой способ использования итератора в листинге 3-5, где мы использовали цикл for с итератором, чтобы выполнить код для каждого элемента коллекции.

Поиск запроса в каждой строке

Далее мы проверим, содержит ли текущая строка нашу строку запроса. К счастью, у строк есть удобный метод contains, который делает это за нас! Добавьте вызов метода contains в функцию search, как показано в листинге 12-18. Обратите внимание, что этот код пока все еще не скомпилируется.

Filename: src/lib.rs
pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
    for line in contents.lines() {
        if line.contains(query) {
            // do something with line
        }
    }
}

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

    #[test]
    fn one_result() {
        let query = "duct";
        let contents = "\
Rust:
safe, fast, productive.
Pick three.";

        assert_eq!(vec!["safe, fast, productive."], search(query, contents));
    }
}
Listing 12-18: Добавление функциональности, которая проверяет, содержит ли строка строку из query

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

Сохранение совпавших строк

Чтобы завершить эту функцию, нам нужен способ сохранить совпавшие строки, которые мы хотим вернуть. Для этого можно создать изменяемый вектор перед циклом for и вызвать метод push, чтобы сохранить line в векторе. После цикла for мы возвращаем вектор, как показано в листинге 12-19.

Filename: src/lib.rs
pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
    let mut results = Vec::new();

    for line in contents.lines() {
        if line.contains(query) {
            results.push(line);
        }
    }

    results
}

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

    #[test]
    fn one_result() {
        let query = "duct";
        let contents = "\
Rust:
safe, fast, productive.
Pick three.";

        assert_eq!(vec!["safe, fast, productive."], search(query, contents));
    }
}
Listing 12-19: Сохранение совпавших строк, чтобы мы могли их вернуть

Теперь функция search должна возвращать только строки, содержащие query, и наш тест должен пройти. Запустим тест:

$ cargo test
   Compiling minigrep v0.1.0 (file:///projects/minigrep)
    Finished `test` profile [unoptimized + debuginfo] target(s) in 1.22s
     Running unittests src/lib.rs (target/debug/deps/minigrep-9cd200e5fac0fc94)

running 1 test
test tests::one_result ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

     Running unittests src/main.rs (target/debug/deps/minigrep-9cd200e5fac0fc94)

running 0 tests

test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

   Doc-tests minigrep

running 0 tests

test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

Наш тест прошел, значит, мы знаем, что все работает!

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

Теперь вся программа должна работать! Попробуем ее сначала со словом, которое должно вернуть ровно одну строку из стихотворения Эмили Дикинсон: frog.

$ cargo run -- frog poem.txt
   Compiling minigrep v0.1.0 (file:///projects/minigrep)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.38s
     Running `target/debug/minigrep frog poem.txt`
How public, like a frog

Отлично! Теперь попробуем слово, которое совпадет с несколькими строками, например body:

$ cargo run -- body poem.txt
   Compiling minigrep v0.1.0 (file:///projects/minigrep)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.0s
     Running `target/debug/minigrep body poem.txt`
I'm nobody! Who are you?
Are you nobody, too?
How dreary to be somebody!

И наконец, убедимся, что мы не получаем ни одной строки, когда ищем слово, которого вообще нет в стихотворении, например monomorphization:

$ cargo run -- monomorphization poem.txt
   Compiling minigrep v0.1.0 (file:///projects/minigrep)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.0s
     Running `target/debug/minigrep monomorphization poem.txt`

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

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

Работа с переменными окружения

Работа с переменными окружения

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

Написание падающего теста для регистронезависимого поиска

Сначала мы добавим в библиотеку minigrep новую функцию search_case_insensitive, которая будет вызываться, когда переменная окружения имеет значение. Мы продолжим следовать процессу TDD, поэтому первый шаг снова состоит в том, чтобы написать падающий тест. Мы добавим новый тест для новой функции search_case_insensitive и переименуем старый тест из one_result в case_sensitive, чтобы прояснить различия между двумя тестами, как показано в листинге 12-20.

Filename: src/lib.rs
pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
    let mut results = Vec::new();

    for line in contents.lines() {
        if line.contains(query) {
            results.push(line);
        }
    }

    results
}

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

    #[test]
    fn case_sensitive() {
        let query = "duct";
        let contents = "\
Rust:
safe, fast, productive.
Pick three.
Duct tape.";

        assert_eq!(vec!["safe, fast, productive."], search(query, contents));
    }

    #[test]
    fn case_insensitive() {
        let query = "rUsT";
        let contents = "\
Rust:
safe, fast, productive.
Pick three.
Trust me.";

        assert_eq!(
            vec!["Rust:", "Trust me."],
            search_case_insensitive(query, contents)
        );
    }
}
Listing 12-20: Добавление нового падающего теста для регистронезависимой функции, которую мы собираемся добавить

Обратите внимание, что мы также изменили contents в старом тесте. Мы добавили новую строку с текстом "Duct tape.", используя заглавную D, которая не должна совпадать с запросом "duct", когда мы выполняем поиск с учетом регистра. Такое изменение старого теста помогает убедиться, что мы случайно не сломаем уже реализованную функциональность поиска с учетом регистра. Этот тест должен проходить сейчас и должен продолжать проходить, пока мы работаем над регистронезависимым поиском.

Новый тест для регистро_независимого_ поиска использует "rUsT" как запрос. В функции search_case_insensitive, которую мы собираемся добавить, запрос "rUsT" должен совпасть со строкой, содержащей "Rust:" с заглавной R, и со строкой "Trust me.", хотя регистр в обеих строках отличается от запроса. Это наш падающий тест, и он не скомпилируется, потому что мы еще не определили функцию search_case_insensitive. Можете добавить каркас реализации, который всегда возвращает пустой вектор, подобно тому, как мы сделали для функции search в листинге 12-16, чтобы увидеть, как тест компилируется и падает.

Реализация функции search_case_insensitive

Функция search_case_insensitive, показанная в листинге 12-21, будет почти такой же, как функция search. Единственная разница в том, что мы приведем query и каждую line к нижнему регистру, чтобы, независимо от регистра входных аргументов, они имели один и тот же регистр, когда мы проверяем, содержит ли строка запрос.

Filename: src/lib.rs
pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
    let mut results = Vec::new();

    for line in contents.lines() {
        if line.contains(query) {
            results.push(line);
        }
    }

    results
}

pub fn search_case_insensitive<'a>(
    query: &str,
    contents: &'a str,
) -> Vec<&'a str> {
    let query = query.to_lowercase();
    let mut results = Vec::new();

    for line in contents.lines() {
        if line.to_lowercase().contains(&query) {
            results.push(line);
        }
    }

    results
}

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

    #[test]
    fn case_sensitive() {
        let query = "duct";
        let contents = "\
Rust:
safe, fast, productive.
Pick three.
Duct tape.";

        assert_eq!(vec!["safe, fast, productive."], search(query, contents));
    }

    #[test]
    fn case_insensitive() {
        let query = "rUsT";
        let contents = "\
Rust:
safe, fast, productive.
Pick three.
Trust me.";

        assert_eq!(
            vec!["Rust:", "Trust me."],
            search_case_insensitive(query, contents)
        );
    }
}
Listing 12-21: Определение функции search_case_insensitive, которая приводит запрос и строку к нижнему регистру перед их сравнением

Сначала мы приводим строку query к нижнему регистру и сохраняем ее в новой переменной с тем же именем, затеняя исходную query. Вызов to_lowercase для запроса необходим, чтобы независимо от того, равен ли запрос пользователя "rust", "RUST", "Rust" или "rUsT", мы обрабатывали его так, как если бы это было "rust", и не учитывали регистр. Хотя to_lowercase обработает базовый Unicode, он не будет точен на 100 процентов. Если бы мы писали настоящее приложение, здесь стоило бы сделать немного больше работы, но этот раздел о переменных окружения, а не о Unicode, поэтому здесь мы оставим все как есть.

Обратите внимание, что теперь query является String, а не строковым срезом, потому что вызов to_lowercase создает новые данные, а не ссылается на уже существующие. Возьмем запрос "rUsT" как пример: этот строковый срез не содержит строчные u или t, которые мы могли бы использовать, поэтому нам нужно выделить новую String, содержащую "rust". Когда теперь мы передаем query как аргумент методу contains, нам нужно добавить амперсанд, потому что сигнатура contains определена так, чтобы принимать строковый срез.

Затем мы добавляем вызов to_lowercase для каждой line, чтобы привести все символы к нижнему регистру. Теперь, когда мы преобразовали line и query к нижнему регистру, мы найдем совпадения независимо от регистра запроса.

Посмотрим, проходят ли тесты с этой реализацией:

$ cargo test
   Compiling minigrep v0.1.0 (file:///projects/minigrep)
    Finished `test` profile [unoptimized + debuginfo] target(s) in 1.33s
     Running unittests src/lib.rs (target/debug/deps/minigrep-9cd200e5fac0fc94)

running 2 tests
test tests::case_insensitive ... ok
test tests::case_sensitive ... ok

test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

     Running unittests src/main.rs (target/debug/deps/minigrep-9cd200e5fac0fc94)

running 0 tests

test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

   Doc-tests minigrep

running 0 tests

test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

Отлично! Они прошли. Теперь вызовем новую функцию search_case_insensitive из функции run. Сначала мы добавим параметр конфигурации в структуру Config, чтобы переключаться между поиском с учетом регистра и регистронезависимым поиском. Добавление этого поля вызовет ошибки компилятора, потому что мы пока нигде не инициализируем это поле:

Имя файла: src/main.rs

use std::env;
use std::error::Error;
use std::fs;
use std::process;

use minigrep::{search, search_case_insensitive};

// --snip--


fn main() {
    let args: Vec<String> = env::args().collect();

    let config = Config::build(&args).unwrap_or_else(|err| {
        println!("Problem parsing arguments: {err}");
        process::exit(1);
    });

    if let Err(e) = run(config) {
        println!("Application error: {e}");
        process::exit(1);
    }
}

pub struct Config {
    pub query: String,
    pub file_path: String,
    pub ignore_case: bool,
}

impl Config {
    fn build(args: &[String]) -> Result<Config, &'static str> {
        if args.len() < 3 {
            return Err("not enough arguments");
        }

        let query = args[1].clone();
        let file_path = args[2].clone();

        Ok(Config { query, file_path })
    }
}

fn run(config: Config) -> Result<(), Box<dyn Error>> {
    let contents = fs::read_to_string(config.file_path)?;

    let results = if config.ignore_case {
        search_case_insensitive(&config.query, &contents)
    } else {
        search(&config.query, &contents)
    };

    for line in results {
        println!("{line}");
    }

    Ok(())
}

Мы добавили поле ignore_case, которое хранит булево значение. Далее функция run должна проверять значение поля ignore_case и на его основе решать, вызывать функцию search или функцию search_case_insensitive, как показано в листинге 12-22. Этот код пока все еще не скомпилируется.

Filename: src/main.rs
use std::env;
use std::error::Error;
use std::fs;
use std::process;

use minigrep::{search, search_case_insensitive};

// --snip--


fn main() {
    let args: Vec<String> = env::args().collect();

    let config = Config::build(&args).unwrap_or_else(|err| {
        println!("Problem parsing arguments: {err}");
        process::exit(1);
    });

    if let Err(e) = run(config) {
        println!("Application error: {e}");
        process::exit(1);
    }
}

pub struct Config {
    pub query: String,
    pub file_path: String,
    pub ignore_case: bool,
}

impl Config {
    fn build(args: &[String]) -> Result<Config, &'static str> {
        if args.len() < 3 {
            return Err("not enough arguments");
        }

        let query = args[1].clone();
        let file_path = args[2].clone();

        Ok(Config { query, file_path })
    }
}

fn run(config: Config) -> Result<(), Box<dyn Error>> {
    let contents = fs::read_to_string(config.file_path)?;

    let results = if config.ignore_case {
        search_case_insensitive(&config.query, &contents)
    } else {
        search(&config.query, &contents)
    };

    for line in results {
        println!("{line}");
    }

    Ok(())
}
Listing 12-22: Вызов либо search, либо search_case_insensitive на основе значения в config.ignore_case

Наконец, нам нужно проверить переменную окружения. Функции для работы с переменными окружения находятся в модуле env стандартной библиотеки, который уже находится в области видимости в начале src/main.rs. Мы используем функцию var из модуля env, чтобы проверить, задано ли какое-либо значение для переменной окружения с именем IGNORE_CASE, как показано в листинге 12-23.

Filename: src/main.rs
use std::env;
use std::error::Error;
use std::fs;
use std::process;

use minigrep::{search, search_case_insensitive};

fn main() {
    let args: Vec<String> = env::args().collect();

    let config = Config::build(&args).unwrap_or_else(|err| {
        println!("Problem parsing arguments: {err}");
        process::exit(1);
    });

    if let Err(e) = run(config) {
        println!("Application error: {e}");
        process::exit(1);
    }
}

pub struct Config {
    pub query: String,
    pub file_path: String,
    pub ignore_case: bool,
}

impl Config {
    fn build(args: &[String]) -> Result<Config, &'static str> {
        if args.len() < 3 {
            return Err("not enough arguments");
        }

        let query = args[1].clone();
        let file_path = args[2].clone();

        let ignore_case = env::var("IGNORE_CASE").is_ok();

        Ok(Config {
            query,
            file_path,
            ignore_case,
        })
    }
}

fn run(config: Config) -> Result<(), Box<dyn Error>> {
    let contents = fs::read_to_string(config.file_path)?;

    let results = if config.ignore_case {
        search_case_insensitive(&config.query, &contents)
    } else {
        search(&config.query, &contents)
    };

    for line in results {
        println!("{line}");
    }

    Ok(())
}
Listing 12-23: Проверка любого значения в переменной окружения с именем IGNORE_CASE

Здесь мы создаем новую переменную ignore_case. Чтобы задать ее значение, мы вызываем функцию env::var и передаем ей имя переменной окружения IGNORE_CASE. Функция env::var возвращает Result, который будет успешным вариантом Ok, содержащим значение переменной окружения, если переменная окружения задана каким-либо значением. Она вернет вариант Err, если переменная окружения не задана.

Мы используем метод is_ok для Result, чтобы проверить, задана ли переменная окружения; это значит, что программа должна выполнить регистронезависимый поиск. Если переменная окружения IGNORE_CASE ничем не задана, is_ok вернет false, и программа выполнит поиск с учетом регистра. Нас не интересует значение переменной окружения, важно только, задана она или нет, поэтому мы проверяем is_ok, а не используем unwrap, expect или какие-либо другие методы для Result, которые мы уже видели.

Мы передаем значение из переменной ignore_case в экземпляр Config, чтобы функция run могла прочитать это значение и решить, вызывать search_case_insensitive или search, как мы реализовали в листинге 12-22.

Попробуем! Сначала запустим нашу программу без заданной переменной окружения и с запросом to, который должен совпасть с любой строкой, содержащей слово to полностью в нижнем регистре:

$ cargo run -- to poem.txt
   Compiling minigrep v0.1.0 (file:///projects/minigrep)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.0s
     Running `target/debug/minigrep to poem.txt`
Are you nobody, too?
How dreary to be somebody!

Похоже, это по-прежнему работает! Теперь запустим программу с IGNORE_CASE, заданной равной 1, но с тем же запросом to:

$ IGNORE_CASE=1 cargo run -- to poem.txt

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

PS> $Env:IGNORE_CASE=1; cargo run -- to poem.txt

Это сохранит IGNORE_CASE до конца сеанса вашей оболочки. Ее можно убрать с помощью командлета Remove-Item:

PS> Remove-Item Env:IGNORE_CASE

Мы должны получить строки, которые содержат to и могут иметь заглавные буквы:

Are you nobody, too?
How dreary to be somebody!
To tell your name the livelong day
To an admiring bog!

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

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

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

Перенаправление ошибок в стандартный поток ошибок (stderr)

Перенаправление ошибок в стандартный поток ошибок

Сейчас мы записываем весь наш вывод в терминал с помощью макроса println!. В большинстве терминалов есть два вида вывода: стандартный вывод (stdout) для общей информации и стандартный поток ошибок (stderr) для сообщений об ошибках. Это различие позволяет пользователям направлять успешный вывод программы в файл, но при этом продолжать печатать сообщения об ошибках на экран.

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

Проверка того, куда записываются ошибки

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

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

Чтобы продемонстрировать это поведение, мы запустим программу с > и путем к файлу output.txt, в который хотим перенаправить поток стандартного вывода. Мы не будем передавать аргументы, что должно вызвать ошибку:

$ cargo run > output.txt

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

Problem parsing arguments: not enough arguments

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

Печать ошибок в стандартный поток ошибок

Мы используем код из листинга 12-24, чтобы изменить способ печати сообщений об ошибках. Благодаря рефакторингу, который мы выполнили ранее в этой главе, весь код, печатающий сообщения об ошибках, находится в одной функции, main. Стандартная библиотека предоставляет макрос eprintln!, который печатает в стандартный поток ошибок, поэтому изменим два места, где мы вызывали println! для печати ошибок, чтобы вместо него использовать eprintln!.

Filename: src/main.rs
use std::env;
use std::error::Error;
use std::fs;
use std::process;

use minigrep::{search, search_case_insensitive};

fn main() {
    let args: Vec<String> = env::args().collect();

    let config = Config::build(&args).unwrap_or_else(|err| {
        eprintln!("Problem parsing arguments: {err}");
        process::exit(1);
    });

    if let Err(e) = run(config) {
        eprintln!("Application error: {e}");
        process::exit(1);
    }
}

pub struct Config {
    pub query: String,
    pub file_path: String,
    pub ignore_case: bool,
}

impl Config {
    fn build(args: &[String]) -> Result<Config, &'static str> {
        if args.len() < 3 {
            return Err("not enough arguments");
        }

        let query = args[1].clone();
        let file_path = args[2].clone();

        let ignore_case = env::var("IGNORE_CASE").is_ok();

        Ok(Config {
            query,
            file_path,
            ignore_case,
        })
    }
}

fn run(config: Config) -> Result<(), Box<dyn Error>> {
    let contents = fs::read_to_string(config.file_path)?;

    let results = if config.ignore_case {
        search_case_insensitive(&config.query, &contents)
    } else {
        search(&config.query, &contents)
    };

    for line in results {
        println!("{line}");
    }

    Ok(())
}
Listing 12-24: Запись сообщений об ошибках в стандартный поток ошибок вместо стандартного вывода с помощью eprintln!

Теперь снова запустим программу тем же способом: без аргументов и с перенаправлением стандартного вывода с помощью >:

$ cargo run > output.txt
Problem parsing arguments: not enough arguments

Теперь мы видим ошибку на экране, а output.txt ничего не содержит; именно такого поведения мы ожидаем от программ командной строки.

Снова запустим программу с аргументами, которые не вызывают ошибку, но все еще перенаправим стандартный вывод в файл:

$ cargo run -- to poem.txt > output.txt

Мы не увидим вывода в терминале, а output.txt будет содержать наши результаты:

Имя файла: output.txt

Are you nobody, too?
How dreary to be somebody!

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

Итоги

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

Далее мы изучим некоторые возможности Rust, на которые повлияли функциональные языки: замыкания и итераторы.

Возможности функциональных языков: итераторы и замыкания

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

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

Если точнее, мы рассмотрим:

  • Замыкания, конструкцию, похожую на функцию, которую можно сохранить в переменной
  • Итераторы, способ обработки последовательности элементов
  • Как использовать замыкания и итераторы для улучшения проекта ввода-вывода из главы 12
  • Производительность замыканий и итераторов (спойлер: они быстрее, чем вы можете подумать!)

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

Замыкания

Замыкания

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

Захват окружения

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

Есть много способов реализовать это. Для этого примера мы будем использовать enum с именем ShirtColor, у которого есть варианты Red и Blue (для простоты мы ограничим количество доступных цветов). Запасы компании мы представим структурой Inventory, у которой есть поле shirts, содержащее Vec<ShirtColor> с цветами футболок, которые сейчас есть в наличии. Метод giveaway, определенный для Inventory, получает необязательное предпочтение цвета футболки победителя и возвращает цвет футболки, которую он получит. Эта настройка показана в листинге 13-1.

Filename: src/main.rs
#[derive(Debug, PartialEq, Copy, Clone)]
enum ShirtColor {
    Red,
    Blue,
}

struct Inventory {
    shirts: Vec<ShirtColor>,
}

impl Inventory {
    fn giveaway(&self, user_preference: Option<ShirtColor>) -> ShirtColor {
        user_preference.unwrap_or_else(|| self.most_stocked())
    }

    fn most_stocked(&self) -> ShirtColor {
        let mut num_red = 0;
        let mut num_blue = 0;

        for color in &self.shirts {
            match color {
                ShirtColor::Red => num_red += 1,
                ShirtColor::Blue => num_blue += 1,
            }
        }
        if num_red > num_blue {
            ShirtColor::Red
        } else {
            ShirtColor::Blue
        }
    }
}

fn main() {
    let store = Inventory {
        shirts: vec![ShirtColor::Blue, ShirtColor::Red, ShirtColor::Blue],
    };

    let user_pref1 = Some(ShirtColor::Red);
    let giveaway1 = store.giveaway(user_pref1);
    println!(
        "The user with preference {:?} gets {:?}",
        user_pref1, giveaway1
    );

    let user_pref2 = None;
    let giveaway2 = store.giveaway(user_pref2);
    println!(
        "The user with preference {:?} gets {:?}",
        user_pref2, giveaway2
    );
}
Listing 13-1: Ситуация с раздачей футболок компанией

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

Повторим: этот код можно было бы реализовать многими способами, и здесь, чтобы сфокусироваться на замыканиях, мы придерживаемся уже изученных вами концепций, за исключением тела метода giveaway, где используется замыкание. В методе giveaway мы получаем пользовательское предпочтение как параметр типа Option<ShirtColor> и вызываем метод unwrap_or_else для user_preference. Метод unwrap_or_else для Option<T> определен стандартной библиотекой. Он принимает один аргумент: замыкание без аргументов, которое возвращает значение T (того же типа, который хранится в варианте Some у Option<T>, в этом случае ShirtColor). Если Option<T> – это вариант Some, unwrap_or_else возвращает значение из Some. Если Option<T> – это вариант None, unwrap_or_else вызывает замыкание и возвращает значение, возвращенное замыканием.

Мы указываем выражение замыкания || self.most_stocked() как аргумент для unwrap_or_else. Это замыкание само не принимает параметров (если бы у замыкания были параметры, они находились бы между двумя вертикальными чертами). Тело замыкания вызывает self.most_stocked(). Мы определяем замыкание здесь, а реализация unwrap_or_else выполнит это замыкание позже, если результат понадобится.

Запуск этого кода печатает следующее:

$ cargo run
   Compiling shirt-company v0.1.0 (file:///projects/shirt-company)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.27s
     Running `target/debug/shirt-company`
The user with preference Some(Red) gets Red
The user with preference None gets Blue

Один интересный момент здесь в том, что мы передали замыкание, которое вызывает self.most_stocked() для текущего экземпляра Inventory. Стандартной библиотеке не нужно было ничего знать ни о типах Inventory или ShirtColor, которые мы определили, ни о логике, которую мы хотим использовать в этом сценарии. Замыкание захватывает неизменяемую ссылку на экземпляр Inventory в self и передает ее вместе с указанным нами кодом в метод unwrap_or_else. Функции, с другой стороны, не способны захватывать свое окружение таким образом.

Вывод и аннотирование типов замыканий

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

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

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

Filename: src/main.rs
use std::thread;
use std::time::Duration;

fn generate_workout(intensity: u32, random_number: u32) {
    let expensive_closure = |num: u32| -> u32 {
        println!("calculating slowly...");
        thread::sleep(Duration::from_secs(2));
        num
    };

    if intensity < 25 {
        println!("Today, do {} pushups!", expensive_closure(intensity));
        println!("Next, do {} situps!", expensive_closure(intensity));
    } else {
        if random_number == 3 {
            println!("Take a break today! Remember to stay hydrated!");
        } else {
            println!(
                "Today, run for {} minutes!",
                expensive_closure(intensity)
            );
        }
    }
}

fn main() {
    let simulated_user_specified_value = 10;
    let simulated_random_number = 7;

    generate_workout(simulated_user_specified_value, simulated_random_number);
}
Listing 13-2: Добавление необязательных аннотаций типов параметра и возвращаемого значения в замыкании

С добавленными аннотациями типов синтаксис замыканий больше похож на синтаксис функций. Здесь для сравнения мы определяем функцию, которая добавляет 1 к своему параметру, и замыкание с тем же поведением. Мы добавили пробелы, чтобы выровнять соответствующие части. Это показывает, как синтаксис замыканий похож на синтаксис функций, за исключением использования вертикальных черт и объема синтаксиса, который является необязательным:

fn  add_one_v1   (x: u32) -> u32 { x + 1 }
let add_one_v2 = |x: u32| -> u32 { x + 1 };
let add_one_v3 = |x|             { x + 1 };
let add_one_v4 = |x|               x + 1  ;

Первая строка показывает определение функции, а вторая – полностью аннотированное определение замыкания. В третьей строке мы убираем аннотации типов из определения замыкания. В четвертой строке мы убираем фигурные скобки: они необязательны, потому что тело замыкания содержит только одно выражение. Все эти определения допустимы и при вызове дадут одно и то же поведение. Строки add_one_v3 и add_one_v4 требуют, чтобы замыкания были использованы для компиляции, потому что типы будут выведены из их использования. Это похоже на то, как let v = Vec::new(); требует либо аннотации типа, либо вставки значений какого-то типа в Vec, чтобы Rust смог вывести тип.

Для определений замыканий компилятор выведет один конкретный тип для каждого параметра и для возвращаемого значения. Например, в листинге 13-3 показано определение короткого замыкания, которое просто возвращает значение, полученное как параметр. Это замыкание не очень полезно, кроме целей данного примера. Обратите внимание, что мы не добавили никаких аннотаций типов в определение. Поскольку аннотаций типов нет, мы можем вызвать замыкание с любым типом; здесь мы сделали это с String при первом вызове. Если затем мы попробуем вызвать example_closure с целым числом, получим ошибку.

Filename: src/main.rs
fn main() {
    let example_closure = |x| x;

    let s = example_closure(String::from("hello"));
    let n = example_closure(5);
}
Listing 13-3: Попытка вызвать замыкание, типы которого выведены, с двумя разными типами

Компилятор выдает такую ошибку:

$ cargo run
   Compiling closure-example v0.1.0 (file:///projects/closure-example)
error[E0308]: mismatched types
 --> src/main.rs:5:29
  |
5 |     let n = example_closure(5);
  |             --------------- ^ expected `String`, found integer
  |             |
  |             arguments to this function are incorrect
  |
note: expected because the closure was earlier called with an argument of type `String`
 --> src/main.rs:4:29
  |
4 |     let s = example_closure(String::from("hello"));
  |             --------------- ^^^^^^^^^^^^^^^^^^^^^ expected because this argument is of type `String`
  |             |
  |             in this closure call
note: closure parameter defined here
 --> src/main.rs:2:28
  |
2 |     let example_closure = |x| x;
  |                            ^
help: try using a conversion method
  |
5 |     let n = example_closure(5.to_string());
  |                              ++++++++++++

For more information about this error, try `rustc --explain E0308`.
error: could not compile `closure-example` (bin "closure-example") due to 1 previous error

Когда мы впервые вызываем example_closure со значением String, компилятор выводит, что тип x и возвращаемый тип замыкания – String. Затем эти типы закрепляются за замыканием в example_closure, и мы получаем ошибку типа, когда далее пытаемся использовать другой тип с тем же замыканием.

Захват ссылок или перемещение владения

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

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

Filename: src/main.rs
fn main() {
    let list = vec![1, 2, 3];
    println!("Before defining closure: {list:?}");

    let only_borrows = || println!("From closure: {list:?}");

    println!("Before calling closure: {list:?}");
    only_borrows();
    println!("After calling closure: {list:?}");
}
Listing 13-4: Определение и вызов замыкания, которое захватывает неизменяемую ссылку

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

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

$ cargo run
   Compiling closure-example v0.1.0 (file:///projects/closure-example)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.43s
     Running `target/debug/closure-example`
Before defining closure: [1, 2, 3]
Before calling closure: [1, 2, 3]
From closure: [1, 2, 3]
After calling closure: [1, 2, 3]

Далее, в листинге 13-5, мы изменяем тело замыкания так, чтобы оно добавляло элемент в вектор list. Теперь замыкание захватывает изменяемую ссылку.

Filename: src/main.rs
fn main() {
    let mut list = vec![1, 2, 3];
    println!("Before defining closure: {list:?}");

    let mut borrows_mutably = || list.push(7);

    borrows_mutably();
    println!("After calling closure: {list:?}");
}
Listing 13-5: Определение и вызов замыкания, которое захватывает изменяемую ссылку

Этот код компилируется, запускается и печатает:

$ cargo run
   Compiling closure-example v0.1.0 (file:///projects/closure-example)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.43s
     Running `target/debug/closure-example`
Before defining closure: [1, 2, 3]
After calling closure: [1, 2, 3, 7]

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

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

Этот прием в основном полезен при передаче замыкания новому потоку, чтобы переместить данные и сделать их принадлежащими новому потоку. Мы подробно обсудим потоки и причины их использования в главе 16, когда будем говорить о конкурентности, но пока кратко рассмотрим создание нового потока с помощью замыкания, которому нужно ключевое слово move. В листинге 13-6 показан листинг 13-4, измененный так, чтобы печатать вектор в новом потоке, а не в главном потоке.

Filename: src/main.rs
use std::thread;

fn main() {
    let list = vec![1, 2, 3];
    println!("Before defining closure: {list:?}");

    thread::spawn(move || println!("From thread: {list:?}"))
        .join()
        .unwrap();
}
Listing 13-6: Использование move, чтобы заставить замыкание для потока забрать владение list

Мы создаем новый поток, передавая потоку замыкание для выполнения как аргумент. Тело замыкания печатает список. В листинге 13-4 замыкание захватывало list только через неизменяемую ссылку, потому что это минимальный объем доступа к list, необходимый для печати. В этом примере, хотя телу замыкания по-прежнему нужна только неизменяемая ссылка, нам нужно указать, что list должен быть перемещен в замыкание, поставив ключевое слово move в начало определения замыкания. Если бы главный поток выполнил больше операций перед вызовом join для нового потока, новый поток мог бы завершиться до завершения остальной части главного потока или главный поток мог бы завершиться первым. Если бы главный поток сохранил владение list, но завершился раньше нового потока и удалил list, неизменяемая ссылка в потоке стала бы недействительной. Поэтому компилятор требует переместить list в замыкание, переданное новому потоку, чтобы ссылка была действительной. Попробуйте убрать ключевое слово move или использовать list в главном потоке после определения замыкания, чтобы увидеть, какие ошибки компилятора вы получите!

Перемещение захваченных значений из замыканий

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

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

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

  • FnOnce применяется к замыканиям, которые можно вызвать один раз. Все замыкания реализуют как минимум этот трейт, потому что все замыкания можно вызвать. Замыкание, которое перемещает захваченные значения из своего тела, реализует только FnOnce и ни один из других трейтов Fn, потому что его можно вызвать только один раз.
  • FnMut применяется к замыканиям, которые не перемещают захваченные значения из своего тела, но могут изменять захваченные значения. Такие замыкания можно вызвать больше одного раза.
  • Fn применяется к замыканиям, которые не перемещают захваченные значения из своего тела и не изменяют захваченные значения, а также к замыканиям, которые ничего не захватывают из окружения. Такие замыкания можно вызвать больше одного раза без изменения их окружения, что важно в случаях вроде многократного одновременного вызова замыкания.

Посмотрим на определение метода unwrap_or_else для Option<T>, который мы использовали в листинге 13-1:

impl<T> Option<T> {
    pub fn unwrap_or_else<F>(self, f: F) -> T
    where
        F: FnOnce() -> T
    {
        match self {
            Some(x) => x,
            None => f(),
        }
    }
}

Вспомните, что T – это обобщенный тип, представляющий тип значения в варианте Some у Option. Этот тип T также является возвращаемым типом функции unwrap_or_else: код, который вызывает unwrap_or_else для Option<String>, например, получит String.

Далее обратите внимание, что у функции unwrap_or_else есть дополнительный обобщенный параметр типа F. Тип F – это тип параметра с именем f, то есть замыкания, которое мы предоставляем при вызове unwrap_or_else.

Ограничение трейта, указанное для обобщенного типа F, – это FnOnce() -> T, что означает: значение типа F должно быть возможно вызвать один раз, оно не принимает аргументов и возвращает T. Использование FnOnce в ограничении трейта выражает ограничение, что unwrap_or_else не будет вызывать f больше одного раза. В теле unwrap_or_else видно, что если Option содержит Some, f не будет вызвано. Если Option содержит None, f будет вызвано один раз. Поскольку все замыкания реализуют FnOnce, unwrap_or_else принимает все три вида замыканий и настолько гибок, насколько это возможно.

Примечание: если то, что мы хотим сделать, не требует захвата значения из окружения, мы можем использовать имя функции вместо замыкания там, где нам нужно что-то, реализующее один из трейтов Fn. Например, для значения Option<Vec<T>> можно вызвать unwrap_or_else(Vec::new), чтобы получить новый пустой вектор, если значение равно None. Компилятор автоматически реализует для определения функции тот из трейтов Fn, который применим.

Теперь посмотрим на метод стандартной библиотеки sort_by_key, определенный для срезов, чтобы увидеть, чем он отличается от unwrap_or_else и почему sort_by_key использует FnMut, а не FnOnce, в ограничении трейта. Замыкание получает один аргумент в виде ссылки на текущий рассматриваемый элемент среза и возвращает значение типа K, которое можно упорядочить. Эта функция полезна, когда вы хотите отсортировать срез по определенному атрибуту каждого элемента. В листинге 13-7 у нас есть список экземпляров Rectangle, и мы используем sort_by_key, чтобы упорядочить их по атрибуту width от меньшего к большему.

Filename: src/main.rs
#[derive(Debug)]
struct Rectangle {
    width: u32,
    height: u32,
}

fn main() {
    let mut list = [
        Rectangle { width: 10, height: 1 },
        Rectangle { width: 3, height: 5 },
        Rectangle { width: 7, height: 12 },
    ];

    list.sort_by_key(|r| r.width);
    println!("{list:#?}");
}
Listing 13-7: Использование sort_by_key для упорядочивания прямоугольников по ширине

Этот код печатает:

$ cargo run
   Compiling rectangles v0.1.0 (file:///projects/rectangles)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.41s
     Running `target/debug/rectangles`
[
    Rectangle {
        width: 3,
        height: 5,
    },
    Rectangle {
        width: 7,
        height: 12,
    },
    Rectangle {
        width: 10,
        height: 1,
    },
]

Причина, по которой sort_by_key определен так, чтобы принимать замыкание FnMut, состоит в том, что он вызывает замыкание несколько раз: по одному разу для каждого элемента среза. Замыкание |r| r.width ничего не захватывает, не изменяет и не перемещает из своего окружения, поэтому оно удовлетворяет требованиям ограничения трейта.

Напротив, в листинге 13-8 показан пример замыкания, которое реализует только трейт FnOnce, потому что оно перемещает значение из окружения. Компилятор не позволит использовать это замыкание с sort_by_key.

Filename: src/main.rs
#[derive(Debug)]
struct Rectangle {
    width: u32,
    height: u32,
}

fn main() {
    let mut list = [
        Rectangle { width: 10, height: 1 },
        Rectangle { width: 3, height: 5 },
        Rectangle { width: 7, height: 12 },
    ];

    let mut sort_operations = vec![];
    let value = String::from("closure called");

    list.sort_by_key(|r| {
        sort_operations.push(value);
        r.width
    });
    println!("{list:#?}");
}
Listing 13-8: Попытка использовать замыкание FnOnce с sort_by_key

Это искусственный, запутанный способ (который не работает) попытаться посчитать, сколько раз sort_by_key вызывает замыкание при сортировке list. Этот код пытается выполнить подсчет, добавляя valueString из окружения замыкания – в вектор sort_operations. Замыкание захватывает value, а затем перемещает value из замыкания, передавая владение value вектору sort_operations. Это замыкание можно вызвать один раз; попытка вызвать его во второй раз не сработала бы, потому что value больше не было бы в окружении, чтобы снова добавить его в sort_operations! Поэтому это замыкание реализует только FnOnce. Когда мы пытаемся скомпилировать этот код, получаем ошибку, что value нельзя переместить из замыкания, потому что замыкание должно реализовывать FnMut:

$ cargo run
   Compiling rectangles v0.1.0 (file:///projects/rectangles)
error[E0507]: cannot move out of `value`, a captured variable in an `FnMut` closure
  --> src/main.rs:18:30
   |
15 |     let value = String::from("closure called");
   |         -----   ------------------------------ move occurs because `value` has type `String`, which does not implement the `Copy` trait
   |         |
   |         captured outer variable
16 |
17 |     list.sort_by_key(|r| {
   |                      --- captured by this `FnMut` closure
18 |         sort_operations.push(value);
   |                              ^^^^^ `value` is moved here
   |
help: consider cloning the value if the performance cost is acceptable
   |
18 |         sort_operations.push(value.clone());
   |                                   ++++++++

For more information about this error, try `rustc --explain E0507`.
error: could not compile `rectangles` (bin "rectangles") due to 1 previous error

Ошибка указывает на строку в теле замыкания, которая перемещает value из окружения. Чтобы исправить это, нужно изменить тело замыкания так, чтобы оно не перемещало значения из окружения. Хранение счетчика в окружении и увеличение его значения в теле замыкания – более прямой способ посчитать, сколько раз вызывается замыкание. Замыкание в листинге 13-9 работает с sort_by_key, потому что оно захватывает только изменяемую ссылку на счетчик num_sort_operations и поэтому может быть вызвано больше одного раза.

Filename: src/main.rs
#[derive(Debug)]
struct Rectangle {
    width: u32,
    height: u32,
}

fn main() {
    let mut list = [
        Rectangle { width: 10, height: 1 },
        Rectangle { width: 3, height: 5 },
        Rectangle { width: 7, height: 12 },
    ];

    let mut num_sort_operations = 0;
    list.sort_by_key(|r| {
        num_sort_operations += 1;
        r.width
    });
    println!("{list:#?}, sorted in {num_sort_operations} operations");
}
Listing 13-9: Использование замыкания FnMut с sort_by_key разрешено.

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

Обработка последовательности элементов с помощью итераторов

Обработка последовательности элементов с помощью итераторов

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

В Rust итераторы ленивые: они не имеют эффекта, пока вы не вызовете методы, которые потребляют итератор, чтобы его использовать. Например, код в листинге 13-10 создает итератор по элементам вектора v1, вызывая метод iter, определенный для Vec<T>. Сам по себе этот код не делает ничего полезного.

Filename: src/main.rs
fn main() {
    let v1 = vec![1, 2, 3];

    let v1_iter = v1.iter();
}
Listing 13-10: Создание итератора

Итератор сохраняется в переменной v1_iter. После создания итератора мы можем использовать его разными способами. В листинге 3-5 мы перебирали массив с помощью цикла for, чтобы выполнить некоторый код для каждого его элемента. Под капотом это неявно создавало, а затем потребляло итератор, но до этого момента мы не разбирали подробно, как именно это работает.

В примере из листинга 13-11 мы отделяем создание итератора от использования итератора в цикле for. Когда цикл for вызывается с использованием итератора в v1_iter, каждый элемент итератора используется в одной итерации цикла, которая печатает каждое значение.

Filename: src/main.rs
fn main() {
    let v1 = vec![1, 2, 3];

    let v1_iter = v1.iter();

    for val in v1_iter {
        println!("Got: {val}");
    }
}
Listing 13-11: Использование итератора в цикле for

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

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

Трейт Iterator и метод next

Все итераторы реализуют трейт с именем Iterator, определенный в стандартной библиотеке. Определение трейта выглядит так:

#![allow(unused)]
fn main() {
pub trait Iterator {
    type Item;

    fn next(&mut self) -> Option<Self::Item>;

    // methods with default implementations elided
}
}

Обратите внимание, что это определение использует новый синтаксис: type Item и Self::Item, которые определяют связанный тип для этого трейта. Мы подробно поговорим о связанных типах в главе 20. Пока вам нужно знать только то, что этот код говорит: реализация трейта Iterator требует также определить тип Item, и этот тип Item используется в возвращаемом типе метода next. Другими словами, тип Item будет типом, возвращаемым из итератора.

Трейт Iterator требует от реализующих его типов определить только один метод: метод next, который возвращает по одному элементу итератора за раз, обернутому в Some, а когда итерация завершена, возвращает None.

Мы можем вызывать метод next у итераторов напрямую; листинг 13-12 показывает, какие значения возвращаются при повторных вызовах next у итератора, созданного из вектора.

Filename: src/lib.rs
#[cfg(test)]
mod tests {
    #[test]
    fn iterator_demonstration() {
        let v1 = vec![1, 2, 3];

        let mut v1_iter = v1.iter();

        assert_eq!(v1_iter.next(), Some(&1));
        assert_eq!(v1_iter.next(), Some(&2));
        assert_eq!(v1_iter.next(), Some(&3));
        assert_eq!(v1_iter.next(), None);
    }
}
Listing 13-12: Вызов метода next у итератора

Обратите внимание, что нам пришлось сделать v1_iter изменяемым: вызов метода next у итератора изменяет внутреннее состояние, которое итератор использует, чтобы отслеживать свое положение в последовательности. Другими словами, этот код потребляет, или расходует, итератор. Каждый вызов next забирает один элемент из итератора. Когда мы использовали цикл for, нам не нужно было делать v1_iter изменяемым, потому что цикл забрал владение v1_iter и сделал его изменяемым за кулисами.

Также обратите внимание, что значения, которые мы получаем из вызовов next, являются неизменяемыми ссылками на значения в векторе. Метод iter создает итератор по неизменяемым ссылкам. Если мы хотим создать итератор, который забирает владение v1 и возвращает собственные значения, можно вызвать into_iter вместо iter. Аналогично, если мы хотим итерироваться по изменяемым ссылкам, можно вызвать iter_mut вместо iter.

Методы, которые потребляют итератор

У трейта Iterator есть множество разных методов с реализациями по умолчанию, предоставленными стандартной библиотекой; вы можете узнать об этих методах в документации API стандартной библиотеки для трейта Iterator. Некоторые из этих методов вызывают метод next в своем определении, поэтому при реализации трейта Iterator требуется реализовать метод next.

Методы, которые вызывают next, называются потребляющими адаптерами, потому что их вызов расходует итератор. Один пример – метод sum, который забирает владение итератором и проходит по элементам, многократно вызывая next, тем самым потребляя итератор. По мере перебора он добавляет каждый элемент к накапливаемой сумме и возвращает сумму, когда итерация завершена. В листинге 13-13 есть тест, иллюстрирующий использование метода sum.

Filename: src/lib.rs
#[cfg(test)]
mod tests {
    #[test]
    fn iterator_sum() {
        let v1 = vec![1, 2, 3];

        let v1_iter = v1.iter();

        let total: i32 = v1_iter.sum();

        assert_eq!(total, 6);
    }
}
Listing 13-13: Вызов метода sum для получения суммы всех элементов итератора

Мы не можем использовать v1_iter после вызова sum, потому что sum забирает владение итератором, для которого он вызывается.

Методы, которые создают другие итераторы

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

В листинге 13-14 показан пример вызова метода-адаптера итератора map, который принимает замыкание, вызываемое для каждого элемента по мере перебора. Метод map возвращает новый итератор, который создает измененные элементы. Замыкание здесь создает новый итератор, в котором каждый элемент из вектора будет увеличен на 1.

Filename: src/main.rs
fn main() {
    let v1: Vec<i32> = vec![1, 2, 3];

    v1.iter().map(|x| x + 1);
}
Listing 13-14: Вызов адаптера итератора map для создания нового итератора

Однако этот код выводит предупреждение:

$ cargo run
   Compiling iterators v0.1.0 (file:///projects/iterators)
warning: unused `Map` that must be used
 --> src/main.rs:4:5
  |
4 |     v1.iter().map(|x| x + 1);
  |     ^^^^^^^^^^^^^^^^^^^^^^^^
  |
  = note: iterators are lazy and do nothing unless consumed
  = note: `#[warn(unused_must_use)]` on by default
help: use `let _ = ...` to ignore the resulting value
  |
4 |     let _ = v1.iter().map(|x| x + 1);
  |     +++++++

warning: `iterators` (bin "iterators") generated 1 warning
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.47s
     Running `target/debug/iterators`

Код в листинге 13-14 ничего не делает; указанное нами замыкание ни разу не вызывается. Предупреждение напоминает почему: адаптеры итераторов ленивые, и здесь нам нужно потребить итератор.

Чтобы исправить это предупреждение и потребить итератор, мы используем метод collect, который использовали с env::args в листинге 12-1. Этот метод потребляет итератор и собирает получившиеся значения в тип данных коллекции.

В листинге 13-15 мы собираем результаты перебора итератора, возвращенного вызовом map, в вектор. В итоге этот вектор будет содержать каждый элемент из исходного вектора, увеличенный на 1.

Filename: src/main.rs
fn main() {
    let v1: Vec<i32> = vec![1, 2, 3];

    let v2: Vec<_> = v1.iter().map(|x| x + 1).collect();

    assert_eq!(v2, vec![2, 3, 4]);
}
Listing 13-15: Вызов метода map для создания нового итератора, а затем вызов метода collect, чтобы потребить новый итератор и создать вектор

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

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

Замыкания, которые захватывают свое окружение

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

Для этого примера мы используем метод filter, который принимает замыкание. Замыкание получает элемент из итератора и возвращает bool. Если замыкание возвращает true, значение будет включено в итерацию, создаваемую filter. Если замыкание возвращает false, значение не будет включено.

В листинге 13-16 мы используем filter с замыканием, которое захватывает переменную shoe_size из своего окружения, чтобы итерироваться по коллекции экземпляров структуры Shoe. Оно вернет только обувь указанного размера.

Filename: src/lib.rs
#[derive(PartialEq, Debug)]
struct Shoe {
    size: u32,
    style: String,
}

fn shoes_in_size(shoes: Vec<Shoe>, shoe_size: u32) -> Vec<Shoe> {
    shoes.into_iter().filter(|s| s.size == shoe_size).collect()
}

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

    #[test]
    fn filters_by_size() {
        let shoes = vec![
            Shoe {
                size: 10,
                style: String::from("sneaker"),
            },
            Shoe {
                size: 13,
                style: String::from("sandal"),
            },
            Shoe {
                size: 10,
                style: String::from("boot"),
            },
        ];

        let in_my_size = shoes_in_size(shoes, 10);

        assert_eq!(
            in_my_size,
            vec![
                Shoe {
                    size: 10,
                    style: String::from("sneaker")
                },
                Shoe {
                    size: 10,
                    style: String::from("boot")
                },
            ]
        );
    }
}
Listing 13-16: Использование метода filter с замыканием, которое захватывает shoe_size

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

В теле shoes_in_size мы вызываем into_iter, чтобы создать итератор, забирающий владение вектором. Затем мы вызываем filter, чтобы адаптировать этот итератор в новый итератор, содержащий только элементы, для которых замыкание возвращает true.

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

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

Улучшение нашего проекта ввода-вывода

Улучшение нашего проекта ввода-вывода

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

Удаление clone с помощью итератора

В листинге 12-6 мы добавили код, который брал срез значений String и создавал экземпляр структуры Config, индексируясь в срез и клонируя значения, что позволяло структуре Config владеть этими значениями. В листинге 13-17 мы воспроизвели реализацию функции Config::build такой, какой она была в листинге 12-23.

Filename: src/main.rs
use std::env;
use std::error::Error;
use std::fs;
use std::process;

use minigrep::{search, search_case_insensitive};

fn main() {
    let args: Vec<String> = env::args().collect();

    let config = Config::build(&args).unwrap_or_else(|err| {
        println!("Problem parsing arguments: {err}");
        process::exit(1);
    });

    if let Err(e) = run(config) {
        println!("Application error: {e}");
        process::exit(1);
    }
}

pub struct Config {
    pub query: String,
    pub file_path: String,
    pub ignore_case: bool,
}

impl Config {
    fn build(args: &[String]) -> Result<Config, &'static str> {
        if args.len() < 3 {
            return Err("not enough arguments");
        }

        let query = args[1].clone();
        let file_path = args[2].clone();

        let ignore_case = env::var("IGNORE_CASE").is_ok();

        Ok(Config {
            query,
            file_path,
            ignore_case,
        })
    }
}

fn run(config: Config) -> Result<(), Box<dyn Error>> {
    let contents = fs::read_to_string(config.file_path)?;

    let results = if config.ignore_case {
        search_case_insensitive(&config.query, &contents)
    } else {
        search(&config.query, &contents)
    };

    for line in results {
        println!("{line}");
    }

    Ok(())
}
Listing 13-17: Воспроизведение функции Config::build из листинга 12-23

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

Здесь нам был нужен clone, потому что в параметре args у нас есть срез с элементами String, но функция build не владеет args. Чтобы вернуть владение экземпляром Config, нам пришлось клонировать значения из полей query и file_path структуры Config, чтобы экземпляр Config мог владеть своими значениями.

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

После того как Config::build будет принимать владение итератором и перестанет использовать операции индексирования, которые заимствуют, мы сможем перемещать значения String из итератора в Config, вместо того чтобы вызывать clone и создавать новое выделение памяти.

Прямое использование возвращенного итератора

Откройте файл src/main.rs вашего проекта ввода-вывода, который должен выглядеть так:

Имя файла: src/main.rs

use std::env;
use std::error::Error;
use std::fs;
use std::process;

use minigrep::{search, search_case_insensitive};

fn main() {
    let args: Vec<String> = env::args().collect();

    let config = Config::build(&args).unwrap_or_else(|err| {
        eprintln!("Problem parsing arguments: {err}");
        process::exit(1);
    });

    // --snip--

    if let Err(e) = run(config) {
        eprintln!("Application error: {e}");
        process::exit(1);
    }
}

pub struct Config {
    pub query: String,
    pub file_path: String,
    pub ignore_case: bool,
}

impl Config {
    fn build(args: &[String]) -> Result<Config, &'static str> {
        if args.len() < 3 {
            return Err("not enough arguments");
        }

        let query = args[1].clone();
        let file_path = args[2].clone();

        let ignore_case = env::var("IGNORE_CASE").is_ok();

        Ok(Config {
            query,
            file_path,
            ignore_case,
        })
    }
}

fn run(config: Config) -> Result<(), Box<dyn Error>> {
    let contents = fs::read_to_string(config.file_path)?;

    let results = if config.ignore_case {
        search_case_insensitive(&config.query, &contents)
    } else {
        search(&config.query, &contents)
    };

    for line in results {
        println!("{line}");
    }

    Ok(())
}

Сначала мы изменим начало функции main, которое у нас было в листинге 12-24, на код из листинга 13-18, который на этот раз использует итератор. Этот код не скомпилируется, пока мы также не обновим Config::build.

Filename: src/main.rs
use std::env;
use std::error::Error;
use std::fs;
use std::process;

use minigrep::{search, search_case_insensitive};

fn main() {
    let config = Config::build(env::args()).unwrap_or_else(|err| {
        eprintln!("Problem parsing arguments: {err}");
        process::exit(1);
    });

    // --snip--

    if let Err(e) = run(config) {
        eprintln!("Application error: {e}");
        process::exit(1);
    }
}

pub struct Config {
    pub query: String,
    pub file_path: String,
    pub ignore_case: bool,
}

impl Config {
    fn build(args: &[String]) -> Result<Config, &'static str> {
        if args.len() < 3 {
            return Err("not enough arguments");
        }

        let query = args[1].clone();
        let file_path = args[2].clone();

        let ignore_case = env::var("IGNORE_CASE").is_ok();

        Ok(Config {
            query,
            file_path,
            ignore_case,
        })
    }
}

fn run(config: Config) -> Result<(), Box<dyn Error>> {
    let contents = fs::read_to_string(config.file_path)?;

    let results = if config.ignore_case {
        search_case_insensitive(&config.query, &contents)
    } else {
        search(&config.query, &contents)
    };

    for line in results {
        println!("{line}");
    }

    Ok(())
}
Listing 13-18: Передача возвращаемого значения env::args в Config::build

Функция env::args возвращает итератор! Вместо того чтобы собирать значения итератора в вектор, а затем передавать срез в Config::build, теперь мы напрямую передаем владение итератором, возвращенным из env::args, в Config::build.

Далее нужно обновить определение Config::build. Изменим сигнатуру Config::build, чтобы она выглядела как в листинге 13-19. Этот код все еще не скомпилируется, потому что нам нужно обновить тело функции.

Filename: src/main.rs
use std::env;
use std::error::Error;
use std::fs;
use std::process;

use minigrep::{search, search_case_insensitive};

fn main() {
    let config = Config::build(env::args()).unwrap_or_else(|err| {
        eprintln!("Problem parsing arguments: {err}");
        process::exit(1);
    });

    if let Err(e) = run(config) {
        eprintln!("Application error: {e}");
        process::exit(1);
    }
}

pub struct Config {
    pub query: String,
    pub file_path: String,
    pub ignore_case: bool,
}

impl Config {
    fn build(
        mut args: impl Iterator<Item = String>,
    ) -> Result<Config, &'static str> {
        // --snip--
        if args.len() < 3 {
            return Err("not enough arguments");
        }

        let query = args[1].clone();
        let file_path = args[2].clone();

        let ignore_case = env::var("IGNORE_CASE").is_ok();

        Ok(Config {
            query,
            file_path,
            ignore_case,
        })
    }
}

fn run(config: Config) -> Result<(), Box<dyn Error>> {
    let contents = fs::read_to_string(config.file_path)?;

    let results = if config.ignore_case {
        search_case_insensitive(&config.query, &contents)
    } else {
        search(&config.query, &contents)
    };

    for line in results {
        println!("{line}");
    }

    Ok(())
}
Listing 13-19: Обновление сигнатуры Config::build, чтобы она ожидала итератор

Документация стандартной библиотеки для функции env::args показывает, что тип возвращаемого ею итератора – std::env::Args, и этот тип реализует трейт Iterator и возвращает значения String.

Мы обновили сигнатуру функции Config::build так, что параметр args имеет обобщенный тип с ограничениями трейта impl Iterator<Item = String> вместо &[String]. Это использование синтаксиса impl Trait, который мы обсуждали в разделе «Использование трейтов как параметров» главы 10, означает, что args может быть любым типом, который реализует трейт Iterator и возвращает элементы String.

Поскольку мы принимаем владение args и будем изменять args, итерируясь по нему, мы можем добавить ключевое слово mut в описание параметра args, чтобы сделать его изменяемым.

Использование методов трейта Iterator

Далее мы исправим тело Config::build. Поскольку args реализует трейт Iterator, мы знаем, что можем вызвать у него метод next! Листинг 13-20 обновляет код из листинга 12-23, чтобы использовать метод next.

Filename: src/main.rs
use std::env;
use std::error::Error;
use std::fs;
use std::process;

use minigrep::{search, search_case_insensitive};

fn main() {
    let config = Config::build(env::args()).unwrap_or_else(|err| {
        eprintln!("Problem parsing arguments: {err}");
        process::exit(1);
    });

    if let Err(e) = run(config) {
        eprintln!("Application error: {e}");
        process::exit(1);
    }
}

pub struct Config {
    pub query: String,
    pub file_path: String,
    pub ignore_case: bool,
}

impl Config {
    fn build(
        mut args: impl Iterator<Item = String>,
    ) -> Result<Config, &'static str> {
        args.next();

        let query = match args.next() {
            Some(arg) => arg,
            None => return Err("Didn't get a query string"),
        };

        let file_path = match args.next() {
            Some(arg) => arg,
            None => return Err("Didn't get a file path"),
        };

        let ignore_case = env::var("IGNORE_CASE").is_ok();

        Ok(Config {
            query,
            file_path,
            ignore_case,
        })
    }
}

fn run(config: Config) -> Result<(), Box<dyn Error>> {
    let contents = fs::read_to_string(config.file_path)?;

    let results = if config.ignore_case {
        search_case_insensitive(&config.query, &contents)
    } else {
        search(&config.query, &contents)
    };

    for line in results {
        println!("{line}");
    }

    Ok(())
}
Listing 13-20: Изменение тела Config::build для использования методов итератора

Помните, что первое значение в возвращаемом значении env::args – это имя программы. Мы хотим проигнорировать его и перейти к следующему значению, поэтому сначала вызываем next и ничего не делаем с возвращаемым значением. Затем мы вызываем next, чтобы получить значение, которое хотим поместить в поле query структуры Config. Если next возвращает Some, мы используем match, чтобы извлечь значение. Если он возвращает None, это означает, что было передано недостаточно аргументов, и мы досрочно возвращаем значение Err. То же самое мы делаем для значения file_path.

Прояснение кода с помощью адаптеров итераторов

Мы также можем воспользоваться итераторами в функции search нашего проекта ввода-вывода, которая воспроизведена здесь в листинге 13-21 такой, какой она была в листинге 12-19.

Filename: src/lib.rs
pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
    let mut results = Vec::new();

    for line in contents.lines() {
        if line.contains(query) {
            results.push(line);
        }
    }

    results
}

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

    #[test]
    fn one_result() {
        let query = "duct";
        let contents = "\
Rust:
safe, fast, productive.
Pick three.";

        assert_eq!(vec!["safe, fast, productive."], search(query, contents));
    }
}
Listing 13-21: Реализация функции search из листинга 12-19

Мы можем написать этот код более кратко, используя методы адаптеров итераторов. Это также позволяет избежать промежуточного изменяемого вектора results. Стиль функционального программирования предпочитает минимизировать объем изменяемого состояния, чтобы сделать код яснее. Удаление изменяемого состояния может позволить в будущем улучшить поиск так, чтобы он выполнялся параллельно, потому что нам не пришлось бы управлять конкурентным доступом к вектору results. Листинг 13-22 показывает это изменение.

Filename: src/lib.rs
pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
    contents
        .lines()
        .filter(|line| line.contains(query))
        .collect()
}

pub fn search_case_insensitive<'a>(
    query: &str,
    contents: &'a str,
) -> Vec<&'a str> {
    let query = query.to_lowercase();
    let mut results = Vec::new();

    for line in contents.lines() {
        if line.to_lowercase().contains(&query) {
            results.push(line);
        }
    }

    results
}

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

    #[test]
    fn case_sensitive() {
        let query = "duct";
        let contents = "\
Rust:
safe, fast, productive.
Pick three.
Duct tape.";

        assert_eq!(vec!["safe, fast, productive."], search(query, contents));
    }

    #[test]
    fn case_insensitive() {
        let query = "rUsT";
        let contents = "\
Rust:
safe, fast, productive.
Pick three.
Trust me.";

        assert_eq!(
            vec!["Rust:", "Trust me."],
            search_case_insensitive(query, contents)
        );
    }
}
Listing 13-22: Использование методов адаптеров итераторов в реализации функции search

Вспомните, что назначение функции search – вернуть все строки из contents, которые содержат query. Подобно примеру с filter в листинге 13-16, этот код использует адаптер filter, чтобы оставить только те строки, для которых line.contains(query) возвращает true. Затем мы собираем совпавшие строки в другой вектор с помощью collect. Гораздо проще! Можете внести такое же изменение, чтобы использовать методы итераторов и в функции search_case_insensitive.

Для дальнейшего улучшения верните итератор из функции search, убрав вызов collect и изменив возвращаемый тип на impl Iterator<Item = &'a str>, чтобы функция стала адаптером итератора. Обратите внимание, что вам также нужно будет обновить тесты! Выполните поиск по большому файлу с помощью вашего инструмента minigrep до и после этого изменения, чтобы увидеть разницу в поведении. До этого изменения программа не будет печатать результаты, пока не соберет их все, но после изменения результаты будут печататься по мере нахождения каждой совпавшей строки, потому что цикл for в функции run сможет воспользоваться ленивостью итератора.

Выбор между циклами и итераторами

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

Но действительно ли эти две реализации эквивалентны? Интуитивное предположение может быть таким: более низкоуровневый цикл будет быстрее. Поговорим о производительности.

Производительность: циклы против итераторов

Производительность циклов и итераторов

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

Мы запустили бенчмарк, загрузив все содержимое The Adventures of Sherlock Holmes сэра Артура Конан Дойла в String и выполнив поиск слова the в содержимом. Вот результаты бенчмарка для версии search, использующей цикл for, и версии, использующей итераторы:

test bench_search_for  ... bench:  19,620,300 ns/iter (+/- 915,700)
test bench_search_iter ... bench:  19,234,900 ns/iter (+/- 657,200)

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

Для более всестороннего бенчмарка стоит проверить разные тексты разного размера в качестве contents, разные слова и слова разной длины в качестве query, а также всевозможные другие варианты. Суть вот в чем: итераторы, хотя и являются высокоуровневой абстракцией, компилируются примерно в тот же код, как если бы вы сами написали низкоуровневый код. Итераторы – одна из абстракций с нулевой стоимостью Rust: мы имеем в виду, что использование абстракции не добавляет накладных расходов во время выполнения. Это похоже на то, как Бьерн Страуструп, первоначальный проектировщик и реализатор C++, определяет нулевые накладные расходы в своем докладе “Foundations of C++” на ETAPS в 2012 году:

В целом реализации C++ следуют принципу нулевых накладных расходов: за то, чем вы не пользуетесь, вы не платите. И далее: то, чем вы пользуетесь, вы не смогли бы написать вручную лучше.

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

Итоги

Замыкания и итераторы – возможности Rust, вдохновленные идеями языков функционального программирования. Они помогают Rust ясно выражать высокоуровневые идеи с низкоуровневой производительностью. Реализации замыканий и итераторов устроены так, что производительность во время выполнения не страдает. Это часть цели Rust – стремиться предоставлять абстракции с нулевой стоимостью.

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

Подробнее о Cargo и Crates.io

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

  • Настраивать сборку через профили выпуска.
  • Публиковать библиотеки на crates.io.
  • Организовывать большие проекты с помощью рабочих пространств.
  • Устанавливать бинарные файлы с crates.io.
  • Расширять Cargo с помощью пользовательских команд.

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

Настройка сборок с помощью профилей выпуска

Настройка сборок с помощью профилей выпуска

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

У Cargo есть два основных профиля: профиль dev, который Cargo использует при запуске cargo build, и профиль release, который Cargo использует при запуске cargo build --release. Профиль dev определен с хорошими значениями по умолчанию для разработки, а профиль release имеет хорошие значения по умолчанию для release-сборок.

Эти имена профилей могут быть знакомы вам по выводу ваших сборок:

$ cargo build
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.00s
$ cargo build --release
    Finished `release` profile [optimized] target(s) in 0.32s

dev и release – это те самые разные профили, которые использует компилятор.

У Cargo есть настройки по умолчанию для каждого профиля; они применяются, если вы явно не добавили разделы [profile.*] в файл Cargo.toml проекта. Добавляя разделы [profile.*] для любого профиля, который хотите настроить, вы переопределяете любую часть настроек по умолчанию. Например, вот значения по умолчанию для настройки opt-level у профилей dev и release:

Имя файла: Cargo.toml

[profile.dev]
opt-level = 0

[profile.release]
opt-level = 3

Настройка opt-level управляет количеством оптимизаций, которые Rust применит к вашему коду, в диапазоне от 0 до 3. Применение большего числа оптимизаций увеличивает время компиляции, поэтому если вы находитесь в процессе разработки и часто компилируете код, вам понадобится меньше оптимизаций, чтобы компиляция шла быстрее, даже если итоговый код будет работать медленнее. Поэтому значение opt-level по умолчанию для dev равно 0. Когда вы готовы выпустить код, лучше потратить больше времени на компиляцию. В режиме выпуска вы скомпилируете программу только один раз, но запускать скомпилированную программу будете много раз, поэтому режим выпуска обменивает более долгое время компиляции на более быстрый код. Именно поэтому значение opt-level по умолчанию для профиля release равно 3.

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

Имя файла: Cargo.toml

[profile.dev]
opt-level = 1

Этот код переопределяет настройку по умолчанию 0. Теперь, когда мы запускаем cargo build, Cargo будет использовать значения по умолчанию для профиля dev плюс нашу настройку opt-level. Поскольку мы установили opt-level в 1, Cargo применит больше оптимизаций, чем по умолчанию, но не так много, как в release-сборке.

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

Публикация крейта в Crates.io

Публикация крейта на Crates.io

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

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

Создание полезных комментариев документации

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

Комментарии документации используют три косые черты, ///, вместо двух и поддерживают синтаксис Markdown для форматирования текста. Размещайте комментарии документации прямо перед элементом, который они документируют. В листинге 14-1 показаны комментарии документации для функции add_one в крейте с именем my_crate.

Filename: src/lib.rs
/// Adds one to the number given.
///
/// # Examples
///
/// ```
/// let arg = 5;
/// let answer = my_crate::add_one(arg);
///
/// assert_eq!(6, answer);
/// ```
pub fn add_one(x: i32) -> i32 {
    x + 1
}
Listing 14-1: Комментарий документации для функции

Здесь мы даем описание того, что делает функция add_one, начинаем раздел с заголовком Examples, а затем приводим код, показывающий, как использовать функцию add_one. Мы можем сгенерировать HTML-документацию из этого комментария документации, запустив cargo doc. Эта команда запускает инструмент rustdoc, поставляемый вместе с Rust, и помещает сгенерированную HTML-документацию в каталог target/doc.

Для удобства запуск cargo doc --open соберет HTML для документации текущего крейта (а также документацию всех зависимостей вашего крейта) и откроет результат в веб-браузере. Перейдите к функции add_one, и вы увидите, как отображается текст из комментариев документации, как показано на рисунке 14-1.

Отрисованная HTML-документация для функции `add_one` из `my_crate`

Рисунок 14-1: HTML-документация для функции add_one

Часто используемые разделы

Мы использовали Markdown-заголовок # Examples в листинге 14-1, чтобы создать раздел в HTML с названием “Examples.” Вот некоторые другие разделы, которые авторы крейтов часто используют в своей документации:

  • Panics: сценарии, в которых документируемая функция может запаниковать. Вызывающий код, который не хочет, чтобы его программа паниковала, должен убедиться, что не вызывает функцию в таких ситуациях.
  • Errors: если функция возвращает Result, описание видов ошибок, которые могут возникнуть, и условий, которые могут привести к возврату этих ошибок, может быть полезно вызывающему коду, чтобы он мог писать код для обработки разных видов ошибок разными способами.
  • Safety: если вызов функции является unsafe (мы обсудим небезопасность в главе 20), должен быть раздел, объясняющий, почему функция небезопасна, и описывающий инварианты, которые функция ожидает от вызывающего кода.

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

Комментарии документации как тесты

Добавление блоков с примером кода в комментарии документации может помочь показать, как использовать вашу библиотеку, и дает дополнительный бонус: запуск cargo test будет запускать примеры кода из вашей документации как тесты! Нет ничего лучше документации с примерами. Но нет ничего хуже примеров, которые не работают, потому что код изменился после написания документации. Если мы запустим cargo test с документацией для функции add_one из листинга 14-1, то увидим в результатах тестов раздел, похожий на этот:

   Doc-tests my_crate

running 1 test
test src/lib.rs - add_one (line 5) ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.27s

Теперь, если мы изменим либо функцию, либо пример так, что assert_eq! в примере запаникует, и снова запустим cargo test, мы увидим, что документационные тесты обнаружат рассинхронизацию между примером и кодом!

Комментарии для содержащих элементов

Стиль комментария документации //! добавляет документацию к элементу, который содержит комментарии, а не к элементам, следующим за комментариями. Обычно мы используем такие doc-комментарии внутри корневого файла крейта (src/lib.rs по соглашению) или внутри модуля, чтобы документировать крейт или модуль в целом.

Например, чтобы добавить документацию, описывающую назначение крейта my_crate, который содержит функцию add_one, мы добавляем комментарии документации, начинающиеся с //!, в начало файла src/lib.rs, как показано в листинге 14-2.

Filename: src/lib.rs
//! # My Crate
//!
//! `my_crate` is a collection of utilities to make performing certain
//! calculations more convenient.

/// Adds one to the number given.
// --snip--
///
/// # Examples
///
/// ```
/// let arg = 5;
/// let answer = my_crate::add_one(arg);
///
/// assert_eq!(6, answer);
/// ```
pub fn add_one(x: i32) -> i32 {
    x + 1
}
Listing 14-2: Документация для крейта my_crate в целом

Обратите внимание, что после последней строки, начинающейся с //!, нет кода. Поскольку мы начали комментарии с //!, а не с ///, мы документируем элемент, который содержит этот комментарий, а не элемент, следующий за этим комментарием. В данном случае этот элемент – файл src/lib.rs, являющийся корнем крейта. Эти комментарии описывают весь крейт.

Когда мы запускаем cargo doc --open, эти комментарии отображаются на главной странице документации для my_crate над списком публичных элементов крейта, как показано на рисунке 14-2.

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

Отрисованная HTML-документация с комментарием для крейта в целом

Рисунок 14-2: Отрисованная документация для my_crate, включающая комментарий, описывающий крейт в целом

Экспорт удобного публичного API

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

В главе 7 мы рассмотрели, как делать элементы публичными с помощью ключевого слова pub и как вводить элементы в область видимости с помощью ключевого слова use. Однако структура, которая кажется вам разумной во время разработки крейта, может быть не очень удобной для ваших пользователей. Возможно, вы захотите организовать структуры в иерархию из нескольких уровней, но тогда людям, которые хотят использовать тип, определенный глубоко в иерархии, может быть трудно узнать, что этот тип существует. Их также может раздражать необходимость писать use my_crate::some_module::another_module::UsefulType; вместо use my_crate::UsefulType;.

Хорошая новость в том, что если структура неудобна для использования из другой библиотеки, вам не нужно перестраивать внутреннюю организацию: вместо этого вы можете реэкспортировать элементы, чтобы создать публичную структуру, отличающуюся от вашей приватной структуры, с помощью pub use. Реэкспорт берет публичный элемент в одном месте и делает его публичным в другом месте, как если бы он был определен там.

Например, допустим, мы создали библиотеку с именем art для моделирования художественных понятий. Внутри этой библиотеки есть два модуля: модуль kinds, содержащий два enum с именами PrimaryColor и SecondaryColor, и модуль utils, содержащий функцию с именем mix, как показано в листинге 14-3.

Filename: src/lib.rs
//! # Art
//!
//! A library for modeling artistic concepts.

pub mod kinds {
    /// The primary colors according to the RYB color model.
    pub enum PrimaryColor {
        Red,
        Yellow,
        Blue,
    }

    /// The secondary colors according to the RYB color model.
    pub enum SecondaryColor {
        Orange,
        Green,
        Purple,
    }
}

pub mod utils {
    use crate::kinds::*;

    /// Combines two primary colors in equal amounts to create
    /// a secondary color.
    pub fn mix(c1: PrimaryColor, c2: PrimaryColor) -> SecondaryColor {
        // --snip--
        unimplemented!();
    }
}
Listing 14-3: Библиотека art с элементами, организованными в модули kinds и utils

На рисунке 14-3 показано, как выглядела бы главная страница документации для этого крейта, сгенерированная cargo doc.

Отрисованная документация для крейта `art`, где перечислены модули `kinds` и `utils`

Рисунок 14-3: Главная страница документации для art, где перечислены модули kinds и utils

Обратите внимание, что типы PrimaryColor и SecondaryColor не перечислены на главной странице, как и функция mix. Чтобы увидеть их, нужно щелкнуть kinds и utils.

Другому крейту, который зависит от этой библиотеки, понадобятся инструкции use, вводящие элементы из art в область видимости и указывающие текущую структуру модулей. В листинге 14-4 показан пример крейта, который использует элементы PrimaryColor и mix из крейта art.

Filename: src/main.rs
use art::kinds::PrimaryColor;
use art::utils::mix;

fn main() {
    let red = PrimaryColor::Red;
    let yellow = PrimaryColor::Yellow;
    mix(red, yellow);
}
Listing 14-4: Крейт, использующий элементы крейта art с экспортированной внутренней структурой

Автору кода в листинге 14-4, который использует крейт art, пришлось выяснить, что PrimaryColor находится в модуле kinds, а mix – в модуле utils. Структура модулей крейта art более важна для разработчиков, работающих над крейтом art, чем для тех, кто его использует. Внутренняя структура не содержит полезной информации для человека, который пытается понять, как использовать крейт art; скорее она вызывает путаницу, потому что разработчикам, использующим крейт, приходится выяснять, где искать, и указывать имена модулей в инструкциях use.

Чтобы убрать внутреннюю организацию из публичного API, мы можем изменить код крейта art из листинга 14-3 и добавить инструкции pub use, чтобы реэкспортировать элементы на верхнем уровне, как показано в листинге 14-5.

Filename: src/lib.rs
//! # Art
//!
//! A library for modeling artistic concepts.

pub use self::kinds::PrimaryColor;
pub use self::kinds::SecondaryColor;
pub use self::utils::mix;

pub mod kinds {
    // --snip--
    /// The primary colors according to the RYB color model.
    pub enum PrimaryColor {
        Red,
        Yellow,
        Blue,
    }

    /// The secondary colors according to the RYB color model.
    pub enum SecondaryColor {
        Orange,
        Green,
        Purple,
    }
}

pub mod utils {
    // --snip--
    use crate::kinds::*;

    /// Combines two primary colors in equal amounts to create
    /// a secondary color.
    pub fn mix(c1: PrimaryColor, c2: PrimaryColor) -> SecondaryColor {
        SecondaryColor::Orange
    }
}
Listing 14-5: Добавление инструкций pub use для реэкспорта элементов

Документация API, которую cargo doc сгенерирует для этого крейта, теперь будет перечислять реэкспорты на главной странице и давать ссылки на них, как показано на рисунке 14-4, благодаря чему типы PrimaryColor и SecondaryColor и функцию mix будет проще найти.

Отрисованная документация для крейта `art` с реэкспортами на главной странице

Рисунок 14-4: Главная страница документации для art, где перечислены реэкспорты

Пользователи крейта art все еще могут видеть и использовать внутреннюю структуру из листинга 14-3, как показано в листинге 14-4, или могут использовать более удобную структуру из листинга 14-5, как показано в листинге 14-6.

Filename: src/main.rs
use art::PrimaryColor;
use art::mix;

fn main() {
    // --snip--
    let red = PrimaryColor::Red;
    let yellow = PrimaryColor::Yellow;
    mix(red, yellow);
}
Listing 14-6: Программа, использующая реэкспортированные элементы из крейта art

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

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

Настройка учетной записи Crates.io

Прежде чем публиковать крейты, нужно создать учетную запись на crates.io и получить API-токен. Для этого перейдите на главную страницу crates.io и войдите через учетную запись GitHub. (Учетная запись GitHub сейчас является требованием, но сайт может поддержать другие способы создания учетной записи в будущем.) После входа перейдите в настройки учетной записи по адресу https://crates.io/me/ и получите свой API-ключ. Затем выполните команду cargo login и вставьте API-ключ, когда вас попросят, например так:

$ cargo login
abcdefghijklmnopqrstuvwxyz012345

Эта команда сообщит Cargo ваш API-токен и сохранит его локально в ~/.cargo/credentials.toml. Обратите внимание, что этот токен является секретом: не делитесь им ни с кем. Если вы по какой-либо причине поделились им с кем-то, нужно отозвать его и сгенерировать новый токен на crates.io.

Добавление метаданных в новый крейт

Допустим, у вас есть крейт, который вы хотите опубликовать. Перед публикацией нужно добавить некоторые метаданные в раздел [package] файла Cargo.toml этого крейта.

Вашему крейту понадобится уникальное имя. Пока вы работаете над крейтом локально, можете назвать его как угодно. Однако имена крейтов на crates.io выдаются в порядке очереди. Когда имя крейта уже занято, никто другой не сможет опубликовать крейт с этим именем. Перед попыткой опубликовать крейт выполните поиск по имени, которое хотите использовать. Если имя уже использовано, нужно найти другое имя и изменить поле name в файле Cargo.toml в разделе [package], чтобы использовать новое имя для публикации, например так:

Имя файла: Cargo.toml

[package]
name = "guessing_game"

Даже если вы выбрали уникальное имя, при запуске cargo publish для публикации крейта на этом этапе вы получите предупреждение, а затем ошибку:

$ cargo publish
    Updating crates.io index
warning: manifest has no description, license, license-file, documentation, homepage or repository.
See https://doc.rust-lang.org/cargo/reference/manifest.html#package-metadata for more info.
--snip--
error: failed to publish to registry at https://crates.io

Caused by:
  the remote server responded with an error (status 400 Bad Request): missing or empty metadata fields: description, license. Please see https://doc.rust-lang.org/cargo/reference/manifest.html for more information on configuring these fields

Это приводит к ошибке, потому что отсутствует важная информация: описание и лицензия обязательны, чтобы люди знали, что делает ваш крейт и на каких условиях они могут его использовать. В Cargo.toml добавьте описание всего в одно-два предложения, потому что оно появится вместе с вашим крейтом в результатах поиска. Для поля license нужно указать значение идентификатора лицензии. Software Package Data Exchange (SPDX) от Linux Foundation перечисляет идентификаторы, которые можно использовать для этого значения. Например, чтобы указать, что вы лицензировали крейт с использованием лицензии MIT, добавьте идентификатор MIT:

Имя файла: Cargo.toml

[package]
name = "guessing_game"
license = "MIT"

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

Рекомендации о том, какая лицензия подходит вашему проекту, выходят за рамки этой книги. Многие люди в сообществе Rust лицензируют свои проекты так же, как Rust, используя двойную лицензию MIT OR Apache-2.0. Эта практика показывает, что вы также можете указать несколько идентификаторов лицензий, разделенных OR, чтобы у проекта было несколько лицензий.

После добавления уникального имени, версии, описания и лицензии файл Cargo.toml для проекта, готового к публикации, может выглядеть так:

Имя файла: Cargo.toml

[package]
name = "guessing_game"
version = "0.1.0"
edition = "2024"
description = "A fun game where you guess what number the computer has chosen."
license = "MIT OR Apache-2.0"

[dependencies]

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

Публикация на Crates.io

Теперь, когда вы создали учетную запись, сохранили API-токен, выбрали имя для крейта и указали обязательные метаданные, вы готовы к публикации! Публикация крейта загружает конкретную версию на crates.io, чтобы другие могли ее использовать.

Будьте осторожны, потому что публикация постоянна. Версию нельзя перезаписать, а код нельзя удалить, кроме некоторых обстоятельств. Одна из главных целей Crates.io – быть постоянным архивом кода, чтобы сборки всех проектов, зависящих от крейтов с crates.io, продолжали работать. Разрешение удалять версии сделало бы выполнение этой цели невозможным. Однако количество версий крейта, которые вы можете опубликовать, не ограничено.

Снова выполните команду cargo publish. Теперь она должна завершиться успешно:

$ cargo publish
    Updating crates.io index
   Packaging guessing_game v0.1.0 (file:///projects/guessing_game)
    Packaged 6 files, 1.2KiB (895.0B compressed)
   Verifying guessing_game v0.1.0 (file:///projects/guessing_game)
   Compiling guessing_game v0.1.0
(file:///projects/guessing_game/target/package/guessing_game-0.1.0)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.19s
   Uploading guessing_game v0.1.0 (file:///projects/guessing_game)
    Uploaded guessing_game v0.1.0 to registry `crates-io`
note: waiting for `guessing_game v0.1.0` to be available at registry
`crates-io`.
You may press ctrl-c to skip waiting; the crate should be available shortly.
   Published guessing_game v0.1.0 at registry `crates-io`

Поздравляем! Теперь вы поделились своим кодом с сообществом Rust, и любой сможет легко добавить ваш крейт как зависимость своего проекта.

Публикация новой версии существующего крейта

Когда вы внесли изменения в крейт и готовы выпустить новую версию, измените значение version, указанное в файле Cargo.toml, и опубликуйте крейт снова. Используйте правила семантического версионирования, чтобы решить, какой следующий номер версии подходит с учетом видов внесенных изменений. Затем выполните cargo publish, чтобы загрузить новую версию.

Вывод версий из использования на Crates.io

Хотя вы не можете удалить предыдущие версии крейта, вы можете запретить будущим проектам добавлять их как новую зависимость. Это полезно, когда версия крейта по той или иной причине сломана. В таких ситуациях Cargo поддерживает вывод версии крейта из использования, или yank.

Yank версии не дает новым проектам зависеть от этой версии, но позволяет всем существующим проектам, которые уже зависят от нее, продолжать работу. По сути, yank означает, что все проекты с Cargo.lock не сломаются, а любые будущие файлы Cargo.lock не будут использовать выведенную из использования версию.

Чтобы вывести версию крейта из использования, в каталоге ранее опубликованного крейта выполните cargo yank и укажите версию, которую хотите вывести. Например, если мы опубликовали крейт с именем guessing_game версии 1.0.1 и хотим вывести ее из использования, то в каталоге проекта guessing_game выполним следующее:

$ cargo yank --vers 1.0.1
    Updating crates.io index
        Yank guessing_game@1.0.1

Добавив к команде --undo, вы также можете отменить yank и снова разрешить проектам начинать зависеть от этой версии:

$ cargo yank --vers 1.0.1 --undo
    Updating crates.io index
      Unyank guessing_game@1.0.1

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

Рабочие пространства Cargo

Рабочие пространства Cargo

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

Создание рабочего пространства

Рабочее пространство – это набор пакетов, которые совместно используют один и тот же Cargo.lock и каталог вывода. Создадим проект с рабочим пространством: мы будем использовать простой код, чтобы сосредоточиться на структуре рабочего пространства. Есть несколько способов структурировать рабочее пространство, поэтому мы покажем один распространенный способ. У нас будет рабочее пространство с одним бинарным крейтом и двумя библиотеками. Бинарный крейт, который будет предоставлять основную функциональность, будет зависеть от двух библиотек. Одна библиотека будет предоставлять функцию add_one, а другая – функцию add_two. Эти три крейта будут частью одного рабочего пространства. Начнем с создания нового каталога для рабочего пространства:

$ mkdir add
$ cd add

Далее в каталоге add мы создаем файл Cargo.toml, который будет настраивать все рабочее пространство. В этом файле не будет раздела [package]. Вместо этого он начнется с раздела [workspace], который позволит нам добавлять участников в рабочее пространство. Мы также явно используем самую новую и лучшую версию алгоритма разрешения зависимостей Cargo в нашем рабочем пространстве, установив значение resolver в "3":

Имя файла: Cargo.toml

[workspace]
resolver = "3"

Далее мы создадим бинарный крейт adder, выполнив cargo new внутри каталога add:

$ cargo new adder
     Created binary (application) `adder` package
      Adding `adder` as member of workspace at `file:///projects/add`

Запуск cargo new внутри рабочего пространства также автоматически добавляет только что созданный пакет в ключ members в определении [workspace] в Cargo.toml рабочего пространства, вот так:

[workspace]
resolver = "3"
members = ["adder"]

На этом этапе мы можем собрать рабочее пространство, выполнив cargo build. Файлы в вашем каталоге add должны выглядеть так:

├── Cargo.lock
├── Cargo.toml
├── adder
│   ├── Cargo.toml
│   └── src
│       └── main.rs
└── target

У рабочего пространства есть один каталог target на верхнем уровне, в который помещаются скомпилированные артефакты; у пакета adder нет собственного каталога target. Даже если бы мы выполнили cargo build изнутри каталога adder, скомпилированные артефакты все равно оказались бы в add/target, а не в add/adder/target. Cargo так структурирует каталог target в рабочем пространстве, потому что крейты в рабочем пространстве предназначены для зависимости друг от друга. Если бы у каждого крейта был собственный каталог target, каждому крейту пришлось бы перекомпилировать каждый из других крейтов в рабочем пространстве, чтобы поместить артефакты в свой собственный каталог target. Совместно используя один каталог target, крейты могут избежать ненужной пересборки.

Создание второго пакета в рабочем пространстве

Далее создадим еще один пакет-участник в рабочем пространстве и назовем его add_one. Сгенерируйте новый библиотечный крейт с именем add_one:

$ cargo new add_one --lib
     Created library `add_one` package
      Adding `add_one` as member of workspace at `file:///projects/add`

Теперь Cargo.toml верхнего уровня будет включать путь add_one в список members:

Имя файла: Cargo.toml

[workspace]
resolver = "3"
members = ["adder", "add_one"]

Теперь в вашем каталоге add должны быть такие каталоги и файлы:

├── Cargo.lock
├── Cargo.toml
├── add_one
│   ├── Cargo.toml
│   └── src
│       └── lib.rs
├── adder
│   ├── Cargo.toml
│   └── src
│       └── main.rs
└── target

В файл add_one/src/lib.rs добавим функцию add_one:

Имя файла: add_one/src/lib.rs

pub fn add_one(x: i32) -> i32 {
    x + 1
}

Теперь пакет adder с нашим бинарным крейтом может зависеть от пакета add_one, содержащего нашу библиотеку. Сначала нужно добавить зависимость по пути от add_one в adder/Cargo.toml.

Имя файла: adder/Cargo.toml

[dependencies]
add_one = { path = "../add_one" }

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

Далее используем функцию add_one (из крейта add_one) в крейте adder. Откройте файл adder/src/main.rs и измените функцию main, чтобы она вызывала функцию add_one, как в листинге 14-7.

Filename: adder/src/main.rs
fn main() {
    let num = 10;
    println!("Hello, world! {num} plus one is {}!", add_one::add_one(num));
}
Listing 14-7: Использование библиотечного крейта add_one из крейта adder

Соберем рабочее пространство, выполнив cargo build в каталоге add верхнего уровня!

$ cargo build
   Compiling add_one v0.1.0 (file:///projects/add/add_one)
   Compiling adder v0.1.0 (file:///projects/add/adder)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.22s

Чтобы запустить бинарный крейт из каталога add, можно указать, какой пакет в рабочем пространстве мы хотим запустить, используя аргумент -p и имя пакета с cargo run:

$ cargo run -p adder
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.00s
     Running `target/debug/adder`
Hello, world! 10 plus one is 11!

Это запускает код из adder/src/main.rs, который зависит от крейта add_one.

Зависимость от внешнего пакета

Обратите внимание, что у рабочего пространства есть только один файл Cargo.lock на верхнем уровне, а не отдельный Cargo.lock в каталоге каждого крейта. Это гарантирует, что все крейты используют одну и ту же версию всех зависимостей. Если мы добавим пакет rand в файлы adder/Cargo.toml и add_one/Cargo.toml, Cargo сведет обе эти зависимости к одной версии rand и запишет ее в один Cargo.lock. Если все крейты в рабочем пространстве используют одни и те же зависимости, крейты всегда будут совместимы друг с другом. Добавим крейт rand в раздел [dependencies] файла add_one/Cargo.toml, чтобы использовать крейт rand в крейте add_one:

Имя файла: add_one/Cargo.toml

[dependencies]
rand = "0.8.5"

Теперь мы можем добавить use rand; в файл add_one/src/lib.rs, и сборка всего рабочего пространства командой cargo build в каталоге add загрузит и скомпилирует крейт rand. Мы получим одно предупреждение, потому что не обращаемся к rand, который ввели в область видимости:

$ cargo build
    Updating crates.io index
  Downloaded rand v0.8.5
   --snip--
   Compiling rand v0.8.5
   Compiling add_one v0.1.0 (file:///projects/add/add_one)
warning: unused import: `rand`
 --> add_one/src/lib.rs:1:5
  |
1 | use rand;
  |     ^^^^
  |
  = note: `#[warn(unused_imports)]` on by default

warning: `add_one` (lib) generated 1 warning (run `cargo fix --lib -p add_one` to apply 1 suggestion)
   Compiling adder v0.1.0 (file:///projects/add/adder)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.95s

Теперь Cargo.lock верхнего уровня содержит информацию о зависимости add_one от rand. Однако, хотя rand используется где-то в рабочем пространстве, мы не можем использовать его в других крейтах рабочего пространства, если также не добавим rand в их файлы Cargo.toml. Например, если мы добавим use rand; в файл adder/src/main.rs для пакета adder, получим ошибку:

$ cargo build
  --snip--
   Compiling adder v0.1.0 (file:///projects/add/adder)
error[E0432]: unresolved import `rand`
 --> adder/src/main.rs:2:5
  |
2 | use rand;
  |     ^^^^ no external crate `rand`

Чтобы исправить это, отредактируйте файл Cargo.toml для пакета adder и укажите, что rand также является его зависимостью. Сборка пакета adder добавит rand в список зависимостей adder в Cargo.lock, но дополнительные копии rand загружены не будут. Cargo проследит, чтобы каждый крейт в каждом пакете рабочего пространства, использующий пакет rand, использовал одну и ту же версию, пока они указывают совместимые версии rand; это экономит место и гарантирует, что крейты рабочего пространства будут совместимы друг с другом.

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

Добавление теста в рабочее пространство

В качестве еще одного улучшения добавим тест функции add_one::add_one внутри крейта add_one:

Имя файла: add_one/src/lib.rs

pub fn add_one(x: i32) -> i32 {
    x + 1
}

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

    #[test]
    fn it_works() {
        assert_eq!(3, add_one(2));
    }
}

Теперь выполните cargo test в каталоге add верхнего уровня. Запуск cargo test в рабочем пространстве, структурированном таким образом, выполнит тесты для всех крейтов в рабочем пространстве:

$ cargo test
   Compiling add_one v0.1.0 (file:///projects/add/add_one)
   Compiling adder v0.1.0 (file:///projects/add/adder)
    Finished `test` profile [unoptimized + debuginfo] target(s) in 0.20s
     Running unittests src/lib.rs (target/debug/deps/add_one-93c49ee75dc46543)

running 1 test
test tests::it_works ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

     Running unittests src/main.rs (target/debug/deps/adder-3a47283c568d2b6a)

running 0 tests

test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

   Doc-tests add_one

running 0 tests

test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

Первый раздел вывода показывает, что тест it_works в крейте add_one прошел. Следующий раздел показывает, что в крейте adder найдено ноль тестов, а последний раздел показывает, что в крейте add_one найдено ноль документационных тестов.

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

$ cargo test -p add_one
    Finished `test` profile [unoptimized + debuginfo] target(s) in 0.00s
     Running unittests src/lib.rs (target/debug/deps/add_one-93c49ee75dc46543)

running 1 test
test tests::it_works ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

   Doc-tests add_one

running 0 tests

test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

Этот вывод показывает, что cargo test запустил только тесты для крейта add_one и не запускал тесты крейта adder.

Если вы публикуете крейты из рабочего пространства на crates.io, каждый крейт в рабочем пространстве нужно публиковать отдельно. Как и с cargo test, мы можем опубликовать конкретный крейт в нашем рабочем пространстве, используя флаг -p и указывая имя крейта, который хотим опубликовать.

Для дополнительной практики добавьте крейт add_two в это рабочее пространство так же, как крейт add_one!

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

Установка бинарных файлов с помощью cargo install

Установка бинарных файлов с помощью cargo install

Команда cargo install позволяет локально устанавливать и использовать бинарные крейты. Она не предназначена для замены системных пакетов; ее цель – дать разработчикам Rust удобный способ устанавливать инструменты, которыми другие поделились на crates.io. Обратите внимание, что установить можно только пакеты, у которых есть бинарные цели. Бинарная цель – это запускаемая программа, которая создается, если в крейте есть файл src/main.rs или другой файл, указанный как бинарный, в отличие от библиотечной цели, которая сама по себе не запускается, но подходит для включения в другие программы. Обычно в файле README крейта есть информация о том, является ли крейт библиотекой, имеет ли бинарную цель или и то и другое.

Все бинарные файлы, установленные с помощью cargo install, сохраняются в папке bin корня установки. Если вы установили Rust с помощью rustup.rs и не имеете пользовательских настроек, этот каталог будет $HOME/.cargo/bin. Убедитесь, что этот каталог находится в вашем $PATH, чтобы можно было запускать программы, установленные с помощью cargo install.

Например, в главе 12 мы упоминали, что существует реализация инструмента grep на Rust под названием ripgrep для поиска в файлах. Чтобы установить ripgrep, можно выполнить следующее:

$ cargo install ripgrep
    Updating crates.io index
  Downloaded ripgrep v14.1.1
  Downloaded 1 crate (213.6 KB) in 0.40s
  Installing ripgrep v14.1.1
--snip--
   Compiling grep v0.3.2
    Finished `release` profile [optimized + debuginfo] target(s) in 6.73s
  Installing ~/.cargo/bin/rg
   Installed package `ripgrep v14.1.1` (executable `rg`)

Предпоследняя строка вывода показывает расположение и имя установленного бинарного файла; в случае ripgrep это rg. Если каталог установки находится в вашем $PATH, как упоминалось ранее, затем вы можете выполнить rg --help и начать использовать более быстрый инструмент для поиска в файлах, написанный в духе Rust!

Расширение Cargo с помощью пользовательских команд

Расширение Cargo с помощью пользовательских команд

Cargo спроектирован так, чтобы вы могли расширять его новыми подкомандами без изменения самого Cargo. Если бинарный файл в вашем $PATH называется cargo-something, вы можете запустить его как подкоманду Cargo, выполнив cargo something. Такие пользовательские команды также перечисляются при выполнении cargo --list. Возможность использовать cargo install для установки расширений, а затем запускать их так же, как встроенные инструменты Cargo, – очень удобное преимущество дизайна Cargo!

Итоги

Обмен кодом с помощью Cargo и crates.io – часть того, что делает экосистему Rust полезной для множества разных задач. Стандартная библиотека Rust маленькая и стабильная, но крейтами легко делиться, пользоваться и улучшать их в темпе, отличном от темпа развития языка. Не стесняйтесь делиться полезным для вас кодом на crates.io; вполне вероятно, что он будет полезен и кому-то еще!

Умные указатели

Указатель – это общее понятие для переменной, которая содержит адрес в памяти. Этот адрес ссылается, или “указывает”, на какие-то другие данные. Самый распространенный вид указателя в Rust – это ссылка, о которой вы узнали в главе 4. Ссылки обозначаются символом & и заимствуют значение, на которое указывают. У них нет никаких особых возможностей, кроме ссылки на данные, и они не имеют накладных расходов.

Умные указатели, с другой стороны, – это структуры данных, которые ведут себя как указатель, но также имеют дополнительные метаданные и возможности. Концепция умных указателей не уникальна для Rust: умные указатели появились в C++ и существуют также в других языках. В стандартной библиотеке Rust определено несколько умных указателей, которые предоставляют функциональность сверх той, что дают ссылки. Чтобы изучить общую концепцию, мы рассмотрим несколько разных примеров умных указателей, включая тип умного указателя с подсчетом ссылок. Такой указатель позволяет данным иметь нескольких владельцев, отслеживая количество владельцев и очищая данные, когда владельцев больше не остается.

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

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

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

  • Box<T> для выделения значений в куче
  • Rc<T>, тип с подсчетом ссылок, который делает возможным множественное владение
  • Ref<T> и RefMut<T>, доступные через RefCell<T>, тип, который обеспечивает правила заимствования во время выполнения, а не во время компиляции

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

Приступим!

Использование Box<T> для указания на данные в куче

Using Box<T> to Point to Data on the Heap

The most straightforward smart pointer is a box, whose type is written Box<T>. Boxes allow you to store data on the heap rather than the stack. What remains on the stack is the pointer to the heap data. Refer to Chapter 4 to review the difference between the stack and the heap.

Boxes don’t have performance overhead, other than storing their data on the heap instead of on the stack. But they don’t have many extra capabilities either. You’ll use them most often in these situations:

  • When you have a type whose size can’t be known at compile time, and you want to use a value of that type in a context that requires an exact size
  • When you have a large amount of data, and you want to transfer ownership but ensure that the data won’t be copied when you do so
  • When you want to own a value, and you care only that it’s a type that implements a particular trait rather than being of a specific type

We’ll demonstrate the first situation in “Enabling Recursive Types with Boxes”. In the second case, transferring ownership of a large amount of data can take a long time because the data is copied around on the stack. To improve performance in this situation, we can store the large amount of data on the heap in a box. Then, only the small amount of pointer data is copied around on the stack, while the data it references stays in one place on the heap. The third case is known as a trait object, and “Using Trait Objects to Abstract over Shared Behavior” in Chapter 18 is devoted to that topic. So, what you learn here you’ll apply again in that section!

Storing Data on the Heap

Before we discuss the heap storage use case for Box<T>, we’ll cover the syntax and how to interact with values stored within a Box<T>.

Listing 15-1 shows how to use a box to store an i32 value on the heap.

Filename: src/main.rs
fn main() {
    let b = Box::new(5);
    println!("b = {b}");
}
Listing 15-1: Storing an i32 value on the heap using a box

We define the variable b to have the value of a Box that points to the value 5, which is allocated on the heap. This program will print b = 5; in this case, we can access the data in the box similarly to how we would if this data were on the stack. Just like any owned value, when a box goes out of scope, as b does at the end of main, it will be deallocated. The deallocation happens both for the box (stored on the stack) and the data it points to (stored on the heap).

Putting a single value on the heap isn’t very useful, so you won’t use boxes by themselves in this way very often. Having values like a single i32 on the stack, where they’re stored by default, is more appropriate in the majority of situations. Let’s look at a case where boxes allow us to define types that we wouldn’t be allowed to define if we didn’t have boxes.

Enabling Recursive Types with Boxes

A value of a recursive type can have another value of the same type as part of itself. Recursive types pose an issue because Rust needs to know at compile time how much space a type takes up. However, the nesting of values of recursive types could theoretically continue infinitely, so Rust can’t know how much space the value needs. Because boxes have a known size, we can enable recursive types by inserting a box in the recursive type definition.

As an example of a recursive type, let’s explore the cons list. This is a data type commonly found in functional programming languages. The cons list type we’ll define is straightforward except for the recursion; therefore, the concepts in the example we’ll work with will be useful anytime you get into more complex situations involving recursive types.

Understanding the Cons List

A cons list is a data structure that comes from the Lisp programming language and its dialects, is made up of nested pairs, and is the Lisp version of a linked list. Its name comes from the cons function (short for construct function) in Lisp that constructs a new pair from its two arguments. By calling cons on a pair consisting of a value and another pair, we can construct cons lists made up of recursive pairs.

For example, here’s a pseudocode representation of a cons list containing the list 1, 2, 3 with each pair in parentheses:

(1, (2, (3, Nil)))

Each item in a cons list contains two elements: the value of the current item and of the next item. The last item in the list contains only a value called Nil without a next item. A cons list is produced by recursively calling the cons function. The canonical name to denote the base case of the recursion is Nil. Note that this is not the same as the “null” or “nil” concept discussed in Chapter 6, which is an invalid or absent value.

The cons list isn’t a commonly used data structure in Rust. Most of the time when you have a list of items in Rust, Vec<T> is a better choice to use. Other, more complex recursive data types are useful in various situations, but by starting with the cons list in this chapter, we can explore how boxes let us define a recursive data type without much distraction.

Listing 15-2 contains an enum definition for a cons list. Note that this code won’t compile yet, because the List type doesn’t have a known size, which we’ll demonstrate.

Filename: src/main.rs
enum List {
    Cons(i32, List),
    Nil,
}

fn main() {}
Listing 15-2: The first attempt at defining an enum to represent a cons list data structure of i32 values

Note: We’re implementing a cons list that holds only i32 values for the purposes of this example. We could have implemented it using generics, as we discussed in Chapter 10, to define a cons list type that could store values of any type.

Using the List type to store the list 1, 2, 3 would look like the code in Listing 15-3.

Filename: src/main.rs
enum List {
    Cons(i32, List),
    Nil,
}

// --snip--

use crate::List::{Cons, Nil};

fn main() {
    let list = Cons(1, Cons(2, Cons(3, Nil)));
}
Listing 15-3: Using the List enum to store the list 1, 2, 3

The first Cons value holds 1 and another List value. This List value is another Cons value that holds 2 and another List value. This List value is one more Cons value that holds 3 and a List value, which is finally Nil, the non-recursive variant that signals the end of the list.

If we try to compile the code in Listing 15-3, we get the error shown in Listing 15-4.

$ cargo run
   Compiling cons-list v0.1.0 (file:///projects/cons-list)
error[E0072]: recursive type `List` has infinite size
 --> src/main.rs:1:1
  |
1 | enum List {
  | ^^^^^^^^^
2 |     Cons(i32, List),
  |               ---- recursive without indirection
  |
help: insert some indirection (e.g., a `Box`, `Rc`, or `&`) to break the cycle
  |
2 |     Cons(i32, Box<List>),
  |               ++++    +

error[E0391]: cycle detected when computing when `List` needs drop
 --> src/main.rs:1:1
  |
1 | enum List {
  | ^^^^^^^^^
  |
  = note: ...which immediately requires computing when `List` needs drop again
  = note: cycle used when computing whether `List` needs drop
  = note: see https://rustc-dev-guide.rust-lang.org/overview.html#queries and https://rustc-dev-guide.rust-lang.org/query.html for more information

Some errors have detailed explanations: E0072, E0391.
For more information about an error, try `rustc --explain E0072`.
error: could not compile `cons-list` (bin "cons-list") due to 2 previous errors
Listing 15-4: The error we get when attempting to define a recursive enum

The error shows this type “has infinite size.” The reason is that we’ve defined List with a variant that is recursive: It holds another value of itself directly. As a result, Rust can’t figure out how much space it needs to store a List value. Let’s break down why we get this error. First, we’ll look at how Rust decides how much space it needs to store a value of a non-recursive type.

Computing the Size of a Non-Recursive Type

Recall the Message enum we defined in Listing 6-2 when we discussed enum definitions in Chapter 6:

enum Message {
    Quit,
    Move { x: i32, y: i32 },
    Write(String),
    ChangeColor(i32, i32, i32),
}

fn main() {}

To determine how much space to allocate for a Message value, Rust goes through each of the variants to see which variant needs the most space. Rust sees that Message::Quit doesn’t need any space, Message::Move needs enough space to store two i32 values, and so forth. Because only one variant will be used, the most space a Message value will need is the space it would take to store the largest of its variants.

Contrast this with what happens when Rust tries to determine how much space a recursive type like the List enum in Listing 15-2 needs. The compiler starts by looking at the Cons variant, which holds a value of type i32 and a value of type List. Therefore, Cons needs an amount of space equal to the size of an i32 plus the size of a List. To figure out how much memory the List type needs, the compiler looks at the variants, starting with the Cons variant. The Cons variant holds a value of type i32 and a value of type List, and this process continues infinitely, as shown in Figure 15-1.

An infinite Cons list: a rectangle labeled 'Cons' split into two smaller rectangles. The first smaller rectangle holds the label 'i32', and the second smaller rectangle holds the label 'Cons' and a smaller version of the outer 'Cons' rectangle. The 'Cons' rectangles continue to hold smaller and smaller versions of themselves until the smallest comfortably sized rectangle holds an infinity symbol, indicating that this repetition goes on forever.

Figure 15-1: An infinite List consisting of infinite Cons variants

Getting a Recursive Type with a Known Size

Because Rust can’t figure out how much space to allocate for recursively defined types, the compiler gives an error with this helpful suggestion:

help: insert some indirection (e.g., a `Box`, `Rc`, or `&`) to break the cycle
  |
2 |     Cons(i32, Box<List>),
  |               ++++    +

In this suggestion, indirection means that instead of storing a value directly, we should change the data structure to store the value indirectly by storing a pointer to the value instead.

Because a Box<T> is a pointer, Rust always knows how much space a Box<T> needs: A pointer’s size doesn’t change based on the amount of data it’s pointing to. This means we can put a Box<T> inside the Cons variant instead of another List value directly. The Box<T> will point to the next List value that will be on the heap rather than inside the Cons variant. Conceptually, we still have a list, created with lists holding other lists, but this implementation is now more like placing the items next to one another rather than inside one another.

We can change the definition of the List enum in Listing 15-2 and the usage of the List in Listing 15-3 to the code in Listing 15-5, which will compile.

Filename: src/main.rs
enum List {
    Cons(i32, Box<List>),
    Nil,
}

use crate::List::{Cons, Nil};

fn main() {
    let list = Cons(1, Box::new(Cons(2, Box::new(Cons(3, Box::new(Nil))))));
}
Listing 15-5: The definition of List that uses Box<T> in order to have a known size

The Cons variant needs the size of an i32 plus the space to store the box’s pointer data. The Nil variant stores no values, so it needs less space on the stack than the Cons variant. We now know that any List value will take up the size of an i32 plus the size of a box’s pointer data. By using a box, we’ve broken the infinite, recursive chain, so the compiler can figure out the size it needs to store a List value. Figure 15-2 shows what the Cons variant looks like now.

A rectangle labeled 'Cons' split into two smaller rectangles. The first smaller rectangle holds the label 'i32', and the second smaller rectangle holds the label 'Box' with one inner rectangle that contains the label 'usize', representing the finite size of the box's pointer.

Figure 15-2: A List that is not infinitely sized, because Cons holds a Box

Boxes provide only the indirection and heap allocation; they don’t have any other special capabilities, like those we’ll see with the other smart pointer types. They also don’t have the performance overhead that these special capabilities incur, so they can be useful in cases like the cons list where the indirection is the only feature we need. We’ll look at more use cases for boxes in Chapter 18.

The Box<T> type is a smart pointer because it implements the Deref trait, which allows Box<T> values to be treated like references. When a Box<T> value goes out of scope, the heap data that the box is pointing to is cleaned up as well because of the Drop trait implementation. These two traits will be even more important to the functionality provided by the other smart pointer types we’ll discuss in the rest of this chapter. Let’s explore these two traits in more detail.

Использование умных указателей как обычных ссылок

Treating Smart Pointers Like Regular References

Implementing the Deref trait allows you to customize the behavior of the dereference operator * (not to be confused with the multiplication or glob operator). By implementing Deref in such a way that a smart pointer can be treated like a regular reference, you can write code that operates on references and use that code with smart pointers too.

Let’s first look at how the dereference operator works with regular references. Then, we’ll try to define a custom type that behaves like Box<T> and see why the dereference operator doesn’t work like a reference on our newly defined type. We’ll explore how implementing the Deref trait makes it possible for smart pointers to work in ways similar to references. Then, we’ll look at Rust’s deref coercion feature and how it lets us work with either references or smart pointers.

Following the Reference to the Value

A regular reference is a type of pointer, and one way to think of a pointer is as an arrow to a value stored somewhere else. In Listing 15-6, we create a reference to an i32 value and then use the dereference operator to follow the reference to the value.

Filename: src/main.rs
fn main() {
    let x = 5;
    let y = &x;

    assert_eq!(5, x);
    assert_eq!(5, *y);
}
Listing 15-6: Using the dereference operator to follow a reference to an i32 value

The variable x holds an i32 value 5. We set y equal to a reference to x. We can assert that x is equal to 5. However, if we want to make an assertion about the value in y, we have to use *y to follow the reference to the value it’s pointing to (hence, dereference) so that the compiler can compare the actual value. Once we dereference y, we have access to the integer value y is pointing to that we can compare with 5.

If we tried to write assert_eq!(5, y); instead, we would get this compilation error:

$ cargo run
   Compiling deref-example v0.1.0 (file:///projects/deref-example)
error[E0277]: can't compare `{integer}` with `&{integer}`
 --> src/main.rs:6:5
  |
6 |     assert_eq!(5, y);
  |     ^^^^^^^^^^^^^^^^ no implementation for `{integer} == &{integer}`
  |
  = help: the trait `PartialEq<&{integer}>` is not implemented for `{integer}`
  = note: this error originates in the macro `assert_eq` (in Nightly builds, run with -Z macro-backtrace for more info)

For more information about this error, try `rustc --explain E0277`.
error: could not compile `deref-example` (bin "deref-example") due to 1 previous error

Comparing a number and a reference to a number isn’t allowed because they’re different types. We must use the dereference operator to follow the reference to the value it’s pointing to.

Using Box<T> Like a Reference

We can rewrite the code in Listing 15-6 to use a Box<T> instead of a reference; the dereference operator used on the Box<T> in Listing 15-7 functions in the same way as the dereference operator used on the reference in Listing 15-6.

Filename: src/main.rs
fn main() {
    let x = 5;
    let y = Box::new(x);

    assert_eq!(5, x);
    assert_eq!(5, *y);
}
Listing 15-7: Using the dereference operator on a Box<i32>

The main difference between Listing 15-7 and Listing 15-6 is that here we set y to be an instance of a box pointing to a copied value of x rather than a reference pointing to the value of x. In the last assertion, we can use the dereference operator to follow the box’s pointer in the same way that we did when y was a reference. Next, we’ll explore what is special about Box<T> that enables us to use the dereference operator by defining our own box type.

Defining Our Own Smart Pointer

Let’s build a wrapper type similar to the Box<T> type provided by the standard library to experience how smart pointer types behave differently from references by default. Then, we’ll look at how to add the ability to use the dereference operator.

Note: There’s one big difference between the MyBox<T> type we’re about to build and the real Box<T>: Our version will not store its data on the heap. We are focusing this example on Deref, so where the data is actually stored is less important than the pointer-like behavior.

The Box<T> type is ultimately defined as a tuple struct with one element, so Listing 15-8 defines a MyBox<T> type in the same way. We’ll also define a new function to match the new function defined on Box<T>.

Filename: src/main.rs
struct MyBox<T>(T);

impl<T> MyBox<T> {
    fn new(x: T) -> MyBox<T> {
        MyBox(x)
    }
}

fn main() {}
Listing 15-8: Defining a MyBox<T> type

We define a struct named MyBox and declare a generic parameter T because we want our type to hold values of any type. The MyBox type is a tuple struct with one element of type T. The MyBox::new function takes one parameter of type T and returns a MyBox instance that holds the value passed in.

Let’s try adding the main function in Listing 15-7 to Listing 15-8 and changing it to use the MyBox<T> type we’ve defined instead of Box<T>. The code in Listing 15-9 won’t compile, because Rust doesn’t know how to dereference MyBox.

Filename: src/main.rs
struct MyBox<T>(T);

impl<T> MyBox<T> {
    fn new(x: T) -> MyBox<T> {
        MyBox(x)
    }
}

fn main() {
    let x = 5;
    let y = MyBox::new(x);

    assert_eq!(5, x);
    assert_eq!(5, *y);
}
Listing 15-9: Attempting to use MyBox<T> in the same way we used references and Box<T>

Here’s the resultant compilation error:

$ cargo run
   Compiling deref-example v0.1.0 (file:///projects/deref-example)
error[E0614]: type `MyBox<{integer}>` cannot be dereferenced
  --> src/main.rs:14:19
   |
14 |     assert_eq!(5, *y);
   |                   ^^ can't be dereferenced

For more information about this error, try `rustc --explain E0614`.
error: could not compile `deref-example` (bin "deref-example") due to 1 previous error

Our MyBox<T> type can’t be dereferenced because we haven’t implemented that ability on our type. To enable dereferencing with the * operator, we implement the Deref trait.

Implementing the Deref Trait

As discussed in “Implementing a Trait on a Type” in Chapter 10, to implement a trait we need to provide implementations for the trait’s required methods. The Deref trait, provided by the standard library, requires us to implement one method named deref that borrows self and returns a reference to the inner data. Listing 15-10 contains an implementation of Deref to add to the definition of MyBox<T>.

Filename: src/main.rs
use std::ops::Deref;

impl<T> Deref for MyBox<T> {
    type Target = T;

    fn deref(&self) -> &Self::Target {
        &self.0
    }
}

struct MyBox<T>(T);

impl<T> MyBox<T> {
    fn new(x: T) -> MyBox<T> {
        MyBox(x)
    }
}

fn main() {
    let x = 5;
    let y = MyBox::new(x);

    assert_eq!(5, x);
    assert_eq!(5, *y);
}
Listing 15-10: Implementing Deref on MyBox<T>

The type Target = T; syntax defines an associated type for the Deref trait to use. Associated types are a slightly different way of declaring a generic parameter, but you don’t need to worry about them for now; we’ll cover them in more detail in Chapter 20.

We fill in the body of the deref method with &self.0 so that deref returns a reference to the value we want to access with the * operator; recall from “Creating Different Types with Tuple Structs” in Chapter 5 that .0 accesses the first value in a tuple struct. The main function in Listing 15-9 that calls * on the MyBox<T> value now compiles, and the assertions pass!

Without the Deref trait, the compiler can only dereference & references. The deref method gives the compiler the ability to take a value of any type that implements Deref and call the deref method to get a reference that it knows how to dereference.

When we entered *y in Listing 15-9, behind the scenes Rust actually ran this code:

*(y.deref())

Rust substitutes the * operator with a call to the deref method and then a plain dereference so that we don’t have to think about whether or not we need to call the deref method. This Rust feature lets us write code that functions identically whether we have a regular reference or a type that implements Deref.

The reason the deref method returns a reference to a value, and that the plain dereference outside the parentheses in *(y.deref()) is still necessary, has to do with the ownership system. If the deref method returned the value directly instead of a reference to the value, the value would be moved out of self. We don’t want to take ownership of the inner value inside MyBox<T> in this case or in most cases where we use the dereference operator.

Note that the * operator is replaced with a call to the deref method and then a call to the * operator just once, each time we use a * in our code. Because the substitution of the * operator does not recurse infinitely, we end up with data of type i32, which matches the 5 in assert_eq! in Listing 15-9.

Using Deref Coercion in Functions and Methods

Deref coercion converts a reference to a type that implements the Deref trait into a reference to another type. For example, deref coercion can convert &String to &str because String implements the Deref trait such that it returns &str. Deref coercion is a convenience Rust performs on arguments to functions and methods, and it works only on types that implement the Deref trait. It happens automatically when we pass a reference to a particular type’s value as an argument to a function or method that doesn’t match the parameter type in the function or method definition. A sequence of calls to the deref method converts the type we provided into the type the parameter needs.

Deref coercion was added to Rust so that programmers writing function and method calls don’t need to add as many explicit references and dereferences with & and *. The deref coercion feature also lets us write more code that can work for either references or smart pointers.

To see deref coercion in action, let’s use the MyBox<T> type we defined in Listing 15-8 as well as the implementation of Deref that we added in Listing 15-10. Listing 15-11 shows the definition of a function that has a string slice parameter.

Filename: src/main.rs
fn hello(name: &str) {
    println!("Hello, {name}!");
}

fn main() {}
Listing 15-11: A hello function that has the parameter name of type &str

We can call the hello function with a string slice as an argument, such as hello("Rust");, for example. Deref coercion makes it possible to call hello with a reference to a value of type MyBox<String>, as shown in Listing 15-12.

Filename: src/main.rs
use std::ops::Deref;

impl<T> Deref for MyBox<T> {
    type Target = T;

    fn deref(&self) -> &T {
        &self.0
    }
}

struct MyBox<T>(T);

impl<T> MyBox<T> {
    fn new(x: T) -> MyBox<T> {
        MyBox(x)
    }
}

fn hello(name: &str) {
    println!("Hello, {name}!");
}

fn main() {
    let m = MyBox::new(String::from("Rust"));
    hello(&m);
}
Listing 15-12: Calling hello with a reference to a MyBox<String> value, which works because of deref coercion

Here we’re calling the hello function with the argument &m, which is a reference to a MyBox<String> value. Because we implemented the Deref trait on MyBox<T> in Listing 15-10, Rust can turn &MyBox<String> into &String by calling deref. The standard library provides an implementation of Deref on String that returns a string slice, and this is in the API documentation for Deref. Rust calls deref again to turn the &String into &str, which matches the hello function’s definition.

If Rust didn’t implement deref coercion, we would have to write the code in Listing 15-13 instead of the code in Listing 15-12 to call hello with a value of type &MyBox<String>.

Filename: src/main.rs
use std::ops::Deref;

impl<T> Deref for MyBox<T> {
    type Target = T;

    fn deref(&self) -> &T {
        &self.0
    }
}

struct MyBox<T>(T);

impl<T> MyBox<T> {
    fn new(x: T) -> MyBox<T> {
        MyBox(x)
    }
}

fn hello(name: &str) {
    println!("Hello, {name}!");
}

fn main() {
    let m = MyBox::new(String::from("Rust"));
    hello(&(*m)[..]);
}
Listing 15-13: The code we would have to write if Rust didn’t have deref coercion

The (*m) dereferences the MyBox<String> into a String. Then, the & and [..] take a string slice of the String that is equal to the whole string to match the signature of hello. This code without deref coercions is harder to read, write, and understand with all of these symbols involved. Deref coercion allows Rust to handle these conversions for us automatically.

When the Deref trait is defined for the types involved, Rust will analyze the types and use Deref::deref as many times as necessary to get a reference to match the parameter’s type. The number of times that Deref::deref needs to be inserted is resolved at compile time, so there is no runtime penalty for taking advantage of deref coercion!

Handling Deref Coercion with Mutable References

Similar to how you use the Deref trait to override the * operator on immutable references, you can use the DerefMut trait to override the * operator on mutable references.

Rust does deref coercion when it finds types and trait implementations in three cases:

  1. From &T to &U when T: Deref<Target=U>
  2. From &mut T to &mut U when T: DerefMut<Target=U>
  3. From &mut T to &U when T: Deref<Target=U>

The first two cases are the same except that the second implements mutability. The first case states that if you have a &T, and T implements Deref to some type U, you can get a &U transparently. The second case states that the same deref coercion happens for mutable references.

The third case is trickier: Rust will also coerce a mutable reference to an immutable one. But the reverse is not possible: Immutable references will never coerce to mutable references. Because of the borrowing rules, if you have a mutable reference, that mutable reference must be the only reference to that data (otherwise, the program wouldn’t compile). Converting one mutable reference to one immutable reference will never break the borrowing rules. Converting an immutable reference to a mutable reference would require that the initial immutable reference is the only immutable reference to that data, but the borrowing rules don’t guarantee that. Therefore, Rust can’t make the assumption that converting an immutable reference to a mutable reference is possible.

Выполнение кода при очистке с помощью трейта Drop

Running Code on Cleanup with the Drop Trait

The second trait important to the smart pointer pattern is Drop, which lets you customize what happens when a value is about to go out of scope. You can provide an implementation for the Drop trait on any type, and that code can be used to release resources like files or network connections.

We’re introducing Drop in the context of smart pointers because the functionality of the Drop trait is almost always used when implementing a smart pointer. For example, when a Box<T> is dropped, it will deallocate the space on the heap that the box points to.

In some languages, for some types, the programmer must call code to free memory or resources every time they finish using an instance of those types. Examples include file handles, sockets, and locks. If the programmer forgets, the system might become overloaded and crash. In Rust, you can specify that a particular bit of code be run whenever a value goes out of scope, and the compiler will insert this code automatically. As a result, you don’t need to be careful about placing cleanup code everywhere in a program that an instance of a particular type is finished with—you still won’t leak resources!

You specify the code to run when a value goes out of scope by implementing the Drop trait. The Drop trait requires you to implement one method named drop that takes a mutable reference to self. To see when Rust calls drop, let’s implement drop with println! statements for now.

Listing 15-14 shows a CustomSmartPointer struct whose only custom functionality is that it will print Dropping CustomSmartPointer! when the instance goes out of scope, to show when Rust runs the drop method.

Filename: src/main.rs
struct CustomSmartPointer {
    data: String,
}

impl Drop for CustomSmartPointer {
    fn drop(&mut self) {
        println!("Dropping CustomSmartPointer with data `{}`!", self.data);
    }
}

fn main() {
    let c = CustomSmartPointer {
        data: String::from("my stuff"),
    };
    let d = CustomSmartPointer {
        data: String::from("other stuff"),
    };
    println!("CustomSmartPointers created");
}
Listing 15-14: A CustomSmartPointer struct that implements the Drop trait where we would put our cleanup code

The Drop trait is included in the prelude, so we don’t need to bring it into scope. We implement the Drop trait on CustomSmartPointer and provide an implementation for the drop method that calls println!. The body of the drop method is where you would place any logic that you wanted to run when an instance of your type goes out of scope. We’re printing some text here to demonstrate visually when Rust will call drop.

In main, we create two instances of CustomSmartPointer and then print CustomSmartPointers created. At the end of main, our instances of CustomSmartPointer will go out of scope, and Rust will call the code we put in the drop method, printing our final message. Note that we didn’t need to call the drop method explicitly.

When we run this program, we’ll see the following output:

$ cargo run
   Compiling drop-example v0.1.0 (file:///projects/drop-example)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.60s
     Running `target/debug/drop-example`
CustomSmartPointers created
Dropping CustomSmartPointer with data `other stuff`!
Dropping CustomSmartPointer with data `my stuff`!

Rust automatically called drop for us when our instances went out of scope, calling the code we specified. Variables are dropped in the reverse order of their creation, so d was dropped before c. This example’s purpose is to give you a visual guide to how the drop method works; usually you would specify the cleanup code that your type needs to run rather than a print message.

Unfortunately, it’s not straightforward to disable the automatic drop functionality. Disabling drop isn’t usually necessary; the whole point of the Drop trait is that it’s taken care of automatically. Occasionally, however, you might want to clean up a value early. One example is when using smart pointers that manage locks: You might want to force the drop method that releases the lock so that other code in the same scope can acquire the lock. Rust doesn’t let you call the Drop trait’s drop method manually; instead, you have to call the std::mem::drop function provided by the standard library if you want to force a value to be dropped before the end of its scope.

Trying to call the Drop trait’s drop method manually by modifying the main function from Listing 15-14 won’t work, as shown in Listing 15-15.

Filename: src/main.rs
struct CustomSmartPointer {
    data: String,
}

impl Drop for CustomSmartPointer {
    fn drop(&mut self) {
        println!("Dropping CustomSmartPointer with data `{}`!", self.data);
    }
}

fn main() {
    let c = CustomSmartPointer {
        data: String::from("some data"),
    };
    println!("CustomSmartPointer created");
    c.drop();
    println!("CustomSmartPointer dropped before the end of main");
}
Listing 15-15: Attempting to call the drop method from the Drop trait manually to clean up early

When we try to compile this code, we’ll get this error:

$ cargo run
   Compiling drop-example v0.1.0 (file:///projects/drop-example)
error[E0040]: explicit use of destructor method
  --> src/main.rs:16:7
   |
16 |     c.drop();
   |       ^^^^ explicit destructor calls not allowed
   |
help: consider using `drop` function
   |
16 -     c.drop();
16 +     drop(c);
   |

For more information about this error, try `rustc --explain E0040`.
error: could not compile `drop-example` (bin "drop-example") due to 1 previous error

This error message states that we’re not allowed to explicitly call drop. The error message uses the term destructor, which is the general programming term for a function that cleans up an instance. A destructor is analogous to a constructor, which creates an instance. The drop function in Rust is one particular destructor.

Rust doesn’t let us call drop explicitly, because Rust would still automatically call drop on the value at the end of main. This would cause a double free error because Rust would be trying to clean up the same value twice.

We can’t disable the automatic insertion of drop when a value goes out of scope, and we can’t call the drop method explicitly. So, if we need to force a value to be cleaned up early, we use the std::mem::drop function.

The std::mem::drop function is different from the drop method in the Drop trait. We call it by passing as an argument the value we want to force-drop. The function is in the prelude, so we can modify main in Listing 15-15 to call the drop function, as shown in Listing 15-16.

Filename: src/main.rs
struct CustomSmartPointer {
    data: String,
}

impl Drop for CustomSmartPointer {
    fn drop(&mut self) {
        println!("Dropping CustomSmartPointer with data `{}`!", self.data);
    }
}

fn main() {
    let c = CustomSmartPointer {
        data: String::from("some data"),
    };
    println!("CustomSmartPointer created");
    drop(c);
    println!("CustomSmartPointer dropped before the end of main");
}
Listing 15-16: Calling std::mem::drop to explicitly drop a value before it goes out of scope

Running this code will print the following:

$ cargo run
   Compiling drop-example v0.1.0 (file:///projects/drop-example)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.73s
     Running `target/debug/drop-example`
CustomSmartPointer created
Dropping CustomSmartPointer with data `some data`!
CustomSmartPointer dropped before the end of main

The text Dropping CustomSmartPointer with data `some data`! is printed between the CustomSmartPointer created and CustomSmartPointer dropped before the end of main text, showing that the drop method code is called to drop c at that point.

You can use code specified in a Drop trait implementation in many ways to make cleanup convenient and safe: For instance, you could use it to create your own memory allocator! With the Drop trait and Rust’s ownership system, you don’t have to remember to clean up, because Rust does it automatically.

You also don’t have to worry about problems resulting from accidentally cleaning up values still in use: The ownership system that makes sure references are always valid also ensures that drop gets called only once when the value is no longer being used.

Now that we’ve examined Box<T> and some of the characteristics of smart pointers, let’s look at a few other smart pointers defined in the standard library.

Rc<T> — умный указатель с подсчётом ссылок

Rc<T>, the Reference-Counted Smart Pointer

In the majority of cases, ownership is clear: You know exactly which variable owns a given value. However, there are cases when a single value might have multiple owners. For example, in graph data structures, multiple edges might point to the same node, and that node is conceptually owned by all of the edges that point to it. A node shouldn’t be cleaned up unless it doesn’t have any edges pointing to it and so has no owners.

You have to enable multiple ownership explicitly by using the Rust type Rc<T>, which is an abbreviation for reference counting. The Rc<T> type keeps track of the number of references to a value to determine whether or not the value is still in use. If there are zero references to a value, the value can be cleaned up without any references becoming invalid.

Imagine Rc<T> as a TV in a family room. When one person enters to watch TV, they turn it on. Others can come into the room and watch the TV. When the last person leaves the room, they turn off the TV because it’s no longer being used. If someone turns off the TV while others are still watching it, there would be an uproar from the remaining TV watchers!

We use the Rc<T> type when we want to allocate some data on the heap for multiple parts of our program to read and we can’t determine at compile time which part will finish using the data last. If we knew which part would finish last, we could just make that part the data’s owner, and the normal ownership rules enforced at compile time would take effect.

Note that Rc<T> is only for use in single-threaded scenarios. When we discuss concurrency in Chapter 16, we’ll cover how to do reference counting in multithreaded programs.

Sharing Data

Let’s return to our cons list example in Listing 15-5. Recall that we defined it using Box<T>. This time, we’ll create two lists that both share ownership of a third list. Conceptually, this looks similar to Figure 15-3.

A linked list with the label 'a' pointing to three elements. The first element contains the integer 5 and points to the second element. Th
e second element contains the integer 10 and points to the third element. The third element contains the value 'Nil' that signifies the end of the l
ist; it does not point anywhere. A linked list with the label 'b' points to an element that contains the integer 3 and points to the first element o
f list 'a'. A linked list with the label 'c' points to an element that contains the integer 4 and also points to the first element of list 'a' so th
at the tails of lists 'b' and 'c' are both list 'a'.

Figure 15-3: Two lists, b and c, sharing ownership of a third list, a

We’ll create list a that contains 5 and then 10. Then, we’ll make two more lists: b that starts with 3 and c that starts with 4. Both the b and c lists will then continue on to the first a list containing 5 and 10. In other words, both lists will share the first list containing 5 and 10.

Trying to implement this scenario using our definition of List with Box<T> won’t work, as shown in Listing 15-17.

Filename: src/main.rs
enum List {
    Cons(i32, Box<List>),
    Nil,
}

use crate::List::{Cons, Nil};

fn main() {
    let a = Cons(5, Box::new(Cons(10, Box::new(Nil))));
    let b = Cons(3, Box::new(a));
    let c = Cons(4, Box::new(a));
}
Listing 15-17: Demonstrating that we’re not allowed to have two lists using Box<T> that try to share ownership of a third list

When we compile this code, we get this error:

$ cargo run
   Compiling cons-list v0.1.0 (file:///projects/cons-list)
error[E0382]: use of moved value: `a`
  --> src/main.rs:11:30
   |
 9 |     let a = Cons(5, Box::new(Cons(10, Box::new(Nil))));
   |         - move occurs because `a` has type `List`, which does not implement the `Copy` trait
10 |     let b = Cons(3, Box::new(a));
   |                              - value moved here
11 |     let c = Cons(4, Box::new(a));
   |                              ^ value used here after move
   |
note: if `List` implemented `Clone`, you could clone the value
  --> src/main.rs:1:1
   |
 1 | enum List {
   | ^^^^^^^^^ consider implementing `Clone` for this type
...
10 |     let b = Cons(3, Box::new(a));
   |                              - you could clone this value

For more information about this error, try `rustc --explain E0382`.
error: could not compile `cons-list` (bin "cons-list") due to 1 previous error

The Cons variants own the data they hold, so when we create the b list, a is moved into b and b owns a. Then, when we try to use a again when creating c, we’re not allowed to because a has been moved.

We could change the definition of Cons to hold references instead, but then we would have to specify lifetime parameters. By specifying lifetime parameters, we would be specifying that every element in the list will live at least as long as the entire list. This is the case for the elements and lists in Listing 15-17, but not in every scenario.

Instead, we’ll change our definition of List to use Rc<T> in place of Box<T>, as shown in Listing 15-18. Each Cons variant will now hold a value and an Rc<T> pointing to a List. When we create b, instead of taking ownership of a, we’ll clone the Rc<List> that a is holding, thereby increasing the number of references from one to two and letting a and b share ownership of the data in that Rc<List>. We’ll also clone a when creating c, increasing the number of references from two to three. Every time we call Rc::clone, the reference count to the data within the Rc<List> will increase, and the data won’t be cleaned up unless there are zero references to it.

Filename: src/main.rs
enum List {
    Cons(i32, Rc<List>),
    Nil,
}

use crate::List::{Cons, Nil};
use std::rc::Rc;

fn main() {
    let a = Rc::new(Cons(5, Rc::new(Cons(10, Rc::new(Nil)))));
    let b = Cons(3, Rc::clone(&a));
    let c = Cons(4, Rc::clone(&a));
}
Listing 15-18: A definition of List that uses Rc<T>

We need to add a use statement to bring Rc<T> into scope because it’s not in the prelude. In main, we create the list holding 5 and 10 and store it in a new Rc<List> in a. Then, when we create b and c, we call the Rc::clone function and pass a reference to the Rc<List> in a as an argument.

We could have called a.clone() rather than Rc::clone(&a), but Rust’s convention is to use Rc::clone in this case. The implementation of Rc::clone doesn’t make a deep copy of all the data like most types’ implementations of clone do. The call to Rc::clone only increments the reference count, which doesn’t take much time. Deep copies of data can take a lot of time. By using Rc::clone for reference counting, we can visually distinguish between the deep-copy kinds of clones and the kinds of clones that increase the reference count. When looking for performance problems in the code, we only need to consider the deep-copy clones and can disregard calls to Rc::clone.

Cloning to Increase the Reference Count

Let’s change our working example in Listing 15-18 so that we can see the reference counts changing as we create and drop references to the Rc<List> in a.

In Listing 15-19, we’ll change main so that it has an inner scope around list c; then, we can see how the reference count changes when c goes out of scope.

Filename: src/main.rs
enum List {
    Cons(i32, Rc<List>),
    Nil,
}

use crate::List::{Cons, Nil};
use std::rc::Rc;

// --snip--

fn main() {
    let a = Rc::new(Cons(5, Rc::new(Cons(10, Rc::new(Nil)))));
    println!("count after creating a = {}", Rc::strong_count(&a));
    let b = Cons(3, Rc::clone(&a));
    println!("count after creating b = {}", Rc::strong_count(&a));
    {
        let c = Cons(4, Rc::clone(&a));
        println!("count after creating c = {}", Rc::strong_count(&a));
    }
    println!("count after c goes out of scope = {}", Rc::strong_count(&a));
}
Listing 15-19: Printing the reference count

At each point in the program where the reference count changes, we print the reference count, which we get by calling the Rc::strong_count function. This function is named strong_count rather than count because the Rc<T> type also has a weak_count; we’ll see what weak_count is used for in “Preventing Reference Cycles Using Weak<T>.

This code prints the following:

$ cargo run
   Compiling cons-list v0.1.0 (file:///projects/cons-list)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.45s
     Running `target/debug/cons-list`
count after creating a = 1
count after creating b = 2
count after creating c = 3
count after c goes out of scope = 2

We can see that the Rc<List> in a has an initial reference count of 1; then, each time we call clone, the count goes up by 1. When c goes out of scope, the count goes down by 1. We don’t have to call a function to decrease the reference count like we have to call Rc::clone to increase the reference count: The implementation of the Drop trait decreases the reference count automatically when an Rc<T> value goes out of scope.

What we can’t see in this example is that when b and then a go out of scope at the end of main, the count is 0, and the Rc<List> is cleaned up completely. Using Rc<T> allows a single value to have multiple owners, and the count ensures that the value remains valid as long as any of the owners still exist.

Via immutable references, Rc<T> allows you to share data between multiple parts of your program for reading only. If Rc<T> allowed you to have multiple mutable references too, you might violate one of the borrowing rules discussed in Chapter 4: Multiple mutable borrows to the same place can cause data races and inconsistencies. But being able to mutate data is very useful! In the next section, we’ll discuss the interior mutability pattern and the RefCell<T> type that you can use in conjunction with an Rc<T> to work with this immutability restriction.

RefCell<T> и паттерн внутренней изменяемости

RefCell<T> and the Interior Mutability Pattern

Interior mutability is a design pattern in Rust that allows you to mutate data even when there are immutable references to that data; normally, this action is disallowed by the borrowing rules. To mutate data, the pattern uses unsafe code inside a data structure to bend Rust’s usual rules that govern mutation and borrowing. Unsafe code indicates to the compiler that we’re checking the rules manually instead of relying on the compiler to check them for us; we will discuss unsafe code more in Chapter 20.

We can use types that use the interior mutability pattern only when we can ensure that the borrowing rules will be followed at runtime, even though the compiler can’t guarantee that. The unsafe code involved is then wrapped in a safe API, and the outer type is still immutable.

Let’s explore this concept by looking at the RefCell<T> type that follows the interior mutability pattern.

Enforcing Borrowing Rules at Runtime

Unlike Rc<T>, the RefCell<T> type represents single ownership over the data it holds. So, what makes RefCell<T> different from a type like Box<T>? Recall the borrowing rules you learned in Chapter 4:

  • At any given time, you can have either one mutable reference or any number of immutable references (but not both).
  • References must always be valid.

With references and Box<T>, the borrowing rules’ invariants are enforced at compile time. With RefCell<T>, these invariants are enforced at runtime. With references, if you break these rules, you’ll get a compiler error. With RefCell<T>, if you break these rules, your program will panic and exit.

The advantages of checking the borrowing rules at compile time are that errors will be caught sooner in the development process, and there is no impact on runtime performance because all the analysis is completed beforehand. For those reasons, checking the borrowing rules at compile time is the best choice in the majority of cases, which is why this is Rust’s default.

The advantage of checking the borrowing rules at runtime instead is that certain memory-safe scenarios are then allowed, where they would’ve been disallowed by the compile-time checks. Static analysis, like the Rust compiler, is inherently conservative. Some properties of code are impossible to detect by analyzing the code: The most famous example is the Halting Problem, which is beyond the scope of this book but is an interesting topic to research.

Because some analysis is impossible, if the Rust compiler can’t be sure the code complies with the ownership rules, it might reject a correct program; in this way, it’s conservative. If Rust accepted an incorrect program, users wouldn’t be able to trust the guarantees Rust makes. However, if Rust rejects a correct program, the programmer will be inconvenienced, but nothing catastrophic can occur. The RefCell<T> type is useful when you’re sure your code follows the borrowing rules but the compiler is unable to understand and guarantee that.

Similar to Rc<T>, RefCell<T> is only for use in single-threaded scenarios and will give you a compile-time error if you try using it in a multithreaded context. We’ll talk about how to get the functionality of RefCell<T> in a multithreaded program in Chapter 16.

Here is a recap of the reasons to choose Box<T>, Rc<T>, or RefCell<T>:

  • Rc<T> enables multiple owners of the same data; Box<T> and RefCell<T> have single owners.
  • Box<T> allows immutable or mutable borrows checked at compile time; Rc<T> allows only immutable borrows checked at compile time; RefCell<T> allows immutable or mutable borrows checked at runtime.
  • Because RefCell<T> allows mutable borrows checked at runtime, you can mutate the value inside the RefCell<T> even when the RefCell<T> is immutable.

Mutating the value inside an immutable value is the interior mutability pattern. Let’s look at a situation in which interior mutability is useful and examine how it’s possible.

Using Interior Mutability

A consequence of the borrowing rules is that when you have an immutable value, you can’t borrow it mutably. For example, this code won’t compile:

fn main() {
    let x = 5;
    let y = &mut x;
}

If you tried to compile this code, you’d get the following error:

$ cargo run
   Compiling borrowing v0.1.0 (file:///projects/borrowing)
error[E0596]: cannot borrow `x` as mutable, as it is not declared as mutable
 --> src/main.rs:3:13
  |
3 |     let y = &mut x;
  |             ^^^^^^ cannot borrow as mutable
  |
help: consider changing this to be mutable
  |
2 |     let mut x = 5;
  |         +++

For more information about this error, try `rustc --explain E0596`.
error: could not compile `borrowing` (bin "borrowing") due to 1 previous error

However, there are situations in which it would be useful for a value to mutate itself in its methods but appear immutable to other code. Code outside the value’s methods would not be able to mutate the value. Using RefCell<T> is one way to get the ability to have interior mutability, but RefCell<T> doesn’t get around the borrowing rules completely: The borrow checker in the compiler allows this interior mutability, and the borrowing rules are checked at runtime instead. If you violate the rules, you’ll get a panic! instead of a compiler error.

Let’s work through a practical example where we can use RefCell<T> to mutate an immutable value and see why that is useful.

Testing with Mock Objects

Sometimes during testing a programmer will use a type in place of another type, in order to observe particular behavior and assert that it’s implemented correctly. This placeholder type is called a test double. Think of it in the sense of a stunt double in filmmaking, where a person steps in and substitutes for an actor to do a particularly tricky scene. Test doubles stand in for other types when we’re running tests. Mock objects are specific types of test doubles that record what happens during a test so that you can assert that the correct actions took place.

Rust doesn’t have objects in the same sense as other languages have objects, and Rust doesn’t have mock object functionality built into the standard library as some other languages do. However, you can definitely create a struct that will serve the same purposes as a mock object.

Here’s the scenario we’ll test: We’ll create a library that tracks a value against a maximum value and sends messages based on how close to the maximum value the current value is. This library could be used to keep track of a user’s quota for the number of API calls they’re allowed to make, for example.

Our library will only provide the functionality of tracking how close to the maximum a value is and what the messages should be at what times. Applications that use our library will be expected to provide the mechanism for sending the messages: The application could show the message to the user directly, send an email, send a text message, or do something else. The library doesn’t need to know that detail. All it needs is something that implements a trait we’ll provide, called Messenger. Listing 15-20 shows the library code.

Filename: src/lib.rs
pub trait Messenger {
    fn send(&self, msg: &str);
}

pub struct LimitTracker<'a, T: Messenger> {
    messenger: &'a T,
    value: usize,
    max: usize,
}

impl<'a, T> LimitTracker<'a, T>
where
    T: Messenger,
{
    pub fn new(messenger: &'a T, max: usize) -> LimitTracker<'a, T> {
        LimitTracker {
            messenger,
            value: 0,
            max,
        }
    }

    pub fn set_value(&mut self, value: usize) {
        self.value = value;

        let percentage_of_max = self.value as f64 / self.max as f64;

        if percentage_of_max >= 1.0 {
            self.messenger.send("Error: You are over your quota!");
        } else if percentage_of_max >= 0.9 {
            self.messenger
                .send("Urgent warning: You've used up over 90% of your quota!");
        } else if percentage_of_max >= 0.75 {
            self.messenger
                .send("Warning: You've used up over 75% of your quota!");
        }
    }
}
Listing 15-20: A library to keep track of how close a value is to a maximum value and warn when the value is at certain levels

One important part of this code is that the Messenger trait has one method called send that takes an immutable reference to self and the text of the message. This trait is the interface our mock object needs to implement so that the mock can be used in the same way a real object is. The other important part is that we want to test the behavior of the set_value method on the LimitTracker. We can change what we pass in for the value parameter, but set_value doesn’t return anything for us to make assertions on. We want to be able to say that if we create a LimitTracker with something that implements the Messenger trait and a particular value for max, the messenger is told to send the appropriate messages when we pass different numbers for value.

We need a mock object that, instead of sending an email or text message when we call send, will only keep track of the messages it’s told to send. We can create a new instance of the mock object, create a LimitTracker that uses the mock object, call the set_value method on LimitTracker, and then check that the mock object has the messages we expect. Listing 15-21 shows an attempt to implement a mock object to do just that, but the borrow checker won’t allow it.

Filename: src/lib.rs
pub trait Messenger {
    fn send(&self, msg: &str);
}

pub struct LimitTracker<'a, T: Messenger> {
    messenger: &'a T,
    value: usize,
    max: usize,
}

impl<'a, T> LimitTracker<'a, T>
where
    T: Messenger,
{
    pub fn new(messenger: &'a T, max: usize) -> LimitTracker<'a, T> {
        LimitTracker {
            messenger,
            value: 0,
            max,
        }
    }

    pub fn set_value(&mut self, value: usize) {
        self.value = value;

        let percentage_of_max = self.value as f64 / self.max as f64;

        if percentage_of_max >= 1.0 {
            self.messenger.send("Error: You are over your quota!");
        } else if percentage_of_max >= 0.9 {
            self.messenger
                .send("Urgent warning: You've used up over 90% of your quota!");
        } else if percentage_of_max >= 0.75 {
            self.messenger
                .send("Warning: You've used up over 75% of your quota!");
        }
    }
}

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

    struct MockMessenger {
        sent_messages: Vec<String>,
    }

    impl MockMessenger {
        fn new() -> MockMessenger {
            MockMessenger {
                sent_messages: vec![],
            }
        }
    }

    impl Messenger for MockMessenger {
        fn send(&self, message: &str) {
            self.sent_messages.push(String::from(message));
        }
    }

    #[test]
    fn it_sends_an_over_75_percent_warning_message() {
        let mock_messenger = MockMessenger::new();
        let mut limit_tracker = LimitTracker::new(&mock_messenger, 100);

        limit_tracker.set_value(80);

        assert_eq!(mock_messenger.sent_messages.len(), 1);
    }
}
Listing 15-21: An attempt to implement a MockMessenger that isn’t allowed by the borrow checker

This test code defines a MockMessenger struct that has a sent_messages field with a Vec of String values to keep track of the messages it’s told to send. We also define an associated function new to make it convenient to create new MockMessenger values that start with an empty list of messages. We then implement the Messenger trait for MockMessenger so that we can give a MockMessenger to a LimitTracker. In the definition of the send method, we take the message passed in as a parameter and store it in the MockMessenger list of sent_messages.

In the test, we’re testing what happens when the LimitTracker is told to set value to something that is more than 75 percent of the max value. First, we create a new MockMessenger, which will start with an empty list of messages. Then, we create a new LimitTracker and give it a reference to the new MockMessenger and a max value of 100. We call the set_value method on the LimitTracker with a value of 80, which is more than 75 percent of 100. Then, we assert that the list of messages that the MockMessenger is keeping track of should now have one message in it.

However, there’s one problem with this test, as shown here:

$ cargo test
   Compiling limit-tracker v0.1.0 (file:///projects/limit-tracker)
error[E0596]: cannot borrow `self.sent_messages` as mutable, as it is behind a `&` reference
  --> src/lib.rs:58:13
   |
58 |             self.sent_messages.push(String::from(message));
   |             ^^^^^^^^^^^^^^^^^^ `self` is a `&` reference, so the data it refers to cannot be borrowed as mutable
   |
help: consider changing this to be a mutable reference in the `impl` method and the `trait` definition
   |
 2 ~     fn send(&mut self, msg: &str);
 3 | }
...
56 |     impl Messenger for MockMessenger {
57 ~         fn send(&mut self, message: &str) {
   |

For more information about this error, try `rustc --explain E0596`.
error: could not compile `limit-tracker` (lib test) due to 1 previous error

We can’t modify the MockMessenger to keep track of the messages, because the send method takes an immutable reference to self. We also can’t take the suggestion from the error text to use &mut self in both the impl method and the trait definition. We do not want to change the Messenger trait solely for the sake of testing. Instead, we need to find a way to make our test code work correctly with our existing design.

This is a situation in which interior mutability can help! We’ll store the sent_messages within a RefCell<T>, and then the send method will be able to modify sent_messages to store the messages we’ve seen. Listing 15-22 shows what that looks like.

Filename: src/lib.rs
pub trait Messenger {
    fn send(&self, msg: &str);
}

pub struct LimitTracker<'a, T: Messenger> {
    messenger: &'a T,
    value: usize,
    max: usize,
}

impl<'a, T> LimitTracker<'a, T>
where
    T: Messenger,
{
    pub fn new(messenger: &'a T, max: usize) -> LimitTracker<'a, T> {
        LimitTracker {
            messenger,
            value: 0,
            max,
        }
    }

    pub fn set_value(&mut self, value: usize) {
        self.value = value;

        let percentage_of_max = self.value as f64 / self.max as f64;

        if percentage_of_max >= 1.0 {
            self.messenger.send("Error: You are over your quota!");
        } else if percentage_of_max >= 0.9 {
            self.messenger
                .send("Urgent warning: You've used up over 90% of your quota!");
        } else if percentage_of_max >= 0.75 {
            self.messenger
                .send("Warning: You've used up over 75% of your quota!");
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::cell::RefCell;

    struct MockMessenger {
        sent_messages: RefCell<Vec<String>>,
    }

    impl MockMessenger {
        fn new() -> MockMessenger {
            MockMessenger {
                sent_messages: RefCell::new(vec![]),
            }
        }
    }

    impl Messenger for MockMessenger {
        fn send(&self, message: &str) {
            self.sent_messages.borrow_mut().push(String::from(message));
        }
    }

    #[test]
    fn it_sends_an_over_75_percent_warning_message() {
        // --snip--
        let mock_messenger = MockMessenger::new();
        let mut limit_tracker = LimitTracker::new(&mock_messenger, 100);

        limit_tracker.set_value(80);

        assert_eq!(mock_messenger.sent_messages.borrow().len(), 1);
    }
}
Listing 15-22: Using RefCell<T> to mutate an inner value while the outer value is considered immutable

The sent_messages field is now of type RefCell<Vec<String>> instead of Vec<String>. In the new function, we create a new RefCell<Vec<String>> instance around the empty vector.

For the implementation of the send method, the first parameter is still an immutable borrow of self, which matches the trait definition. We call borrow_mut on the RefCell<Vec<String>> in self.sent_messages to get a mutable reference to the value inside the RefCell<Vec<String>>, which is the vector. Then, we can call push on the mutable reference to the vector to keep track of the messages sent during the test.

The last change we have to make is in the assertion: To see how many items are in the inner vector, we call borrow on the RefCell<Vec<String>> to get an immutable reference to the vector.

Now that you’ve seen how to use RefCell<T>, let’s dig into how it works!

Tracking Borrows at Runtime

When creating immutable and mutable references, we use the & and &mut syntax, respectively. With RefCell<T>, we use the borrow and borrow_mut methods, which are part of the safe API that belongs to RefCell<T>. The borrow method returns the smart pointer type Ref<T>, and borrow_mut returns the smart pointer type RefMut<T>. Both types implement Deref, so we can treat them like regular references.

The RefCell<T> keeps track of how many Ref<T> and RefMut<T> smart pointers are currently active. Every time we call borrow, the RefCell<T> increases its count of how many immutable borrows are active. When a Ref<T> value goes out of scope, the count of immutable borrows goes down by 1. Just like the compile-time borrowing rules, RefCell<T> lets us have many immutable borrows or one mutable borrow at any point in time.

If we try to violate these rules, rather than getting a compiler error as we would with references, the implementation of RefCell<T> will panic at runtime. Listing 15-23 shows a modification of the implementation of send in Listing 15-22. We’re deliberately trying to create two mutable borrows active for the same scope to illustrate that RefCell<T> prevents us from doing this at runtime.

Filename: src/lib.rs
pub trait Messenger {
    fn send(&self, msg: &str);
}

pub struct LimitTracker<'a, T: Messenger> {
    messenger: &'a T,
    value: usize,
    max: usize,
}

impl<'a, T> LimitTracker<'a, T>
where
    T: Messenger,
{
    pub fn new(messenger: &'a T, max: usize) -> LimitTracker<'a, T> {
        LimitTracker {
            messenger,
            value: 0,
            max,
        }
    }

    pub fn set_value(&mut self, value: usize) {
        self.value = value;

        let percentage_of_max = self.value as f64 / self.max as f64;

        if percentage_of_max >= 1.0 {
            self.messenger.send("Error: You are over your quota!");
        } else if percentage_of_max >= 0.9 {
            self.messenger
                .send("Urgent warning: You've used up over 90% of your quota!");
        } else if percentage_of_max >= 0.75 {
            self.messenger
                .send("Warning: You've used up over 75% of your quota!");
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::cell::RefCell;

    struct MockMessenger {
        sent_messages: RefCell<Vec<String>>,
    }

    impl MockMessenger {
        fn new() -> MockMessenger {
            MockMessenger {
                sent_messages: RefCell::new(vec![]),
            }
        }
    }

    impl Messenger for MockMessenger {
        fn send(&self, message: &str) {
            let mut one_borrow = self.sent_messages.borrow_mut();
            let mut two_borrow = self.sent_messages.borrow_mut();

            one_borrow.push(String::from(message));
            two_borrow.push(String::from(message));
        }
    }

    #[test]
    fn it_sends_an_over_75_percent_warning_message() {
        let mock_messenger = MockMessenger::new();
        let mut limit_tracker = LimitTracker::new(&mock_messenger, 100);

        limit_tracker.set_value(80);

        assert_eq!(mock_messenger.sent_messages.borrow().len(), 1);
    }
}
Listing 15-23: Creating two mutable references in the same scope to see that RefCell<T> will panic

We create a variable one_borrow for the RefMut<T> smart pointer returned from borrow_mut. Then, we create another mutable borrow in the same way in the variable two_borrow. This makes two mutable references in the same scope, which isn’t allowed. When we run the tests for our library, the code in Listing 15-23 will compile without any errors, but the test will fail:

$ cargo test
   Compiling limit-tracker v0.1.0 (file:///projects/limit-tracker)
    Finished `test` profile [unoptimized + debuginfo] target(s) in 0.91s
     Running unittests src/lib.rs (target/debug/deps/limit_tracker-e599811fa246dbde)

running 1 test
test tests::it_sends_an_over_75_percent_warning_message ... FAILED

failures:

---- tests::it_sends_an_over_75_percent_warning_message stdout ----

thread 'tests::it_sends_an_over_75_percent_warning_message' panicked at src/lib.rs:60:53:
RefCell already borrowed
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace


failures:
    tests::it_sends_an_over_75_percent_warning_message

test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

error: test failed, to rerun pass `--lib`

Notice that the code panicked with the message already borrowed: BorrowMutError. This is how RefCell<T> handles violations of the borrowing rules at runtime.

Choosing to catch borrowing errors at runtime rather than compile time, as we’ve done here, means you’d potentially be finding mistakes in your code later in the development process: possibly not until your code was deployed to production. Also, your code would incur a small runtime performance penalty as a result of keeping track of the borrows at runtime rather than compile time. However, using RefCell<T> makes it possible to write a mock object that can modify itself to keep track of the messages it has seen while you’re using it in a context where only immutable values are allowed. You can use RefCell<T> despite its trade-offs to get more functionality than regular references provide.

Allowing Multiple Owners of Mutable Data

A common way to use RefCell<T> is in combination with Rc<T>. Recall that Rc<T> lets you have multiple owners of some data, but it only gives immutable access to that data. If you have an Rc<T> that holds a RefCell<T>, you can get a value that can have multiple owners and that you can mutate!

For example, recall the cons list example in Listing 15-18 where we used Rc<T> to allow multiple lists to share ownership of another list. Because Rc<T> holds only immutable values, we can’t change any of the values in the list once we’ve created them. Let’s add in RefCell<T> for its ability to change the values in the lists. Listing 15-24 shows that by using a RefCell<T> in the Cons definition, we can modify the value stored in all the lists.

Filename: src/main.rs
#[derive(Debug)]
enum List {
    Cons(Rc<RefCell<i32>>, Rc<List>),
    Nil,
}

use crate::List::{Cons, Nil};
use std::cell::RefCell;
use std::rc::Rc;

fn main() {
    let value = Rc::new(RefCell::new(5));

    let a = Rc::new(Cons(Rc::clone(&value), Rc::new(Nil)));

    let b = Cons(Rc::new(RefCell::new(3)), Rc::clone(&a));
    let c = Cons(Rc::new(RefCell::new(4)), Rc::clone(&a));

    *value.borrow_mut() += 10;

    println!("a after = {a:?}");
    println!("b after = {b:?}");
    println!("c after = {c:?}");
}
Listing 15-24: Using Rc<RefCell<i32>> to create a List that we can mutate

We create a value that is an instance of Rc<RefCell<i32>> and store it in a variable named value so that we can access it directly later. Then, we create a List in a with a Cons variant that holds value. We need to clone value so that both a and value have ownership of the inner 5 value rather than transferring ownership from value to a or having a borrow from value.

We wrap the list a in an Rc<T> so that when we create lists b and c, they can both refer to a, which is what we did in Listing 15-18.

After we’ve created the lists in a, b, and c, we want to add 10 to the value in value. We do this by calling borrow_mut on value, which uses the automatic dereferencing feature we discussed in “Where’s the -> Operator?” in Chapter 5 to dereference the Rc<T> to the inner RefCell<T> value. The borrow_mut method returns a RefMut<T> smart pointer, and we use the dereference operator on it and change the inner value.

When we print a, b, and c, we can see that they all have the modified value of 15 rather than 5:

$ cargo run
   Compiling cons-list v0.1.0 (file:///projects/cons-list)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.63s
     Running `target/debug/cons-list`
a after = Cons(RefCell { value: 15 }, Nil)
b after = Cons(RefCell { value: 3 }, Cons(RefCell { value: 15 }, Nil))
c after = Cons(RefCell { value: 4 }, Cons(RefCell { value: 15 }, Nil))

This technique is pretty neat! By using RefCell<T>, we have an outwardly immutable List value. But we can use the methods on RefCell<T> that provide access to its interior mutability so that we can modify our data when we need to. The runtime checks of the borrowing rules protect us from data races, and it’s sometimes worth trading a bit of speed for this flexibility in our data structures. Note that RefCell<T> does not work for multithreaded code! Mutex<T> is the thread-safe version of RefCell<T>, and we’ll discuss Mutex<T> in Chapter 16.

Циклические ссылки могут приводить к утечкам памяти

Reference Cycles Can Leak Memory

Rust’s memory safety guarantees make it difficult, but not impossible, to accidentally create memory that is never cleaned up (known as a memory leak). Preventing memory leaks entirely is not one of Rust’s guarantees, meaning memory leaks are memory safe in Rust. We can see that Rust allows memory leaks by using Rc<T> and RefCell<T>: It’s possible to create references where items refer to each other in a cycle. This creates memory leaks because the reference count of each item in the cycle will never reach 0, and the values will never be dropped.

Creating a Reference Cycle

Let’s look at how a reference cycle might happen and how to prevent it, starting with the definition of the List enum and a tail method in Listing 15-25.

Filename: src/main.rs
use crate::List::{Cons, Nil};
use std::cell::RefCell;
use std::rc::Rc;

#[derive(Debug)]
enum List {
    Cons(i32, RefCell<Rc<List>>),
    Nil,
}

impl List {
    fn tail(&self) -> Option<&RefCell<Rc<List>>> {
        match self {
            Cons(_, item) => Some(item),
            Nil => None,
        }
    }
}

fn main() {}
Listing 15-25: A cons list definition that holds a RefCell<T> so that we can modify what a Cons variant is referring to

We’re using another variation of the List definition from Listing 15-5. The second element in the Cons variant is now RefCell<Rc<List>>, meaning that instead of having the ability to modify the i32 value as we did in Listing 15-24, we want to modify the List value a Cons variant is pointing to. We’re also adding a tail method to make it convenient for us to access the second item if we have a Cons variant.

In Listing 15-26, we’re adding a main function that uses the definitions in Listing 15-25. This code creates a list in a and a list in b that points to the list in a. Then, it modifies the list in a to point to b, creating a reference cycle. There are println! statements along the way to show what the reference counts are at various points in this process.

Filename: src/main.rs
use crate::List::{Cons, Nil};
use std::cell::RefCell;
use std::rc::Rc;

#[derive(Debug)]
enum List {
    Cons(i32, RefCell<Rc<List>>),
    Nil,
}

impl List {
    fn tail(&self) -> Option<&RefCell<Rc<List>>> {
        match self {
            Cons(_, item) => Some(item),
            Nil => None,
        }
    }
}

fn main() {
    let a = Rc::new(Cons(5, RefCell::new(Rc::new(Nil))));

    println!("a initial rc count = {}", Rc::strong_count(&a));
    println!("a next item = {:?}", a.tail());

    let b = Rc::new(Cons(10, RefCell::new(Rc::clone(&a))));

    println!("a rc count after b creation = {}", Rc::strong_count(&a));
    println!("b initial rc count = {}", Rc::strong_count(&b));
    println!("b next item = {:?}", b.tail());

    if let Some(link) = a.tail() {
        *link.borrow_mut() = Rc::clone(&b);
    }

    println!("b rc count after changing a = {}", Rc::strong_count(&b));
    println!("a rc count after changing a = {}", Rc::strong_count(&a));

    // Uncomment the next line to see that we have a cycle;
    // it will overflow the stack.
    // println!("a next item = {:?}", a.tail());
}
Listing 15-26: Creating a reference cycle of two List values pointing to each other

We create an Rc<List> instance holding a List value in the variable a with an initial list of 5, Nil. We then create an Rc<List> instance holding another List value in the variable b that contains the value 10 and points to the list in a.

We modify a so that it points to b instead of Nil, creating a cycle. We do that by using the tail method to get a reference to the RefCell<Rc<List>> in a, which we put in the variable link. Then, we use the borrow_mut method on the RefCell<Rc<List>> to change the value inside from an Rc<List> that holds a Nil value to the Rc<List> in b.

When we run this code, keeping the last println! commented out for the moment, we’ll get this output:

$ cargo run
   Compiling cons-list v0.1.0 (file:///projects/cons-list)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.53s
     Running `target/debug/cons-list`
a initial rc count = 1
a next item = Some(RefCell { value: Nil })
a rc count after b creation = 2
b initial rc count = 1
b next item = Some(RefCell { value: Cons(5, RefCell { value: Nil }) })
b rc count after changing a = 2
a rc count after changing a = 2

The reference count of the Rc<List> instances in both a and b is 2 after we change the list in a to point to b. At the end of main, Rust drops the variable b, which decreases the reference count of the b Rc<List> instance from 2 to 1. The memory that Rc<List> has on the heap won’t be dropped at this point because its reference count is 1, not 0. Then, Rust drops a, which decreases the reference count of the a Rc<List> instance from 2 to 1 as well. This instance’s memory can’t be dropped either, because the other Rc<List> instance still refers to it. The memory allocated to the list will remain uncollected forever. To visualize this reference cycle, we’ve created the diagram in Figure 15-4.

A rectangle labeled 'a' that points to a rectangle containing the integer 5. A rectangle labeled 'b' that points to a rectangle containing the integer 10. The rectangle containing 5 points to the rectangle containing 10, and the rectangle containing 10 points back to the rectangle containing 5, creating a cycle.

Figure 15-4: A reference cycle of lists a and b pointing to each other

If you uncomment the last println! and run the program, Rust will try to print this cycle with a pointing to b pointing to a and so forth until it overflows the stack.

Compared to a real-world program, the consequences of creating a reference cycle in this example aren’t very dire: Right after we create the reference cycle, the program ends. However, if a more complex program allocated lots of memory in a cycle and held onto it for a long time, the program would use more memory than it needed and might overwhelm the system, causing it to run out of available memory.

Creating reference cycles is not easily done, but it’s not impossible either. If you have RefCell<T> values that contain Rc<T> values or similar nested combinations of types with interior mutability and reference counting, you must ensure that you don’t create cycles; you can’t rely on Rust to catch them. Creating a reference cycle would be a logic bug in your program that you should use automated tests, code reviews, and other software development practices to minimize.

Another solution for avoiding reference cycles is reorganizing your data structures so that some references express ownership and some references don’t. As a result, you can have cycles made up of some ownership relationships and some non-ownership relationships, and only the ownership relationships affect whether or not a value can be dropped. In Listing 15-25, we always want Cons variants to own their list, so reorganizing the data structure isn’t possible. Let’s look at an example using graphs made up of parent nodes and child nodes to see when non-ownership relationships are an appropriate way to prevent reference cycles.

Preventing Reference Cycles Using Weak<T>

So far, we’ve demonstrated that calling Rc::clone increases the strong_count of an Rc<T> instance, and an Rc<T> instance is only cleaned up if its strong_count is 0. You can also create a weak reference to the value within an Rc<T> instance by calling Rc::downgrade and passing a reference to the Rc<T>. Strong references are how you can share ownership of an Rc<T> instance. Weak references don’t express an ownership relationship, and their count doesn’t affect when an Rc<T> instance is cleaned up. They won’t cause a reference cycle, because any cycle involving some weak references will be broken once the strong reference count of values involved is 0.

When you call Rc::downgrade, you get a smart pointer of type Weak<T>. Instead of increasing the strong_count in the Rc<T> instance by 1, calling Rc::downgrade increases the weak_count by 1. The Rc<T> type uses weak_count to keep track of how many Weak<T> references exist, similar to strong_count. The difference is the weak_count doesn’t need to be 0 for the Rc<T> instance to be cleaned up.

Because the value that Weak<T> references might have been dropped, to do anything with the value that a Weak<T> is pointing to you must make sure the value still exists. Do this by calling the upgrade method on a Weak<T> instance, which will return an Option<Rc<T>>. You’ll get a result of Some if the Rc<T> value has not been dropped yet and a result of None if the Rc<T> value has been dropped. Because upgrade returns an Option<Rc<T>>, Rust will ensure that the Some case and the None case are handled, and there won’t be an invalid pointer.

As an example, rather than using a list whose items know only about the next item, we’ll create a tree whose items know about their child items and their parent items.

Creating a Tree Data Structure

To start, we’ll build a tree with nodes that know about their child nodes. We’ll create a struct named Node that holds its own i32 value as well as references to its child Node values:

Filename: src/main.rs

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

#[derive(Debug)]
struct Node {
    value: i32,
    children: RefCell<Vec<Rc<Node>>>,
}

fn main() {
    let leaf = Rc::new(Node {
        value: 3,
        children: RefCell::new(vec![]),
    });

    let branch = Rc::new(Node {
        value: 5,
        children: RefCell::new(vec![Rc::clone(&leaf)]),
    });
}

We want a Node to own its children, and we want to share that ownership with variables so that we can access each Node in the tree directly. To do this, we define the Vec<T> items to be values of type Rc<Node>. We also want to modify which nodes are children of another node, so we have a RefCell<T> in children around the Vec<Rc<Node>>.

Next, we’ll use our struct definition and create one Node instance named leaf with the value 3 and no children, and another instance named branch with the value 5 and leaf as one of its children, as shown in Listing 15-27.

Filename: src/main.rs
use std::cell::RefCell;
use std::rc::Rc;

#[derive(Debug)]
struct Node {
    value: i32,
    children: RefCell<Vec<Rc<Node>>>,
}

fn main() {
    let leaf = Rc::new(Node {
        value: 3,
        children: RefCell::new(vec![]),
    });

    let branch = Rc::new(Node {
        value: 5,
        children: RefCell::new(vec![Rc::clone(&leaf)]),
    });
}
Listing 15-27: Creating a leaf node with no children and a branch node with leaf as one of its children

We clone the Rc<Node> in leaf and store that in branch, meaning the Node in leaf now has two owners: leaf and branch. We can get from branch to leaf through branch.children, but there’s no way to get from leaf to branch. The reason is that leaf has no reference to branch and doesn’t know they’re related. We want leaf to know that branch is its parent. We’ll do that next.

Adding a Reference from a Child to Its Parent

To make the child node aware of its parent, we need to add a parent field to our Node struct definition. The trouble is in deciding what the type of parent should be. We know it can’t contain an Rc<T>, because that would create a reference cycle with leaf.parent pointing to branch and branch.children pointing to leaf, which would cause their strong_count values to never be 0.

Thinking about the relationships another way, a parent node should own its children: If a parent node is dropped, its child nodes should be dropped as well. However, a child should not own its parent: If we drop a child node, the parent should still exist. This is a case for weak references!

So, instead of Rc<T>, we’ll make the type of parent use Weak<T>, specifically a RefCell<Weak<Node>>. Now our Node struct definition looks like this:

Filename: src/main.rs

use std::cell::RefCell;
use std::rc::{Rc, Weak};

#[derive(Debug)]
struct Node {
    value: i32,
    parent: RefCell<Weak<Node>>,
    children: RefCell<Vec<Rc<Node>>>,
}

fn main() {
    let leaf = Rc::new(Node {
        value: 3,
        parent: RefCell::new(Weak::new()),
        children: RefCell::new(vec![]),
    });

    println!("leaf parent = {:?}", leaf.parent.borrow().upgrade());

    let branch = Rc::new(Node {
        value: 5,
        parent: RefCell::new(Weak::new()),
        children: RefCell::new(vec![Rc::clone(&leaf)]),
    });

    *leaf.parent.borrow_mut() = Rc::downgrade(&branch);

    println!("leaf parent = {:?}", leaf.parent.borrow().upgrade());
}

A node will be able to refer to its parent node but doesn’t own its parent. In Listing 15-28, we update main to use this new definition so that the leaf node will have a way to refer to its parent, branch.

Filename: src/main.rs
use std::cell::RefCell;
use std::rc::{Rc, Weak};

#[derive(Debug)]
struct Node {
    value: i32,
    parent: RefCell<Weak<Node>>,
    children: RefCell<Vec<Rc<Node>>>,
}

fn main() {
    let leaf = Rc::new(Node {
        value: 3,
        parent: RefCell::new(Weak::new()),
        children: RefCell::new(vec![]),
    });

    println!("leaf parent = {:?}", leaf.parent.borrow().upgrade());

    let branch = Rc::new(Node {
        value: 5,
        parent: RefCell::new(Weak::new()),
        children: RefCell::new(vec![Rc::clone(&leaf)]),
    });

    *leaf.parent.borrow_mut() = Rc::downgrade(&branch);

    println!("leaf parent = {:?}", leaf.parent.borrow().upgrade());
}
Listing 15-28: A leaf node with a weak reference to its parent node, branch

Creating the leaf node looks similar to Listing 15-27 with the exception of the parent field: leaf starts out without a parent, so we create a new, empty Weak<Node> reference instance.

At this point, when we try to get a reference to the parent of leaf by using the upgrade method, we get a None value. We see this in the output from the first println! statement:

leaf parent = None

When we create the branch node, it will also have a new Weak<Node> reference in the parent field because branch doesn’t have a parent node. We still have leaf as one of the children of branch. Once we have the Node instance in branch, we can modify leaf to give it a Weak<Node> reference to its parent. We use the borrow_mut method on the RefCell<Weak<Node>> in the parent field of leaf, and then we use the Rc::downgrade function to create a Weak<Node> reference to branch from the Rc<Node> in branch.

When we print the parent of leaf again, this time we’ll get a Some variant holding branch: Now leaf can access its parent! When we print leaf, we also avoid the cycle that eventually ended in a stack overflow like we had in Listing 15-26; the Weak<Node> references are printed as (Weak):

leaf parent = Some(Node { value: 5, parent: RefCell { value: (Weak) },
children: RefCell { value: [Node { value: 3, parent: RefCell { value: (Weak) },
children: RefCell { value: [] } }] } })

The lack of infinite output indicates that this code didn’t create a reference cycle. We can also tell this by looking at the values we get from calling Rc::strong_count and Rc::weak_count.

Visualizing Changes to strong_count and weak_count

Let’s look at how the strong_count and weak_count values of the Rc<Node> instances change by creating a new inner scope and moving the creation of branch into that scope. By doing so, we can see what happens when branch is created and then dropped when it goes out of scope. The modifications are shown in Listing 15-29.

Filename: src/main.rs
use std::cell::RefCell;
use std::rc::{Rc, Weak};

#[derive(Debug)]
struct Node {
    value: i32,
    parent: RefCell<Weak<Node>>,
    children: RefCell<Vec<Rc<Node>>>,
}

fn main() {
    let leaf = Rc::new(Node {
        value: 3,
        parent: RefCell::new(Weak::new()),
        children: RefCell::new(vec![]),
    });

    println!(
        "leaf strong = {}, weak = {}",
        Rc::strong_count(&leaf),
        Rc::weak_count(&leaf),
    );

    {
        let branch = Rc::new(Node {
            value: 5,
            parent: RefCell::new(Weak::new()),
            children: RefCell::new(vec![Rc::clone(&leaf)]),
        });

        *leaf.parent.borrow_mut() = Rc::downgrade(&branch);

        println!(
            "branch strong = {}, weak = {}",
            Rc::strong_count(&branch),
            Rc::weak_count(&branch),
        );

        println!(
            "leaf strong = {}, weak = {}",
            Rc::strong_count(&leaf),
            Rc::weak_count(&leaf),
        );
    }

    println!("leaf parent = {:?}", leaf.parent.borrow().upgrade());
    println!(
        "leaf strong = {}, weak = {}",
        Rc::strong_count(&leaf),
        Rc::weak_count(&leaf),
    );
}
Listing 15-29: Creating branch in an inner scope and examining strong and weak reference counts

After leaf is created, its Rc<Node> has a strong count of 1 and a weak count of 0. In the inner scope, we create branch and associate it with leaf, at which point when we print the counts, the Rc<Node> in branch will have a strong count of 1 and a weak count of 1 (for leaf.parent pointing to branch with a Weak<Node>). When we print the counts in leaf, we’ll see it will have a strong count of 2 because branch now has a clone of the Rc<Node> of leaf stored in branch.children but will still have a weak count of 0.

When the inner scope ends, branch goes out of scope and the strong count of the Rc<Node> decreases to 0, so its Node is dropped. The weak count of 1 from leaf.parent has no bearing on whether or not Node is dropped, so we don’t get any memory leaks!

If we try to access the parent of leaf after the end of the scope, we’ll get None again. At the end of the program, the Rc<Node> in leaf has a strong count of 1 and a weak count of 0 because the variable leaf is now the only reference to the Rc<Node> again.

All of the logic that manages the counts and value dropping is built into Rc<T> and Weak<T> and their implementations of the Drop trait. By specifying that the relationship from a child to its parent should be a Weak<T> reference in the definition of Node, you’re able to have parent nodes point to child nodes and vice versa without creating a reference cycle and memory leaks.

Summary

This chapter covered how to use smart pointers to make different guarantees and trade-offs from those Rust makes by default with regular references. The Box<T> type has a known size and points to data allocated on the heap. The Rc<T> type keeps track of the number of references to data on the heap so that the data can have multiple owners. The RefCell<T> type with its interior mutability gives us a type that we can use when we need an immutable type but need to change an inner value of that type; it also enforces the borrowing rules at runtime instead of at compile time.

Also discussed were the Deref and Drop traits, which enable a lot of the functionality of smart pointers. We explored reference cycles that can cause memory leaks and how to prevent them using Weak<T>.

If this chapter has piqued your interest and you want to implement your own smart pointers, check out “The Rustonomicon” for more useful information.

Next, we’ll talk about concurrency in Rust. You’ll even learn about a few new smart pointers.

Fearless Concurrency

Handling concurrent programming safely and efficiently is another of Rust’s major goals. Concurrent programming, in which different parts of a program execute independently, and parallel programming, in which different parts of a program execute at the same time, are becoming increasingly important as more computers take advantage of their multiple processors. Historically, programming in these contexts has been difficult and error-prone. Rust hopes to change that.

Initially, the Rust team thought that ensuring memory safety and preventing concurrency problems were two separate challenges to be solved with different methods. Over time, the team discovered that the ownership and type systems are a powerful set of tools to help manage memory safety and concurrency problems! By leveraging ownership and type checking, many concurrency errors are compile-time errors in Rust rather than runtime errors. Therefore, rather than making you spend lots of time trying to reproduce the exact circumstances under which a runtime concurrency bug occurs, incorrect code will refuse to compile and present an error explaining the problem. As a result, you can fix your code while you’re working on it rather than potentially after it has been shipped to production. We’ve nicknamed this aspect of Rust fearless concurrency. Fearless concurrency allows you to write code that is free of subtle bugs and is easy to refactor without introducing new bugs.

Note: For simplicity’s sake, we’ll refer to many of the problems as concurrent rather than being more precise by saying concurrent and/or parallel. For this chapter, please mentally substitute concurrent and/or parallel whenever we use concurrent. In the next chapter, where the distinction matters more, we’ll be more specific.

Many languages are dogmatic about the solutions they offer for handling concurrent problems. For example, Erlang has elegant functionality for message-passing concurrency but has only obscure ways to share state between threads. Supporting only a subset of possible solutions is a reasonable strategy for higher-level languages because a higher-level language promises benefits from giving up some control to gain abstractions. However, lower-level languages are expected to provide the solution with the best performance in any given situation and have fewer abstractions over the hardware. Therefore, Rust offers a variety of tools for modeling problems in whatever way is appropriate for your situation and requirements.

Here are the topics we’ll cover in this chapter:

  • How to create threads to run multiple pieces of code at the same time
  • Message-passing concurrency, where channels send messages between threads
  • Shared-state concurrency, where multiple threads have access to some piece of data
  • The Sync and Send traits, which extend Rust’s concurrency guarantees to user-defined types as well as types provided by the standard library

Использование потоков для одновременного выполнения кода

Using Threads to Run Code Simultaneously

In most current operating systems, an executed program’s code is run in a process, and the operating system will manage multiple processes at once. Within a program, you can also have independent parts that run simultaneously. The features that run these independent parts are called threads. For example, a web server could have multiple threads so that it can respond to more than one request at the same time.

Splitting the computation in your program into multiple threads to run multiple tasks at the same time can improve performance, but it also adds complexity. Because threads can run simultaneously, there’s no inherent guarantee about the order in which parts of your code on different threads will run. This can lead to problems, such as:

  • Race conditions, in which threads are accessing data or resources in an inconsistent order
  • Deadlocks, in which two threads are waiting for each other, preventing both threads from continuing
  • Bugs that only happen in certain situations and are hard to reproduce and fix reliably

Rust attempts to mitigate the negative effects of using threads, but programming in a multithreaded context still takes careful thought and requires a code structure that is different from that in programs running in a single thread.

Programming languages implement threads in a few different ways, and many operating systems provide an API the programming language can call for creating new threads. The Rust standard library uses a 1:1 model of thread implementation, whereby a program uses one operating system thread per one language thread. There are crates that implement other models of threading that make different trade-offs to the 1:1 model. (Rust’s async system, which we will see in the next chapter, provides another approach to concurrency as well.)

Creating a New Thread with spawn

To create a new thread, we call the thread::spawn function and pass it a closure (we talked about closures in Chapter 13) containing the code we want to run in the new thread. The example in Listing 16-1 prints some text from a main thread and other text from a new thread.

Filename: src/main.rs
use std::thread;
use std::time::Duration;

fn main() {
    thread::spawn(|| {
        for i in 1..10 {
            println!("hi number {i} from the spawned thread!");
            thread::sleep(Duration::from_millis(1));
        }
    });

    for i in 1..5 {
        println!("hi number {i} from the main thread!");
        thread::sleep(Duration::from_millis(1));
    }
}
Listing 16-1: Creating a new thread to print one thing while the main thread prints something else

Note that when the main thread of a Rust program completes, all spawned threads are shut down, whether or not they have finished running. The output from this program might be a little different every time, but it will look similar to the following:

hi number 1 from the main thread!
hi number 1 from the spawned thread!
hi number 2 from the main thread!
hi number 2 from the spawned thread!
hi number 3 from the main thread!
hi number 3 from the spawned thread!
hi number 4 from the main thread!
hi number 4 from the spawned thread!
hi number 5 from the spawned thread!

The calls to thread::sleep force a thread to stop its execution for a short duration, allowing a different thread to run. The threads will probably take turns, but that isn’t guaranteed: It depends on how your operating system schedules the threads. In this run, the main thread printed first, even though the print statement from the spawned thread appears first in the code. And even though we told the spawned thread to print until i is 9, it only got to 5 before the main thread shut down.

If you run this code and only see output from the main thread, or don’t see any overlap, try increasing the numbers in the ranges to create more opportunities for the operating system to switch between the threads.

Waiting for All Threads to Finish

The code in Listing 16-1 not only stops the spawned thread prematurely most of the time due to the main thread ending, but because there is no guarantee on the order in which threads run, we also can’t guarantee that the spawned thread will get to run at all!

We can fix the problem of the spawned thread not running or of it ending prematurely by saving the return value of thread::spawn in a variable. The return type of thread::spawn is JoinHandle<T>. A JoinHandle<T> is an owned value that, when we call the join method on it, will wait for its thread to finish. Listing 16-2 shows how to use the JoinHandle<T> of the thread we created in Listing 16-1 and how to call join to make sure the spawned thread finishes before main exits.

Filename: src/main.rs
use std::thread;
use std::time::Duration;

fn main() {
    let handle = thread::spawn(|| {
        for i in 1..10 {
            println!("hi number {i} from the spawned thread!");
            thread::sleep(Duration::from_millis(1));
        }
    });

    for i in 1..5 {
        println!("hi number {i} from the main thread!");
        thread::sleep(Duration::from_millis(1));
    }

    handle.join().unwrap();
}
Listing 16-2: Saving a JoinHandle<T> from thread::spawn to guarantee the thread is run to completion

Calling join on the handle blocks the thread currently running until the thread represented by the handle terminates. Blocking a thread means that thread is prevented from performing work or exiting. Because we’ve put the call to join after the main thread’s for loop, running Listing 16-2 should produce output similar to this:

hi number 1 from the main thread!
hi number 2 from the main thread!
hi number 1 from the spawned thread!
hi number 3 from the main thread!
hi number 2 from the spawned thread!
hi number 4 from the main thread!
hi number 3 from the spawned thread!
hi number 4 from the spawned thread!
hi number 5 from the spawned thread!
hi number 6 from the spawned thread!
hi number 7 from the spawned thread!
hi number 8 from the spawned thread!
hi number 9 from the spawned thread!

The two threads continue alternating, but the main thread waits because of the call to handle.join() and does not end until the spawned thread is finished.

But let’s see what happens when we instead move handle.join() before the for loop in main, like this:

Filename: src/main.rs
use std::thread;
use std::time::Duration;

fn main() {
    let handle = thread::spawn(|| {
        for i in 1..10 {
            println!("hi number {i} from the spawned thread!");
            thread::sleep(Duration::from_millis(1));
        }
    });

    handle.join().unwrap();

    for i in 1..5 {
        println!("hi number {i} from the main thread!");
        thread::sleep(Duration::from_millis(1));
    }
}

The main thread will wait for the spawned thread to finish and then run its for loop, so the output won’t be interleaved anymore, as shown here:

hi number 1 from the spawned thread!
hi number 2 from the spawned thread!
hi number 3 from the spawned thread!
hi number 4 from the spawned thread!
hi number 5 from the spawned thread!
hi number 6 from the spawned thread!
hi number 7 from the spawned thread!
hi number 8 from the spawned thread!
hi number 9 from the spawned thread!
hi number 1 from the main thread!
hi number 2 from the main thread!
hi number 3 from the main thread!
hi number 4 from the main thread!

Small details, such as where join is called, can affect whether or not your threads run at the same time.

Using move Closures with Threads

We’ll often use the move keyword with closures passed to thread::spawn because the closure will then take ownership of the values it uses from the environment, thus transferring ownership of those values from one thread to another. In “Capturing References or Moving Ownership” in Chapter 13, we discussed move in the context of closures. Now we’ll concentrate more on the interaction between move and thread::spawn.

Notice in Listing 16-1 that the closure we pass to thread::spawn takes no arguments: We’re not using any data from the main thread in the spawned thread’s code. To use data from the main thread in the spawned thread, the spawned thread’s closure must capture the values it needs. Listing 16-3 shows an attempt to create a vector in the main thread and use it in the spawned thread. However, this won’t work yet, as you’ll see in a moment.

Filename: src/main.rs
use std::thread;

fn main() {
    let v = vec![1, 2, 3];

    let handle = thread::spawn(|| {
        println!("Here's a vector: {v:?}");
    });

    handle.join().unwrap();
}
Listing 16-3: Attempting to use a vector created by the main thread in another thread

The closure uses v, so it will capture v and make it part of the closure’s environment. Because thread::spawn runs this closure in a new thread, we should be able to access v inside that new thread. But when we compile this example, we get the following error:

$ cargo run
   Compiling threads v0.1.0 (file:///projects/threads)
error[E0373]: closure may outlive the current function, but it borrows `v`, which is owned by the current function
 --> src/main.rs:6:32
  |
6 |     let handle = thread::spawn(|| {
  |                                ^^ may outlive borrowed value `v`
7 |         println!("Here's a vector: {v:?}");
  |                                     - `v` is borrowed here
  |
note: function requires argument type to outlive `'static`
 --> src/main.rs:6:18
  |
6 |       let handle = thread::spawn(|| {
  |  __________________^
7 | |         println!("Here's a vector: {v:?}");
8 | |     });
  | |______^
help: to force the closure to take ownership of `v` (and any other referenced variables), use the `move` keyword
  |
6 |     let handle = thread::spawn(move || {
  |                                ++++

For more information about this error, try `rustc --explain E0373`.
error: could not compile `threads` (bin "threads") due to 1 previous error

Rust infers how to capture v, and because println! only needs a reference to v, the closure tries to borrow v. However, there’s a problem: Rust can’t tell how long the spawned thread will run, so it doesn’t know whether the reference to v will always be valid.

Listing 16-4 provides a scenario that’s more likely to have a reference to v that won’t be valid.

Filename: src/main.rs
use std::thread;

fn main() {
    let v = vec![1, 2, 3];

    let handle = thread::spawn(|| {
        println!("Here's a vector: {v:?}");
    });

    drop(v); // oh no!

    handle.join().unwrap();
}
Listing 16-4: A thread with a closure that attempts to capture a reference to v from a main thread that drops v

If Rust allowed us to run this code, there’s a possibility that the spawned thread would be immediately put in the background without running at all. The spawned thread has a reference to v inside, but the main thread immediately drops v, using the drop function we discussed in Chapter 15. Then, when the spawned thread starts to execute, v is no longer valid, so a reference to it is also invalid. Oh no!

To fix the compiler error in Listing 16-3, we can use the error message’s advice:

help: to force the closure to take ownership of `v` (and any other referenced variables), use the `move` keyword
  |
6 |     let handle = thread::spawn(move || {
  |                                ++++

By adding the move keyword before the closure, we force the closure to take ownership of the values it’s using rather than allowing Rust to infer that it should borrow the values. The modification to Listing 16-3 shown in Listing 16-5 will compile and run as we intend.

Filename: src/main.rs
use std::thread;

fn main() {
    let v = vec![1, 2, 3];

    let handle = thread::spawn(move || {
        println!("Here's a vector: {v:?}");
    });

    handle.join().unwrap();
}
Listing 16-5: Using the move keyword to force a closure to take ownership of the values it uses

We might be tempted to try the same thing to fix the code in Listing 16-4 where the main thread called drop by using a move closure. However, this fix will not work because what Listing 16-4 is trying to do is disallowed for a different reason. If we added move to the closure, we would move v into the closure’s environment, and we could no longer call drop on it in the main thread. We would get this compiler error instead:

$ cargo run
   Compiling threads v0.1.0 (file:///projects/threads)
error[E0382]: use of moved value: `v`
  --> src/main.rs:10:10
   |
 4 |     let v = vec![1, 2, 3];
   |         - move occurs because `v` has type `Vec<i32>`, which does not implement the `Copy` trait
 5 |
 6 |     let handle = thread::spawn(move || {
   |                                ------- value moved into closure here
 7 |         println!("Here's a vector: {v:?}");
   |                                     - variable moved due to use in closure
...
10 |     drop(v); // oh no!
   |          ^ value used here after move
   |
help: consider cloning the value before moving it into the closure
   |
 6 ~     let value = v.clone();
 7 ~     let handle = thread::spawn(move || {
 8 ~         println!("Here's a vector: {value:?}");
   |

For more information about this error, try `rustc --explain E0382`.
error: could not compile `threads` (bin "threads") due to 1 previous error

Rust’s ownership rules have saved us again! We got an error from the code in Listing 16-3 because Rust was being conservative and only borrowing v for the thread, which meant the main thread could theoretically invalidate the spawned thread’s reference. By telling Rust to move ownership of v to the spawned thread, we’re guaranteeing to Rust that the main thread won’t use v anymore. If we change Listing 16-4 in the same way, we’re then violating the ownership rules when we try to use v in the main thread. The move keyword overrides Rust’s conservative default of borrowing; it doesn’t let us violate the ownership rules.

Now that we’ve covered what threads are and the methods supplied by the thread API, let’s look at some situations in which we can use threads.

Передача данных между потоками через передачу сообщений

Transfer Data Between Threads with Message Passing

One increasingly popular approach to ensuring safe concurrency is message passing, where threads or actors communicate by sending each other messages containing data. Here’s the idea in a slogan from the Go language documentation: “Do not communicate by sharing memory; instead, share memory by communicating.”

To accomplish message-sending concurrency, Rust’s standard library provides an implementation of channels. A channel is a general programming concept by which data is sent from one thread to another.

You can imagine a channel in programming as being like a directional channel of water, such as a stream or a river. If you put something like a rubber duck into a river, it will travel downstream to the end of the waterway.

A channel has two halves: a transmitter and a receiver. The transmitter half is the upstream location where you put the rubber duck into the river, and the receiver half is where the rubber duck ends up downstream. One part of your code calls methods on the transmitter with the data you want to send, and another part checks the receiving end for arriving messages. A channel is said to be closed if either the transmitter or receiver half is dropped.

Here, we’ll work up to a program that has one thread to generate values and send them down a channel, and another thread that will receive the values and print them out. We’ll be sending simple values between threads using a channel to illustrate the feature. Once you’re familiar with the technique, you could use channels for any threads that need to communicate with each other, such as a chat system or a system where many threads perform parts of a calculation and send the parts to one thread that aggregates the results.

First, in Listing 16-6, we’ll create a channel but not do anything with it. Note that this won’t compile yet because Rust can’t tell what type of values we want to send over the channel.

Filename: src/main.rs
use std::sync::mpsc;

fn main() {
    let (tx, rx) = mpsc::channel();
}
Listing 16-6: Creating a channel and assigning the two halves to tx and rx

We create a new channel using the mpsc::channel function; mpsc stands for multiple producer, single consumer. In short, the way Rust’s standard library implements channels means a channel can have multiple sending ends that produce values but only one receiving end that consumes those values. Imagine multiple streams flowing together into one big river: Everything sent down any of the streams will end up in one river at the end. We’ll start with a single producer for now, but we’ll add multiple producers when we get this example working.

The mpsc::channel function returns a tuple, the first element of which is the sending end—the transmitter—and the second element of which is the receiving end—the receiver. The abbreviations tx and rx are traditionally used in many fields for transmitter and receiver, respectively, so we name our variables as such to indicate each end. We’re using a let statement with a pattern that destructures the tuples; we’ll discuss the use of patterns in let statements and destructuring in Chapter 19. For now, know that using a let statement in this way is a convenient approach to extract the pieces of the tuple returned by mpsc::channel.

Let’s move the transmitting end into a spawned thread and have it send one string so that the spawned thread is communicating with the main thread, as shown in Listing 16-7. This is like putting a rubber duck in the river upstream or sending a chat message from one thread to another.

Filename: src/main.rs
use std::sync::mpsc;
use std::thread;

fn main() {
    let (tx, rx) = mpsc::channel();

    thread::spawn(move || {
        let val = String::from("hi");
        tx.send(val).unwrap();
    });
}
Listing 16-7: Moving tx to a spawned thread and sending "hi"

Again, we’re using thread::spawn to create a new thread and then using move to move tx into the closure so that the spawned thread owns tx. The spawned thread needs to own the transmitter to be able to send messages through the channel.

The transmitter has a send method that takes the value we want to send. The send method returns a Result<T, E> type, so if the receiver has already been dropped and there’s nowhere to send a value, the send operation will return an error. In this example, we’re calling unwrap to panic in case of an error. But in a real application, we would handle it properly: Return to Chapter 9 to review strategies for proper error handling.

In Listing 16-8, we’ll get the value from the receiver in the main thread. This is like retrieving the rubber duck from the water at the end of the river or receiving a chat message.

Filename: src/main.rs
use std::sync::mpsc;
use std::thread;

fn main() {
    let (tx, rx) = mpsc::channel();

    thread::spawn(move || {
        let val = String::from("hi");
        tx.send(val).unwrap();
    });

    let received = rx.recv().unwrap();
    println!("Got: {received}");
}
Listing 16-8: Receiving the value "hi" in the main thread and printing it

The receiver has two useful methods: recv and try_recv. We’re using recv, short for receive, which will block the main thread’s execution and wait until a value is sent down the channel. Once a value is sent, recv will return it in a Result<T, E>. When the transmitter closes, recv will return an error to signal that no more values will be coming.

The try_recv method doesn’t block, but will instead return a Result<T, E> immediately: an Ok value holding a message if one is available and an Err value if there aren’t any messages this time. Using try_recv is useful if this thread has other work to do while waiting for messages: We could write a loop that calls try_recv every so often, handles a message if one is available, and otherwise does other work for a little while until checking again.

We’ve used recv in this example for simplicity; we don’t have any other work for the main thread to do other than wait for messages, so blocking the main thread is appropriate.

When we run the code in Listing 16-8, we’ll see the value printed from the main thread:

Got: hi

Perfect!

Transferring Ownership Through Channels

The ownership rules play a vital role in message sending because they help you write safe, concurrent code. Preventing errors in concurrent programming is the advantage of thinking about ownership throughout your Rust programs. Let’s do an experiment to show how channels and ownership work together to prevent problems: We’ll try to use a val value in the spawned thread after we’ve sent it down the channel. Try compiling the code in Listing 16-9 to see why this code isn’t allowed.

Filename: src/main.rs
use std::sync::mpsc;
use std::thread;

fn main() {
    let (tx, rx) = mpsc::channel();

    thread::spawn(move || {
        let val = String::from("hi");
        tx.send(val).unwrap();
        println!("val is {val}");
    });

    let received = rx.recv().unwrap();
    println!("Got: {received}");
}
Listing 16-9: Attempting to use val after we’ve sent it down the channel

Here, we try to print val after we’ve sent it down the channel via tx.send. Allowing this would be a bad idea: Once the value has been sent to another thread, that thread could modify or drop it before we try to use the value again. Potentially, the other thread’s modifications could cause errors or unexpected results due to inconsistent or nonexistent data. However, Rust gives us an error if we try to compile the code in Listing 16-9:

$ cargo run
   Compiling message-passing v0.1.0 (file:///projects/message-passing)
error[E0382]: borrow of moved value: `val`
  --> src/main.rs:10:27
   |
 8 |         let val = String::from("hi");
   |             --- move occurs because `val` has type `String`, which does not implement the `Copy` trait
 9 |         tx.send(val).unwrap();
   |                 --- value moved here
10 |         println!("val is {val}");
   |                           ^^^ value borrowed here after move
   |
   = note: this error originates in the macro `$crate::format_args_nl` which comes from the expansion of the macro `println` (in Nightly builds, run with -Z macro-backtrace for more info)

For more information about this error, try `rustc --explain E0382`.
error: could not compile `message-passing` (bin "message-passing") due to 1 previous error

Our concurrency mistake has caused a compile-time error. The send function takes ownership of its parameter, and when the value is moved the receiver takes ownership of it. This stops us from accidentally using the value again after sending it; the ownership system checks that everything is okay.

Sending Multiple Values

The code in Listing 16-8 compiled and ran, but it didn’t clearly show us that two separate threads were talking to each other over the channel.

In Listing 16-10, we’ve made some modifications that will prove the code in Listing 16-8 is running concurrently: The spawned thread will now send multiple messages and pause for a second between each message.

Filename: src/main.rs
use std::sync::mpsc;
use std::thread;
use std::time::Duration;

fn main() {
    let (tx, rx) = mpsc::channel();

    thread::spawn(move || {
        let vals = vec![
            String::from("hi"),
            String::from("from"),
            String::from("the"),
            String::from("thread"),
        ];

        for val in vals {
            tx.send(val).unwrap();
            thread::sleep(Duration::from_secs(1));
        }
    });

    for received in rx {
        println!("Got: {received}");
    }
}
Listing 16-10: Sending multiple messages and pausing between each one

This time, the spawned thread has a vector of strings that we want to send to the main thread. We iterate over them, sending each individually, and pause between each by calling the thread::sleep function with a Duration value of one second.

In the main thread, we’re not calling the recv function explicitly anymore: Instead, we’re treating rx as an iterator. For each value received, we’re printing it. When the channel is closed, iteration will end.

When running the code in Listing 16-10, you should see the following output with a one-second pause in between each line:

Got: hi
Got: from
Got: the
Got: thread

Because we don’t have any code that pauses or delays in the for loop in the main thread, we can tell that the main thread is waiting to receive values from the spawned thread.

Creating Multiple Producers

Earlier we mentioned that mpsc was an acronym for multiple producer, single consumer. Let’s put mpsc to use and expand the code in Listing 16-10 to create multiple threads that all send values to the same receiver. We can do so by cloning the transmitter, as shown in Listing 16-11.

Filename: src/main.rs
use std::sync::mpsc;
use std::thread;
use std::time::Duration;

fn main() {
    // --snip--

    let (tx, rx) = mpsc::channel();

    let tx1 = tx.clone();
    thread::spawn(move || {
        let vals = vec![
            String::from("hi"),
            String::from("from"),
            String::from("the"),
            String::from("thread"),
        ];

        for val in vals {
            tx1.send(val).unwrap();
            thread::sleep(Duration::from_secs(1));
        }
    });

    thread::spawn(move || {
        let vals = vec![
            String::from("more"),
            String::from("messages"),
            String::from("for"),
            String::from("you"),
        ];

        for val in vals {
            tx.send(val).unwrap();
            thread::sleep(Duration::from_secs(1));
        }
    });

    for received in rx {
        println!("Got: {received}");
    }

    // --snip--
}
Listing 16-11: Sending multiple messages from multiple producers

This time, before we create the first spawned thread, we call clone on the transmitter. This will give us a new transmitter we can pass to the first spawned thread. We pass the original transmitter to a second spawned thread. This gives us two threads, each sending different messages to the one receiver.

When you run the code, your output should look something like this:

Got: hi
Got: more
Got: from
Got: messages
Got: for
Got: the
Got: thread
Got: you

You might see the values in another order, depending on your system. This is what makes concurrency interesting as well as difficult. If you experiment with thread::sleep, giving it various values in the different threads, each run will be more nondeterministic and create different output each time.

Now that we’ve looked at how channels work, let’s look at a different method of concurrency.

Конкурентность через совместное состояние

Shared-State Concurrency

Message passing is a fine way to handle concurrency, but it’s not the only way. Another method would be for multiple threads to access the same shared data. Consider this part of the slogan from the Go language documentation again: “Do not communicate by sharing memory.”

What would communicating by sharing memory look like? In addition, why would message-passing enthusiasts caution not to use memory sharing?

In a way, channels in any programming language are similar to single ownership because once you transfer a value down a channel, you should no longer use that value. Shared-memory concurrency is like multiple ownership: Multiple threads can access the same memory location at the same time. As you saw in Chapter 15, where smart pointers made multiple ownership possible, multiple ownership can add complexity because these different owners need managing. Rust’s type system and ownership rules greatly assist in getting this management correct. For an example, let’s look at mutexes, one of the more common concurrency primitives for shared memory.

Controlling Access with Mutexes

Mutex is an abbreviation for mutual exclusion, as in a mutex allows only one thread to access some data at any given time. To access the data in a mutex, a thread must first signal that it wants access by asking to acquire the mutex’s lock. The lock is a data structure that is part of the mutex that keeps track of who currently has exclusive access to the data. Therefore, the mutex is described as guarding the data it holds via the locking system.

Mutexes have a reputation for being difficult to use because you have to remember two rules:

  1. You must attempt to acquire the lock before using the data.
  2. When you’re done with the data that the mutex guards, you must unlock the data so that other threads can acquire the lock.

For a real-world metaphor for a mutex, imagine a panel discussion at a conference with only one microphone. Before a panelist can speak, they have to ask or signal that they want to use the microphone. When they get the microphone, they can talk for as long as they want to and then hand the microphone to the next panelist who requests to speak. If a panelist forgets to hand the microphone off when they’re finished with it, no one else is able to speak. If management of the shared microphone goes wrong, the panel won’t work as planned!

Management of mutexes can be incredibly tricky to get right, which is why so many people are enthusiastic about channels. However, thanks to Rust’s type system and ownership rules, you can’t get locking and unlocking wrong.

The API of Mutex<T>

As an example of how to use a mutex, let’s start by using a mutex in a single-threaded context, as shown in Listing 16-12.

Filename: src/main.rs
use std::sync::Mutex;

fn main() {
    let m = Mutex::new(5);

    {
        let mut num = m.lock().unwrap();
        *num = 6;
    }

    println!("m = {m:?}");
}
Listing 16-12: Exploring the API of Mutex<T> in a single-threaded context for simplicity

As with many types, we create a Mutex<T> using the associated function new. To access the data inside the mutex, we use the lock method to acquire the lock. This call will block the current thread so that it can’t do any work until it’s our turn to have the lock.

The call to lock would fail if another thread holding the lock panicked. In that case, no one would ever be able to get the lock, so we’ve chosen to unwrap and have this thread panic if we’re in that situation.

After we’ve acquired the lock, we can treat the return value, named num in this case, as a mutable reference to the data inside. The type system ensures that we acquire a lock before using the value in m. The type of m is Mutex<i32>, not i32, so we must call lock to be able to use the i32 value. We can’t forget; the type system won’t let us access the inner i32 otherwise.

The call to lock returns a type called MutexGuard, wrapped in a LockResult that we handled with the call to unwrap. The MutexGuard type implements Deref to point at our inner data; the type also has a Drop implementation that releases the lock automatically when a MutexGuard goes out of scope, which happens at the end of the inner scope. As a result, we don’t risk forgetting to release the lock and blocking the mutex from being used by other threads because the lock release happens automatically.

After dropping the lock, we can print the mutex value and see that we were able to change the inner i32 to 6.

Shared Access to Mutex<T>

Now let’s try to share a value between multiple threads using Mutex<T>. We’ll spin up 10 threads and have them each increment a counter value by 1, so the counter goes from 0 to 10. The example in Listing 16-13 will have a compiler error, and we’ll use that error to learn more about using Mutex<T> and how Rust helps us use it correctly.

Filename: src/main.rs
use std::sync::Mutex;
use std::thread;

fn main() {
    let counter = Mutex::new(0);
    let mut handles = vec![];

    for _ in 0..10 {
        let handle = thread::spawn(move || {
            let mut num = counter.lock().unwrap();

            *num += 1;
        });
        handles.push(handle);
    }

    for handle in handles {
        handle.join().unwrap();
    }

    println!("Result: {}", *counter.lock().unwrap());
}
Listing 16-13: Ten threads, each incrementing a counter guarded by a Mutex<T>

We create a counter variable to hold an i32 inside a Mutex<T>, as we did in Listing 16-12. Next, we create 10 threads by iterating over a range of numbers. We use thread::spawn and give all the threads the same closure: one that moves the counter into the thread, acquires a lock on the Mutex<T> by calling the lock method, and then adds 1 to the value in the mutex. When a thread finishes running its closure, num will go out of scope and release the lock so that another thread can acquire it.

In the main thread, we collect all the join handles. Then, as we did in Listing 16-2, we call join on each handle to make sure all the threads finish. At that point, the main thread will acquire the lock and print the result of this program.

We hinted that this example wouldn’t compile. Now let’s find out why!

$ cargo run
   Compiling shared-state v0.1.0 (file:///projects/shared-state)
error[E0382]: borrow of moved value: `counter`
  --> src/main.rs:21:29
   |
 5 |     let counter = Mutex::new(0);
   |         ------- move occurs because `counter` has type `std::sync::Mutex<i32>`, which does not implement the `Copy` trait
...
 8 |     for _ in 0..10 {
   |     -------------- inside of this loop
 9 |         let handle = thread::spawn(move || {
   |                                    ------- value moved into closure here, in previous iteration of loop
...
21 |     println!("Result: {}", *counter.lock().unwrap());
   |                             ^^^^^^^ value borrowed here after move
   |
help: consider moving the expression out of the loop so it is only moved once
   |
 8 ~     let mut value = counter.lock();
 9 ~     for _ in 0..10 {
10 |         let handle = thread::spawn(move || {
11 ~             let mut num = value.unwrap();
   |

For more information about this error, try `rustc --explain E0382`.
error: could not compile `shared-state` (bin "shared-state") due to 1 previous error

The error message states that the counter value was moved in the previous iteration of the loop. Rust is telling us that we can’t move the ownership of lock counter into multiple threads. Let’s fix the compiler error with the multiple-ownership method we discussed in Chapter 15.

Multiple Ownership with Multiple Threads

In Chapter 15, we gave a value to multiple owners by using the smart pointer Rc<T> to create a reference-counted value. Let’s do the same here and see what happens. We’ll wrap the Mutex<T> in Rc<T> in Listing 16-14 and clone the Rc<T> before moving ownership to the thread.

Filename: src/main.rs
use std::rc::Rc;
use std::sync::Mutex;
use std::thread;

fn main() {
    let counter = Rc::new(Mutex::new(0));
    let mut handles = vec![];

    for _ in 0..10 {
        let counter = Rc::clone(&counter);
        let handle = thread::spawn(move || {
            let mut num = counter.lock().unwrap();

            *num += 1;
        });
        handles.push(handle);
    }

    for handle in handles {
        handle.join().unwrap();
    }

    println!("Result: {}", *counter.lock().unwrap());
}
Listing 16-14: Attempting to use Rc<T> to allow multiple threads to own the Mutex<T>

Once again, we compile and get… different errors! The compiler is teaching us a lot:

$ cargo run
   Compiling shared-state v0.1.0 (file:///projects/shared-state)
error[E0277]: `Rc<std::sync::Mutex<i32>>` cannot be sent between threads safely
  --> src/main.rs:11:36
   |
11 |           let handle = thread::spawn(move || {
   |                        ------------- ^------
   |                        |             |
   |  ______________________|_____________within this `{closure@src/main.rs:11:36: 11:43}`
   | |                      |
   | |                      required by a bound introduced by this call
12 | |             let mut num = counter.lock().unwrap();
13 | |
14 | |             *num += 1;
15 | |         });
   | |_________^ `Rc<std::sync::Mutex<i32>>` cannot be sent between threads safely
   |
   = help: within `{closure@src/main.rs:11:36: 11:43}`, the trait `Send` is not implemented for `Rc<std::sync::Mutex<i32>>`
note: required because it's used within this closure
  --> src/main.rs:11:36
   |
11 |         let handle = thread::spawn(move || {
   |                                    ^^^^^^^
note: required by a bound in `spawn`
  --> /rustc/1159e78c4747b02ef996e55082b704c09b970588/library/std/src/thread/mod.rs:723:1

For more information about this error, try `rustc --explain E0277`.
error: could not compile `shared-state` (bin "shared-state") due to 1 previous error

Wow, that error message is very wordy! Here’s the important part to focus on: `Rc<Mutex<i32>>` cannot be sent between threads safely. The compiler is also telling us the reason why: the trait `Send` is not implemented for `Rc<Mutex<i32>>`. We’ll talk about Send in the next section: It’s one of the traits that ensures that the types we use with threads are meant for use in concurrent situations.

Unfortunately, Rc<T> is not safe to share across threads. When Rc<T> manages the reference count, it adds to the count for each call to clone and subtracts from the count when each clone is dropped. But it doesn’t use any concurrency primitives to make sure that changes to the count can’t be interrupted by another thread. This could lead to wrong counts—subtle bugs that could in turn lead to memory leaks or a value being dropped before we’re done with it. What we need is a type that is exactly like Rc<T>, but that makes changes to the reference count in a thread-safe way.

Atomic Reference Counting with Arc<T>

Fortunately, Arc<T> is a type like Rc<T> that is safe to use in concurrent situations. The a stands for atomic, meaning it’s an atomically reference-counted type. Atomics are an additional kind of concurrency primitive that we won’t cover in detail here: See the standard library documentation for std::sync::atomic for more details. At this point, you just need to know that atomics work like primitive types but are safe to share across threads.

You might then wonder why all primitive types aren’t atomic and why standard library types aren’t implemented to use Arc<T> by default. The reason is that thread safety comes with a performance penalty that you only want to pay when you really need to. If you’re just performing operations on values within a single thread, your code can run faster if it doesn’t have to enforce the guarantees atomics provide.

Let’s return to our example: Arc<T> and Rc<T> have the same API, so we fix our program by changing the use line, the call to new, and the call to clone. The code in Listing 16-15 will finally compile and run.

Filename: src/main.rs
use std::sync::{Arc, Mutex};
use std::thread;

fn main() {
    let counter = Arc::new(Mutex::new(0));
    let mut handles = vec![];

    for _ in 0..10 {
        let counter = Arc::clone(&counter);
        let handle = thread::spawn(move || {
            let mut num = counter.lock().unwrap();

            *num += 1;
        });
        handles.push(handle);
    }

    for handle in handles {
        handle.join().unwrap();
    }

    println!("Result: {}", *counter.lock().unwrap());
}
Listing 16-15: Using an Arc<T> to wrap the Mutex<T> to be able to share ownership across multiple threads

This code will print the following:

Result: 10

We did it! We counted from 0 to 10, which may not seem very impressive, but it did teach us a lot about Mutex<T> and thread safety. You could also use this program’s structure to do more complicated operations than just incrementing a counter. Using this strategy, you can divide a calculation into independent parts, split those parts across threads, and then use a Mutex<T> to have each thread update the final result with its part.

Note that if you are doing simple numerical operations, there are types simpler than Mutex<T> types provided by the std::sync::atomic module of the standard library. These types provide safe, concurrent, atomic access to primitive types. We chose to use Mutex<T> with a primitive type for this example so that we could concentrate on how Mutex<T> works.

Comparing RefCell<T>/Rc<T> and Mutex<T>/Arc<T>

You might have noticed that counter is immutable but that we could get a mutable reference to the value inside it; this means Mutex<T> provides interior mutability, as the Cell family does. In the same way we used RefCell<T> in Chapter 15 to allow us to mutate contents inside an Rc<T>, we use Mutex<T> to mutate contents inside an Arc<T>.

Another detail to note is that Rust can’t protect you from all kinds of logic errors when you use Mutex<T>. Recall from Chapter 15 that using Rc<T> came with the risk of creating reference cycles, where two Rc<T> values refer to each other, causing memory leaks. Similarly, Mutex<T> comes with the risk of creating deadlocks. These occur when an operation needs to lock two resources and two threads have each acquired one of the locks, causing them to wait for each other forever. If you’re interested in deadlocks, try creating a Rust program that has a deadlock; then, research deadlock mitigation strategies for mutexes in any language and have a go at implementing them in Rust. The standard library API documentation for Mutex<T> and MutexGuard offers useful information.

We’ll round out this chapter by talking about the Send and Sync traits and how we can use them with custom types.

Расширяемая конкурентность с Send и Sync

Extensible Concurrency with Send and Sync

Interestingly, almost every concurrency feature we’ve talked about so far in this chapter has been part of the standard library, not the language. Your options for handling concurrency are not limited to the language or the standard library; you can write your own concurrency features or use those written by others.

However, among the key concurrency concepts that are embedded in the language rather than the standard library are the std::marker traits Send and Sync.

Transferring Ownership Between Threads

The Send marker trait indicates that ownership of values of the type implementing Send can be transferred between threads. Almost every Rust type implements Send, but there are some exceptions, including Rc<T>: This cannot implement Send because if you cloned an Rc<T> value and tried to transfer ownership of the clone to another thread, both threads might update the reference count at the same time. For this reason, Rc<T> is implemented for use in single-threaded situations where you don’t want to pay the thread-safe performance penalty.

Therefore, Rust’s type system and trait bounds ensure that you can never accidentally send an Rc<T> value across threads unsafely. When we tried to do this in Listing 16-14, we got the error the trait `Send` is not implemented for `Rc<Mutex<i32>>`. When we switched to Arc<T>, which does implement Send, the code compiled.

Any type composed entirely of Send types is automatically marked as Send as well. Almost all primitive types are Send, aside from raw pointers, which we’ll discuss in Chapter 20.

Accessing from Multiple Threads

The Sync marker trait indicates that it is safe for the type implementing Sync to be referenced from multiple threads. In other words, any type T implements Sync if &T (an immutable reference to T) implements Send, meaning the reference can be sent safely to another thread. Similar to Send, primitive types all implement Sync, and types composed entirely of types that implement Sync also implement Sync.

The smart pointer Rc<T> also doesn’t implement Sync for the same reasons that it doesn’t implement Send. The RefCell<T> type (which we talked about in Chapter 15) and the family of related Cell<T> types don’t implement Sync. The implementation of borrow checking that RefCell<T> does at runtime is not thread-safe. The smart pointer Mutex<T> implements Sync and can be used to share access with multiple threads, as you saw in “Shared Access to Mutex<T>.

Implementing Send and Sync Manually Is Unsafe

Because types composed entirely of other types that implement the Send and Sync traits also automatically implement Send and Sync, we don’t have to implement those traits manually. As marker traits, they don’t even have any methods to implement. They’re just useful for enforcing invariants related to concurrency.

Manually implementing these traits involves implementing unsafe Rust code. We’ll talk about using unsafe Rust code in Chapter 20; for now, the important information is that building new concurrent types not made up of Send and Sync parts requires careful thought to uphold the safety guarantees. “The Rustonomicon” has more information about these guarantees and how to uphold them.

Summary

This isn’t the last you’ll see of concurrency in this book: The next chapter focuses on async programming, and the project in Chapter 21 will use the concepts in this chapter in a more realistic situation than the smaller examples discussed here.

As mentioned earlier, because very little of how Rust handles concurrency is part of the language, many concurrency solutions are implemented as crates. These evolve more quickly than the standard library, so be sure to search online for the current, state-of-the-art crates to use in multithreaded situations.

The Rust standard library provides channels for message passing and smart pointer types, such as Mutex<T> and Arc<T>, that are safe to use in concurrent contexts. The type system and the borrow checker ensure that the code using these solutions won’t end up with data races or invalid references. Once you get your code to compile, you can rest assured that it will happily run on multiple threads without the kinds of hard-to-track-down bugs common in other languages. Concurrent programming is no longer a concept to be afraid of: Go forth and make your programs concurrent, fearlessly!

Fundamentals of Asynchronous Programming: Async, Await, Futures, and Streams

Many operations we ask the computer to do can take a while to finish. It would be nice if we could do something else while we’re waiting for those long-running processes to complete. Modern computers offer two techniques for working on more than one operation at a time: parallelism and concurrency. Our programs’ logic, however, is written in a mostly linear fashion. We’d like to be able to specify the operations a program should perform and points at which a function could pause and some other part of the program could run instead, without needing to specify up front exactly the order and manner in which each bit of code should run. Asynchronous programming is an abstraction that lets us express our code in terms of potential pausing points and eventual results that takes care of the details of coordination for us.

This chapter builds on Chapter 16’s use of threads for parallelism and concurrency by introducing an alternative approach to writing code: Rust’s futures, streams, and the async and await syntax that let us express how operations could be asynchronous, and the third-party crates that implement asynchronous runtimes: code that manages and coordinates the execution of asynchronous operations.

Let’s consider an example. Say you’re exporting a video you’ve created of a family celebration, an operation that could take anywhere from minutes to hours. The video export will use as much CPU and GPU power as it can. If you had only one CPU core and your operating system didn’t pause that export until it completed—that is, if it executed the export synchronously—you couldn’t do anything else on your computer while that task was running. That would be a pretty frustrating experience. Fortunately, your computer’s operating system can, and does, invisibly interrupt the export often enough to let you get other work done simultaneously.

Now say you’re downloading a video shared by someone else, which can also take a while but does not take up as much CPU time. In this case, the CPU has to wait for data to arrive from the network. While you can start reading the data once it starts to arrive, it might take some time for all of it to show up. Even once the data is all present, if the video is quite large, it could take at least a second or two to load it all. That might not sound like much, but it’s a very long time for a modern processor, which can perform billions of operations every second. Again, your operating system will invisibly interrupt your program to allow the CPU to perform other work while waiting for the network call to finish.

The video export is an example of a CPU-bound or compute-bound operation. It’s limited by the computer’s potential data processing speed within the CPU or GPU, and how much of that speed it can dedicate to the operation. The video download is an example of an I/O-bound operation, because it’s limited by the speed of the computer’s input and output; it can only go as fast as the data can be sent across the network.

In both of these examples, the operating system’s invisible interrupts provide a form of concurrency. That concurrency happens only at the level of the entire program, though: the operating system interrupts one program to let other programs get work done. In many cases, because we understand our programs at a much more granular level than the operating system does, we can spot opportunities for concurrency that the operating system can’t see.

For example, if we’re building a tool to manage file downloads, we should be able to write our program so that starting one download won’t lock up the UI, and users should be able to start multiple downloads at the same time. Many operating system APIs for interacting with the network are blocking, though; that is, they block the program’s progress until the data they’re processing is completely ready.

Note: This is how most function calls work, if you think about it. However, the term blocking is usually reserved for function calls that interact with files, the network, or other resources on the computer, because those are the cases where an individual program would benefit from the operation being non-blocking.

We could avoid blocking our main thread by spawning a dedicated thread to download each file. However, the overhead of the system resources used by those threads would eventually become a problem. It would be preferable if the call didn’t block in the first place, and instead we could define a number of tasks that we’d like our program to complete and allow the runtime to choose the best order and manner in which to run them.

That is exactly what Rust’s async (short for asynchronous) abstraction gives us. In this chapter, you’ll learn all about async as we cover the following topics:

  • How to use Rust’s async and await syntax and execute asynchronous functions with a runtime
  • How to use the async model to solve some of the same challenges we looked at in Chapter 16
  • How multithreading and async provide complementary solutions that you can combine in many cases

Before we see how async works in practice, though, we need to take a short detour to discuss the differences between parallelism and concurrency.

Parallelism and Concurrency

We’ve treated parallelism and concurrency as mostly interchangeable so far. Now we need to distinguish between them more precisely, because the differences will show up as we start working.

Consider the different ways a team could split up work on a software project. You could assign a single member multiple tasks, assign each member one task, or use a mix of the two approaches.

When an individual works on several different tasks before any of them is complete, this is concurrency. One way to implement concurrency is similar to having two different projects checked out on your computer, and when you get bored or stuck on one project, you switch to the other. You’re just one person, so you can’t make progress on both tasks at the exact same time, but you can multitask, making progress on one at a time by switching between them (see Figure 17-1).

A diagram with stacked boxes labeled Task A and Task B, with diamonds in them representing subtasks. Arrows point from A1 to B1, B1 to A2, A2 to B2, B2 to A3, A3 to A4, and A4 to B3. The arrows between the subtasks cross the boxes between Task A and Task B.
Figure 17-1: A concurrent workflow, switching between Task A and Task B

When the team splits up a group of tasks by having each member take one task and work on it alone, this is parallelism. Each person on the team can make progress at the exact same time (see Figure 17-2).

A diagram with stacked boxes labeled Task A and Task B, with diamonds in them representing subtasks. Arrows point from A1 to A2, A2 to A3, A3 to A4, B1 to B2, and B2 to B3. No arrows cross between the boxes for Task A and Task B.
Figure 17-2: A parallel workflow, where work happens on Task A and Task B independently

In both of these workflows, you might have to coordinate between different tasks. Maybe you thought the task assigned to one person was totally independent from everyone else’s work, but it actually requires another person on the team to finish their task first. Some of the work could be done in parallel, but some of it was actually serial: it could only happen in a series, one task after the other, as in Figure 17-3.

A diagram with stacked boxes labeled Task A and Task B, with diamonds in them representing subtasks. In Task A, arrows point from A1 to A2, from A2 to a pair of thick vertical lines like a “pause” symbol, and from that symbol to A3. In task B, arrows point from B1 to B2, from B2 to B3, from B3 to A3, and from B3 to B4.
Figure 17-3: A partially parallel workflow, where work happens on Task A and Task B independently until Task A3 is blocked on the results of Task B3.

Likewise, you might realize that one of your own tasks depends on another of your tasks. Now your concurrent work has also become serial.

Parallelism and concurrency can intersect with each other, too. If you learn that a colleague is stuck until you finish one of your tasks, you’ll probably focus all your efforts on that task to “unblock” your colleague. You and your coworker are no longer able to work in parallel, and you’re also no longer able to work concurrently on your own tasks.

The same basic dynamics come into play with software and hardware. On a machine with a single CPU core, the CPU can perform only one operation at a time, but it can still work concurrently. Using tools such as threads, processes, and async, the computer can pause one activity and switch to others before eventually cycling back to that first activity again. On a machine with multiple CPU cores, it can also do work in parallel. One core can be performing one task while another core performs a completely unrelated one, and those operations actually happen at the same time.

Running async code in Rust usually happens concurrently. Depending on the hardware, the operating system, and the async runtime we are using (more on async runtimes shortly), that concurrency may also use parallelism under the hood.

Now, let’s dive into how async programming in Rust actually works.

Future и синтаксис Async

Futures and the Async Syntax

The key elements of asynchronous programming in Rust are futures and Rust’s async and await keywords.

A future is a value that may not be ready now but will become ready at some point in the future. (This same concept shows up in many languages, sometimes under other names such as task or promise.) Rust provides a Future trait as a building block so that different async operations can be implemented with different data structures but with a common interface. In Rust, futures are types that implement the Future trait. Each future holds its own information about the progress that has been made and what “ready” means.

You can apply the async keyword to blocks and functions to specify that they can be interrupted and resumed. Within an async block or async function, you can use the await keyword to await a future (that is, wait for it to become ready). Any point where you await a future within an async block or function is a potential spot for that block or function to pause and resume. The process of checking with a future to see if its value is available yet is called polling.

Some other languages, such as C# and JavaScript, also use async and await keywords for async programming. If you’re familiar with those languages, you may notice some significant differences in how Rust handles the syntax. That’s for good reason, as we’ll see!

When writing async Rust, we use the async and await keywords most of the time. Rust compiles them into equivalent code using the Future trait, much as it compiles for loops into equivalent code using the Iterator trait. Because Rust provides the Future trait, though, you can also implement it for your own data types when you need to. Many of the functions we’ll see throughout this chapter return types with their own implementations of Future. We’ll return to the definition of the trait at the end of the chapter and dig into more of how it works, but this is enough detail to keep us moving forward.

This may all feel a bit abstract, so let’s write our first async program: a little web scraper. We’ll pass in two URLs from the command line, fetch both of them concurrently, and return the result of whichever one finishes first. This example will have a fair bit of new syntax, but don’t worry—we’ll explain everything you need to know as we go.

Our First Async Program

To keep the focus of this chapter on learning async rather than juggling parts of the ecosystem, we’ve created the trpl crate (trpl is short for “The Rust Programming Language”). It re-exports all the types, traits, and functions you’ll need, primarily from the futures and tokio crates. The futures crate is an official home for Rust experimentation for async code, and it’s actually where the Future trait was originally designed. Tokio is the most widely used async runtime in Rust today, especially for web applications. There are other great runtimes out there, and they may be more suitable for your purposes. We use the tokio crate under the hood for trpl because it’s well tested and widely used.

In some cases, trpl also renames or wraps the original APIs to keep you focused on the details relevant to this chapter. If you want to understand what the crate does, we encourage you to check out its source code. You’ll be able to see what crate each re-export comes from, and we’ve left extensive comments explaining what the crate does.

Create a new binary project named hello-async and add the trpl crate as a dependency:

$ cargo new hello-async
$ cd hello-async
$ cargo add trpl

Now we can use the various pieces provided by trpl to write our first async program. We’ll build a little command line tool that fetches two web pages, pulls the <title> element from each, and prints out the title of whichever page finishes that whole process first.

Defining the page_title Function

Let’s start by writing a function that takes one page URL as a parameter, makes a request to it, and returns the text of the <title> element (see Listing 17-1).

Filename: src/main.rs
extern crate trpl; // required for mdbook test

fn main() {
    // TODO: we'll add this next!
}

use trpl::Html;

async fn page_title(url: &str) -> Option<String> {
    let response = trpl::get(url).await;
    let response_text = response.text().await;
    Html::parse(&response_text)
        .select_first("title")
        .map(|title| title.inner_html())
}
Listing 17-1: Defining an async function to get the title element from an HTML page

First, we define a function named page_title and mark it with the async keyword. Then we use the trpl::get function to fetch whatever URL is passed in and add the await keyword to await the response. To get the text of the response, we call its text method and once again await it with the await keyword. Both of these steps are asynchronous. For the get function, we have to wait for the server to send back the first part of its response, which will include HTTP headers, cookies, and so on and can be delivered separately from the response body. Especially if the body is very large, it can take some time for it all to arrive. Because we have to wait for the entirety of the response to arrive, the text method is also async.

We have to explicitly await both of these futures, because futures in Rust are lazy: they don’t do anything until you ask them to with the await keyword. (In fact, Rust will show a compiler warning if you don’t use a future.) This might remind you of the discussion of iterators in the “Processing a Series of Items with Iterators” section in Chapter 13. Iterators do nothing unless you call their next method—whether directly or by using for loops or methods such as map that use next under the hood. Likewise, futures do nothing unless you explicitly ask them to. This laziness allows Rust to avoid running async code until it’s actually needed.

Note: This is different from the behavior we saw when using thread::spawn in the “Creating a New Thread with spawn” section in Chapter 16, where the closure we passed to another thread started running immediately. It’s also different from how many other languages approach async. But it’s important for Rust to be able to provide its performance guarantees, just as it is with iterators.

Once we have response_text, we can parse it into an instance of the Html type using Html::parse. Instead of a raw string, we now have a data type we can use to work with the HTML as a richer data structure. In particular, we can use the select_first method to find the first instance of a given CSS selector. By passing the string "title", we’ll get the first <title> element in the document, if there is one. Because there may not be any matching element, select_first returns an Option<ElementRef>. Finally, we use the Option::map method, which lets us work with the item in the Option if it’s present, and do nothing if it isn’t. (We could also use a match expression here, but map is more idiomatic.) In the body of the function we supply to map, we call inner_html on the title to get its content, which is a String. When all is said and done, we have an Option<String>.

Notice that Rust’s await keyword goes after the expression you’re awaiting, not before it. That is, it’s a postfix keyword. This may differ from what you’re used to if you’ve used async in other languages, but in Rust it makes chains of methods much nicer to work with. As a result, we could change the body of page_title to chain the trpl::get and text function calls together with await between them, as shown in Listing 17-2.

Filename: src/main.rs
extern crate trpl; // required for mdbook test

use trpl::Html;

fn main() {
    // TODO: we'll add this next!
}

async fn page_title(url: &str) -> Option<String> {
    let response_text = trpl::get(url).await.text().await;
    Html::parse(&response_text)
        .select_first("title")
        .map(|title| title.inner_html())
}
Listing 17-2: Chaining with the await keyword

With that, we have successfully written our first async function! Before we add some code in main to call it, let’s talk a little more about what we’ve written and what it means.

When Rust sees a block marked with the async keyword, it compiles it into a unique, anonymous data type that implements the Future trait. When Rust sees a function marked with async, it compiles it into a non-async function whose body is an async block. An async function’s return type is the type of the anonymous data type the compiler creates for that async block.

Thus, writing async fn is equivalent to writing a function that returns a future of the return type. To the compiler, a function definition such as the async fn page_title in Listing 17-1 is roughly equivalent to a non-async function defined like this:

#![allow(unused)]
fn main() {
extern crate trpl; // required for mdbook test
use std::future::Future;
use trpl::Html;

fn page_title(url: &str) -> impl Future<Output = Option<String>> {
    async move {
        let text = trpl::get(url).await.text().await;
        Html::parse(&text)
            .select_first("title")
            .map(|title| title.inner_html())
    }
}
}

Let’s walk through each part of the transformed version:

  • It uses the impl Trait syntax we discussed back in Chapter 10 in the “Traits as Parameters” section.
  • The returned value implements the Future trait with an associated type of Output. Notice that the Output type is Option<String>, which is the same as the original return type from the async fn version of page_title.
  • All of the code called in the body of the original function is wrapped in an async move block. Remember that blocks are expressions. This whole block is the expression returned from the function.
  • This async block produces a value with the type Option<String>, as just described. That value matches the Output type in the return type. This is just like other blocks you have seen.
  • The new function body is an async move block because of how it uses the url parameter. (We’ll talk much more about async versus async move later in the chapter.)

Now we can call page_title in main.

Executing an Async Function with a Runtime

To start, we’ll get the title for a single page, shown in Listing 17-3. Unfortunately, this code doesn’t compile yet.

Filename: src/main.rs
extern crate trpl; // required for mdbook test

use trpl::Html;

async fn main() {
    let args: Vec<String> = std::env::args().collect();
    let url = &args[1];
    match page_title(url).await {
        Some(title) => println!("The title for {url} was {title}"),
        None => println!("{url} had no title"),
    }
}

async fn page_title(url: &str) -> Option<String> {
    let response_text = trpl::get(url).await.text().await;
    Html::parse(&response_text)
        .select_first("title")
        .map(|title| title.inner_html())
}
Listing 17-3: Calling the page_title function from main with a user-supplied argument

We follow the same pattern we used to get command line arguments in the “Accepting Command Line Arguments” section in Chapter 12. Then we pass the URL argument to page_title and await the result. Because the value produced by the future is an Option<String>, we use a match expression to print different messages to account for whether the page had a <title>.

The only place we can use the await keyword is in async functions or blocks, and Rust won’t let us mark the special main function as async.

error[E0752]: `main` function is not allowed to be `async`
 --> src/main.rs:6:1
  |
6 | async fn main() {
  | ^^^^^^^^^^^^^^^ `main` function is not allowed to be `async`

The reason main can’t be marked async is that async code needs a runtime: a Rust crate that manages the details of executing asynchronous code. A program’s main function can initialize a runtime, but it’s not a runtime itself. (We’ll see more about why this is the case in a bit.) Every Rust program that executes async code has at least one place where it sets up a runtime that executes the futures.

Most languages that support async bundle a runtime, but Rust does not. Instead, there are many different async runtimes available, each of which makes different tradeoffs suitable to the use case it targets. For example, a high-throughput web server with many CPU cores and a large amount of RAM has very different needs than a microcontroller with a single core, a small amount of RAM, and no heap allocation ability. The crates that provide those runtimes also often supply async versions of common functionality such as file or network I/O.

Here, and throughout the rest of this chapter, we’ll use the block_on function from the trpl crate, which takes a future as an argument and blocks the current thread until this future runs to completion. Behind the scenes, calling block_on sets up a runtime using the tokio crate that’s used to run the future passed in (the trpl crate’s block_on behavior is similar to other runtime crates’ block_on functions). Once the future completes, block_on returns whatever value the future produced.

We could pass the future returned by page_title directly to block_on and, once it completed, we could match on the resulting Option<String> as we tried to do in Listing 17-3. However, for most of the examples in the chapter (and most async code in the real world), we’ll be doing more than just one async function call, so instead we’ll pass an async block and explicitly await the result of the page_title call, as in Listing 17-4.

Filename: src/main.rs
extern crate trpl; // required for mdbook test

use trpl::Html;

fn main() {
    let args: Vec<String> = std::env::args().collect();

    trpl::block_on(async {
        let url = &args[1];
        match page_title(url).await {
            Some(title) => println!("The title for {url} was {title}"),
            None => println!("{url} had no title"),
        }
    })
}

async fn page_title(url: &str) -> Option<String> {
    let response_text = trpl::get(url).await.text().await;
    Html::parse(&response_text)
        .select_first("title")
        .map(|title| title.inner_html())
}
Listing 17-4: Awaiting an async block with trpl::block_on

When we run this code, we get the behavior we expected initially:

$ cargo run -- "https://www.rust-lang.org"
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.05s
     Running `target/debug/async_await 'https://www.rust-lang.org'`
The title for https://www.rust-lang.org was
            Rust Programming Language

Phew—we finally have some working async code! But before we add the code to race two sites against each other, let’s briefly turn our attention back to how futures work.

Each await point—that is, every place where the code uses the await keyword—represents a place where control is handed back to the runtime. To make that work, Rust needs to keep track of the state involved in the async block so that the runtime could kick off some other work and then come back when it’s ready to try advancing the first one again. This is an invisible state machine, as if you’d written an enum like this to save the current state at each await point:

#![allow(unused)]
fn main() {
extern crate trpl; // required for mdbook test

enum PageTitleFuture<'a> {
    Initial { url: &'a str },
    GetAwaitPoint { url: &'a str },
    TextAwaitPoint { response: trpl::Response },
}
}

Writing the code to transition between each state by hand would be tedious and error-prone, however, especially when you need to add more functionality and more states to the code later. Fortunately, the Rust compiler creates and manages the state machine data structures for async code automatically. The normal borrowing and ownership rules around data structures all still apply, and happily, the compiler also handles checking those for us and provides useful error messages. We’ll work through a few of those later in the chapter.

Ultimately, something has to execute this state machine, and that something is a runtime. (This is why you may come across mentions of executors when looking into runtimes: an executor is the part of a runtime responsible for executing the async code.)

Now you can see why the compiler stopped us from making main itself an async function back in Listing 17-3. If main were an async function, something else would need to manage the state machine for whatever future main returned, but main is the starting point for the program! Instead, we called the trpl::block_on function in main to set up a runtime and run the future returned by the async block until it’s done.

Note: Some runtimes provide macros so you can write an async main function. Those macros rewrite async fn main() { ... } to be a normal fn main, which does the same thing we did by hand in Listing 17-4: call a function that runs a future to completion the way trpl::block_on does.

Now let’s put these pieces together and see how we can write concurrent code.

Racing Two URLs Against Each Other Concurrently

In Listing 17-5, we call page_title with two different URLs passed in from the command line and race them by selecting whichever future finishes first.

Filename: src/main.rs
extern crate trpl; // required for mdbook test

use trpl::{Either, Html};

fn main() {
    let args: Vec<String> = std::env::args().collect();

    trpl::block_on(async {
        let title_fut_1 = page_title(&args[1]);
        let title_fut_2 = page_title(&args[2]);

        let (url, maybe_title) =
            match trpl::select(title_fut_1, title_fut_2).await {
                Either::Left(left) => left,
                Either::Right(right) => right,
            };

        println!("{url} returned first");
        match maybe_title {
            Some(title) => println!("Its page title was: '{title}'"),
            None => println!("It had no title."),
        }
    })
}

async fn page_title(url: &str) -> (&str, Option<String>) {
    let response_text = trpl::get(url).await.text().await;
    let title = Html::parse(&response_text)
        .select_first("title")
        .map(|title| title.inner_html());
    (url, title)
}
Listing 17-5: Calling page_title for two URLs to see which returns first

We begin by calling page_title for each of the user-supplied URLs. We save the resulting futures as title_fut_1 and title_fut_2. Remember, these don’t do anything yet, because futures are lazy and we haven’t yet awaited them. Then we pass the futures to trpl::select, which returns a value to indicate which of the futures passed to it finishes first.

Note: Under the hood, trpl::select is built on a more general select function defined in the futures crate. The futures crate’s select function can do a lot of things that the trpl::select function can’t, but it also has some additional complexity that we can skip over for now.

Either future can legitimately “win,” so it doesn’t make sense to return a Result. Instead, trpl::select returns a type we haven’t seen before, trpl::Either. The Either type is somewhat similar to a Result in that it has two cases. Unlike Result, though, there is no notion of success or failure baked into Either. Instead, it uses Left and Right to indicate “one or the other”:

#![allow(unused)]
fn main() {
enum Either<A, B> {
    Left(A),
    Right(B),
}
}

The select function returns Left with that future’s output if the first argument wins, and Right with the second future argument’s output if that one wins. This matches the order the arguments appear in when calling the function: the first argument is to the left of the second argument.

We also update page_title to return the same URL passed in. That way, if the page that returns first does not have a <title> we can resolve, we can still print a meaningful message. With that information available, we wrap up by updating our println! output to indicate both which URL finished first and what, if any, the <title> is for the web page at that URL.

You have built a small working web scraper now! Pick a couple URLs and run the command line tool. You may discover that some sites are consistently faster than others, while in other cases the faster site varies from run to run. More importantly, you’ve learned the basics of working with futures, so now we can dig deeper into what we can do with async.

Применение конкурентности с Async

Applying Concurrency with Async

In this section, we’ll apply async to some of the same concurrency challenges we tackled with threads in Chapter 16. Because we already talked about a lot of the key ideas there, in this section we’ll focus on what’s different between threads and futures.

In many cases, the APIs for working with concurrency using async are very similar to those for using threads. In other cases, they end up being quite different. Even when the APIs look similar between threads and async, they often have different behavior—and they nearly always have different performance characteristics.

Creating a New Task with spawn_task

The first operation we tackled in the “Creating a New Thread with spawn section in Chapter 16 was counting up on two separate threads. Let’s do the same using async. The trpl crate supplies a spawn_task function that looks very similar to the thread::spawn API, and a sleep function that is an async version of the thread::sleep API. We can use these together to implement the counting example, as shown in Listing 17-6.

Filename: src/main.rs
extern crate trpl; // required for mdbook test

use std::time::Duration;

fn main() {
    trpl::block_on(async {
        trpl::spawn_task(async {
            for i in 1..10 {
                println!("hi number {i} from the first task!");
                trpl::sleep(Duration::from_millis(500)).await;
            }
        });

        for i in 1..5 {
            println!("hi number {i} from the second task!");
            trpl::sleep(Duration::from_millis(500)).await;
        }
    });
}
Listing 17-6: Creating a new task to print one thing while the main task prints something else

As our starting point, we set up our main function with trpl::block_on so that our top-level function can be async.

Note: From this point forward in the chapter, every example will include this exact same wrapping code with trpl::block_on in main, so we’ll often skip it just as we do with main. Remember to include it in your code!

Then we write two loops within that block, each containing a trpl::sleep call, which waits for half a second (500 milliseconds) before sending the next message. We put one loop in the body of a trpl::spawn_task and the other in a top-level for loop. We also add an await after the sleep calls.

This code behaves similarly to the thread-based implementation—including the fact that you may see the messages appear in a different order in your own terminal when you run it:

hi number 1 from the second task!
hi number 1 from the first task!
hi number 2 from the first task!
hi number 2 from the second task!
hi number 3 from the first task!
hi number 3 from the second task!
hi number 4 from the first task!
hi number 4 from the second task!
hi number 5 from the first task!

This version stops as soon as the for loop in the body of the main async block finishes, because the task spawned by spawn_task is shut down when the main function ends. If you want it to run all the way to the task’s completion, you will need to use a join handle to wait for the first task to complete. With threads, we used the join method to “block” until the thread was done running. In Listing 17-7, we can use await to do the same thing, because the task handle itself is a future. Its Output type is a Result, so we also unwrap it after awaiting it.

Filename: src/main.rs
extern crate trpl; // required for mdbook test

use std::time::Duration;

fn main() {
    trpl::block_on(async {
        let handle = trpl::spawn_task(async {
            for i in 1..10 {
                println!("hi number {i} from the first task!");
                trpl::sleep(Duration::from_millis(500)).await;
            }
        });

        for i in 1..5 {
            println!("hi number {i} from the second task!");
            trpl::sleep(Duration::from_millis(500)).await;
        }

        handle.await.unwrap();
    });
}
Listing 17-7: Using await with a join handle to run a task to completion

This updated version runs until both loops finish:

hi number 1 from the second task!
hi number 1 from the first task!
hi number 2 from the first task!
hi number 2 from the second task!
hi number 3 from the first task!
hi number 3 from the second task!
hi number 4 from the first task!
hi number 4 from the second task!
hi number 5 from the first task!
hi number 6 from the first task!
hi number 7 from the first task!
hi number 8 from the first task!
hi number 9 from the first task!

So far, it looks like async and threads give us similar outcomes, just with different syntax: using await instead of calling join on the join handle, and awaiting the sleep calls.

The bigger difference is that we didn’t need to spawn another operating system thread to do this. In fact, we don’t even need to spawn a task here. Because async blocks compile to anonymous futures, we can put each loop in an async block and have the runtime run them both to completion using the trpl::join function.

In the “Waiting for All Threads to Finish” section in Chapter 16, we showed how to use the join method on the JoinHandle type returned when you call std::thread::spawn. The trpl::join function is similar, but for futures. When you give it two futures, it produces a single new future whose output is a tuple containing the output of each future you passed in once they both complete. Thus, in Listing 17-8, we use trpl::join to wait for both fut1 and fut2 to finish. We do not await fut1 and fut2 but instead the new future produced by trpl::join. We ignore the output, because it’s just a tuple containing two unit values.

Filename: src/main.rs
extern crate trpl; // required for mdbook test

use std::time::Duration;

fn main() {
    trpl::block_on(async {
        let fut1 = async {
            for i in 1..10 {
                println!("hi number {i} from the first task!");
                trpl::sleep(Duration::from_millis(500)).await;
            }
        };

        let fut2 = async {
            for i in 1..5 {
                println!("hi number {i} from the second task!");
                trpl::sleep(Duration::from_millis(500)).await;
            }
        };

        trpl::join(fut1, fut2).await;
    });
}
Listing 17-8: Using trpl::join to await two anonymous futures

When we run this, we see both futures run to completion:

hi number 1 from the first task!
hi number 1 from the second task!
hi number 2 from the first task!
hi number 2 from the second task!
hi number 3 from the first task!
hi number 3 from the second task!
hi number 4 from the first task!
hi number 4 from the second task!
hi number 5 from the first task!
hi number 6 from the first task!
hi number 7 from the first task!
hi number 8 from the first task!
hi number 9 from the first task!

Now, you’ll see the exact same order every time, which is very different from what we saw with threads and with trpl::spawn_task in Listing 17-7. That is because the trpl::join function is fair, meaning it checks each future equally often, alternating between them, and never lets one race ahead if the other is ready. With threads, the operating system decides which thread to check and how long to let it run. With async Rust, the runtime decides which task to check. (In practice, the details get complicated because an async runtime might use operating system threads under the hood as part of how it manages concurrency, so guaranteeing fairness can be more work for a runtime—but it’s still possible!) Runtimes don’t have to guarantee fairness for any given operation, and they often offer different APIs to let you choose whether or not you want fairness.

Try some of these variations on awaiting the futures and see what they do:

  • Remove the async block from around either or both of the loops.
  • Await each async block immediately after defining it.
  • Wrap only the first loop in an async block, and await the resulting future after the body of second loop.

For an extra challenge, see if you can figure out what the output will be in each case before running the code!

Sending Data Between Two Tasks Using Message Passing

Sharing data between futures will also be familiar: we’ll use message passing again, but this time with async versions of the types and functions. We’ll take a slightly different path than we did in the “Transfer Data Between Threads with Message Passing” section in Chapter 16 to illustrate some of the key differences between thread-based and futures-based concurrency. In Listing 17-9, we’ll begin with just a single async block—not spawning a separate task as we spawned a separate thread.

Filename: src/main.rs
extern crate trpl; // required for mdbook test

fn main() {
    trpl::block_on(async {
        let (tx, mut rx) = trpl::channel();

        let val = String::from("hi");
        tx.send(val).unwrap();

        let received = rx.recv().await.unwrap();
        println!("received '{received}'");
    });
}
Listing 17-9: Creating an async channel and assigning the two halves to tx and rx

Here, we use trpl::channel, an async version of the multiple-producer, single-consumer channel API we used with threads back in Chapter 16. The async version of the API is only a little different from the thread-based version: it uses a mutable rather than an immutable receiver rx, and its recv method produces a future we need to await rather than producing the value directly. Now we can send messages from the sender to the receiver. Notice that we don’t have to spawn a separate thread or even a task; we merely need to await the rx.recv call.

The synchronous Receiver::recv method in std::mpsc::channel blocks until it receives a message. The trpl::Receiver::recv method does not, because it is async. Instead of blocking, it hands control back to the runtime until either a message is received or the send side of the channel closes. By contrast, we don’t await the send call, because it doesn’t block. It doesn’t need to, because the channel we’re sending it into is unbounded.

Note: Because all of this async code runs in an async block in a trpl::block_on call, everything within it can avoid blocking. However, the code outside it will block on the block_on function returning. That’s the whole point of the trpl::block_on function: it lets you choose where to block on some set of async code, and thus where to transition between sync and async code.

Notice two things about this example. First, the message will arrive right away. Second, although we use a future here, there’s no concurrency yet. Everything in the listing happens in sequence, just as it would if there were no futures involved.

Let’s address the first part by sending a series of messages and sleeping in between them, as shown in Listing 17-10.

Filename: src/main.rs
extern crate trpl; // required for mdbook test

use std::time::Duration;

fn main() {
    trpl::block_on(async {
        let (tx, mut rx) = trpl::channel();

        let vals = vec![
            String::from("hi"),
            String::from("from"),
            String::from("the"),
            String::from("future"),
        ];

        for val in vals {
            tx.send(val).unwrap();
            trpl::sleep(Duration::from_millis(500)).await;
        }

        while let Some(value) = rx.recv().await {
            println!("received '{value}'");
        }
    });
}
Listing 17-10: Sending and receiving multiple messages over the async channel and sleeping with an await between each message

In addition to sending the messages, we need to receive them. In this case, because we know how many messages are coming in, we could do that manually by calling rx.recv().await four times. In the real world, though, we’ll generally be waiting on some unknown number of messages, so we need to keep waiting until we determine that there are no more messages.

In Listing 16-10, we used a for loop to process all the items received from a synchronous channel. Rust doesn’t yet have a way to use a for loop with an asynchronously produced series of items, however, so we need to use a loop we haven’t seen before: the while let conditional loop. This is the loop version of the if let construct we saw back in the “Concise Control Flow with if let and let...else section in Chapter 6. The loop will continue executing as long as the pattern it specifies continues to match the value.

The rx.recv call produces a future, which we await. The runtime will pause the future until it is ready. Once a message arrives, the future will resolve to Some(message) as many times as a message arrives. When the channel closes, regardless of whether any messages have arrived, the future will instead resolve to None to indicate that there are no more values and thus we should stop polling—that is, stop awaiting.

The while let loop pulls all of this together. If the result of calling rx.recv().await is Some(message), we get access to the message and we can use it in the loop body, just as we could with if let. If the result is None, the loop ends. Every time the loop completes, it hits the await point again, so the runtime pauses it again until another message arrives.

The code now successfully sends and receives all of the messages. Unfortunately, there are still a couple of problems. For one thing, the messages do not arrive at half-second intervals. They arrive all at once, 2 seconds (2,000 milliseconds) after we start the program. For another, this program also never exits! Instead, it waits forever for new messages. You will need to shut it down using ctrl-C.

Code Within One Async Block Executes Linearly

Let’s start by examining why the messages come in all at once after the full delay, rather than coming in with delays between each one. Within a given async block, the order in which await keywords appear in the code is also the order in which they’re executed when the program runs.

There’s only one async block in Listing 17-10, so everything in it runs linearly. There’s still no concurrency. All the tx.send calls happen, interspersed with all of the trpl::sleep calls and their associated await points. Only then does the while let loop get to go through any of the await points on the recv calls.

To get the behavior we want, where the sleep delay happens between each message, we need to put the tx and rx operations in their own async blocks, as shown in Listing 17-11. Then the runtime can execute each of them separately using trpl::join, just as in Listing 17-8. Once again, we await the result of calling trpl::join, not the individual futures. If we awaited the individual futures in sequence, we would just end up back in a sequential flow—exactly what we’re trying not to do.

Filename: src/main.rs
extern crate trpl; // required for mdbook test

use std::time::Duration;

fn main() {
    trpl::block_on(async {
        let (tx, mut rx) = trpl::channel();

        let tx_fut = async {
            let vals = vec![
                String::from("hi"),
                String::from("from"),
                String::from("the"),
                String::from("future"),
            ];

            for val in vals {
                tx.send(val).unwrap();
                trpl::sleep(Duration::from_millis(500)).await;
            }
        };

        let rx_fut = async {
            while let Some(value) = rx.recv().await {
                println!("received '{value}'");
            }
        };

        trpl::join(tx_fut, rx_fut).await;
    });
}
Listing 17-11: Separating send and recv into their own async blocks and awaiting the futures for those blocks

With the updated code in Listing 17-11, the messages get printed at 500-millisecond intervals, rather than all in a rush after 2 seconds.

Moving Ownership Into an Async Block

The program still never exits, though, because of the way the while let loop interacts with trpl::join:

  • The future returned from trpl::join completes only once both futures passed to it have completed.
  • The tx_fut future completes once it finishes sleeping after sending the last message in vals.
  • The rx_fut future won’t complete until the while let loop ends.
  • The while let loop won’t end until awaiting rx.recv produces None.
  • Awaiting rx.recv will return None only once the other end of the channel is closed.
  • The channel will close only if we call rx.close or when the sender side, tx, is dropped.
  • We don’t call rx.close anywhere, and tx won’t be dropped until the outermost async block passed to trpl::block_on ends.
  • The block can’t end because it is blocked on trpl::join completing, which takes us back to the top of this list.

Right now, the async block where we send the messages only borrows tx because sending a message doesn’t require ownership, but if we could move tx into that async block, it would be dropped once that block ends. In the “Capturing References or Moving Ownership” section in Chapter 13, you learned how to use the move keyword with closures, and, as discussed in the “Using move Closures with Threads” section in Chapter 16, we often need to move data into closures when working with threads. The same basic dynamics apply to async blocks, so the move keyword works with async blocks just as it does with closures.

In Listing 17-12, we change the block used to send messages from async to async move.

Filename: src/main.rs
extern crate trpl; // required for mdbook test

use std::time::Duration;

fn main() {
    trpl::block_on(async {
        let (tx, mut rx) = trpl::channel();

        let tx_fut = async move {
            // --snip--
            let vals = vec![
                String::from("hi"),
                String::from("from"),
                String::from("the"),
                String::from("future"),
            ];

            for val in vals {
                tx.send(val).unwrap();
                trpl::sleep(Duration::from_millis(500)).await;
            }
        };

        let rx_fut = async {
            while let Some(value) = rx.recv().await {
                println!("received '{value}'");
            }
        };

        trpl::join(tx_fut, rx_fut).await;
    });
}
Listing 17-12: A revision of the code from Listing 17-11 that correctly shuts down when complete

When we run this version of the code, it shuts down gracefully after the last message is sent and received. Next, let’s see what would need to change to send data from more than one future.

Joining a Number of Futures with the join! Macro

This async channel is also a multiple-producer channel, so we can call clone on tx if we want to send messages from multiple futures, as shown in Listing 17-13.

Filename: src/main.rs
extern crate trpl; // required for mdbook test

use std::time::Duration;

fn main() {
    trpl::block_on(async {
        let (tx, mut rx) = trpl::channel();

        let tx1 = tx.clone();
        let tx1_fut = async move {
            let vals = vec![
                String::from("hi"),
                String::from("from"),
                String::from("the"),
                String::from("future"),
            ];

            for val in vals {
                tx1.send(val).unwrap();
                trpl::sleep(Duration::from_millis(500)).await;
            }
        };

        let rx_fut = async {
            while let Some(value) = rx.recv().await {
                println!("received '{value}'");
            }
        };

        let tx_fut = async move {
            let vals = vec![
                String::from("more"),
                String::from("messages"),
                String::from("for"),
                String::from("you"),
            ];

            for val in vals {
                tx.send(val).unwrap();
                trpl::sleep(Duration::from_millis(1500)).await;
            }
        };

        trpl::join!(tx1_fut, tx_fut, rx_fut);
    });
}
Listing 17-13: Using multiple producers with async blocks

First, we clone tx, creating tx1 outside the first async block. We move tx1 into that block just as we did before with tx. Then, later, we move the original tx into a new async block, where we send more messages on a slightly slower delay. We happen to put this new async block after the async block for receiving messages, but it could go before it just as well. The key is the order in which the futures are awaited, not in which they’re created.

Both of the async blocks for sending messages need to be async move blocks so that both tx and tx1 get dropped when those blocks finish. Otherwise, we’ll end up back in the same infinite loop we started out in.

Finally, we switch from trpl::join to trpl::join! to handle the additional future: the join! macro awaits an arbitrary number of futures where we know the number of futures at compile time. We’ll discuss awaiting a collection of an unknown number of futures later in this chapter.

Now we see all the messages from both sending futures, and because the sending futures use slightly different delays after sending, the messages are also received at those different intervals:

received 'hi'
received 'more'
received 'from'
received 'the'
received 'messages'
received 'future'
received 'for'
received 'you'

We’ve explored how to use message passing to send data between futures, how code within an async block runs sequentially, how to move ownership into an async block, and how to join multiple futures. Next, let’s discuss how and why to tell the runtime it can switch to another task.

Работа с произвольным количеством Future

Yielding Control to the Runtime

Recall from the “Our First Async Program” section that at each await point, Rust gives a runtime a chance to pause the task and switch to another one if the future being awaited isn’t ready. The inverse is also true: Rust only pauses async blocks and hands control back to a runtime at an await point. Everything between await points is synchronous.

That means if you do a bunch of work in an async block without an await point, that future will block any other futures from making progress. You may sometimes hear this referred to as one future starving other futures. In some cases, that may not be a big deal. However, if you are doing some kind of expensive setup or long-running work, or if you have a future that will keep doing some particular task indefinitely, you’ll need to think about when and where to hand control back to the runtime.

Let’s simulate a long-running operation to illustrate the starvation problem, then explore how to solve it. Listing 17-14 introduces a slow function.

Filename: src/main.rs
extern crate trpl; // required for mdbook test

use std::{thread, time::Duration};

fn main() {
    trpl::block_on(async {
        // We will call `slow` here later
    });
}

fn slow(name: &str, ms: u64) {
    thread::sleep(Duration::from_millis(ms));
    println!("'{name}' ran for {ms}ms");
}
Listing 17-14: Using thread::sleep to simulate slow operations

This code uses std::thread::sleep instead of trpl::sleep so that calling slow will block the current thread for some number of milliseconds. We can use slow to stand in for real-world operations that are both long-running and blocking.

In Listing 17-15, we use slow to emulate doing this kind of CPU-bound work in a pair of futures.

Filename: src/main.rs
extern crate trpl; // required for mdbook test

use std::{thread, time::Duration};

fn main() {
    trpl::block_on(async {
        let a = async {
            println!("'a' started.");
            slow("a", 30);
            slow("a", 10);
            slow("a", 20);
            trpl::sleep(Duration::from_millis(50)).await;
            println!("'a' finished.");
        };

        let b = async {
            println!("'b' started.");
            slow("b", 75);
            slow("b", 10);
            slow("b", 15);
            slow("b", 350);
            trpl::sleep(Duration::from_millis(50)).await;
            println!("'b' finished.");
        };

        trpl::select(a, b).await;
    });
}

fn slow(name: &str, ms: u64) {
    thread::sleep(Duration::from_millis(ms));
    println!("'{name}' ran for {ms}ms");
}
Listing 17-15: Calling the slow function to simulate slow operations

Each future hands control back to the runtime only after carrying out a bunch of slow operations. If you run this code, you will see this output:

'a' started.
'a' ran for 30ms
'a' ran for 10ms
'a' ran for 20ms
'b' started.
'b' ran for 75ms
'b' ran for 10ms
'b' ran for 15ms
'b' ran for 350ms
'a' finished.

As with Listing 17-5 where we used trpl::select to race futures fetching two URLs, select still finishes as soon as a is done. There’s no interleaving between the calls to slow in the two futures, though. The a future does all of its work until the trpl::sleep call is awaited, then the b future does all of its work until its own trpl::sleep call is awaited, and finally the a future completes. To allow both futures to make progress between their slow tasks, we need await points so we can hand control back to the runtime. That means we need something we can await!

We can already see this kind of handoff happening in Listing 17-15: if we removed the trpl::sleep at the end of the a future, it would complete without the b future running at all. Let’s try using the trpl::sleep function as a starting point for letting operations switch off making progress, as shown in Listing 17-16.

Filename: src/main.rs
extern crate trpl; // required for mdbook test

use std::{thread, time::Duration};

fn main() {
    trpl::block_on(async {
        let one_ms = Duration::from_millis(1);

        let a = async {
            println!("'a' started.");
            slow("a", 30);
            trpl::sleep(one_ms).await;
            slow("a", 10);
            trpl::sleep(one_ms).await;
            slow("a", 20);
            trpl::sleep(one_ms).await;
            println!("'a' finished.");
        };

        let b = async {
            println!("'b' started.");
            slow("b", 75);
            trpl::sleep(one_ms).await;
            slow("b", 10);
            trpl::sleep(one_ms).await;
            slow("b", 15);
            trpl::sleep(one_ms).await;
            slow("b", 350);
            trpl::sleep(one_ms).await;
            println!("'b' finished.");
        };

        trpl::select(a, b).await;
    });
}

fn slow(name: &str, ms: u64) {
    thread::sleep(Duration::from_millis(ms));
    println!("'{name}' ran for {ms}ms");
}
Listing 17-16: Using trpl::sleep to let operations switch off making progress

We’ve added trpl::sleep calls with await points between each call to slow. Now the two futures’ work is interleaved:

'a' started.
'a' ran for 30ms
'b' started.
'b' ran for 75ms
'a' ran for 10ms
'b' ran for 10ms
'a' ran for 20ms
'b' ran for 15ms
'a' finished.

The a future still runs for a bit before handing off control to b, because it calls slow before ever calling trpl::sleep, but after that the futures swap back and forth each time one of them hits an await point. In this case, we have done that after every call to slow, but we could break up the work in whatever way makes the most sense to us.

We don’t really want to sleep here, though: we want to make progress as fast as we can. We just need to hand back control to the runtime. We can do that directly, using the trpl::yield_now function. In Listing 17-17, we replace all those trpl::sleep calls with trpl::yield_now.

Filename: src/main.rs
extern crate trpl; // required for mdbook test

use std::{thread, time::Duration};

fn main() {
    trpl::block_on(async {
        let a = async {
            println!("'a' started.");
            slow("a", 30);
            trpl::yield_now().await;
            slow("a", 10);
            trpl::yield_now().await;
            slow("a", 20);
            trpl::yield_now().await;
            println!("'a' finished.");
        };

        let b = async {
            println!("'b' started.");
            slow("b", 75);
            trpl::yield_now().await;
            slow("b", 10);
            trpl::yield_now().await;
            slow("b", 15);
            trpl::yield_now().await;
            slow("b", 350);
            trpl::yield_now().await;
            println!("'b' finished.");
        };

        trpl::select(a, b).await;
    });
}

fn slow(name: &str, ms: u64) {
    thread::sleep(Duration::from_millis(ms));
    println!("'{name}' ran for {ms}ms");
}
Listing 17-17: Using yield_now to let operations switch off making progress

This code is both clearer about the actual intent and can be significantly faster than using sleep, because timers such as the one used by sleep often have limits on how granular they can be. The version of sleep we are using, for example, will always sleep for at least a millisecond, even if we pass it a Duration of one nanosecond. Again, modern computers are fast: they can do a lot in one millisecond!

This means that async can be useful even for compute-bound tasks, depending on what else your program is doing, because it provides a useful tool for structuring the relationships between different parts of the program (but at a cost of the overhead of the async state machine). This is a form of cooperative multitasking, where each future has the power to determine when it hands over control via await points. Each future therefore also has the responsibility to avoid blocking for too long. In some Rust-based embedded operating systems, this is the only kind of multitasking!

In real-world code, you won’t usually be alternating function calls with await points on every single line, of course. While yielding control in this way is relatively inexpensive, it’s not free. In many cases, trying to break up a compute-bound task might make it significantly slower, so sometimes it’s better for overall performance to let an operation block briefly. Always measure to see what your code’s actual performance bottlenecks are. The underlying dynamic is important to keep in mind, though, if you are seeing a lot of work happening in serial that you expected to happen concurrently!

Building Our Own Async Abstractions

We can also compose futures together to create new patterns. For example, we can build a timeout function with async building blocks we already have. When we’re done, the result will be another building block we could use to create still more async abstractions.

Listing 17-18 shows how we would expect this timeout to work with a slow future.

Filename: src/main.rs
extern crate trpl; // required for mdbook test

use std::time::Duration;

fn main() {
    trpl::block_on(async {
        let slow = async {
            trpl::sleep(Duration::from_secs(5)).await;
            "Finally finished"
        };

        match timeout(slow, Duration::from_secs(2)).await {
            Ok(message) => println!("Succeeded with '{message}'"),
            Err(duration) => {
                println!("Failed after {} seconds", duration.as_secs())
            }
        }
    });
}
Listing 17-18: Using our imagined timeout to run a slow operation with a time limit

Let’s implement this! To begin, let’s think about the API for timeout:

  • It needs to be an async function itself so we can await it.
  • Its first parameter should be a future to run. We can make it generic to allow it to work with any future.
  • Its second parameter will be the maximum time to wait. If we use a Duration, that will make it easy to pass along to trpl::sleep.
  • It should return a Result. If the future completes successfully, the Result will be Ok with the value produced by the future. If the timeout elapses first, the Result will be Err with the duration that the timeout waited for.

Listing 17-19 shows this declaration.

Filename: src/main.rs
extern crate trpl; // required for mdbook test

use std::time::Duration;

fn main() {
    trpl::block_on(async {
        let slow = async {
            trpl::sleep(Duration::from_secs(5)).await;
            "Finally finished"
        };

        match timeout(slow, Duration::from_secs(2)).await {
            Ok(message) => println!("Succeeded with '{message}'"),
            Err(duration) => {
                println!("Failed after {} seconds", duration.as_secs())
            }
        }
    });
}

async fn timeout<F: Future>(
    future_to_try: F,
    max_time: Duration,
) -> Result<F::Output, Duration> {
    // Here is where our implementation will go!
}
Listing 17-19: Defining the signature of timeout

That satisfies our goals for the types. Now let’s think about the behavior we need: we want to race the future passed in against the duration. We can use trpl::sleep to make a timer future from the duration, and use trpl::select to run that timer with the future the caller passes in.

In Listing 17-20, we implement timeout by matching on the result of awaiting trpl::select.

Filename: src/main.rs
extern crate trpl; // required for mdbook test

use std::time::Duration;

use trpl::Either;

// --snip--

fn main() {
    trpl::block_on(async {
        let slow = async {
            trpl::sleep(Duration::from_secs(5)).await;
            "Finally finished"
        };

        match timeout(slow, Duration::from_secs(2)).await {
            Ok(message) => println!("Succeeded with '{message}'"),
            Err(duration) => {
                println!("Failed after {} seconds", duration.as_secs())
            }
        }
    });
}

async fn timeout<F: Future>(
    future_to_try: F,
    max_time: Duration,
) -> Result<F::Output, Duration> {
    match trpl::select(future_to_try, trpl::sleep(max_time)).await {
        Either::Left(output) => Ok(output),
        Either::Right(_) => Err(max_time),
    }
}
Listing 17-20: Defining timeout with select and sleep

The implementation of trpl::select is not fair: it always polls arguments in the order in which they are passed (other select implementations will randomly choose which argument to poll first). Thus, we pass future_to_try to select first so it gets a chance to complete even if max_time is a very short duration. If future_to_try finishes first, select will return Left with the output from future_to_try. If timer finishes first, select will return Right with the timer’s output of ().

If the future_to_try succeeds and we get a Left(output), we return Ok(output). If the sleep timer elapses instead and we get a Right(()), we ignore the () with _ and return Err(max_time) instead.

With that, we have a working timeout built out of two other async helpers. If we run our code, it will print the failure mode after the timeout:

Failed after 2 seconds

Because futures compose with other futures, you can build really powerful tools using smaller async building blocks. For example, you can use this same approach to combine timeouts with retries, and in turn use those with operations such as network calls (such as those in Listing 17-5).

In practice, you’ll usually work directly with async and await, and secondarily with functions such as select and macros such as the join! macro to control how the outermost futures are executed.

We’ve now seen a number of ways to work with multiple futures at the same time. Up next, we’ll look at how we can work with multiple futures in a sequence over time with streams.

Потоки (Streams): последовательность Future

Streams: Futures in Sequence

Recall how we used the receiver for our async channel earlier in this chapter in the “Message Passing” section. The async recv method produces a sequence of items over time. This is an instance of a much more general pattern known as a stream. Many concepts are naturally represented as streams: items becoming available in a queue, chunks of data being pulled incrementally from the filesystem when the full data set is too large for the computer’s memory, or data arriving over the network over time. Because streams are futures, we can use them with any other kind of future and combine them in interesting ways. For example, we can batch up events to avoid triggering too many network calls, set timeouts on sequences of long-running operations, or throttle user interface events to avoid doing needless work.

We saw a sequence of items back in Chapter 13, when we looked at the Iterator trait in “The Iterator Trait and the next Method” section, but there are two differences between iterators and the async channel receiver. The first difference is time: iterators are synchronous, while the channel receiver is asynchronous. The second difference is the API. When working directly with Iterator, we call its synchronous next method. With the trpl::Receiver stream in particular, we called an asynchronous recv method instead. Otherwise, these APIs feel very similar, and that similarity isn’t a coincidence. A stream is like an asynchronous form of iteration. Whereas the trpl::Receiver specifically waits to receive messages, though, the general-purpose stream API is much broader: it provides the next item the way Iterator does, but asynchronously.

The similarity between iterators and streams in Rust means we can actually create a stream from any iterator. As with an iterator, we can work with a stream by calling its next method and then awaiting the output, as in Listing 17-21, which won’t compile yet.

Filename: src/main.rs
extern crate trpl; // required for mdbook test

fn main() {
    trpl::block_on(async {
        let values = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
        let iter = values.iter().map(|n| n * 2);
        let mut stream = trpl::stream_from_iter(iter);

        while let Some(value) = stream.next().await {
            println!("The value was: {value}");
        }
    });
}
Listing 17-21: Creating a stream from an iterator and printing its values

We start with an array of numbers, which we convert to an iterator and then call map on to double all the values. Then we convert the iterator into a stream using the trpl::stream_from_iter function. Next, we loop over the items in the stream as they arrive with the while let loop.

Unfortunately, when we try to run the code, it doesn’t compile but instead reports that there’s no next method available:

error[E0599]: no method named `next` found for struct `tokio_stream::iter::Iter` in the current scope
  --> src/main.rs:10:40
   |
10 |         while let Some(value) = stream.next().await {
   |                                        ^^^^
   |
   = help: items from traits can only be used if the trait is in scope
help: the following traits which provide `next` are implemented but not in scope; perhaps you want to import one of them
   |
1  + use crate::trpl::StreamExt;
   |
1  + use futures_util::stream::stream::StreamExt;
   |
1  + use std::iter::Iterator;
   |
1  + use std::str::pattern::Searcher;
   |
help: there is a method `try_next` with a similar name
   |
10 |         while let Some(value) = stream.try_next().await {
   |                                        ~~~~~~~~

As this output explains, the reason for the compiler error is that we need the right trait in scope to be able to use the next method. Given our discussion so far, you might reasonably expect that trait to be Stream, but it’s actually StreamExt. Short for extension, Ext is a common pattern in the Rust community for extending one trait with another.

The Stream trait defines a low-level interface that effectively combines the Iterator and Future traits. StreamExt supplies a higher-level set of APIs on top of Stream, including the next method as well as other utility methods similar to those provided by the Iterator trait. Stream and StreamExt are not yet part of Rust’s standard library, but most ecosystem crates use similar definitions.

The fix to the compiler error is to add a use statement for trpl::StreamExt, as in Listing 17-22.

Filename: src/main.rs
extern crate trpl; // required for mdbook test

use trpl::StreamExt;

fn main() {
    trpl::block_on(async {
        let values = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
        // --snip--
        let iter = values.iter().map(|n| n * 2);
        let mut stream = trpl::stream_from_iter(iter);

        while let Some(value) = stream.next().await {
            println!("The value was: {value}");
        }
    });
}
Listing 17-22: Successfully using an iterator as the basis for a stream

With all those pieces put together, this code works the way we want! What’s more, now that we have StreamExt in scope, we can use all of its utility methods, just as with iterators.

Более подробный взгляд на трейты для Async

A Closer Look at the Traits for Async

Throughout the chapter, we’ve used the Future, Stream, and StreamExt traits in various ways. So far, though, we’ve avoided getting too far into the details of how they work or how they fit together, which is fine most of the time for your day-to-day Rust work. Sometimes, though, you’ll encounter situations where you’ll need to understand a few more of these traits’ details, along with the Pin type and the Unpin trait. In this section, we’ll dig in just enough to help in those scenarios, still leaving the really deep dive for other documentation.

The Future Trait

Let’s start by taking a closer look at how the Future trait works. Here’s how Rust defines it:

#![allow(unused)]
fn main() {
use std::pin::Pin;
use std::task::{Context, Poll};

pub trait Future {
    type Output;

    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output>;
}
}

That trait definition includes a bunch of new types and also some syntax we haven’t seen before, so let’s walk through the definition piece by piece.

First, Future’s associated type Output says what the future resolves to. This is analogous to the Item associated type for the Iterator trait. Second, Future has the poll method, which takes a special Pin reference for its self parameter and a mutable reference to a Context type, and returns a Poll<Self::Output>. We’ll talk more about Pin and Context in a moment. For now, let’s focus on what the method returns, the Poll type:

#![allow(unused)]
fn main() {
pub enum Poll<T> {
    Ready(T),
    Pending,
}
}

This Poll type is similar to an Option. It has one variant that has a value, Ready(T), and one that does not, Pending. Poll means something quite different from Option, though! The Pending variant indicates that the future still has work to do, so the caller will need to check again later. The Ready variant indicates that the Future has finished its work and the T value is available.

Note: It’s rare to need to call poll directly, but if you do need to, keep in mind that with most futures, the caller should not call poll again after the future has returned Ready. Many futures will panic if polled again after becoming ready. Futures that are safe to poll again will say so explicitly in their documentation. This is similar to how Iterator::next behaves.

When you see code that uses await, Rust compiles it under the hood to code that calls poll. If you look back at Listing 17-4, where we printed out the page title for a single URL once it resolved, Rust compiles it into something kind of (although not exactly) like this:

match page_title(url).poll() {
    Ready(page_title) => match page_title {
        Some(title) => println!("The title for {url} was {title}"),
        None => println!("{url} had no title"),
    }
    Pending => {
        // But what goes here?
    }
}

What should we do when the future is still Pending? We need some way to try again, and again, and again, until the future is finally ready. In other words, we need a loop:

let mut page_title_fut = page_title(url);
loop {
    match page_title_fut.poll() {
        Ready(value) => match page_title {
            Some(title) => println!("The title for {url} was {title}"),
            None => println!("{url} had no title"),
        }
        Pending => {
            // continue
        }
    }
}

If Rust compiled it to exactly that code, though, every await would be blocking—exactly the opposite of what we were going for! Instead, Rust ensures that the loop can hand off control to something that can pause work on this future to work on other futures and then check this one again later. As we’ve seen, that something is an async runtime, and this scheduling and coordination work is one of its main jobs.

In the “Sending Data Between Two Tasks Using Message Passing” section, we described waiting on rx.recv. The recv call returns a future, and awaiting the future polls it. We noted that a runtime will pause the future until it’s ready with either Some(message) or None when the channel closes. With our deeper understanding of the Future trait, and specifically Future::poll, we can see how that works. The runtime knows the future isn’t ready when it returns Poll::Pending. Conversely, the runtime knows the future is ready and advances it when poll returns Poll::Ready(Some(message)) or Poll::Ready(None).

The exact details of how a runtime does that are beyond the scope of this book, but the key is to see the basic mechanics of futures: a runtime polls each future it is responsible for, putting the future back to sleep when it is not yet ready.

The Pin Type and the Unpin Trait

Back in Listing 17-13, we used the trpl::join! macro to await three futures. However, it’s common to have a collection such as a vector containing some number futures that won’t be known until runtime. Let’s change Listing 17-13 to the code in Listing 17-23 that puts the three futures into a vector and calls the trpl::join_all function instead, which won’t compile yet.

Filename: src/main.rs
extern crate trpl; // required for mdbook test

use std::time::Duration;

fn main() {
    trpl::block_on(async {
        let (tx, mut rx) = trpl::channel();

        let tx1 = tx.clone();
        let tx1_fut = async move {
            let vals = vec![
                String::from("hi"),
                String::from("from"),
                String::from("the"),
                String::from("future"),
            ];

            for val in vals {
                tx1.send(val).unwrap();
                trpl::sleep(Duration::from_secs(1)).await;
            }
        };

        let rx_fut = async {
            while let Some(value) = rx.recv().await {
                println!("received '{value}'");
            }
        };

        let tx_fut = async move {
            // --snip--
            let vals = vec![
                String::from("more"),
                String::from("messages"),
                String::from("for"),
                String::from("you"),
            ];

            for val in vals {
                tx.send(val).unwrap();
                trpl::sleep(Duration::from_secs(1)).await;
            }
        };

        let futures: Vec<Box<dyn Future<Output = ()>>> =
            vec![Box::new(tx1_fut), Box::new(rx_fut), Box::new(tx_fut)];

        trpl::join_all(futures).await;
    });
}
Listing 17-23: Awaiting futures in a collection

We put each future within a Box to make them into trait objects, just as we did in the “Returning Errors from run” section in Chapter 12. (We’ll cover trait objects in detail in Chapter 18.) Using trait objects lets us treat each of the anonymous futures produced by these types as the same type, because all of them implement the Future trait.

This might be surprising. After all, none of the async blocks returns anything, so each one produces a Future<Output = ()>. Remember that Future is a trait, though, and that the compiler creates a unique enum for each async block, even when they have identical output types. Just as you can’t put two different handwritten structs in a Vec, you can’t mix compiler-generated enums.

Then we pass the collection of futures to the trpl::join_all function and await the result. However, this doesn’t compile; here’s the relevant part of the error messages.

error[E0277]: `dyn Future<Output = ()>` cannot be unpinned
  --> src/main.rs:48:33
   |
48 |         trpl::join_all(futures).await;
   |                                 ^^^^^ the trait `Unpin` is not implemented for `dyn Future<Output = ()>`
   |
   = note: consider using the `pin!` macro
           consider using `Box::pin` if you need to access the pinned value outside of the current scope
   = note: required for `Box<dyn Future<Output = ()>>` to implement `Future`
note: required by a bound in `futures_util::future::join_all::JoinAll`
  --> file:///home/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/futures-util-0.3.30/src/future/join_all.rs:29:8
   |
27 | pub struct JoinAll<F>
   |            ------- required by a bound in this struct
28 | where
29 |     F: Future,
   |        ^^^^^^ required by this bound in `JoinAll`

The note in this error message tells us that we should use the pin! macro to pin the values, which means putting them inside the Pin type that guarantees the values won’t be moved in memory. The error message says pinning is required because dyn Future<Output = ()> needs to implement the Unpin trait and it currently does not.

The trpl::join_all function returns a struct called JoinAll. That struct is generic over a type F, which is constrained to implement the Future trait. Directly awaiting a future with await pins the future implicitly. That’s why we don’t need to use pin! everywhere we want to await futures.

However, we’re not directly awaiting a future here. Instead, we construct a new future, JoinAll, by passing a collection of futures to the join_all function. The signature for join_all requires that the types of the items in the collection all implement the Future trait, and Box<T> implements Future only if the T it wraps is a future that implements the Unpin trait.

That’s a lot to absorb! To really understand it, let’s dive a little further into how the Future trait actually works, in particular around pinning. Look again at the definition of the Future trait:

#![allow(unused)]
fn main() {
use std::pin::Pin;
use std::task::{Context, Poll};

pub trait Future {
    type Output;

    // Required method
    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output>;
}
}

The cx parameter and its Context type are the key to how a runtime actually knows when to check any given future while still being lazy. Again, the details of how that works are beyond the scope of this chapter, and you generally only need to think about this when writing a custom Future implementation. We’ll focus instead on the type for self, as this is the first time we’ve seen a method where self has a type annotation. A type annotation for self works like type annotations for other function parameters but with two key differences:

  • It tells Rust what type self must be for the method to be called.
  • It can’t be just any type. It’s restricted to the type on which the method is implemented, a reference or smart pointer to that type, or a Pin wrapping a reference to that type.

We’ll see more on this syntax in Chapter 18. For now, it’s enough to know that if we want to poll a future to check whether it is Pending or Ready(Output), we need a Pin-wrapped mutable reference to the type.

Pin is a wrapper for pointer-like types such as &, &mut, Box, and Rc. (Technically, Pin works with types that implement the Deref or DerefMut traits, but this is effectively equivalent to working only with references and smart pointers.) Pin is not a pointer itself and doesn’t have any behavior of its own like Rc and Arc do with reference counting; it’s purely a tool the compiler can use to enforce constraints on pointer usage.

Recalling that await is implemented in terms of calls to poll starts to explain the error message we saw earlier, but that was in terms of Unpin, not Pin. So how exactly does Pin relate to Unpin, and why does Future need self to be in a Pin type to call poll?

Remember from earlier in this chapter that a series of await points in a future get compiled into a state machine, and the compiler makes sure that state machine follows all of Rust’s normal rules around safety, including borrowing and ownership. To make that work, Rust looks at what data is needed between one await point and either the next await point or the end of the async block. It then creates a corresponding variant in the compiled state machine. Each variant gets the access it needs to the data that will be used in that section of the source code, whether by taking ownership of that data or by getting a mutable or immutable reference to it.

So far, so good: if we get anything wrong about the ownership or references in a given async block, the borrow checker will tell us. When we want to move around the future that corresponds to that block—like moving it into a Vec to pass to join_all—things get trickier.

When we move a future—whether by pushing it into a data structure to use as an iterator with join_all or by returning it from a function—that actually means moving the state machine Rust creates for us. And unlike most other types in Rust, the futures Rust creates for async blocks can end up with references to themselves in the fields of any given variant, as shown in the simplified illustration in Figure 17-4.

A single-column, three-row table representing a future, fut1, which has data values 0 and 1 in the first two rows and an arrow pointing from the third row back to the second row, representing an internal reference within the future.
Figure 17-4: A self-referential data type

By default, though, any object that has a reference to itself is unsafe to move, because references always point to the actual memory address of whatever they refer to (see Figure 17-5). If you move the data structure itself, those internal references will be left pointing to the old location. However, that memory location is now invalid. For one thing, its value will not be updated when you make changes to the data structure. For another—more important—thing, the computer is now free to reuse that memory for other purposes! You could end up reading completely unrelated data later.

Two tables, depicting two futures, fut1 and fut2, each of which has one column and three rows, representing the result of having moved a future out of fut1 into fut2. The first, fut1, is grayed out, with a question mark in each index, representing unknown memory. The second, fut2, has 0 and 1 in the first and second rows and an arrow pointing from its third row back to the second row of fut1, representing a pointer that is referencing the old location in memory of the future before it was moved.
Figure 17-5: The unsafe result of moving a self-referential data type

Theoretically, the Rust compiler could try to update every reference to an object whenever it gets moved, but that could add a lot of performance overhead, especially if a whole web of references needs updating. If we could instead make sure the data structure in question doesn’t move in memory, we wouldn’t have to update any references. This is exactly what Rust’s borrow checker is for: in safe code, it prevents you from moving any item with an active reference to it.

Pin builds on that to give us the exact guarantee we need. When we pin a value by wrapping a pointer to that value in Pin, it can no longer move. Thus, if you have Pin<Box<SomeType>>, you actually pin the SomeType value, not the Box pointer. Figure 17-6 illustrates this process.

Three boxes laid out side by side. The first is labeled “Pin”, the second “b1”, and the third “pinned”. Within “pinned” is a table labeled “fut”, with a single column; it represents a future with cells for each part of the data structure. Its first cell has the value “0”, its second cell has an arrow coming out of it and pointing to the fourth and final cell, which has the value “1” in it, and the third cell has dashed lines and an ellipsis to indicate there may be other parts to the data structure. All together, the “fut” table represents a future which is self-referential. An arrow leaves the box labeled “Pin”, goes through the box labeled “b1” and terminates inside the “pinned” box at the “fut” table.
Figure 17-6: Pinning a `Box` that points to a self-referential future type

In fact, the Box pointer can still move around freely. Remember: we care about making sure the data ultimately being referenced stays in place. If a pointer moves around, but the data it points to is in the same place, as in Figure 17-7, there’s no potential problem. (As an independent exercise, look at the docs for the types as well as the std::pin module and try to work out how you’d do this with a Pin wrapping a Box.) The key is that the self-referential type itself cannot move, because it is still pinned.

Four boxes laid out in three rough columns, identical to the previous diagram with a change to the second column. Now there are two boxes in the second column, labeled “b1” and “b2”, “b1” is grayed out, and the arrow from “Pin” goes through “b2” instead of “b1”, indicating that the pointer has moved from “b1” to “b2”, but the data in “pinned” has not moved.
Figure 17-7: Moving a `Box` which points to a self-referential future type

However, most types are perfectly safe to move around, even if they happen to be behind a Pin pointer. We only need to think about pinning when items have internal references. Primitive values such as numbers and Booleans are safe because they obviously don’t have any internal references. Neither do most types you normally work with in Rust. You can move around a Vec, for example, without worrying. Given what we have seen so far, if you have a Pin<Vec<String>>, you’d have to do everything via the safe but restrictive APIs provided by Pin, even though a Vec<String> is always safe to move if there are no other references to it. We need a way to tell the compiler that it’s fine to move items around in cases like this—and that’s where Unpin comes into play.

Unpin is a marker trait, similar to the Send and Sync traits we saw in Chapter 16, and thus has no functionality of its own. Marker traits exist only to tell the compiler it’s safe to use the type implementing a given trait in a particular context. Unpin informs the compiler that a given type does not need to uphold any guarantees about whether the value in question can be safely moved.

Just as with Send and Sync, the compiler implements Unpin automatically for all types where it can prove it is safe. A special case, again similar to Send and Sync, is where Unpin is not implemented for a type. The notation for this is impl !Unpin for SomeType, where SomeType is the name of a type that does need to uphold those guarantees to be safe whenever a pointer to that type is used in a Pin.

In other words, there are two things to keep in mind about the relationship between Pin and Unpin. First, Unpin is the “normal” case, and !Unpin is the special case. Second, whether a type implements Unpin or !Unpin only matters when you’re using a pinned pointer to that type like Pin<&mut SomeType>.

To make that concrete, think about a String: it has a length and the Unicode characters that make it up. We can wrap a String in Pin, as seen in Figure 17-8. However, String automatically implements Unpin, as do most other types in Rust.

A box labeled “Pin” on the left with an arrow going from it to a box labeled “String” on the right. The “String” box contains the data 5usize, representing the length of the string, and the letters “h”, “e”, “l”, “l”, and “o” representing the characters of the string “hello” stored in this String instance. A dotted rectangle surrounds the “String” box and its label, but not the “Pin” box.
Figure 17-8: Pinning a `String`; the dotted line indicates that the `String` implements the `Unpin` trait and thus is not pinned

As a result, we can do things that would be illegal if String implemented !Unpin instead, such as replacing one string with another at the exact same location in memory as in Figure 17-9. This doesn’t violate the Pin contract, because String has no internal references that make it unsafe to move around. That is precisely why it implements Unpin rather than !Unpin.

The same “hello” string data from the previous example, now labeled “s1” and grayed out. The “Pin” box from the previous example now points to a different String instance, one that is labeled “s2”, is valid, has a length of 7usize, and contains the characters of the string “goodbye”. s2 is surrounded by a dotted rectangle because it, too, implements the Unpin trait.
Figure 17-9: Replacing the `String` with an entirely different `String` in memory

Now we know enough to understand the errors reported for that join_all call from back in Listing 17-23. We originally tried to move the futures produced by async blocks into a Vec<Box<dyn Future<Output = ()>>>, but as we’ve seen, those futures may have internal references, so they don’t automatically implement Unpin. Once we pin them, we can pass the resulting Pin type into the Vec, confident that the underlying data in the futures will not be moved. Listing 17-24 shows how to fix the code by calling the pin! macro where each of the three futures are defined and adjusting the trait object type.

extern crate trpl; // required for mdbook test

use std::pin::{Pin, pin};

// --snip--

use std::time::Duration;

fn main() {
    trpl::block_on(async {
        let (tx, mut rx) = trpl::channel();

        let tx1 = tx.clone();
        let tx1_fut = pin!(async move {
            // --snip--
            let vals = vec![
                String::from("hi"),
                String::from("from"),
                String::from("the"),
                String::from("future"),
            ];

            for val in vals {
                tx1.send(val).unwrap();
                trpl::sleep(Duration::from_secs(1)).await;
            }
        });

        let rx_fut = pin!(async {
            // --snip--
            while let Some(value) = rx.recv().await {
                println!("received '{value}'");
            }
        });

        let tx_fut = pin!(async move {
            // --snip--
            let vals = vec![
                String::from("more"),
                String::from("messages"),
                String::from("for"),
                String::from("you"),
            ];

            for val in vals {
                tx.send(val).unwrap();
                trpl::sleep(Duration::from_secs(1)).await;
            }
        });

        let futures: Vec<Pin<&mut dyn Future<Output = ()>>> =
            vec![tx1_fut, rx_fut, tx_fut];

        trpl::join_all(futures).await;
    });
}
Listing 17-24: Pinning the futures to enable moving them into the vector

This example now compiles and runs, and we could add or remove futures from the vector at runtime and join them all.

Pin and Unpin are mostly important for building lower-level libraries, or when you’re building a runtime itself, rather than for day-to-day Rust code. When you see these traits in error messages, though, now you’ll have a better idea of how to fix your code!

Note: This combination of Pin and Unpin makes it possible to safely implement a whole class of complex types in Rust that would otherwise prove challenging because they’re self-referential. Types that require Pin show up most commonly in async Rust today, but every once in a while, you might see them in other contexts, too.

The specifics of how Pin and Unpin work, and the rules they’re required to uphold, are covered extensively in the API documentation for std::pin, so if you’re interested in learning more, that’s a great place to start.

If you want to understand how things work under the hood in even more detail, see Chapters 2 and 4 of Asynchronous Programming in Rust.

The Stream Trait

Now that you have a deeper grasp on the Future, Pin, and Unpin traits, we can turn our attention to the Stream trait. As you learned earlier in the chapter, streams are similar to asynchronous iterators. Unlike Iterator and Future, however, Stream has no definition in the standard library as of this writing, but there is a very common definition from the futures crate used throughout the ecosystem.

Let’s review the definitions of the Iterator and Future traits before looking at how a Stream trait might merge them together. From Iterator, we have the idea of a sequence: its next method provides an Option<Self::Item>. From Future, we have the idea of readiness over time: its poll method provides a Poll<Self::Output>. To represent a sequence of items that become ready over time, we define a Stream trait that puts those features together:

#![allow(unused)]
fn main() {
use std::pin::Pin;
use std::task::{Context, Poll};

trait Stream {
    type Item;

    fn poll_next(
        self: Pin<&mut Self>,
        cx: &mut Context<'_>
    ) -> Poll<Option<Self::Item>>;
}
}

The Stream trait defines an associated type called Item for the type of the items produced by the stream. This is similar to Iterator, where there may be zero to many items, and unlike Future, where there is always a single Output, even if it’s the unit type ().

Stream also defines a method to get those items. We call it poll_next, to make it clear that it polls in the same way Future::poll does and produces a sequence of items in the same way Iterator::next does. Its return type combines Poll with Option. The outer type is Poll, because it has to be checked for readiness, just as a future does. The inner type is Option, because it needs to signal whether there are more messages, just as an iterator does.

Something very similar to this definition will likely end up as part of Rust’s standard library. In the meantime, it’s part of the toolkit of most runtimes, so you can rely on it, and everything we cover next should generally apply!

In the examples we saw in the “Streams: Futures in Sequence” section, though, we didn’t use poll_next or Stream, but instead used next and StreamExt. We could work directly in terms of the poll_next API by hand-writing our own Stream state machines, of course, just as we could work with futures directly via their poll method. Using await is much nicer, though, and the StreamExt trait supplies the next method so we can do just that:

#![allow(unused)]
fn main() {
use std::pin::Pin;
use std::task::{Context, Poll};

trait Stream {
    type Item;
    fn poll_next(
        self: Pin<&mut Self>,
        cx: &mut Context<'_>,
    ) -> Poll<Option<Self::Item>>;
}

trait StreamExt: Stream {
    async fn next(&mut self) -> Option<Self::Item>
    where
        Self: Unpin;

    // other methods...
}
}

Note: The actual definition we used earlier in the chapter looks slightly different than this, because it supports versions of Rust that did not yet support using async functions in traits. As a result, it looks like this:

fn next(&mut self) -> Next<'_, Self> where Self: Unpin;

That Next type is a struct that implements Future and allows us to name the lifetime of the reference to self with Next<'_, Self>, so that await can work with this method.

The StreamExt trait is also the home of all the interesting methods available to use with streams. StreamExt is automatically implemented for every type that implements Stream, but these traits are defined separately to enable the community to iterate on convenience APIs without affecting the foundational trait.

In the version of StreamExt used in the trpl crate, the trait not only defines the next method but also supplies a default implementation of next that correctly handles the details of calling Stream::poll_next. This means that even when you need to write your own streaming data type, you only have to implement Stream, and then anyone who uses your data type can use StreamExt and its methods with it automatically.

That’s all we’re going to cover for the lower-level details on these traits. To wrap up, let’s consider how futures (including streams), tasks, and threads all fit together!

Future, задачи и потоки

Putting It All Together: Futures, Tasks, and Threads

As we saw in Chapter 16, threads provide one approach to concurrency. We’ve seen another approach in this chapter: using async with futures and streams. If you’re wondering when to choose one method over the other, the answer is: it depends! And in many cases, the choice isn’t threads or async but rather threads and async.

Many operating systems have supplied threading-based concurrency models for decades now, and many programming languages support them as a result. However, these models are not without their tradeoffs. On many operating systems, they use a fair bit of memory for each thread. Threads are also only an option when your operating system and hardware support them. Unlike mainstream desktop and mobile computers, some embedded systems don’t have an OS at all, so they also don’t have threads.

The async model provides a different—and ultimately complementary—set of tradeoffs. In the async model, concurrent operations don’t require their own threads. Instead, they can run on tasks, as when we used trpl::spawn_task to kick off work from a synchronous function in the streams section. A task is similar to a thread, but instead of being managed by the operating system, it’s managed by library-level code: the runtime.

There’s a reason the APIs for spawning threads and spawning tasks are so similar. Threads act as a boundary for sets of synchronous operations; concurrency is possible between threads. Tasks act as a boundary for sets of asynchronous operations; concurrency is possible both between and within tasks, because a task can switch between futures in its body. Finally, futures are Rust’s most granular unit of concurrency, and each future may represent a tree of other futures. The runtime—specifically, its executor—manages tasks, and tasks manage futures. In that regard, tasks are similar to lightweight, runtime-managed threads with added capabilities that come from being managed by a runtime instead of by the operating system.

This doesn’t mean that async tasks are always better than threads (or vice versa). Concurrency with threads is in some ways a simpler programming model than concurrency with async. That can be a strength or a weakness. Threads are somewhat “fire and forget”; they have no native equivalent to a future, so they simply run to completion without being interrupted except by the operating system itself.

And it turns out that threads and tasks often work very well together, because tasks can (at least in some runtimes) be moved around between threads. In fact, under the hood, the runtime we’ve been using—including the spawn_blocking and spawn_task functions—is multithreaded by default! Many runtimes use an approach called work stealing to transparently move tasks around between threads, based on how the threads are currently being utilized, to improve the system’s overall performance. That approach actually requires threads and tasks, and therefore futures.

When thinking about which method to use when, consider these rules of thumb:

  • If the work is very parallelizable (that is, CPU-bound), such as processing a bunch of data where each part can be processed separately, threads are a better choice.
  • If the work is very concurrent (that is, I/O-bound), such as handling messages from a bunch of different sources that may come in at different intervals or different rates, async is a better choice.

And if you need both parallelism and concurrency, you don’t have to choose between threads and async. You can use them together freely, letting each play the part it’s best at. For example, Listing 17-25 shows a fairly common example of this kind of mix in real-world Rust code.

Filename: src/main.rs
extern crate trpl; // for mdbook test

use std::{thread, time::Duration};

fn main() {
    let (tx, mut rx) = trpl::channel();

    thread::spawn(move || {
        for i in 1..11 {
            tx.send(i).unwrap();
            thread::sleep(Duration::from_secs(1));
        }
    });

    trpl::block_on(async {
        while let Some(message) = rx.recv().await {
            println!("{message}");
        }
    });
}
Listing 17-25: Sending messages with blocking code in a thread and awaiting the messages in an async block

We begin by creating an async channel, then spawning a thread that takes ownership of the sender side of the channel using the move keyword. Within the thread, we send the numbers 1 through 10, sleeping for a second between each. Finally, we run a future created with an async block passed to trpl::block_on just as we have throughout the chapter. In that future, we await those messages, just as in the other message-passing examples we have seen.

To return to the scenario we opened the chapter with, imagine running a set of video encoding tasks using a dedicated thread (because video encoding is compute-bound) but notifying the UI that those operations are done with an async channel. There are countless examples of these kinds of combinations in real-world use cases.

Summary

This isn’t the last you’ll see of concurrency in this book. The project in Chapter 21 will apply these concepts in a more realistic situation than the simpler examples discussed here and compare problem-solving with threading versus tasks and futures more directly.

No matter which of these approaches you choose, Rust gives you the tools you need to write safe, fast, concurrent code—whether for a high-throughput web server or an embedded operating system.

Next, we’ll talk about idiomatic ways to model problems and structure solutions as your Rust programs get bigger. In addition, we’ll discuss how Rust’s idioms relate to those you might be familiar with from object-oriented programming.

Object-Oriented Programming Features

Object-oriented programming (OOP) is a way of modeling programs. Objects as a programmatic concept were introduced in the programming language Simula in the 1960s. Those objects influenced Alan Kay’s programming architecture in which objects pass messages to each other. To describe this architecture, he coined the term object-oriented programming in 1967. Many competing definitions describe what OOP is, and by some of these definitions Rust is object oriented but by others it is not. In this chapter, we’ll explore certain characteristics that are commonly considered object oriented and how those characteristics translate to idiomatic Rust. We’ll then show you how to implement an object-oriented design pattern in Rust and discuss the trade-offs of doing so versus implementing a solution using some of Rust’s strengths instead.

Характеристики объектно-ориентированных языков

Characteristics of Object-Oriented Languages

There is no consensus in the programming community about what features a language must have to be considered object oriented. Rust is influenced by many programming paradigms, including OOP; for example, we explored the features that came from functional programming in Chapter 13. Arguably, OOP languages share certain common characteristics—namely, objects, encapsulation, and inheritance. Let’s look at what each of those characteristics means and whether Rust supports it.

Objects Contain Data and Behavior

The book Design Patterns: Elements of Reusable Object-Oriented Software by Erich Gamma, Richard Helm, Ralph Johnson, and John Vlissides (Addison-Wesley, 1994), colloquially referred to as The Gang of Four book, is a catalog of object-oriented design patterns. It defines OOP in this way:

Object-oriented programs are made up of objects. An object packages both data and the procedures that operate on that data. The procedures are typically called methods or operations.

Using this definition, Rust is object oriented: Structs and enums have data, and impl blocks provide methods on structs and enums. Even though structs and enums with methods aren’t called objects, they provide the same functionality, according to the Gang of Four’s definition of objects.

Encapsulation That Hides Implementation Details

Another aspect commonly associated with OOP is the idea of encapsulation, which means that the implementation details of an object aren’t accessible to code using that object. Therefore, the only way to interact with an object is through its public API; code using the object shouldn’t be able to reach into the object’s internals and change data or behavior directly. This enables the programmer to change and refactor an object’s internals without needing to change the code that uses the object.

We discussed how to control encapsulation in Chapter 7: We can use the pub keyword to decide which modules, types, functions, and methods in our code should be public, and by default everything else is private. For example, we can define a struct AveragedCollection that has a field containing a vector of i32 values. The struct can also have a field that contains the average of the values in the vector, meaning the average doesn’t have to be computed on demand whenever anyone needs it. In other words, AveragedCollection will cache the calculated average for us. Listing 18-1 has the definition of the AveragedCollection struct.

Filename: src/lib.rs
pub struct AveragedCollection {
    list: Vec<i32>,
    average: f64,
}
Listing 18-1: An AveragedCollection struct that maintains a list of integers and the average of the items in the collection

The struct is marked pub so that other code can use it, but the fields within the struct remain private. This is important in this case because we want to ensure that whenever a value is added or removed from the list, the average is also updated. We do this by implementing add, remove, and average methods on the struct, as shown in Listing 18-2.

Filename: src/lib.rs
pub struct AveragedCollection {
    list: Vec<i32>,
    average: f64,
}

impl AveragedCollection {
    pub fn add(&mut self, value: i32) {
        self.list.push(value);
        self.update_average();
    }

    pub fn remove(&mut self) -> Option<i32> {
        let result = self.list.pop();
        match result {
            Some(value) => {
                self.update_average();
                Some(value)
            }
            None => None,
        }
    }

    pub fn average(&self) -> f64 {
        self.average
    }

    fn update_average(&mut self) {
        let total: i32 = self.list.iter().sum();
        self.average = total as f64 / self.list.len() as f64;
    }
}
Listing 18-2: Implementations of the public methods add, remove, and average on AveragedCollection

The public methods add, remove, and average are the only ways to access or modify data in an instance of AveragedCollection. When an item is added to list using the add method or removed using the remove method, the implementations of each call the private update_average method that handles updating the average field as well.

We leave the list and average fields private so that there is no way for external code to add or remove items to or from the list field directly; otherwise, the average field might become out of sync when the list changes. The average method returns the value in the average field, allowing external code to read the average but not modify it.

Because we’ve encapsulated the implementation details of the struct AveragedCollection, we can easily change aspects, such as the data structure, in the future. For instance, we could use a HashSet<i32> instead of a Vec<i32> for the list field. As long as the signatures of the add, remove, and average public methods stayed the same, code using AveragedCollection wouldn’t need to change. If we made list public instead, this wouldn’t necessarily be the case: HashSet<i32> and Vec<i32> have different methods for adding and removing items, so the external code would likely have to change if it were modifying list directly.

If encapsulation is a required aspect for a language to be considered object oriented, then Rust meets that requirement. The option to use pub or not for different parts of code enables encapsulation of implementation details.

Inheritance as a Type System and as Code Sharing

Inheritance is a mechanism whereby an object can inherit elements from another object’s definition, thus gaining the parent object’s data and behavior without you having to define them again.

If a language must have inheritance to be object oriented, then Rust is not such a language. There is no way to define a struct that inherits the parent struct’s fields and method implementations without using a macro.

However, if you’re used to having inheritance in your programming toolbox, you can use other solutions in Rust, depending on your reason for reaching for inheritance in the first place.

You would choose inheritance for two main reasons. One is for reuse of code: You can implement particular behavior for one type, and inheritance enables you to reuse that implementation for a different type. You can do this in a limited way in Rust code using default trait method implementations, which you saw in Listing 10-14 when we added a default implementation of the summarize method on the Summary trait. Any type implementing the Summary trait would have the summarize method available on it without any further code. This is similar to a parent class having an implementation of a method and an inheriting child class also having the implementation of the method. We can also override the default implementation of the summarize method when we implement the Summary trait, which is similar to a child class overriding the implementation of a method inherited from a parent class.

The other reason to use inheritance relates to the type system: to enable a child type to be used in the same places as the parent type. This is also called polymorphism, which means that you can substitute multiple objects for each other at runtime if they share certain characteristics.

Polymorphism

To many people, polymorphism is synonymous with inheritance. But it’s actually a more general concept that refers to code that can work with data of multiple types. For inheritance, those types are generally subclasses.

Rust instead uses generics to abstract over different possible types and trait bounds to impose constraints on what those types must provide. This is sometimes called bounded parametric polymorphism.

Rust has chosen a different set of trade-offs by not offering inheritance. Inheritance is often at risk of sharing more code than necessary. Subclasses shouldn’t always share all characteristics of their parent class but will do so with inheritance. This can make a program’s design less flexible. It also introduces the possibility of calling methods on subclasses that don’t make sense or that cause errors because the methods don’t apply to the subclass. In addition, some languages will only allow single inheritance (meaning a subclass can only inherit from one class), further restricting the flexibility of a program’s design.

For these reasons, Rust takes the different approach of using trait objects instead of inheritance to achieve polymorphism at runtime. Let’s look at how trait objects work.

Использование объектов трейтов для абстрагирования общего поведения

Using Trait Objects to Abstract over Shared Behavior

In Chapter 8, we mentioned that one limitation of vectors is that they can store elements of only one type. We created a workaround in Listing 8-9 where we defined a SpreadsheetCell enum that had variants to hold integers, floats, and text. This meant we could store different types of data in each cell and still have a vector that represented a row of cells. This is a perfectly good solution when our interchangeable items are a fixed set of types that we know when our code is compiled.

However, sometimes we want our library user to be able to extend the set of types that are valid in a particular situation. To show how we might achieve this, we’ll create an example graphical user interface (GUI) tool that iterates through a list of items, calling a draw method on each one to draw it to the screen—a common technique for GUI tools. We’ll create a library crate called gui that contains the structure of a GUI library. This crate might include some types for people to use, such as Button or TextField. In addition, gui users will want to create their own types that can be drawn: For instance, one programmer might add an Image, and another might add a SelectBox.

At the time of writing the library, we can’t know and define all the types other programmers might want to create. But we do know that gui needs to keep track of many values of different types, and it needs to call a draw method on each of these differently typed values. It doesn’t need to know exactly what will happen when we call the draw method, just that the value will have that method available for us to call.

To do this in a language with inheritance, we might define a class named Component that has a method named draw on it. The other classes, such as Button, Image, and SelectBox, would inherit from Component and thus inherit the draw method. They could each override the draw method to define their custom behavior, but the framework could treat all of the types as if they were Component instances and call draw on them. But because Rust doesn’t have inheritance, we need another way to structure the gui library to allow users to create new types compatible with the library.

Defining a Trait for Common Behavior

To implement the behavior that we want gui to have, we’ll define a trait named Draw that will have one method named draw. Then, we can define a vector that takes a trait object. A trait object points to both an instance of a type implementing our specified trait and a table used to look up trait methods on that type at runtime. We create a trait object by specifying some sort of pointer, such as a reference or a Box<T> smart pointer, then the dyn keyword, and then specifying the relevant trait. (We’ll talk about the reason trait objects must use a pointer in “Dynamically Sized Types and the Sized Trait” in Chapter 20.) We can use trait objects in place of a generic or concrete type. Wherever we use a trait object, Rust’s type system will ensure at compile time that any value used in that context will implement the trait object’s trait. Consequently, we don’t need to know all the possible types at compile time.

We’ve mentioned that, in Rust, we refrain from calling structs and enums “objects” to distinguish them from other languages’ objects. In a struct or enum, the data in the struct fields and the behavior in impl blocks are separated, whereas in other languages, the data and behavior combined into one concept is often labeled an object. Trait objects differ from objects in other languages in that we can’t add data to a trait object. Trait objects aren’t as generally useful as objects in other languages: Their specific purpose is to allow abstraction across common behavior.

Listing 18-3 shows how to define a trait named Draw with one method named draw.

Filename: src/lib.rs
pub trait Draw {
    fn draw(&self);
}
Listing 18-3: Definition of the Draw trait

This syntax should look familiar from our discussions on how to define traits in Chapter 10. Next comes some new syntax: Listing 18-4 defines a struct named Screen that holds a vector named components. This vector is of type Box<dyn Draw>, which is a trait object; it’s a stand-in for any type inside a Box that implements the Draw trait.

Filename: src/lib.rs
pub trait Draw {
    fn draw(&self);
}

pub struct Screen {
    pub components: Vec<Box<dyn Draw>>,
}
Listing 18-4: Definition of the Screen struct with a components field holding a vector of trait objects that implement the Draw trait

On the Screen struct, we’ll define a method named run that will call the draw method on each of its components, as shown in Listing 18-5.

Filename: src/lib.rs
pub trait Draw {
    fn draw(&self);
}

pub struct Screen {
    pub components: Vec<Box<dyn Draw>>,
}

impl Screen {
    pub fn run(&self) {
        for component in self.components.iter() {
            component.draw();
        }
    }
}
Listing 18-5: A run method on Screen that calls the draw method on each component

This works differently from defining a struct that uses a generic type parameter with trait bounds. A generic type parameter can be substituted with only one concrete type at a time, whereas trait objects allow for multiple concrete types to fill in for the trait object at runtime. For example, we could have defined the Screen struct using a generic type and a trait bound, as in Listing 18-6.

Filename: src/lib.rs
pub trait Draw {
    fn draw(&self);
}

pub struct Screen<T: Draw> {
    pub components: Vec<T>,
}

impl<T> Screen<T>
where
    T: Draw,
{
    pub fn run(&self) {
        for component in self.components.iter() {
            component.draw();
        }
    }
}
Listing 18-6: An alternate implementation of the Screen struct and its run method using generics and trait bounds

This restricts us to a Screen instance that has a list of components all of type Button or all of type TextField. If you’ll only ever have homogeneous collections, using generics and trait bounds is preferable because the definitions will be monomorphized at compile time to use the concrete types.

On the other hand, with the method using trait objects, one Screen instance can hold a Vec<T> that contains a Box<Button> as well as a Box<TextField>. Let’s look at how this works, and then we’ll talk about the runtime performance implications.

Implementing the Trait

Now we’ll add some types that implement the Draw trait. We’ll provide the Button type. Again, actually implementing a GUI library is beyond the scope of this book, so the draw method won’t have any useful implementation in its body. To imagine what the implementation might look like, a Button struct might have fields for width, height, and label, as shown in Listing 18-7.

Filename: src/lib.rs
pub trait Draw {
    fn draw(&self);
}

pub struct Screen {
    pub components: Vec<Box<dyn Draw>>,
}

impl Screen {
    pub fn run(&self) {
        for component in self.components.iter() {
            component.draw();
        }
    }
}

pub struct Button {
    pub width: u32,
    pub height: u32,
    pub label: String,
}

impl Draw for Button {
    fn draw(&self) {
        // code to actually draw a button
    }
}
Listing 18-7: A Button struct that implements the Draw trait

The width, height, and label fields on Button will differ from the fields on other components; for example, a TextField type might have those same fields plus a placeholder field. Each of the types we want to draw on the screen will implement the Draw trait but will use different code in the draw method to define how to draw that particular type, as Button has here (without the actual GUI code, as mentioned). The Button type, for instance, might have an additional impl block containing methods related to what happens when a user clicks the button. These kinds of methods won’t apply to types like TextField.

If someone using our library decides to implement a SelectBox struct that has width, height, and options fields, they would implement the Draw trait on the SelectBox type as well, as shown in Listing 18-8.

Filename: src/main.rs
use gui::Draw;

struct SelectBox {
    width: u32,
    height: u32,
    options: Vec<String>,
}

impl Draw for SelectBox {
    fn draw(&self) {
        // code to actually draw a select box
    }
}

fn main() {}
Listing 18-8: Another crate using gui and implementing the Draw trait on a SelectBox struct

Our library’s user can now write their main function to create a Screen instance. To the Screen instance, they can add a SelectBox and a Button by putting each in a Box<T> to become a trait object. They can then call the run method on the Screen instance, which will call draw on each of the components. Listing 18-9 shows this implementation.

Filename: src/main.rs
use gui::Draw;

struct SelectBox {
    width: u32,
    height: u32,
    options: Vec<String>,
}

impl Draw for SelectBox {
    fn draw(&self) {
        // code to actually draw a select box
    }
}

use gui::{Button, Screen};

fn main() {
    let screen = Screen {
        components: vec![
            Box::new(SelectBox {
                width: 75,
                height: 10,
                options: vec![
                    String::from("Yes"),
                    String::from("Maybe"),
                    String::from("No"),
                ],
            }),
            Box::new(Button {
                width: 50,
                height: 10,
                label: String::from("OK"),
            }),
        ],
    };

    screen.run();
}
Listing 18-9: Using trait objects to store values of different types that implement the same trait

When we wrote the library, we didn’t know that someone might add the SelectBox type, but our Screen implementation was able to operate on the new type and draw it because SelectBox implements the Draw trait, which means it implements the draw method.

This concept—of being concerned only with the messages a value responds to rather than the value’s concrete type—is similar to the concept of duck typing in dynamically typed languages: If it walks like a duck and quacks like a duck, then it must be a duck! In the implementation of run on Screen in Listing 18-5, run doesn’t need to know what the concrete type of each component is. It doesn’t check whether a component is an instance of a Button or a SelectBox, it just calls the draw method on the component. By specifying Box<dyn Draw> as the type of the values in the components vector, we’ve defined Screen to need values that we can call the draw method on.

The advantage of using trait objects and Rust’s type system to write code similar to code using duck typing is that we never have to check whether a value implements a particular method at runtime or worry about getting errors if a value doesn’t implement a method but we call it anyway. Rust won’t compile our code if the values don’t implement the traits that the trait objects need.

For example, Listing 18-10 shows what happens if we try to create a Screen with a String as a component.

Filename: src/main.rs
use gui::Screen;

fn main() {
    let screen = Screen {
        components: vec![Box::new(String::from("Hi"))],
    };

    screen.run();
}
Listing 18-10: Attempting to use a type that doesn’t implement the trait object’s trait

We’ll get this error because String doesn’t implement the Draw trait:

$ cargo run
   Compiling gui v0.1.0 (file:///projects/gui)
error[E0277]: the trait bound `String: Draw` is not satisfied
 --> src/main.rs:5:26
  |
5 |         components: vec![Box::new(String::from("Hi"))],
  |                          ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ the trait `Draw` is not implemented for `String`
  |
  = help: the trait `Draw` is implemented for `Button`
  = note: required for the cast from `Box<String>` to `Box<dyn Draw>`

For more information about this error, try `rustc --explain E0277`.
error: could not compile `gui` (bin "gui") due to 1 previous error

This error lets us know that either we’re passing something to Screen that we didn’t mean to pass and so should pass a different type, or we should implement Draw on String so that Screen is able to call draw on it.

Performing Dynamic Dispatch

Recall in “Performance of Code Using Generics” in Chapter 10 our discussion on the monomorphization process performed on generics by the compiler: The compiler generates nongeneric implementations of functions and methods for each concrete type that we use in place of a generic type parameter. The code that results from monomorphization is doing static dispatch, which is when the compiler knows what method you’re calling at compile time. This is opposed to dynamic dispatch, which is when the compiler can’t tell at compile time which method you’re calling. In dynamic dispatch cases, the compiler emits code that at runtime will know which method to call.

When we use trait objects, Rust must use dynamic dispatch. The compiler doesn’t know all the types that might be used with the code that’s using trait objects, so it doesn’t know which method implemented on which type to call. Instead, at runtime, Rust uses the pointers inside the trait object to know which method to call. This lookup incurs a runtime cost that doesn’t occur with static dispatch. Dynamic dispatch also prevents the compiler from choosing to inline a method’s code, which in turn prevents some optimizations, and Rust has some rules about where you can and cannot use dynamic dispatch, called dyn compatibility. Those rules are beyond the scope of this discussion, but you can read more about them in the reference. However, we did get extra flexibility in the code that we wrote in Listing 18-5 and were able to support in Listing 18-9, so it’s a trade-off to consider.

Реализация объектно-ориентированного паттерна проектирования

Implementing an Object-Oriented Design Pattern

The state pattern is an object-oriented design pattern. The crux of the pattern is that we define a set of states a value can have internally. The states are represented by a set of state objects, and the value’s behavior changes based on its state. We’re going to work through an example of a blog post struct that has a field to hold its state, which will be a state object from the set “draft,” “review,” or “published.”

The state objects share functionality: In Rust, of course, we use structs and traits rather than objects and inheritance. Each state object is responsible for its own behavior and for governing when it should change into another state. The value that holds a state object knows nothing about the different behavior of the states or when to transition between states.

The advantage of using the state pattern is that, when the business requirements of the program change, we won’t need to change the code of the value holding the state or the code that uses the value. We’ll only need to update the code inside one of the state objects to change its rules or perhaps add more state objects.

First, we’re going to implement the state pattern in a more traditional object-oriented way. Then, we’ll use an approach that’s a bit more natural in Rust. Let’s dig in to incrementally implement a blog post workflow using the state pattern.

The final functionality will look like this:

  1. A blog post starts as an empty draft.
  2. When the draft is done, a review of the post is requested.
  3. When the post is approved, it gets published.
  4. Only published blog posts return content to print so that unapproved posts can’t accidentally be published.

Any other changes attempted on a post should have no effect. For example, if we try to approve a draft blog post before we’ve requested a review, the post should remain an unpublished draft.

Attempting Traditional Object-Oriented Style

There are infinite ways to structure code to solve the same problem, each with different trade-offs. This section’s implementation is more of a traditional object-oriented style, which is possible to write in Rust, but doesn’t take advantage of some of Rust’s strengths. Later, we’ll demonstrate a different solution that still uses the object-oriented design pattern but is structured in a way that might look less familiar to programmers with object-oriented experience. We’ll compare the two solutions to experience the trade-offs of designing Rust code differently than code in other languages.

Listing 18-11 shows this workflow in code form: This is an example usage of the API we’ll implement in a library crate named blog. This won’t compile yet because we haven’t implemented the blog crate.

Filename: src/main.rs
use blog::Post;

fn main() {
    let mut post = Post::new();

    post.add_text("I ate a salad for lunch today");
    assert_eq!("", post.content());

    post.request_review();
    assert_eq!("", post.content());

    post.approve();
    assert_eq!("I ate a salad for lunch today", post.content());
}
Listing 18-11: Code that demonstrates the desired behavior we want our blog crate to have

We want to allow the user to create a new draft blog post with Post::new. We want to allow text to be added to the blog post. If we try to get the post’s content immediately, before approval, we shouldn’t get any text because the post is still a draft. We’ve added assert_eq! in the code for demonstration purposes. An excellent unit test for this would be to assert that a draft blog post returns an empty string from the content method, but we’re not going to write tests for this example.

Next, we want to enable a request for a review of the post, and we want content to return an empty string while waiting for the review. When the post receives approval, it should get published, meaning the text of the post will be returned when content is called.

Notice that the only type we’re interacting with from the crate is the Post type. This type will use the state pattern and will hold a value that will be one of three state objects representing the various states a post can be in—draft, review, or published. Changing from one state to another will be managed internally within the Post type. The states change in response to the methods called by our library’s users on the Post instance, but they don’t have to manage the state changes directly. Also, users can’t make a mistake with the states, such as publishing a post before it’s reviewed.

Defining Post and Creating a New Instance

Let’s get started on the implementation of the library! We know we need a public Post struct that holds some content, so we’ll start with the definition of the struct and an associated public new function to create an instance of Post, as shown in Listing 18-12. We’ll also make a private State trait that will define the behavior that all state objects for a Post must have.

Then, Post will hold a trait object of Box<dyn State> inside an Option<T> in a private field named state to hold the state object. You’ll see why the Option<T> is necessary in a bit.

Filename: src/lib.rs
pub struct Post {
    state: Option<Box<dyn State>>,
    content: String,
}

impl Post {
    pub fn new() -> Post {
        Post {
            state: Some(Box::new(Draft {})),
            content: String::new(),
        }
    }
}

trait State {}

struct Draft {}

impl State for Draft {}
Listing 18-12: Definition of a Post struct and a new function that creates a new Post instance, a State trait, and a Draft struct

The State trait defines the behavior shared by different post states. The state objects are Draft, PendingReview, and Published, and they will all implement the State trait. For now, the trait doesn’t have any methods, and we’ll start by defining just the Draft state because that is the state we want a post to start in.

When we create a new Post, we set its state field to a Some value that holds a Box. This Box points to a new instance of the Draft struct. This ensures that whenever we create a new instance of Post, it will start out as a draft. Because the state field of Post is private, there is no way to create a Post in any other state! In the Post::new function, we set the content field to a new, empty String.

Storing the Text of the Post Content

We saw in Listing 18-11 that we want to be able to call a method named add_text and pass it a &str that is then added as the text content of the blog post. We implement this as a method, rather than exposing the content field as pub, so that later we can implement a method that will control how the content field’s data is read. The add_text method is pretty straightforward, so let’s add the implementation in Listing 18-13 to the impl Post block.

Filename: src/lib.rs
pub struct Post {
    state: Option<Box<dyn State>>,
    content: String,
}

impl Post {
    // --snip--
    pub fn new() -> Post {
        Post {
            state: Some(Box::new(Draft {})),
            content: String::new(),
        }
    }

    pub fn add_text(&mut self, text: &str) {
        self.content.push_str(text);
    }
}

trait State {}

struct Draft {}

impl State for Draft {}
Listing 18-13: Implementing the add_text method to add text to a post’s content

The add_text method takes a mutable reference to self because we’re changing the Post instance that we’re calling add_text on. We then call push_str on the String in content and pass the text argument to add to the saved content. This behavior doesn’t depend on the state the post is in, so it’s not part of the state pattern. The add_text method doesn’t interact with the state field at all, but it is part of the behavior we want to support.

Ensuring That the Content of a Draft Post Is Empty

Even after we’ve called add_text and added some content to our post, we still want the content method to return an empty string slice because the post is still in the draft state, as shown by the first assert_eq! in Listing 18-11. For now, let’s implement the content method with the simplest thing that will fulfill this requirement: always returning an empty string slice. We’ll change this later once we implement the ability to change a post’s state so that it can be published. So far, posts can only be in the draft state, so the post content should always be empty. Listing 18-14 shows this placeholder implementation.

Filename: src/lib.rs
pub struct Post {
    state: Option<Box<dyn State>>,
    content: String,
}

impl Post {
    // --snip--
    pub fn new() -> Post {
        Post {
            state: Some(Box::new(Draft {})),
            content: String::new(),
        }
    }

    pub fn add_text(&mut self, text: &str) {
        self.content.push_str(text);
    }

    pub fn content(&self) -> &str {
        ""
    }
}

trait State {}

struct Draft {}

impl State for Draft {}
Listing 18-14: Adding a placeholder implementation for the content method on Post that always returns an empty string slice

With this added content method, everything in Listing 18-11 through the first assert_eq! works as intended.

Requesting a Review, Which Changes the Post’s State

Next, we need to add functionality to request a review of a post, which should change its state from Draft to PendingReview. Listing 18-15 shows this code.

Filename: src/lib.rs
pub struct Post {
    state: Option<Box<dyn State>>,
    content: String,
}

impl Post {
    // --snip--
    pub fn new() -> Post {
        Post {
            state: Some(Box::new(Draft {})),
            content: String::new(),
        }
    }

    pub fn add_text(&mut self, text: &str) {
        self.content.push_str(text);
    }

    pub fn content(&self) -> &str {
        ""
    }

    pub fn request_review(&mut self) {
        if let Some(s) = self.state.take() {
            self.state = Some(s.request_review())
        }
    }
}

trait State {
    fn request_review(self: Box<Self>) -> Box<dyn State>;
}

struct Draft {}

impl State for Draft {
    fn request_review(self: Box<Self>) -> Box<dyn State> {
        Box::new(PendingReview {})
    }
}

struct PendingReview {}

impl State for PendingReview {
    fn request_review(self: Box<Self>) -> Box<dyn State> {
        self
    }
}
Listing 18-15: Implementing request_review methods on Post and the State trait

We give Post a public method named request_review that will take a mutable reference to self. Then, we call an internal request_review method on the current state of Post, and this second request_review method consumes the current state and returns a new state.

We add the request_review method to the State trait; all types that implement the trait will now need to implement the request_review method. Note that rather than having self, &self, or &mut self as the first parameter of the method, we have self: Box<Self>. This syntax means the method is only valid when called on a Box holding the type. This syntax takes ownership of Box<Self>, invalidating the old state so that the state value of the Post can transform into a new state.

To consume the old state, the request_review method needs to take ownership of the state value. This is where the Option in the state field of Post comes in: We call the take method to take the Some value out of the state field and leave a None in its place because Rust doesn’t let us have unpopulated fields in structs. This lets us move the state value out of Post rather than borrowing it. Then, we’ll set the post’s state value to the result of this operation.

We need to set state to None temporarily rather than setting it directly with code like self.state = self.state.request_review(); to get ownership of the state value. This ensures that Post can’t use the old state value after we’ve transformed it into a new state.

The request_review method on Draft returns a new, boxed instance of a new PendingReview struct, which represents the state when a post is waiting for a review. The PendingReview struct also implements the request_review method but doesn’t do any transformations. Rather, it returns itself because when we request a review on a post already in the PendingReview state, it should stay in the PendingReview state.

Now we can start seeing the advantages of the state pattern: The request_review method on Post is the same no matter its state value. Each state is responsible for its own rules.

We’ll leave the content method on Post as is, returning an empty string slice. We can now have a Post in the PendingReview state as well as in the Draft state, but we want the same behavior in the PendingReview state. Listing 18-11 now works up to the second assert_eq! call!

Adding approve to Change content’s Behavior

The approve method will be similar to the request_review method: It will set state to the value that the current state says it should have when that state is approved, as shown in Listing 18-16.

Filename: src/lib.rs
pub struct Post {
    state: Option<Box<dyn State>>,
    content: String,
}

impl Post {
    // --snip--
    pub fn new() -> Post {
        Post {
            state: Some(Box::new(Draft {})),
            content: String::new(),
        }
    }

    pub fn add_text(&mut self, text: &str) {
        self.content.push_str(text);
    }

    pub fn content(&self) -> &str {
        ""
    }

    pub fn request_review(&mut self) {
        if let Some(s) = self.state.take() {
            self.state = Some(s.request_review())
        }
    }

    pub fn approve(&mut self) {
        if let Some(s) = self.state.take() {
            self.state = Some(s.approve())
        }
    }
}

trait State {
    fn request_review(self: Box<Self>) -> Box<dyn State>;
    fn approve(self: Box<Self>) -> Box<dyn State>;
}

struct Draft {}

impl State for Draft {
    // --snip--
    fn request_review(self: Box<Self>) -> Box<dyn State> {
        Box::new(PendingReview {})
    }

    fn approve(self: Box<Self>) -> Box<dyn State> {
        self
    }
}

struct PendingReview {}

impl State for PendingReview {
    // --snip--
    fn request_review(self: Box<Self>) -> Box<dyn State> {
        self
    }

    fn approve(self: Box<Self>) -> Box<dyn State> {
        Box::new(Published {})
    }
}

struct Published {}

impl State for Published {
    fn request_review(self: Box<Self>) -> Box<dyn State> {
        self
    }

    fn approve(self: Box<Self>) -> Box<dyn State> {
        self
    }
}
Listing 18-16: Implementing the approve method on Post and the State trait

We add the approve method to the State trait and add a new struct that implements State, the Published state.

Similar to the way request_review on PendingReview works, if we call the approve method on a Draft, it will have no effect because approve will return self. When we call approve on PendingReview, it returns a new, boxed instance of the Published struct. The Published struct implements the State trait, and for both the request_review method and the approve method, it returns itself because the post should stay in the Published state in those cases.

Now we need to update the content method on Post. We want the value returned from content to depend on the current state of the Post, so we’re going to have the Post delegate to a content method defined on its state, as shown in Listing 18-17.

Filename: src/lib.rs
pub struct Post {
    state: Option<Box<dyn State>>,
    content: String,
}

impl Post {
    // --snip--
    pub fn new() -> Post {
        Post {
            state: Some(Box::new(Draft {})),
            content: String::new(),
        }
    }

    pub fn add_text(&mut self, text: &str) {
        self.content.push_str(text);
    }

    pub fn content(&self) -> &str {
        self.state.as_ref().unwrap().content(self)
    }
    // --snip--

    pub fn request_review(&mut self) {
        if let Some(s) = self.state.take() {
            self.state = Some(s.request_review())
        }
    }

    pub fn approve(&mut self) {
        if let Some(s) = self.state.take() {
            self.state = Some(s.approve())
        }
    }
}

trait State {
    fn request_review(self: Box<Self>) -> Box<dyn State>;
    fn approve(self: Box<Self>) -> Box<dyn State>;
}

struct Draft {}

impl State for Draft {
    fn request_review(self: Box<Self>) -> Box<dyn State> {
        Box::new(PendingReview {})
    }

    fn approve(self: Box<Self>) -> Box<dyn State> {
        self
    }
}

struct PendingReview {}

impl State for PendingReview {
    fn request_review(self: Box<Self>) -> Box<dyn State> {
        self
    }

    fn approve(self: Box<Self>) -> Box<dyn State> {
        Box::new(Published {})
    }
}

struct Published {}

impl State for Published {
    fn request_review(self: Box<Self>) -> Box<dyn State> {
        self
    }

    fn approve(self: Box<Self>) -> Box<dyn State> {
        self
    }
}
Listing 18-17: Updating the content method on Post to delegate to a content method on State

Because the goal is to keep all of these rules inside the structs that implement State, we call a content method on the value in state and pass the post instance (that is, self) as an argument. Then, we return the value that’s returned from using the content method on the state value.

We call the as_ref method on the Option because we want a reference to the value inside the Option rather than ownership of the value. Because state is an Option<Box<dyn State>>, when we call as_ref, an Option<&Box<dyn State>> is returned. If we didn’t call as_ref, we would get an error because we can’t move state out of the borrowed &self of the function parameter.

We then call the unwrap method, which we know will never panic because we know the methods on Post ensure that state will always contain a Some value when those methods are done. This is one of the cases we talked about in the “When You Have More Information Than the Compiler” section of Chapter 9 when we know that a None value is never possible, even though the compiler isn’t able to understand that.

At this point, when we call content on the &Box<dyn State>, deref coercion will take effect on the & and the Box so that the content method will ultimately be called on the type that implements the State trait. That means we need to add content to the State trait definition, and that is where we’ll put the logic for what content to return depending on which state we have, as shown in Listing 18-18.

Filename: src/lib.rs
pub struct Post {
    state: Option<Box<dyn State>>,
    content: String,
}

impl Post {
    pub fn new() -> Post {
        Post {
            state: Some(Box::new(Draft {})),
            content: String::new(),
        }
    }

    pub fn add_text(&mut self, text: &str) {
        self.content.push_str(text);
    }

    pub fn content(&self) -> &str {
        self.state.as_ref().unwrap().content(self)
    }

    pub fn request_review(&mut self) {
        if let Some(s) = self.state.take() {
            self.state = Some(s.request_review())
        }
    }

    pub fn approve(&mut self) {
        if let Some(s) = self.state.take() {
            self.state = Some(s.approve())
        }
    }
}

trait State {
    // --snip--
    fn request_review(self: Box<Self>) -> Box<dyn State>;
    fn approve(self: Box<Self>) -> Box<dyn State>;

    fn content<'a>(&self, post: &'a Post) -> &'a str {
        ""
    }
}

// --snip--

struct Draft {}

impl State for Draft {
    fn request_review(self: Box<Self>) -> Box<dyn State> {
        Box::new(PendingReview {})
    }

    fn approve(self: Box<Self>) -> Box<dyn State> {
        self
    }
}

struct PendingReview {}

impl State for PendingReview {
    fn request_review(self: Box<Self>) -> Box<dyn State> {
        self
    }

    fn approve(self: Box<Self>) -> Box<dyn State> {
        Box::new(Published {})
    }
}

struct Published {}

impl State for Published {
    // --snip--
    fn request_review(self: Box<Self>) -> Box<dyn State> {
        self
    }

    fn approve(self: Box<Self>) -> Box<dyn State> {
        self
    }

    fn content<'a>(&self, post: &'a Post) -> &'a str {
        &post.content
    }
}
Listing 18-18: Adding the content method to the State trait

We add a default implementation for the content method that returns an empty string slice. That means we don’t need to implement content on the Draft and PendingReview structs. The Published struct will override the content method and return the value in post.content. While convenient, having the content method on State determine the content of the Post is blurring the lines between the responsibility of State and the responsibility of Post.

Note that we need lifetime annotations on this method, as we discussed in Chapter 10. We’re taking a reference to a post as an argument and returning a reference to part of that post, so the lifetime of the returned reference is related to the lifetime of the post argument.

And we’re done—all of Listing 18-11 now works! We’ve implemented the state pattern with the rules of the blog post workflow. The logic related to the rules lives in the state objects rather than being scattered throughout Post.

Why Not An Enum?

You may have been wondering why we didn’t use an enum with the different possible post states as variants. That’s certainly a possible solution; try it and compare the end results to see which you prefer! One disadvantage of using an enum is that every place that checks the value of the enum will need a match expression or similar to handle every possible variant. This could get more repetitive than this trait object solution.

Evaluating the State Pattern

We’ve shown that Rust is capable of implementing the object-oriented state pattern to encapsulate the different kinds of behavior a post should have in each state. The methods on Post know nothing about the various behaviors. Because of the way we organized the code, we have to look in only one place to know the different ways a published post can behave: the implementation of the State trait on the Published struct.

If we were to create an alternative implementation that didn’t use the state pattern, we might instead use match expressions in the methods on Post or even in the main code that checks the state of the post and changes behavior in those places. That would mean we would have to look in several places to understand all the implications of a post being in the published state.

With the state pattern, the Post methods and the places we use Post don’t need match expressions, and to add a new state, we would only need to add a new struct and implement the trait methods on that one struct in one location.

The implementation using the state pattern is easy to extend to add more functionality. To see the simplicity of maintaining code that uses the state pattern, try a few of these suggestions:

  • Add a reject method that changes the post’s state from PendingReview back to Draft.
  • Require two calls to approve before the state can be changed to Published.
  • Allow users to add text content only when a post is in the Draft state. Hint: have the state object responsible for what might change about the content but not responsible for modifying the Post.

One downside of the state pattern is that, because the states implement the transitions between states, some of the states are coupled to each other. If we add another state between PendingReview and Published, such as Scheduled, we would have to change the code in PendingReview to transition to Scheduled instead. It would be less work if PendingReview didn’t need to change with the addition of a new state, but that would mean switching to another design pattern.

Another downside is that we’ve duplicated some logic. To eliminate some of the duplication, we might try to make default implementations for the request_review and approve methods on the State trait that return self. However, this wouldn’t work: When using State as a trait object, the trait doesn’t know what the concrete self will be exactly, so the return type isn’t known at compile time. (This is one of the dyn compatibility rules mentioned earlier.)

Other duplication includes the similar implementations of the request_review and approve methods on Post. Both methods use Option::take with the state field of Post, and if state is Some, they delegate to the wrapped value’s implementation of the same method and set the new value of the state field to the result. If we had a lot of methods on Post that followed this pattern, we might consider defining a macro to eliminate the repetition (see the “Macros” section in Chapter 20).

By implementing the state pattern exactly as it’s defined for object-oriented languages, we’re not taking as full advantage of Rust’s strengths as we could. Let’s look at some changes we can make to the blog crate that can make invalid states and transitions into compile-time errors.

Encoding States and Behavior as Types

We’ll show you how to rethink the state pattern to get a different set of trade-offs. Rather than encapsulating the states and transitions completely so that outside code has no knowledge of them, we’ll encode the states into different types. Consequently, Rust’s type-checking system will prevent attempts to use draft posts where only published posts are allowed by issuing a compiler error.

Let’s consider the first part of main in Listing 18-11:

Filename: src/main.rs
use blog::Post;

fn main() {
    let mut post = Post::new();

    post.add_text("I ate a salad for lunch today");
    assert_eq!("", post.content());

    post.request_review();
    assert_eq!("", post.content());

    post.approve();
    assert_eq!("I ate a salad for lunch today", post.content());
}

We still enable the creation of new posts in the draft state using Post::new and the ability to add text to the post’s content. But instead of having a content method on a draft post that returns an empty string, we’ll make it so that draft posts don’t have the content method at all. That way, if we try to get a draft post’s content, we’ll get a compiler error telling us the method doesn’t exist. As a result, it will be impossible for us to accidentally display draft post content in production because that code won’t even compile. Listing 18-19 shows the definition of a Post struct and a DraftPost struct, as well as methods on each.

Filename: src/lib.rs
pub struct Post {
    content: String,
}

pub struct DraftPost {
    content: String,
}

impl Post {
    pub fn new() -> DraftPost {
        DraftPost {
            content: String::new(),
        }
    }

    pub fn content(&self) -> &str {
        &self.content
    }
}

impl DraftPost {
    pub fn add_text(&mut self, text: &str) {
        self.content.push_str(text);
    }
}
Listing 18-19: A Post with a content method and a DraftPost without a content method

Both the Post and DraftPost structs have a private content field that stores the blog post text. The structs no longer have the state field because we’re moving the encoding of the state to the types of the structs. The Post struct will represent a published post, and it has a content method that returns the content.

We still have a Post::new function, but instead of returning an instance of Post, it returns an instance of DraftPost. Because content is private and there aren’t any functions that return Post, it’s not possible to create an instance of Post right now.

The DraftPost struct has an add_text method, so we can add text to content as before, but note that DraftPost does not have a content method defined! So now the program ensures that all posts start as draft posts, and draft posts don’t have their content available for display. Any attempt to get around these constraints will result in a compiler error.

So, how do we get a published post? We want to enforce the rule that a draft post has to be reviewed and approved before it can be published. A post in the pending review state should still not display any content. Let’s implement these constraints by adding another struct, PendingReviewPost, defining the request_review method on DraftPost to return a PendingReviewPost and defining an approve method on PendingReviewPost to return a Post, as shown in Listing 18-20.

Filename: src/lib.rs
pub struct Post {
    content: String,
}

pub struct DraftPost {
    content: String,
}

impl Post {
    pub fn new() -> DraftPost {
        DraftPost {
            content: String::new(),
        }
    }

    pub fn content(&self) -> &str {
        &self.content
    }
}

impl DraftPost {
    // --snip--
    pub fn add_text(&mut self, text: &str) {
        self.content.push_str(text);
    }

    pub fn request_review(self) -> PendingReviewPost {
        PendingReviewPost {
            content: self.content,
        }
    }
}

pub struct PendingReviewPost {
    content: String,
}

impl PendingReviewPost {
    pub fn approve(self) -> Post {
        Post {
            content: self.content,
        }
    }
}
Listing 18-20: A PendingReviewPost that gets created by calling request_review on DraftPost and an approve method that turns a PendingReviewPost into a published Post

The request_review and approve methods take ownership of self, thus consuming the DraftPost and PendingReviewPost instances and transforming them into a PendingReviewPost and a published Post, respectively. This way, we won’t have any lingering DraftPost instances after we’ve called request_review on them, and so forth. The PendingReviewPost struct doesn’t have a content method defined on it, so attempting to read its content results in a compiler error, as with DraftPost. Because the only way to get a published Post instance that does have a content method defined is to call the approve method on a PendingReviewPost, and the only way to get a PendingReviewPost is to call the request_review method on a DraftPost, we’ve now encoded the blog post workflow into the type system.

But we also have to make some small changes to main. The request_review and approve methods return new instances rather than modifying the struct they’re called on, so we need to add more let post = shadowing assignments to save the returned instances. We also can’t have the assertions about the draft and pending review posts’ contents be empty strings, nor do we need them: We can’t compile code that tries to use the content of posts in those states any longer. The updated code in main is shown in Listing 18-21.

Filename: src/main.rs
use blog::Post;

fn main() {
    let mut post = Post::new();

    post.add_text("I ate a salad for lunch today");

    let post = post.request_review();

    let post = post.approve();

    assert_eq!("I ate a salad for lunch today", post.content());
}
Listing 18-21: Modifications to main to use the new implementation of the blog post workflow

The changes we needed to make to main to reassign post mean that this implementation doesn’t quite follow the object-oriented state pattern anymore: The transformations between the states are no longer encapsulated entirely within the Post implementation. However, our gain is that invalid states are now impossible because of the type system and the type checking that happens at compile time! This ensures that certain bugs, such as display of the content of an unpublished post, will be discovered before they make it to production.

Try the tasks suggested at the start of this section on the blog crate as it is after Listing 18-21 to see what you think about the design of this version of the code. Note that some of the tasks might be completed already in this design.

We’ve seen that even though Rust is capable of implementing object-oriented design patterns, other patterns, such as encoding state into the type system, are also available in Rust. These patterns have different trade-offs. Although you might be very familiar with object-oriented patterns, rethinking the problem to take advantage of Rust’s features can provide benefits, such as preventing some bugs at compile time. Object-oriented patterns won’t always be the best solution in Rust due to certain features, like ownership, that object-oriented languages don’t have.

Summary

Regardless of whether you think Rust is an object-oriented language after reading this chapter, you now know that you can use trait objects to get some object-oriented features in Rust. Dynamic dispatch can give your code some flexibility in exchange for a bit of runtime performance. You can use this flexibility to implement object-oriented patterns that can help your code’s maintainability. Rust also has other features, like ownership, that object-oriented languages don’t have. An object-oriented pattern won’t always be the best way to take advantage of Rust’s strengths, but it is an available option.

Next, we’ll look at patterns, which are another of Rust’s features that enable lots of flexibility. We’ve looked at them briefly throughout the book but haven’t seen their full capability yet. Let’s go!

Patterns and Matching

Patterns are a special syntax in Rust for matching against the structure of types, both complex and simple. Using patterns in conjunction with match expressions and other constructs gives you more control over a program’s control flow. A pattern consists of some combination of the following:

  • Literals
  • Destructured arrays, enums, structs, or tuples
  • Variables
  • Wildcards
  • Placeholders

Some example patterns include x, (a, 3), and Some(Color::Red). In the contexts in which patterns are valid, these components describe the shape of data. Our program then matches values against the patterns to determine whether it has the correct shape of data to continue running a particular piece of code.

To use a pattern, we compare it to some value. If the pattern matches the value, we use the value parts in our code. Recall the match expressions in Chapter 6 that used patterns, such as the coin-sorting machine example. If the value fits the shape of the pattern, we can use the named pieces. If it doesn’t, the code associated with the pattern won’t run.

This chapter is a reference on all things related to patterns. We’ll cover the valid places to use patterns, the difference between refutable and irrefutable patterns, and the different kinds of pattern syntax that you might see. By the end of the chapter, you’ll know how to use patterns to express many concepts in a clear way.

Все места, где могут использоваться шаблоны

All the Places Patterns Can Be Used

Patterns pop up in a number of places in Rust, and you’ve been using them a lot without realizing it! This section discusses all the places where patterns are valid.

match Arms

As discussed in Chapter 6, we use patterns in the arms of match expressions. Formally, match expressions are defined as the keyword match, a value to match on, and one or more match arms that consist of a pattern and an expression to run if the value matches that arm’s pattern, like this:

match VALUE {
    PATTERN => EXPRESSION,
    PATTERN => EXPRESSION,
    PATTERN => EXPRESSION,
}

For example, here’s the match expression from Listing 6-5 that matches on an Option<i32> value in the variable x:

match x {
    None => None,
    Some(i) => Some(i + 1),
}

The patterns in this match expression are the None and Some(i) to the left of each arrow.

One requirement for match expressions is that they need to be exhaustive in the sense that all possibilities for the value in the match expression must be accounted for. One way to ensure that you’ve covered every possibility is to have a catch-all pattern for the last arm: For example, a variable name matching any value can never fail and thus covers every remaining case.

The particular pattern _ will match anything, but it never binds to a variable, so it’s often used in the last match arm. The _ pattern can be useful when you want to ignore any value not specified, for example. We’ll cover the _ pattern in more detail in “Ignoring Values in a Pattern” later in this chapter.

let Statements

Prior to this chapter, we had only explicitly discussed using patterns with match and if let, but in fact, we’ve used patterns in other places as well, including in let statements. For example, consider this straightforward variable assignment with let:

#![allow(unused)]
fn main() {
let x = 5;
}

Every time you’ve used a let statement like this you’ve been using patterns, although you might not have realized it! More formally, a let statement looks like this:

let PATTERN = EXPRESSION;

In statements like let x = 5; with a variable name in the PATTERN slot, the variable name is just a particularly simple form of a pattern. Rust compares the expression against the pattern and assigns any names it finds. So, in the let x = 5; example, x is a pattern that means “bind what matches here to the variable x.” Because the name x is the whole pattern, this pattern effectively means “bind everything to the variable x, whatever the value is.”

To see the pattern-matching aspect of let more clearly, consider Listing 19-1, which uses a pattern with let to destructure a tuple.

fn main() {
    let (x, y, z) = (1, 2, 3);
}
Listing 19-1: Using a pattern to destructure a tuple and create three variables at once

Here, we match a tuple against a pattern. Rust compares the value (1, 2, 3) to the pattern (x, y, z) and sees that the value matches the pattern—that is, it sees that the number of elements is the same in both—so Rust binds 1 to x, 2 to y, and 3 to z. You can think of this tuple pattern as nesting three individual variable patterns inside it.

If the number of elements in the pattern doesn’t match the number of elements in the tuple, the overall type won’t match and we’ll get a compiler error. For example, Listing 19-2 shows an attempt to destructure a tuple with three elements into two variables, which won’t work.

fn main() {
    let (x, y) = (1, 2, 3);
}
Listing 19-2: Incorrectly constructing a pattern whose variables don’t match the number of elements in the tuple

Attempting to compile this code results in this type error:

$ cargo run
   Compiling patterns v0.1.0 (file:///projects/patterns)
error[E0308]: mismatched types
 --> src/main.rs:2:9
  |
2 |     let (x, y) = (1, 2, 3);
  |         ^^^^^^   --------- this expression has type `({integer}, {integer}, {integer})`
  |         |
  |         expected a tuple with 3 elements, found one with 2 elements
  |
  = note: expected tuple `({integer}, {integer}, {integer})`
             found tuple `(_, _)`

For more information about this error, try `rustc --explain E0308`.
error: could not compile `patterns` (bin "patterns") due to 1 previous error

To fix the error, we could ignore one or more of the values in the tuple using _ or .., as you’ll see in the “Ignoring Values in a Pattern” section. If the problem is that we have too many variables in the pattern, the solution is to make the types match by removing variables so that the number of variables equals the number of elements in the tuple.

Conditional if let Expressions

In Chapter 6, we discussed how to use if let expressions mainly as a shorter way to write the equivalent of a match that only matches one case. Optionally, if let can have a corresponding else containing code to run if the pattern in the if let doesn’t match.

Listing 19-3 shows that it’s also possible to mix and match if let, else if, and else if let expressions. Doing so gives us more flexibility than a match expression in which we can express only one value to compare with the patterns. Also, Rust doesn’t require that the conditions in a series of if let, else if, and else if let arms relate to each other.

The code in Listing 19-3 determines what color to make your background based on a series of checks for several conditions. For this example, we’ve created variables with hardcoded values that a real program might receive from user input.

Filename: src/main.rs
fn main() {
    let favorite_color: Option<&str> = None;
    let is_tuesday = false;
    let age: Result<u8, _> = "34".parse();

    if let Some(color) = favorite_color {
        println!("Using your favorite color, {color}, as the background");
    } else if is_tuesday {
        println!("Tuesday is green day!");
    } else if let Ok(age) = age {
        if age > 30 {
            println!("Using purple as the background color");
        } else {
            println!("Using orange as the background color");
        }
    } else {
        println!("Using blue as the background color");
    }
}
Listing 19-3: Mixing if let, else if, else if let, and else

If the user specifies a favorite color, that color is used as the background. If no favorite color is specified and today is Tuesday, the background color is green. Otherwise, if the user specifies their age as a string and we can parse it as a number successfully, the color is either purple or orange depending on the value of the number. If none of these conditions apply, the background color is blue.

This conditional structure lets us support complex requirements. With the hardcoded values we have here, this example will print Using purple as the background color.

You can see that if let can also introduce new variables that shadow existing variables in the same way that match arms can: The line if let Ok(age) = age introduces a new age variable that contains the value inside the Ok variant, shadowing the existing age variable. This means we need to place the if age > 30 condition within that block: We can’t combine these two conditions into if let Ok(age) = age && age > 30. The new age we want to compare to 30 isn’t valid until the new scope starts with the curly bracket.

The downside of using if let expressions is that the compiler doesn’t check for exhaustiveness, whereas with match expressions it does. If we omitted the last else block and therefore missed handling some cases, the compiler would not alert us to the possible logic bug.

while let Conditional Loops

Similar in construction to if let, the while let conditional loop allows a while loop to run for as long as a pattern continues to match. In Listing 19-4, we show a while let loop that waits on messages sent between threads, but in this case checking a Result instead of an Option.

fn main() {
    let (tx, rx) = std::sync::mpsc::channel();
    std::thread::spawn(move || {
        for val in [1, 2, 3] {
            tx.send(val).unwrap();
        }
    });

    while let Ok(value) = rx.recv() {
        println!("{value}");
    }
}
Listing 19-4: Using a while let loop to print values for as long as rx.recv() returns Ok

This example prints 1, 2, and then 3. The recv method takes the first message out of the receiver side of the channel and returns an Ok(value). When we first saw recv back in Chapter 16, we unwrapped the error directly, or we interacted with it as an iterator using a for loop. As Listing 19-4 shows, though, we can also use while let, because the recv method returns an Ok each time a message arrives, as long as the sender exists, and then produces an Err once the sender side disconnects.

for Loops

In a for loop, the value that directly follows the keyword for is a pattern. For example, in for x in y, the x is the pattern. Listing 19-5 demonstrates how to use a pattern in a for loop to destructure, or break apart, a tuple as part of the for loop.

fn main() {
    let v = vec!['a', 'b', 'c'];

    for (index, value) in v.iter().enumerate() {
        println!("{value} is at index {index}");
    }
}
Listing 19-5: Using a pattern in a for loop to destructure a tuple

The code in Listing 19-5 will print the following:

$ cargo run
   Compiling patterns v0.1.0 (file:///projects/patterns)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.52s
     Running `target/debug/patterns`
a is at index 0
b is at index 1
c is at index 2

We adapt an iterator using the enumerate method so that it produces a value and the index for that value, placed into a tuple. The first value produced is the tuple (0, 'a'). When this value is matched to the pattern (index, value), index will be 0 and value will be 'a', printing the first line of the output.

Function Parameters

Function parameters can also be patterns. The code in Listing 19-6, which declares a function named foo that takes one parameter named x of type i32, should by now look familiar.

fn foo(x: i32) {
    // code goes here
}

fn main() {}
Listing 19-6: A function signature using patterns in the parameters

The x part is a pattern! As we did with let, we could match a tuple in a function’s arguments to the pattern. Listing 19-7 splits the values in a tuple as we pass it to a function.

Filename: src/main.rs
fn print_coordinates(&(x, y): &(i32, i32)) {
    println!("Current location: ({x}, {y})");
}

fn main() {
    let point = (3, 5);
    print_coordinates(&point);
}
Listing 19-7: A function with parameters that destructure a tuple

This code prints Current location: (3, 5). The values &(3, 5) match the pattern &(x, y), so x is the value 3 and y is the value 5.

We can also use patterns in closure parameter lists in the same way as in function parameter lists because closures are similar to functions, as discussed in Chapter 13.

At this point, you’ve seen several ways to use patterns, but patterns don’t work the same in every place we can use them. In some places, the patterns must be irrefutable; in other circumstances, they can be refutable. We’ll discuss these two concepts next.

Опровержимость: может ли шаблон не совпасть

Refutability: Whether a Pattern Might Fail to Match

Patterns come in two forms: refutable and irrefutable. Patterns that will match for any possible value passed are irrefutable. An example would be x in the statement let x = 5; because x matches anything and therefore cannot fail to match. Patterns that can fail to match for some possible value are refutable. An example would be Some(x) in the expression if let Some(x) = a_value because if the value in the a_value variable is None rather than Some, the Some(x) pattern will not match.

Function parameters, let statements, and for loops can only accept irrefutable patterns because the program cannot do anything meaningful when values don’t match. The if let and while let expressions and the let...else statement accept refutable and irrefutable patterns, but the compiler warns against irrefutable patterns because, by definition, they’re intended to handle possible failure: The functionality of a conditional is in its ability to perform differently depending on success or failure.

In general, you shouldn’t have to worry about the distinction between refutable and irrefutable patterns; however, you do need to be familiar with the concept of refutability so that you can respond when you see it in an error message. In those cases, you’ll need to change either the pattern or the construct you’re using the pattern with, depending on the intended behavior of the code.

Let’s look at an example of what happens when we try to use a refutable pattern where Rust requires an irrefutable pattern and vice versa. Listing 19-8 shows a let statement, but for the pattern, we’ve specified Some(x), a refutable pattern. As you might expect, this code will not compile.

fn main() {
    let some_option_value: Option<i32> = None;
    let Some(x) = some_option_value;
}
Listing 19-8: Attempting to use a refutable pattern with let

If some_option_value were a None value, it would fail to match the pattern Some(x), meaning the pattern is refutable. However, the let statement can only accept an irrefutable pattern because there is nothing valid the code can do with a None value. At compile time, Rust will complain that we’ve tried to use a refutable pattern where an irrefutable pattern is required:

$ cargo run
   Compiling patterns v0.1.0 (file:///projects/patterns)
error[E0005]: refutable pattern in local binding
 --> src/main.rs:3:9
  |
3 |     let Some(x) = some_option_value;
  |         ^^^^^^^ pattern `None` not covered
  |
  = note: `let` bindings require an "irrefutable pattern", like a `struct` or an `enum` with only one variant
  = note: for more information, visit https://doc.rust-lang.org/book/ch19-02-refutability.html
  = note: the matched value is of type `Option<i32>`
help: you might want to use `let else` to handle the variant that isn't matched
  |
3 |     let Some(x) = some_option_value else { todo!() };
  |                                     ++++++++++++++++

For more information about this error, try `rustc --explain E0005`.
error: could not compile `patterns` (bin "patterns") due to 1 previous error

Because we didn’t cover (and couldn’t cover!) every valid value with the pattern Some(x), Rust rightfully produces a compiler error.

If we have a refutable pattern where an irrefutable pattern is needed, we can fix it by changing the code that uses the pattern: Instead of using let, we can use let...else. Then, if the pattern doesn’t match, the code in the curly brackets will handle the value. Listing 19-9 shows how to fix the code in Listing 19-8.

fn main() {
    let some_option_value: Option<i32> = None;
    let Some(x) = some_option_value else {
        return;
    };
}
Listing 19-9: Using let...else and a block with refutable patterns instead of let

We’ve given the code an out! This code is perfectly valid, although it means we cannot use an irrefutable pattern without receiving a warning. If we give let...else a pattern that will always match, such as x, as shown in Listing 19-10, the compiler will give a warning.

fn main() {
    let x = 5 else {
        return;
    };
}
Listing 19-10: Attempting to use an irrefutable pattern with let...else

Rust complains that it doesn’t make sense to use let...else with an irrefutable pattern:

$ cargo run
   Compiling patterns v0.1.0 (file:///projects/patterns)
warning: irrefutable `let...else` pattern
 --> src/main.rs:2:5
  |
2 |     let x = 5 else {
  |     ^^^^^^^^^
  |
  = note: this pattern will always match, so the `else` clause is useless
  = help: consider removing the `else` clause
  = note: `#[warn(irrefutable_let_patterns)]` on by default

warning: `patterns` (bin "patterns") generated 1 warning
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.39s
     Running `target/debug/patterns`

For this reason, match arms must use refutable patterns, except for the last arm, which should match any remaining values with an irrefutable pattern. Rust allows us to use an irrefutable pattern in a match with only one arm, but this syntax isn’t particularly useful and could be replaced with a simpler let statement.

Now that you know where to use patterns and the difference between refutable and irrefutable patterns, let’s cover all the syntax we can use to create patterns.

Синтаксис шаблонов

Pattern Syntax

In this section, we gather all the syntax that is valid in patterns and discuss why and when you might want to use each one.

Matching Literals

As you saw in Chapter 6, you can match patterns against literals directly. The following code gives some examples:

fn main() {
    let x = 1;

    match x {
        1 => println!("one"),
        2 => println!("two"),
        3 => println!("three"),
        _ => println!("anything"),
    }
}

This code prints one because the value in x is 1. This syntax is useful when you want your code to take an action if it gets a particular concrete value.

Matching Named Variables

Named variables are irrefutable patterns that match any value, and we’ve used them many times in this book. However, there is a complication when you use named variables in match, if let, or while let expressions. Because each of these kinds of expressions starts a new scope, variables declared as part of a pattern inside these expressions will shadow those with the same name outside the constructs, as is the case with all variables. In Listing 19-11, we declare a variable named x with the value Some(5) and a variable y with the value 10. We then create a match expression on the value x. Look at the patterns in the match arms and println! at the end, and try to figure out what the code will print before running this code or reading further.

Filename: src/main.rs
fn main() {
    let x = Some(5);
    let y = 10;

    match x {
        Some(50) => println!("Got 50"),
        Some(y) => println!("Matched, y = {y}"),
        _ => println!("Default case, x = {x:?}"),
    }

    println!("at the end: x = {x:?}, y = {y}");
}
Listing 19-11: A match expression with an arm that introduces a new variable which shadows an existing variable y

Let’s walk through what happens when the match expression runs. The pattern in the first match arm doesn’t match the defined value of x, so the code continues.

The pattern in the second match arm introduces a new variable named y that will match any value inside a Some value. Because we’re in a new scope inside the match expression, this is a new y variable, not the y we declared at the beginning with the value 10. This new y binding will match any value inside a Some, which is what we have in x. Therefore, this new y binds to the inner value of the Some in x. That value is 5, so the expression for that arm executes and prints Matched, y = 5.

If x had been a None value instead of Some(5), the patterns in the first two arms wouldn’t have matched, so the value would have matched to the underscore. We didn’t introduce the x variable in the pattern of the underscore arm, so the x in the expression is still the outer x that hasn’t been shadowed. In this hypothetical case, the match would print Default case, x = None.

When the match expression is done, its scope ends, and so does the scope of the inner y. The last println! produces at the end: x = Some(5), y = 10.

To create a match expression that compares the values of the outer x and y, rather than introducing a new variable that shadows the existing y variable, we would need to use a match guard conditional instead. We’ll talk about match guards later in the “Adding Conditionals with Match Guards” section.

Matching Multiple Patterns

In match expressions, you can match multiple patterns using the | syntax, which is the pattern or operator. For example, in the following code, we match the value of x against the match arms, the first of which has an or option, meaning if the value of x matches either of the values in that arm, that arm’s code will run:

fn main() {
    let x = 1;

    match x {
        1 | 2 => println!("one or two"),
        3 => println!("three"),
        _ => println!("anything"),
    }
}

This code prints one or two.

Matching Ranges of Values with ..=

The ..= syntax allows us to match to an inclusive range of values. In the following code, when a pattern matches any of the values within the given range, that arm will execute:

fn main() {
    let x = 5;

    match x {
        1..=5 => println!("one through five"),
        _ => println!("something else"),
    }
}

If x is 1, 2, 3, 4, or 5, the first arm will match. This syntax is more convenient for multiple match values than using the | operator to express the same idea; if we were to use |, we would have to specify 1 | 2 | 3 | 4 | 5. Specifying a range is much shorter, especially if we want to match, say, any number between 1 and 1,000!

The compiler checks that the range isn’t empty at compile time, and because the only types for which Rust can tell if a range is empty or not are char and numeric values, ranges are only allowed with numeric or char values.

Here is an example using ranges of char values:

fn main() {
    let x = 'c';

    match x {
        'a'..='j' => println!("early ASCII letter"),
        'k'..='z' => println!("late ASCII letter"),
        _ => println!("something else"),
    }
}

Rust can tell that 'c' is within the first pattern’s range and prints early ASCII letter.

Destructuring to Break Apart Values

We can also use patterns to destructure structs, enums, and tuples to use different parts of these values. Let’s walk through each value.

Structs

Listing 19-12 shows a Point struct with two fields, x and y, that we can break apart using a pattern with a let statement.

Filename: src/main.rs
struct Point {
    x: i32,
    y: i32,
}

fn main() {
    let p = Point { x: 0, y: 7 };

    let Point { x: a, y: b } = p;
    assert_eq!(0, a);
    assert_eq!(7, b);
}
Listing 19-12: Destructuring a struct’s fields into separate variables

This code creates the variables a and b that match the values of the x and y fields of the p struct. This example shows that the names of the variables in the pattern don’t have to match the field names of the struct. However, it’s common to match the variable names to the field names to make it easier to remember which variables came from which fields. Because of this common usage, and because writing let Point { x: x, y: y } = p; contains a lot of duplication, Rust has a shorthand for patterns that match struct fields: You only need to list the name of the struct field, and the variables created from the pattern will have the same names. Listing 19-13 behaves in the same way as the code in Listing 19-12, but the variables created in the let pattern are x and y instead of a and b.

Filename: src/main.rs
struct Point {
    x: i32,
    y: i32,
}

fn main() {
    let p = Point { x: 0, y: 7 };

    let Point { x, y } = p;
    assert_eq!(0, x);
    assert_eq!(7, y);
}
Listing 19-13: Destructuring struct fields using struct field shorthand

This code creates the variables x and y that match the x and y fields of the p variable. The outcome is that the variables x and y contain the values from the p struct.

We can also destructure with literal values as part of the struct pattern rather than creating variables for all the fields. Doing so allows us to test some of the fields for particular values while creating variables to destructure the other fields.

In Listing 19-14, we have a match expression that separates Point values into three cases: points that lie directly on the x axis (which is true when y = 0), on the y axis (x = 0), or on neither axis.

Filename: src/main.rs
struct Point {
    x: i32,
    y: i32,
}

fn main() {
    let p = Point { x: 0, y: 7 };

    match p {
        Point { x, y: 0 } => println!("On the x axis at {x}"),
        Point { x: 0, y } => println!("On the y axis at {y}"),
        Point { x, y } => {
            println!("On neither axis: ({x}, {y})");
        }
    }
}
Listing 19-14: Destructuring and matching literal values in one pattern

The first arm will match any point that lies on the x axis by specifying that the y field matches if its value matches the literal 0. The pattern still creates an x variable that we can use in the code for this arm.

Similarly, the second arm matches any point on the y axis by specifying that the x field matches if its value is 0 and creates a variable y for the value of the y field. The third arm doesn’t specify any literals, so it matches any other Point and creates variables for both the x and y fields.

In this example, the value p matches the second arm by virtue of x containing a 0, so this code will print On the y axis at 7.

Remember that a match expression stops checking arms once it has found the first matching pattern, so even though Point { x: 0, y: 0 } is on the x axis and the y axis, this code would only print On the x axis at 0.

Enums

We’ve destructured enums in this book (for example, Listing 6-5 in Chapter 6), but we haven’t yet explicitly discussed that the pattern to destructure an enum corresponds to the way the data stored within the enum is defined. As an example, in Listing 19-15, we use the Message enum from Listing 6-2 and write a match with patterns that will destructure each inner value.

Filename: src/main.rs
enum Message {
    Quit,
    Move { x: i32, y: i32 },
    Write(String),
    ChangeColor(i32, i32, i32),
}

fn main() {
    let msg = Message::ChangeColor(0, 160, 255);

    match msg {
        Message::Quit => {
            println!("The Quit variant has no data to destructure.");
        }
        Message::Move { x, y } => {
            println!("Move in the x direction {x} and in the y direction {y}");
        }
        Message::Write(text) => {
            println!("Text message: {text}");
        }
        Message::ChangeColor(r, g, b) => {
            println!("Change color to red {r}, green {g}, and blue {b}");
        }
    }
}
Listing 19-15: Destructuring enum variants that hold different kinds of values

This code will print Change color to red 0, green 160, and blue 255. Try changing the value of msg to see the code from the other arms run.

For enum variants without any data, like Message::Quit, we can’t destructure the value any further. We can only match on the literal Message::Quit value, and no variables are in that pattern.

For struct-like enum variants, such as Message::Move, we can use a pattern similar to the pattern we specify to match structs. After the variant name, we place curly brackets and then list the fields with variables so that we break apart the pieces to use in the code for this arm. Here we use the shorthand form as we did in Listing 19-13.

For tuple-like enum variants, like Message::Write that holds a tuple with one element and Message::ChangeColor that holds a tuple with three elements, the pattern is similar to the pattern we specify to match tuples. The number of variables in the pattern must match the number of elements in the variant we’re matching.

Nested Structs and Enums

So far, our examples have all been matching structs or enums one level deep, but matching can work on nested items too! For example, we can refactor the code in Listing 19-15 to support RGB and HSV colors in the ChangeColor message, as shown in Listing 19-16.

enum Color {
    Rgb(i32, i32, i32),
    Hsv(i32, i32, i32),
}

enum Message {
    Quit,
    Move { x: i32, y: i32 },
    Write(String),
    ChangeColor(Color),
}

fn main() {
    let msg = Message::ChangeColor(Color::Hsv(0, 160, 255));

    match msg {
        Message::ChangeColor(Color::Rgb(r, g, b)) => {
            println!("Change color to red {r}, green {g}, and blue {b}");
        }
        Message::ChangeColor(Color::Hsv(h, s, v)) => {
            println!("Change color to hue {h}, saturation {s}, value {v}");
        }
        _ => (),
    }
}
Listing 19-16: Matching on nested enums

The pattern of the first arm in the match expression matches a Message::ChangeColor enum variant that contains a Color::Rgb variant; then, the pattern binds to the three inner i32 values. The pattern of the second arm also matches a Message::ChangeColor enum variant, but the inner enum matches Color::Hsv instead. We can specify these complex conditions in one match expression, even though two enums are involved.

Structs and Tuples

We can mix, match, and nest destructuring patterns in even more complex ways. The following example shows a complicated destructure where we nest structs and tuples inside a tuple and destructure all the primitive values out:

fn main() {
    struct Point {
        x: i32,
        y: i32,
    }

    let ((feet, inches), Point { x, y }) = ((3, 10), Point { x: 3, y: -10 });
}

This code lets us break complex types into their component parts so that we can use the values we’re interested in separately.

Destructuring with patterns is a convenient way to use pieces of values, such as the value from each field in a struct, separately from each other.

Ignoring Values in a Pattern

You’ve seen that it’s sometimes useful to ignore values in a pattern, such as in the last arm of a match, to get a catch-all that doesn’t actually do anything but does account for all remaining possible values. There are a few ways to ignore entire values or parts of values in a pattern: using the _ pattern (which you’ve seen), using the _ pattern within another pattern, using a name that starts with an underscore, or using .. to ignore remaining parts of a value. Let’s explore how and why to use each of these patterns.

An Entire Value with _

We’ve used the underscore as a wildcard pattern that will match any value but not bind to the value. This is especially useful as the last arm in a match expression, but we can also use it in any pattern, including function parameters, as shown in Listing 19-17.

Filename: src/main.rs
fn foo(_: i32, y: i32) {
    println!("This code only uses the y parameter: {y}");
}

fn main() {
    foo(3, 4);
}
Listing 19-17: Using _ in a function signature

This code will completely ignore the value 3 passed as the first argument, and will print This code only uses the y parameter: 4.

In most cases when you no longer need a particular function parameter, you would change the signature so that it doesn’t include the unused parameter. Ignoring a function parameter can be especially useful in cases when, for example, you’re implementing a trait when you need a certain type signature but the function body in your implementation doesn’t need one of the parameters. You then avoid getting a compiler warning about unused function parameters, as you would if you used a name instead.

Parts of a Value with a Nested _

We can also use _ inside another pattern to ignore just part of a value, for example, when we want to test for only part of a value but have no use for the other parts in the corresponding code we want to run. Listing 19-18 shows code responsible for managing a setting’s value. The business requirements are that the user should not be allowed to overwrite an existing customization of a setting but can unset the setting and give it a value if it is currently unset.

fn main() {
    let mut setting_value = Some(5);
    let new_setting_value = Some(10);

    match (setting_value, new_setting_value) {
        (Some(_), Some(_)) => {
            println!("Can't overwrite an existing customized value");
        }
        _ => {
            setting_value = new_setting_value;
        }
    }

    println!("setting is {setting_value:?}");
}
Listing 19-18: Using an underscore within patterns that match Some variants when we don’t need to use the value inside the Some

This code will print Can't overwrite an existing customized value and then setting is Some(5). In the first match arm, we don’t need to match on or use the values inside either Some variant, but we do need to test for the case when setting_value and new_setting_value are the Some variant. In that case, we print the reason for not changing setting_value, and it doesn’t get changed.

In all other cases (if either setting_value or new_setting_value is None) expressed by the _ pattern in the second arm, we want to allow new_setting_value to become setting_value.

We can also use underscores in multiple places within one pattern to ignore particular values. Listing 19-19 shows an example of ignoring the second and fourth values in a tuple of five items.

fn main() {
    let numbers = (2, 4, 8, 16, 32);

    match numbers {
        (first, _, third, _, fifth) => {
            println!("Some numbers: {first}, {third}, {fifth}");
        }
    }
}
Listing 19-19: Ignoring multiple parts of a tuple

This code will print Some numbers: 2, 8, 32, and the values 4 and 16 will be ignored.

An Unused Variable by Starting Its Name with _

If you create a variable but don’t use it anywhere, Rust will usually issue a warning because an unused variable could be a bug. However, sometimes it’s useful to be able to create a variable you won’t use yet, such as when you’re prototyping or just starting a project. In this situation, you can tell Rust not to warn you about the unused variable by starting the name of the variable with an underscore. In Listing 19-20, we create two unused variables, but when we compile this code, we should only get a warning about one of them.

Filename: src/main.rs
fn main() {
    let _x = 5;
    let y = 10;
}
Listing 19-20: Starting a variable name with an underscore to avoid getting unused variable warnings

Here, we get a warning about not using the variable y, but we don’t get a warning about not using _x.

Note that there is a subtle difference between using only _ and using a name that starts with an underscore. The syntax _x still binds the value to the variable, whereas _ doesn’t bind at all. To show a case where this distinction matters, Listing 19-21 will provide us with an error.

fn main() {
    let s = Some(String::from("Hello!"));

    if let Some(_s) = s {
        println!("found a string");
    }

    println!("{s:?}");
}
Listing 19-21: An unused variable starting with an underscore still binds the value, which might take ownership of the value.

We’ll receive an error because the s value will still be moved into _s, which prevents us from using s again. However, using the underscore by itself doesn’t ever bind to the value. Listing 19-22 will compile without any errors because s doesn’t get moved into _.

fn main() {
    let s = Some(String::from("Hello!"));

    if let Some(_) = s {
        println!("found a string");
    }

    println!("{s:?}");
}
Listing 19-22: Using an underscore does not bind the value.

This code works just fine because we never bind s to anything; it isn’t moved.

Remaining Parts of a Value with ..

With values that have many parts, we can use the .. syntax to use specific parts and ignore the rest, avoiding the need to list underscores for each ignored value. The .. pattern ignores any parts of a value that we haven’t explicitly matched in the rest of the pattern. In Listing 19-23, we have a Point struct that holds a coordinate in three-dimensional space. In the match expression, we want to operate only on the x coordinate and ignore the values in the y and z fields.

fn main() {
    struct Point {
        x: i32,
        y: i32,
        z: i32,
    }

    let origin = Point { x: 0, y: 0, z: 0 };

    match origin {
        Point { x, .. } => println!("x is {x}"),
    }
}
Listing 19-23: Ignoring all fields of a Point except for x by using ..

We list the x value and then just include the .. pattern. This is quicker than having to list y: _ and z: _, particularly when we’re working with structs that have lots of fields in situations where only one or two fields are relevant.

The syntax .. will expand to as many values as it needs to be. Listing 19-24 shows how to use .. with a tuple.

Filename: src/main.rs
fn main() {
    let numbers = (2, 4, 8, 16, 32);

    match numbers {
        (first, .., last) => {
            println!("Some numbers: {first}, {last}");
        }
    }
}
Listing 19-24: Matching only the first and last values in a tuple and ignoring all other values

In this code, the first and last values are matched with first and last. The .. will match and ignore everything in the middle.

However, using .. must be unambiguous. If it is unclear which values are intended for matching and which should be ignored, Rust will give us an error. Listing 19-25 shows an example of using .. ambiguously, so it will not compile.

Filename: src/main.rs
fn main() {
    let numbers = (2, 4, 8, 16, 32);

    match numbers {
        (.., second, ..) => {
            println!("Some numbers: {second}")
        },
    }
}
Listing 19-25: An attempt to use .. in an ambiguous way

When we compile this example, we get this error:

$ cargo run
   Compiling patterns v0.1.0 (file:///projects/patterns)
error: `..` can only be used once per tuple pattern
 --> src/main.rs:5:22
  |
5 |         (.., second, ..) => {
  |          --          ^^ can only be used once per tuple pattern
  |          |
  |          previously used here

error: could not compile `patterns` (bin "patterns") due to 1 previous error

It’s impossible for Rust to determine how many values in the tuple to ignore before matching a value with second and then how many further values to ignore thereafter. This code could mean that we want to ignore 2, bind second to 4, and then ignore 8, 16, and 32; or that we want to ignore 2 and 4, bind second to 8, and then ignore 16 and 32; and so forth. The variable name second doesn’t mean anything special to Rust, so we get a compiler error because using .. in two places like this is ambiguous.

Adding Conditionals with Match Guards

A match guard is an additional if condition, specified after the pattern in a match arm, that must also match for that arm to be chosen. Match guards are useful for expressing more complex ideas than a pattern alone allows. Note, however, that they are only available in match expressions, not if let or while let expressions.

The condition can use variables created in the pattern. Listing 19-26 shows a match where the first arm has the pattern Some(x) and also has a match guard of if x % 2 == 0 (which will be true if the number is even).

fn main() {
    let num = Some(4);

    match num {
        Some(x) if x % 2 == 0 => println!("The number {x} is even"),
        Some(x) => println!("The number {x} is odd"),
        None => (),
    }
}
Listing 19-26: Adding a match guard to a pattern

This example will print The number 4 is even. When num is compared to the pattern in the first arm, it matches because Some(4) matches Some(x). Then, the match guard checks whether the remainder of dividing x by 2 is equal to 0, and because it is, the first arm is selected.

If num had been Some(5) instead, the match guard in the first arm would have been false because the remainder of 5 divided by 2 is 1, which is not equal to 0. Rust would then go to the second arm, which would match because the second arm doesn’t have a match guard and therefore matches any Some variant.

There is no way to express the if x % 2 == 0 condition within a pattern, so the match guard gives us the ability to express this logic. The downside of this additional expressiveness is that the compiler doesn’t try to check for exhaustiveness when match guard expressions are involved.

When discussing Listing 19-11, we mentioned that we could use match guards to solve our pattern-shadowing problem. Recall that we created a new variable inside the pattern in the match expression instead of using the variable outside the match. That new variable meant we couldn’t test against the value of the outer variable. Listing 19-27 shows how we can use a match guard to fix this problem.

Filename: src/main.rs
fn main() {
    let x = Some(5);
    let y = 10;

    match x {
        Some(50) => println!("Got 50"),
        Some(n) if n == y => println!("Matched, n = {n}"),
        _ => println!("Default case, x = {x:?}"),
    }

    println!("at the end: x = {x:?}, y = {y}");
}
Listing 19-27: Using a match guard to test for equality with an outer variable

This code will now print Default case, x = Some(5). The pattern in the second match arm doesn’t introduce a new variable y that would shadow the outer y, meaning we can use the outer y in the match guard. Instead of specifying the pattern as Some(y), which would have shadowed the outer y, we specify Some(n). This creates a new variable n that doesn’t shadow anything because there is no n variable outside the match.

The match guard if n == y is not a pattern and therefore doesn’t introduce new variables. This y is the outer y rather than a new y shadowing it, and we can look for a value that has the same value as the outer y by comparing n to y.

You can also use the or operator | in a match guard to specify multiple patterns; the match guard condition will apply to all the patterns. Listing 19-28 shows the precedence when combining a pattern that uses | with a match guard. The important part of this example is that the if y match guard applies to 4, 5, and 6, even though it might look like if y only applies to 6.

fn main() {
    let x = 4;
    let y = false;

    match x {
        4 | 5 | 6 if y => println!("yes"),
        _ => println!("no"),
    }
}
Listing 19-28: Combining multiple patterns with a match guard

The match condition states that the arm only matches if the value of x is equal to 4, 5, or 6 and if y is true. When this code runs, the pattern of the first arm matches because x is 4, but the match guard if y is false, so the first arm is not chosen. The code moves on to the second arm, which does match, and this program prints no. The reason is that the if condition applies to the whole pattern 4 | 5 | 6, not just to the last value 6. In other words, the precedence of a match guard in relation to a pattern behaves like this:

(4 | 5 | 6) if y => ...

rather than this:

4 | 5 | (6 if y) => ...

After running the code, the precedence behavior is evident: If the match guard were applied only to the final value in the list of values specified using the | operator, the arm would have matched, and the program would have printed yes.

Using @ Bindings

The at operator @ lets us create a variable that holds a value at the same time we’re testing that value for a pattern match. In Listing 19-29, we want to test that a Message::Hello id field is within the range 3..=7. We also want to bind the value to the variable id so that we can use it in the code associated with the arm.

fn main() {
    enum Message {
        Hello { id: i32 },
    }

    let msg = Message::Hello { id: 5 };

    match msg {
        Message::Hello { id: id @ 3..=7 } => {
            println!("Found an id in range: {id}")
        }
        Message::Hello { id: 10..=12 } => {
            println!("Found an id in another range")
        }
        Message::Hello { id } => println!("Found some other id: {id}"),
    }
}
Listing 19-29: Using @ to bind to a value in a pattern while also testing it

This example will print Found an id in range: 5. By specifying id @ before the range 3..=7, we’re capturing whatever value matched the range in a variable named id while also testing that the value matched the range pattern.

In the second arm, where we only have a range specified in the pattern, the code associated with the arm doesn’t have a variable that contains the actual value of the id field. The id field’s value could have been 10, 11, or 12, but the code that goes with that pattern doesn’t know which it is. The pattern code isn’t able to use the value from the id field because we haven’t saved the id value in a variable.

In the last arm, where we’ve specified a variable without a range, we do have the value available to use in the arm’s code in a variable named id. The reason is that we’ve used the struct field shorthand syntax. But we haven’t applied any test to the value in the id field in this arm, as we did with the first two arms: Any value would match this pattern.

Using @ lets us test a value and save it in a variable within one pattern.

Summary

Rust’s patterns are very useful in distinguishing between different kinds of data. When used in match expressions, Rust ensures that your patterns cover every possible value, or your program won’t compile. Patterns in let statements and function parameters make those constructs more useful, enabling the destructuring of values into smaller parts and assigning those parts to variables. We can create simple or complex patterns to suit our needs.

Next, for the penultimate chapter of the book, we’ll look at some advanced aspects of a variety of Rust’s features.

Advanced Features

By now, you’ve learned the most commonly used parts of the Rust programming language. Before we do one more project, in Chapter 21, we’ll look at a few aspects of the language you might run into every once in a while but may not use every day. You can use this chapter as a reference for when you encounter any unknowns. The features covered here are useful in very specific situations. Although you might not reach for them often, we want to make sure you have a grasp of all the features Rust has to offer.

In this chapter, we’ll cover:

  • Unsafe Rust: How to opt out of some of Rust’s guarantees and take responsibility for manually upholding those guarantees
  • Advanced traits: Associated types, default type parameters, fully qualified syntax, supertraits, and the newtype pattern in relation to traits
  • Advanced types: More about the newtype pattern, type aliases, the never type, and dynamically sized types
  • Advanced functions and closures: Function pointers and returning closures
  • Macros: Ways to define code that defines more code at compile time

It’s a panoply of Rust features with something for everyone! Let’s dive in!

Unsafe Rust

Unsafe Rust

All the code we’ve discussed so far has had Rust’s memory safety guarantees enforced at compile time. However, Rust has a second language hidden inside it that doesn’t enforce these memory safety guarantees: It’s called unsafe Rust and works just like regular Rust but gives us extra superpowers.

Unsafe Rust exists because, by nature, static analysis is conservative. When the compiler tries to determine whether or not code upholds the guarantees, it’s better for it to reject some valid programs than to accept some invalid programs. Although the code might be okay, if the Rust compiler doesn’t have enough information to be confident, it will reject the code. In these cases, you can use unsafe code to tell the compiler, “Trust me, I know what I’m doing.” Be warned, however, that you use unsafe Rust at your own risk: If you use unsafe code incorrectly, problems can occur due to memory unsafety, such as null pointer dereferencing.

Another reason Rust has an unsafe alter ego is that the underlying computer hardware is inherently unsafe. If Rust didn’t let you do unsafe operations, you couldn’t do certain tasks. Rust needs to allow you to do low-level systems programming, such as directly interacting with the operating system or even writing your own operating system. Working with low-level systems programming is one of the goals of the language. Let’s explore what we can do with unsafe Rust and how to do it.

Performing Unsafe Superpowers

To switch to unsafe Rust, use the unsafe keyword and then start a new block that holds the unsafe code. You can take five actions in unsafe Rust that you can’t in safe Rust, which we call unsafe superpowers. Those superpowers include the ability to:

  1. Dereference a raw pointer.
  2. Call an unsafe function or method.
  3. Access or modify a mutable static variable.
  4. Implement an unsafe trait.
  5. Access fields of unions.

It’s important to understand that unsafe doesn’t turn off the borrow checker or disable any of Rust’s other safety checks: If you use a reference in unsafe code, it will still be checked. The unsafe keyword only gives you access to these five features that are then not checked by the compiler for memory safety. You’ll still get some degree of safety inside an unsafe block.

In addition, unsafe does not mean the code inside the block is necessarily dangerous or that it will definitely have memory safety problems: The intent is that as the programmer, you’ll ensure that the code inside an unsafe block will access memory in a valid way.

People are fallible and mistakes will happen, but by requiring these five unsafe operations to be inside blocks annotated with unsafe, you’ll know that any errors related to memory safety must be within an unsafe block. Keep unsafe blocks small; you’ll be thankful later when you investigate memory bugs.

To isolate unsafe code as much as possible, it’s best to enclose such code within a safe abstraction and provide a safe API, which we’ll discuss later in the chapter when we examine unsafe functions and methods. Parts of the standard library are implemented as safe abstractions over unsafe code that has been audited. Wrapping unsafe code in a safe abstraction prevents uses of unsafe from leaking out into all the places that you or your users might want to use the functionality implemented with unsafe code, because using a safe abstraction is safe.

Let’s look at each of the five unsafe superpowers in turn. We’ll also look at some abstractions that provide a safe interface to unsafe code.

Dereferencing a Raw Pointer

In Chapter 4, in the “Dangling References” section, we mentioned that the compiler ensures that references are always valid. Unsafe Rust has two new types called raw pointers that are similar to references. As with references, raw pointers can be immutable or mutable and are written as *const T and *mut T, respectively. The asterisk isn’t the dereference operator; it’s part of the type name. In the context of raw pointers, immutable means that the pointer can’t be directly assigned to after being dereferenced.

Different from references and smart pointers, raw pointers:

  • Are allowed to ignore the borrowing rules by having both immutable and mutable pointers or multiple mutable pointers to the same location
  • Aren’t guaranteed to point to valid memory
  • Are allowed to be null
  • Don’t implement any automatic cleanup

By opting out of having Rust enforce these guarantees, you can give up guaranteed safety in exchange for greater performance or the ability to interface with another language or hardware where Rust’s guarantees don’t apply.

Listing 20-1 shows how to create an immutable and a mutable raw pointer.

fn main() {
    let mut num = 5;

    let r1 = &raw const num;
    let r2 = &raw mut num;
}
Listing 20-1: Creating raw pointers with the raw borrow operators

Notice that we don’t include the unsafe keyword in this code. We can create raw pointers in safe code; we just can’t dereference raw pointers outside an unsafe block, as you’ll see in a bit.

We’ve created raw pointers by using the raw borrow operators: &raw const num creates a *const i32 immutable raw pointer, and &raw mut num creates a *mut i32 mutable raw pointer. Because we created them directly from a local variable, we know these particular raw pointers are valid, but we can’t make that assumption about just any raw pointer.

To demonstrate this, next we’ll create a raw pointer whose validity we can’t be so certain of, using the keyword as to cast a value instead of using the raw borrow operator. Listing 20-2 shows how to create a raw pointer to an arbitrary location in memory. Trying to use arbitrary memory is undefined: There might be data at that address or there might not, the compiler might optimize the code so that there is no memory access, or the program might terminate with a segmentation fault. Usually, there is no good reason to write code like this, especially in cases where you can use a raw borrow operator instead, but it is possible.

fn main() {
    let address = 0x012345usize;
    let r = address as *const i32;
}
Listing 20-2: Creating a raw pointer to an arbitrary memory address

Recall that we can create raw pointers in safe code, but we can’t dereference raw pointers and read the data being pointed to. In Listing 20-3, we use the dereference operator * on a raw pointer that requires an unsafe block.

fn main() {
    let mut num = 5;

    let r1 = &raw const num;
    let r2 = &raw mut num;

    unsafe {
        println!("r1 is: {}", *r1);
        println!("r2 is: {}", *r2);
    }
}
Listing 20-3: Dereferencing raw pointers within an unsafe block

Creating a pointer does no harm; it’s only when we try to access the value that it points at that we might end up dealing with an invalid value.

Note also that in Listings 20-1 and 20-3, we created *const i32 and *mut i32 raw pointers that both pointed to the same memory location, where num is stored. If we instead tried to create an immutable and a mutable reference to num, the code would not have compiled because Rust’s ownership rules don’t allow a mutable reference at the same time as any immutable references. With raw pointers, we can create a mutable pointer and an immutable pointer to the same location and change data through the mutable pointer, potentially creating a data race. Be careful!

With all of these dangers, why would you ever use raw pointers? One major use case is when interfacing with C code, as you’ll see in the next section. Another case is when building up safe abstractions that the borrow checker doesn’t understand. We’ll introduce unsafe functions and then look at an example of a safe abstraction that uses unsafe code.

Calling an Unsafe Function or Method

The second type of operation you can perform in an unsafe block is calling unsafe functions. Unsafe functions and methods look exactly like regular functions and methods, but they have an extra unsafe before the rest of the definition. The unsafe keyword in this context indicates the function has requirements we need to uphold when we call this function, because Rust can’t guarantee we’ve met these requirements. By calling an unsafe function within an unsafe block, we’re saying that we’ve read this function’s documentation and we take responsibility for upholding the function’s contracts.

Here is an unsafe function named dangerous that doesn’t do anything in its body:

fn main() {
    unsafe fn dangerous() {}

    unsafe {
        dangerous();
    }
}

We must call the dangerous function within a separate unsafe block. If we try to call dangerous without the unsafe block, we’ll get an error:

$ cargo run
   Compiling unsafe-example v0.1.0 (file:///projects/unsafe-example)
error[E0133]: call to unsafe function `dangerous` is unsafe and requires unsafe block
 --> src/main.rs:4:5
  |
4 |     dangerous();
  |     ^^^^^^^^^^^ call to unsafe function
  |
  = note: consult the function's documentation for information on how to avoid undefined behavior

For more information about this error, try `rustc --explain E0133`.
error: could not compile `unsafe-example` (bin "unsafe-example") due to 1 previous error

With the unsafe block, we’re asserting to Rust that we’ve read the function’s documentation, we understand how to use it properly, and we’ve verified that we’re fulfilling the contract of the function.

To perform unsafe operations in the body of an unsafe function, you still need to use an unsafe block, just as within a regular function, and the compiler will warn you if you forget. This helps us keep unsafe blocks as small as possible, as unsafe operations may not be needed across the whole function body.

Creating a Safe Abstraction over Unsafe Code

Just because a function contains unsafe code doesn’t mean we need to mark the entire function as unsafe. In fact, wrapping unsafe code in a safe function is a common abstraction. As an example, let’s study the split_at_mut function from the standard library, which requires some unsafe code. We’ll explore how we might implement it. This safe method is defined on mutable slices: It takes one slice and makes it two by splitting the slice at the index given as an argument. Listing 20-4 shows how to use split_at_mut.

fn main() {
    let mut v = vec![1, 2, 3, 4, 5, 6];

    let r = &mut v[..];

    let (a, b) = r.split_at_mut(3);

    assert_eq!(a, &mut [1, 2, 3]);
    assert_eq!(b, &mut [4, 5, 6]);
}
Listing 20-4: Using the safe split_at_mut function

We can’t implement this function using only safe Rust. An attempt might look something like Listing 20-5, which won’t compile. For simplicity, we’ll implement split_at_mut as a function rather than a method and only for slices of i32 values rather than for a generic type T.

fn split_at_mut(values: &mut [i32], mid: usize) -> (&mut [i32], &mut [i32]) {
    let len = values.len();

    assert!(mid <= len);

    (&mut values[..mid], &mut values[mid..])
}

fn main() {
    let mut vector = vec![1, 2, 3, 4, 5, 6];
    let (left, right) = split_at_mut(&mut vector, 3);
}
Listing 20-5: An attempted implementation of split_at_mut using only safe Rust

This function first gets the total length of the slice. Then, it asserts that the index given as a parameter is within the slice by checking whether it’s less than or equal to the length. The assertion means that if we pass an index that is greater than the length to split the slice at, the function will panic before it attempts to use that index.

Then, we return two mutable slices in a tuple: one from the start of the original slice to the mid index and another from mid to the end of the slice.

When we try to compile the code in Listing 20-5, we’ll get an error:

$ cargo run
   Compiling unsafe-example v0.1.0 (file:///projects/unsafe-example)
error[E0499]: cannot borrow `*values` as mutable more than once at a time
 --> src/main.rs:6:31
  |
1 | fn split_at_mut(values: &mut [i32], mid: usize) -> (&mut [i32], &mut [i32]) {
  |                         - let's call the lifetime of this reference `'1`
...
6 |     (&mut values[..mid], &mut values[mid..])
  |     --------------------------^^^^^^--------
  |     |     |                   |
  |     |     |                   second mutable borrow occurs here
  |     |     first mutable borrow occurs here
  |     returning this value requires that `*values` is borrowed for `'1`
  |
  = help: use `.split_at_mut(position)` to obtain two mutable non-overlapping sub-slices

For more information about this error, try `rustc --explain E0499`.
error: could not compile `unsafe-example` (bin "unsafe-example") due to 1 previous error

Rust’s borrow checker can’t understand that we’re borrowing different parts of the slice; it only knows that we’re borrowing from the same slice twice. Borrowing different parts of a slice is fundamentally okay because the two slices aren’t overlapping, but Rust isn’t smart enough to know this. When we know code is okay, but Rust doesn’t, it’s time to reach for unsafe code.

Listing 20-6 shows how to use an unsafe block, a raw pointer, and some calls to unsafe functions to make the implementation of split_at_mut work.

use std::slice;

fn split_at_mut(values: &mut [i32], mid: usize) -> (&mut [i32], &mut [i32]) {
    let len = values.len();
    let ptr = values.as_mut_ptr();

    assert!(mid <= len);

    unsafe {
        (
            slice::from_raw_parts_mut(ptr, mid),
            slice::from_raw_parts_mut(ptr.add(mid), len - mid),
        )
    }
}

fn main() {
    let mut vector = vec![1, 2, 3, 4, 5, 6];
    let (left, right) = split_at_mut(&mut vector, 3);
}
Listing 20-6: Using unsafe code in the implementation of the split_at_mut function

Recall from “The Slice Type” section in Chapter 4 that a slice is a pointer to some data and the length of the slice. We use the len method to get the length of a slice and the as_mut_ptr method to access the raw pointer of a slice. In this case, because we have a mutable slice to i32 values, as_mut_ptr returns a raw pointer with the type *mut i32, which we’ve stored in the variable ptr.

We keep the assertion that the mid index is within the slice. Then, we get to the unsafe code: The slice::from_raw_parts_mut function takes a raw pointer and a length, and it creates a slice. We use this function to create a slice that starts from ptr and is mid items long. Then, we call the add method on ptr with mid as an argument to get a raw pointer that starts at mid, and we create a slice using that pointer and the remaining number of items after mid as the length.

The function slice::from_raw_parts_mut is unsafe because it takes a raw pointer and must trust that this pointer is valid. The add method on raw pointers is also unsafe because it must trust that the offset location is also a valid pointer. Therefore, we had to put an unsafe block around our calls to slice::from_raw_parts_mut and add so that we could call them. By looking at the code and by adding the assertion that mid must be less than or equal to len, we can tell that all the raw pointers used within the unsafe block will be valid pointers to data within the slice. This is an acceptable and appropriate use of unsafe.

Note that we don’t need to mark the resultant split_at_mut function as unsafe, and we can call this function from safe Rust. We’ve created a safe abstraction to the unsafe code with an implementation of the function that uses unsafe code in a safe way, because it creates only valid pointers from the data this function has access to.

In contrast, the use of slice::from_raw_parts_mut in Listing 20-7 would likely crash when the slice is used. This code takes an arbitrary memory location and creates a slice 10,000 items long.

fn main() {
    use std::slice;

    let address = 0x01234usize;
    let r = address as *mut i32;

    let values: &[i32] = unsafe { slice::from_raw_parts_mut(r, 10000) };
}
Listing 20-7: Creating a slice from an arbitrary memory location

We don’t own the memory at this arbitrary location, and there is no guarantee that the slice this code creates contains valid i32 values. Attempting to use values as though it’s a valid slice results in undefined behavior.

Using extern Functions to Call External Code

Sometimes your Rust code might need to interact with code written in another language. For this, Rust has the keyword extern that facilitates the creation and use of a Foreign Function Interface (FFI), which is a way for a programming language to define functions and enable a different (foreign) programming language to call those functions.

Listing 20-8 demonstrates how to set up an integration with the abs function from the C standard library. Functions declared within extern blocks are generally unsafe to call from Rust code, so extern blocks must also be marked unsafe. The reason is that other languages don’t enforce Rust’s rules and guarantees, and Rust can’t check them, so responsibility falls on the programmer to ensure safety.

Filename: src/main.rs
unsafe extern "C" {
    fn abs(input: i32) -> i32;
}

fn main() {
    unsafe {
        println!("Absolute value of -3 according to C: {}", abs(-3));
    }
}
Listing 20-8: Declaring and calling an extern function defined in another language

Within the unsafe extern "C" block, we list the names and signatures of external functions from another language we want to call. The "C" part defines which application binary interface (ABI) the external function uses: The ABI defines how to call the function at the assembly level. The "C" ABI is the most common and follows the C programming language’s ABI. Information about all the ABIs Rust supports is available in the Rust Reference.

Every item declared within an unsafe extern block is implicitly unsafe. However, some FFI functions are safe to call. For example, the abs function from C’s standard library does not have any memory safety considerations, and we know it can be called with any i32. In cases like this, we can use the safe keyword to say that this specific function is safe to call even though it is in an unsafe extern block. Once we make that change, calling it no longer requires an unsafe block, as shown in Listing 20-9.

Filename: src/main.rs
unsafe extern "C" {
    safe fn abs(input: i32) -> i32;
}

fn main() {
    println!("Absolute value of -3 according to C: {}", abs(-3));
}
Listing 20-9: Explicitly marking a function as safe within an unsafe extern block and calling it safely

Marking a function as safe does not inherently make it safe! Instead, it is like a promise you are making to Rust that it is safe. It is still your responsibility to make sure that promise is kept!

Calling Rust Functions from Other Languages

We can also use extern to create an interface that allows other languages to call Rust functions. Instead of creating a whole extern block, we add the extern keyword and specify the ABI to use just before the fn keyword for the relevant function. We also need to add an #[unsafe(no_mangle)] annotation to tell the Rust compiler not to mangle the name of this function. Mangling is when a compiler changes the name we’ve given a function to a different name that contains more information for other parts of the compilation process to consume but is less human readable. Every programming language compiler mangles names slightly differently, so for a Rust function to be nameable by other languages, we must disable the Rust compiler’s name mangling. This is unsafe because there might be name collisions across libraries without the built-in mangling, so it is our responsibility to make sure the name we choose is safe to export without mangling.

In the following example, we make the call_from_c function accessible from C code, after it’s compiled to a shared library and linked from C:

#[unsafe(no_mangle)]
pub extern "C" fn call_from_c() {
    println!("Just called a Rust function from C!");
}

This usage of extern requires unsafe only in the attribute, not on the extern block.

Accessing or Modifying a Mutable Static Variable

In this book, we’ve not yet talked about global variables, which Rust does support but which can be problematic with Rust’s ownership rules. If two threads are accessing the same mutable global variable, it can cause a data race.

In Rust, global variables are called static variables. Listing 20-10 shows an example declaration and use of a static variable with a string slice as a value.

Filename: src/main.rs
static HELLO_WORLD: &str = "Hello, world!";

fn main() {
    println!("value is: {HELLO_WORLD}");
}
Listing 20-10: Defining and using an immutable static variable

Static variables are similar to constants, which we discussed in the “Declaring Constants” section in Chapter 3. The names of static variables are in SCREAMING_SNAKE_CASE by convention. Static variables can only store references with the 'static lifetime, which means the Rust compiler can figure out the lifetime and we aren’t required to annotate it explicitly. Accessing an immutable static variable is safe.

A subtle difference between constants and immutable static variables is that values in a static variable have a fixed address in memory. Using the value will always access the same data. Constants, on the other hand, are allowed to duplicate their data whenever they’re used. Another difference is that static variables can be mutable. Accessing and modifying mutable static variables is unsafe. Listing 20-11 shows how to declare, access, and modify a mutable static variable named COUNTER.

Filename: src/main.rs
static mut COUNTER: u32 = 0;

/// SAFETY: Calling this from more than a single thread at a time is undefined
/// behavior, so you *must* guarantee you only call it from a single thread at
/// a time.
unsafe fn add_to_count(inc: u32) {
    unsafe {
        COUNTER += inc;
    }
}

fn main() {
    unsafe {
        // SAFETY: This is only called from a single thread in `main`.
        add_to_count(3);
        println!("COUNTER: {}", *(&raw const COUNTER));
    }
}
Listing 20-11: Reading from or writing to a mutable static variable is unsafe.

As with regular variables, we specify mutability using the mut keyword. Any code that reads or writes from COUNTER must be within an unsafe block. The code in Listing 20-11 compiles and prints COUNTER: 3 as we would expect because it’s single threaded. Having multiple threads access COUNTER would likely result in data races, so it is undefined behavior. Therefore, we need to mark the entire function as unsafe and document the safety limitation so that anyone calling the function knows what they are and are not allowed to do safely.

Whenever we write an unsafe function, it is idiomatic to write a comment starting with SAFETY and explaining what the caller needs to do to call the function safely. Likewise, whenever we perform an unsafe operation, it is idiomatic to write a comment starting with SAFETY to explain how the safety rules are upheld.

Additionally, the compiler will deny by default any attempt to create references to a mutable static variable through a compiler lint. You must either explicitly opt out of that lint’s protections by adding an #[allow(static_mut_refs)] annotation or access the mutable static variable via a raw pointer created with one of the raw borrow operators. That includes cases where the reference is created invisibly, as when it is used in the println! in this code listing. Requiring references to static mutable variables to be created via raw pointers helps make the safety requirements for using them more obvious.

With mutable data that is globally accessible, it’s difficult to ensure that there are no data races, which is why Rust considers mutable static variables to be unsafe. Where possible, it’s preferable to use the concurrency techniques and thread-safe smart pointers we discussed in Chapter 16 so that the compiler checks that data access from different threads is done safely.

Implementing an Unsafe Trait

We can use unsafe to implement an unsafe trait. A trait is unsafe when at least one of its methods has some invariant that the compiler can’t verify. We declare that a trait is unsafe by adding the unsafe keyword before trait and marking the implementation of the trait as unsafe too, as shown in Listing 20-12.

unsafe trait Foo {
    // methods go here
}

unsafe impl Foo for i32 {
    // method implementations go here
}

fn main() {}
Listing 20-12: Defining and implementing an unsafe trait

By using unsafe impl, we’re promising that we’ll uphold the invariants that the compiler can’t verify.

As an example, recall the Send and Sync marker traits we discussed in the “Extensible Concurrency with Send and Sync section in Chapter 16: The compiler implements these traits automatically if our types are composed entirely of other types that implement Send and Sync. If we implement a type that contains a type that does not implement Send or Sync, such as raw pointers, and we want to mark that type as Send or Sync, we must use unsafe. Rust can’t verify that our type upholds the guarantees that it can be safely sent across threads or accessed from multiple threads; therefore, we need to do those checks manually and indicate as such with unsafe.

Accessing Fields of a Union

The final action that works only with unsafe is accessing fields of a union. A union is similar to a struct, but only one declared field is used in a particular instance at one time. Unions are primarily used to interface with unions in C code. Accessing union fields is unsafe because Rust can’t guarantee the type of the data currently being stored in the union instance. You can learn more about unions in the Rust Reference.

Using Miri to Check Unsafe Code

When writing unsafe code, you might want to check that what you have written actually is safe and correct. One of the best ways to do that is to use Miri, an official Rust tool for detecting undefined behavior. Whereas the borrow checker is a static tool that works at compile time, Miri is a dynamic tool that works at runtime. It checks your code by running your program, or its test suite, and detecting when you violate the rules it understands about how Rust should work.

Using Miri requires a nightly build of Rust (which we talk about more in Appendix G: How Rust is Made and “Nightly Rust”). You can install both a nightly version of Rust and the Miri tool by typing rustup +nightly component add miri. This does not change what version of Rust your project uses; it only adds the tool to your system so you can use it when you want to. You can run Miri on a project by typing cargo +nightly miri run or cargo +nightly miri test.

For an example of how helpful this can be, consider what happens when we run it against Listing 20-7.

$ cargo +nightly miri run
   Compiling unsafe-example v0.1.0 (file:///projects/unsafe-example)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.01s
     Running `file:///home/.rustup/toolchains/nightly/bin/cargo-miri runner target/miri/debug/unsafe-example`
warning: integer-to-pointer cast
 --> src/main.rs:5:13
  |
5 |     let r = address as *mut i32;
  |             ^^^^^^^^^^^^^^^^^^^ integer-to-pointer cast
  |
  = help: this program is using integer-to-pointer casts or (equivalently) `ptr::with_exposed_provenance`, which means that Miri might miss pointer bugs in this program
  = help: see https://doc.rust-lang.org/nightly/std/ptr/fn.with_exposed_provenance.html for more details on that operation
  = help: to ensure that Miri does not miss bugs in your program, use Strict Provenance APIs (https://doc.rust-lang.org/nightly/std/ptr/index.html#strict-provenance, https://crates.io/crates/sptr) instead
  = help: you can then set `MIRIFLAGS=-Zmiri-strict-provenance` to ensure you are not relying on `with_exposed_provenance` semantics
  = help: alternatively, `MIRIFLAGS=-Zmiri-permissive-provenance` disables this warning
  = note: BACKTRACE:
  = note: inside `main` at src/main.rs:5:13: 5:32

error: Undefined Behavior: pointer not dereferenceable: pointer must be dereferenceable for 40000 bytes, but got 0x1234[noalloc] which is a dangling pointer (it has no provenance)
 --> src/main.rs:7:35
  |
7 |     let values: &[i32] = unsafe { slice::from_raw_parts_mut(r, 10000) };
  |                                   ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Undefined Behavior occurred here
  |
  = help: this indicates a bug in the program: it performed an invalid operation, and caused Undefined Behavior
  = help: see https://doc.rust-lang.org/nightly/reference/behavior-considered-undefined.html for further information
  = note: BACKTRACE:
  = note: inside `main` at src/main.rs:7:35: 7:70

note: some details are omitted, run with `MIRIFLAGS=-Zmiri-backtrace=full` for a verbose backtrace

error: aborting due to 1 previous error; 1 warning emitted

Miri correctly warns us that we’re casting an integer to a pointer, which might be a problem, but Miri can’t determine whether a problem exists because it doesn’t know how the pointer originated. Then, Miri returns an error where Listing 20-7 has undefined behavior because we have a dangling pointer. Thanks to Miri, we now know there is a risk of undefined behavior, and we can think about how to make the code safe. In some cases, Miri can even make recommendations about how to fix errors.

Miri doesn’t catch everything you might get wrong when writing unsafe code. Miri is a dynamic analysis tool, so it only catches problems with code that actually gets run. That means you will need to use it in conjunction with good testing techniques to increase your confidence about the unsafe code you have written. Miri also does not cover every possible way your code can be unsound.

Put another way: If Miri does catch a problem, you know there’s a bug, but just because Miri doesn’t catch a bug doesn’t mean there isn’t a problem. It can catch a lot, though. Try running it on the other examples of unsafe code in this chapter and see what it says!

You can learn more about Miri at its GitHub repository.

Using Unsafe Code Correctly

Using unsafe to use one of the five superpowers just discussed isn’t wrong or even frowned upon, but it is trickier to get unsafe code correct because the compiler can’t help uphold memory safety. When you have a reason to use unsafe code, you can do so, and having the explicit unsafe annotation makes it easier to track down the source of problems when they occur. Whenever you write unsafe code, you can use Miri to help you be more confident that the code you have written upholds Rust’s rules.

For a much deeper exploration of how to work effectively with unsafe Rust, read Rust’s official guide for unsafe, The Rustonomicon.

Продвинутые трейты

Advanced Traits

We first covered traits in the “Defining Shared Behavior with Traits” section in Chapter 10, but we didn’t discuss the more advanced details. Now that you know more about Rust, we can get into the nitty-gritty.

Defining Traits with Associated Types

Associated types connect a type placeholder with a trait such that the trait method definitions can use these placeholder types in their signatures. The implementor of a trait will specify the concrete type to be used instead of the placeholder type for the particular implementation. That way, we can define a trait that uses some types without needing to know exactly what those types are until the trait is implemented.

We’ve described most of the advanced features in this chapter as being rarely needed. Associated types are somewhere in the middle: They’re used more rarely than features explained in the rest of the book but more commonly than many of the other features discussed in this chapter.

One example of a trait with an associated type is the Iterator trait that the standard library provides. The associated type is named Item and stands in for the type of the values the type implementing the Iterator trait is iterating over. The definition of the Iterator trait is as shown in Listing 20-13.

pub trait Iterator {
    type Item;

    fn next(&mut self) -> Option<Self::Item>;
}
Listing 20-13: The definition of the Iterator trait that has an associated type Item

The type Item is a placeholder, and the next method’s definition shows that it will return values of type Option<Self::Item>. Implementors of the Iterator trait will specify the concrete type for Item, and the next method will return an Option containing a value of that concrete type.

Associated types might seem like a similar concept to generics, in that the latter allow us to define a function without specifying what types it can handle. To examine the difference between the two concepts, we’ll look at an implementation of the Iterator trait on a type named Counter that specifies the Item type is u32:

Filename: src/lib.rs
struct Counter {
    count: u32,
}

impl Counter {
    fn new() -> Counter {
        Counter { count: 0 }
    }
}

impl Iterator for Counter {
    type Item = u32;

    fn next(&mut self) -> Option<Self::Item> {
        // --snip--
        if self.count < 5 {
            self.count += 1;
            Some(self.count)
        } else {
            None
        }
    }
}

This syntax seems comparable to that of generics. So, why not just define the Iterator trait with generics, as shown in Listing 20-14?

pub trait Iterator<T> {
    fn next(&mut self) -> Option<T>;
}
Listing 20-14: A hypothetical definition of the Iterator trait using generics

The difference is that when using generics, as in Listing 20-14, we must annotate the types in each implementation; because we can also implement Iterator<String> for Counter or any other type, we could have multiple implementations of Iterator for Counter. In other words, when a trait has a generic parameter, it can be implemented for a type multiple times, changing the concrete types of the generic type parameters each time. When we use the next method on Counter, we would have to provide type annotations to indicate which implementation of Iterator we want to use.

With associated types, we don’t need to annotate types, because we can’t implement a trait on a type multiple times. In Listing 20-13 with the definition that uses associated types, we can choose what the type of Item will be only once because there can be only one impl Iterator for Counter. We don’t have to specify that we want an iterator of u32 values everywhere we call next on Counter.

Associated types also become part of the trait’s contract: Implementors of the trait must provide a type to stand in for the associated type placeholder. Associated types often have a name that describes how the type will be used, and documenting the associated type in the API documentation is a good practice.

Using Default Generic Parameters and Operator Overloading

When we use generic type parameters, we can specify a default concrete type for the generic type. This eliminates the need for implementors of the trait to specify a concrete type if the default type works. You specify a default type when declaring a generic type with the <PlaceholderType=ConcreteType> syntax.

A great example of a situation where this technique is useful is with operator overloading, in which you customize the behavior of an operator (such as +) in particular situations.

Rust doesn’t allow you to create your own operators or overload arbitrary operators. But you can overload the operations and corresponding traits listed in std::ops by implementing the traits associated with the operator. For example, in Listing 20-15, we overload the + operator to add two Point instances together. We do this by implementing the Add trait on a Point struct.

Filename: src/main.rs
use std::ops::Add;

#[derive(Debug, Copy, Clone, PartialEq)]
struct Point {
    x: i32,
    y: i32,
}

impl Add for Point {
    type Output = Point;

    fn add(self, other: Point) -> Point {
        Point {
            x: self.x + other.x,
            y: self.y + other.y,
        }
    }
}

fn main() {
    assert_eq!(
        Point { x: 1, y: 0 } + Point { x: 2, y: 3 },
        Point { x: 3, y: 3 }
    );
}
Listing 20-15: Implementing the Add trait to overload the + operator for Point instances

The add method adds the x values of two Point instances and the y values of two Point instances to create a new Point. The Add trait has an associated type named Output that determines the type returned from the add method.

The default generic type in this code is within the Add trait. Here is its definition:

#![allow(unused)]
fn main() {
trait Add<Rhs=Self> {
    type Output;

    fn add(self, rhs: Rhs) -> Self::Output;
}
}

This code should look generally familiar: a trait with one method and an associated type. The new part is Rhs=Self: This syntax is called default type parameters. The Rhs generic type parameter (short for “right-hand side”) defines the type of the rhs parameter in the add method. If we don’t specify a concrete type for Rhs when we implement the Add trait, the type of Rhs will default to Self, which will be the type we’re implementing Add on.

When we implemented Add for Point, we used the default for Rhs because we wanted to add two Point instances. Let’s look at an example of implementing the Add trait where we want to customize the Rhs type rather than using the default.

We have two structs, Millimeters and Meters, holding values in different units. This thin wrapping of an existing type in another struct is known as the newtype pattern, which we describe in more detail in the “Implementing External Traits with the Newtype Pattern” section. We want to add values in millimeters to values in meters and have the implementation of Add do the conversion correctly. We can implement Add for Millimeters with Meters as the Rhs, as shown in Listing 20-16.

Filename: src/lib.rs
use std::ops::Add;

struct Millimeters(u32);
struct Meters(u32);

impl Add<Meters> for Millimeters {
    type Output = Millimeters;

    fn add(self, other: Meters) -> Millimeters {
        Millimeters(self.0 + (other.0 * 1000))
    }
}
Listing 20-16: Implementing the Add trait on Millimeters to add Millimeters and Meters

To add Millimeters and Meters, we specify impl Add<Meters> to set the value of the Rhs type parameter instead of using the default of Self.

You’ll use default type parameters in two main ways:

  1. To extend a type without breaking existing code
  2. To allow customization in specific cases most users won’t need

The standard library’s Add trait is an example of the second purpose: Usually, you’ll add two like types, but the Add trait provides the ability to customize beyond that. Using a default type parameter in the Add trait definition means you don’t have to specify the extra parameter most of the time. In other words, a bit of implementation boilerplate isn’t needed, making it easier to use the trait.

The first purpose is similar to the second but in reverse: If you want to add a type parameter to an existing trait, you can give it a default to allow extension of the functionality of the trait without breaking the existing implementation code.

Disambiguating Between Identically Named Methods

Nothing in Rust prevents a trait from having a method with the same name as another trait’s method, nor does Rust prevent you from implementing both traits on one type. It’s also possible to implement a method directly on the type with the same name as methods from traits.

When calling methods with the same name, you’ll need to tell Rust which one you want to use. Consider the code in Listing 20-17 where we’ve defined two traits, Pilot and Wizard, that both have a method called fly. We then implement both traits on a type Human that already has a method named fly implemented on it. Each fly method does something different.

Filename: src/main.rs
trait Pilot {
    fn fly(&self);
}

trait Wizard {
    fn fly(&self);
}

struct Human;

impl Pilot for Human {
    fn fly(&self) {
        println!("This is your captain speaking.");
    }
}

impl Wizard for Human {
    fn fly(&self) {
        println!("Up!");
    }
}

impl Human {
    fn fly(&self) {
        println!("*waving arms furiously*");
    }
}

fn main() {}
Listing 20-17: Two traits are defined to have a fly method and are implemented on the Human type, and a fly method is implemented on Human directly.

When we call fly on an instance of Human, the compiler defaults to calling the method that is directly implemented on the type, as shown in Listing 20-18.

Filename: src/main.rs
trait Pilot {
    fn fly(&self);
}

trait Wizard {
    fn fly(&self);
}

struct Human;

impl Pilot for Human {
    fn fly(&self) {
        println!("This is your captain speaking.");
    }
}

impl Wizard for Human {
    fn fly(&self) {
        println!("Up!");
    }
}

impl Human {
    fn fly(&self) {
        println!("*waving arms furiously*");
    }
}

fn main() {
    let person = Human;
    person.fly();
}
Listing 20-18: Calling fly on an instance of Human

Running this code will print *waving arms furiously*, showing that Rust called the fly method implemented on Human directly.

To call the fly methods from either the Pilot trait or the Wizard trait, we need to use more explicit syntax to specify which fly method we mean. Listing 20-19 demonstrates this syntax.

Filename: src/main.rs
trait Pilot {
    fn fly(&self);
}

trait Wizard {
    fn fly(&self);
}

struct Human;

impl Pilot for Human {
    fn fly(&self) {
        println!("This is your captain speaking.");
    }
}

impl Wizard for Human {
    fn fly(&self) {
        println!("Up!");
    }
}

impl Human {
    fn fly(&self) {
        println!("*waving arms furiously*");
    }
}

fn main() {
    let person = Human;
    Pilot::fly(&person);
    Wizard::fly(&person);
    person.fly();
}
Listing 20-19: Specifying which trait’s fly method we want to call

Specifying the trait name before the method name clarifies to Rust which implementation of fly we want to call. We could also write Human::fly(&person), which is equivalent to the person.fly() that we used in Listing 20-19, but this is a bit longer to write if we don’t need to disambiguate.

Running this code prints the following:

$ cargo run
   Compiling traits-example v0.1.0 (file:///projects/traits-example)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.46s
     Running `target/debug/traits-example`
This is your captain speaking.
Up!
*waving arms furiously*

Because the fly method takes a self parameter, if we had two types that both implement one trait, Rust could figure out which implementation of a trait to use based on the type of self.

However, associated functions that are not methods don’t have a self parameter. When there are multiple types or traits that define non-method functions with the same function name, Rust doesn’t always know which type you mean unless you use fully qualified syntax. For example, in Listing 20-20, we create a trait for an animal shelter that wants to name all baby dogs Spot. We make an Animal trait with an associated non-method function baby_name. The Animal trait is implemented for the struct Dog, on which we also provide an associated non-method function baby_name directly.

Filename: src/main.rs
trait Animal {
    fn baby_name() -> String;
}

struct Dog;

impl Dog {
    fn baby_name() -> String {
        String::from("Spot")
    }
}

impl Animal for Dog {
    fn baby_name() -> String {
        String::from("puppy")
    }
}

fn main() {
    println!("A baby dog is called a {}", Dog::baby_name());
}
Listing 20-20: A trait with an associated function and a type with an associated function of the same name that also implements the trait

We implement the code for naming all puppies Spot in the baby_name associated function that is defined on Dog. The Dog type also implements the trait Animal, which describes characteristics that all animals have. Baby dogs are called puppies, and that is expressed in the implementation of the Animal trait on Dog in the baby_name function associated with the Animal trait.

In main, we call the Dog::baby_name function, which calls the associated function defined on Dog directly. This code prints the following:

$ cargo run
   Compiling traits-example v0.1.0 (file:///projects/traits-example)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.54s
     Running `target/debug/traits-example`
A baby dog is called a Spot

This output isn’t what we wanted. We want to call the baby_name function that is part of the Animal trait that we implemented on Dog so that the code prints A baby dog is called a puppy. The technique of specifying the trait name that we used in Listing 20-19 doesn’t help here; if we change main to the code in Listing 20-21, we’ll get a compilation error.

Filename: src/main.rs
trait Animal {
    fn baby_name() -> String;
}

struct Dog;

impl Dog {
    fn baby_name() -> String {
        String::from("Spot")
    }
}

impl Animal for Dog {
    fn baby_name() -> String {
        String::from("puppy")
    }
}

fn main() {
    println!("A baby dog is called a {}", Animal::baby_name());
}
Listing 20-21: Attempting to call the baby_name function from the Animal trait, but Rust doesn’t know which implementation to use

Because Animal::baby_name doesn’t have a self parameter, and there could be other types that implement the Animal trait, Rust can’t figure out which implementation of Animal::baby_name we want. We’ll get this compiler error:

$ cargo run
   Compiling traits-example v0.1.0 (file:///projects/traits-example)
error[E0790]: cannot call associated function on trait without specifying the corresponding `impl` type
  --> src/main.rs:20:43
   |
 2 |     fn baby_name() -> String;
   |     ------------------------- `Animal::baby_name` defined here
...
20 |     println!("A baby dog is called a {}", Animal::baby_name());
   |                                           ^^^^^^^^^^^^^^^^^^^ cannot call associated function of trait
   |
help: use the fully-qualified path to the only available implementation
   |
20 |     println!("A baby dog is called a {}", <Dog as Animal>::baby_name());
   |                                           +++++++       +

For more information about this error, try `rustc --explain E0790`.
error: could not compile `traits-example` (bin "traits-example") due to 1 previous error

To disambiguate and tell Rust that we want to use the implementation of Animal for Dog as opposed to the implementation of Animal for some other type, we need to use fully qualified syntax. Listing 20-22 demonstrates how to use fully qualified syntax.

Filename: src/main.rs
trait Animal {
    fn baby_name() -> String;
}

struct Dog;

impl Dog {
    fn baby_name() -> String {
        String::from("Spot")
    }
}

impl Animal for Dog {
    fn baby_name() -> String {
        String::from("puppy")
    }
}

fn main() {
    println!("A baby dog is called a {}", <Dog as Animal>::baby_name());
}
Listing 20-22: Using fully qualified syntax to specify that we want to call the baby_name function from the Animal trait as implemented on Dog

We’re providing Rust with a type annotation within the angle brackets, which indicates we want to call the baby_name method from the Animal trait as implemented on Dog by saying that we want to treat the Dog type as an Animal for this function call. This code will now print what we want:

$ cargo run
   Compiling traits-example v0.1.0 (file:///projects/traits-example)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.48s
     Running `target/debug/traits-example`
A baby dog is called a puppy

In general, fully qualified syntax is defined as follows:

<Type as Trait>::function(receiver_if_method, next_arg, ...);

For associated functions that aren’t methods, there would not be a receiver: There would only be the list of other arguments. You could use fully qualified syntax everywhere that you call functions or methods. However, you’re allowed to omit any part of this syntax that Rust can figure out from other information in the program. You only need to use this more verbose syntax in cases where there are multiple implementations that use the same name and Rust needs help to identify which implementation you want to call.

Using Supertraits

Sometimes you might write a trait definition that depends on another trait: For a type to implement the first trait, you want to require that type to also implement the second trait. You would do this so that your trait definition can make use of the associated items of the second trait. The trait your trait definition is relying on is called a supertrait of your trait.

For example, let’s say we want to make an OutlinePrint trait with an outline_print method that will print a given value formatted so that it’s framed in asterisks. That is, given a Point struct that implements the standard library trait Display to result in (x, y), when we call outline_print on a Point instance that has 1 for x and 3 for y, it should print the following:

**********
*        *
* (1, 3) *
*        *
**********

In the implementation of the outline_print method, we want to use the Display trait’s functionality. Therefore, we need to specify that the OutlinePrint trait will work only for types that also implement Display and provide the functionality that OutlinePrint needs. We can do that in the trait definition by specifying OutlinePrint: Display. This technique is similar to adding a trait bound to the trait. Listing 20-23 shows an implementation of the OutlinePrint trait.

Filename: src/main.rs
use std::fmt;

trait OutlinePrint: fmt::Display {
    fn outline_print(&self) {
        let output = self.to_string();
        let len = output.len();
        println!("{}", "*".repeat(len + 4));
        println!("*{}*", " ".repeat(len + 2));
        println!("* {output} *");
        println!("*{}*", " ".repeat(len + 2));
        println!("{}", "*".repeat(len + 4));
    }
}

fn main() {}
Listing 20-23: Implementing the OutlinePrint trait that requires the functionality from Display

Because we’ve specified that OutlinePrint requires the Display trait, we can use the to_string function that is automatically implemented for any type that implements Display. If we tried to use to_string without adding a colon and specifying the Display trait after the trait name, we’d get an error saying that no method named to_string was found for the type &Self in the current scope.

Let’s see what happens when we try to implement OutlinePrint on a type that doesn’t implement Display, such as the Point struct:

Filename: src/main.rs
use std::fmt;

trait OutlinePrint: fmt::Display {
    fn outline_print(&self) {
        let output = self.to_string();
        let len = output.len();
        println!("{}", "*".repeat(len + 4));
        println!("*{}*", " ".repeat(len + 2));
        println!("* {output} *");
        println!("*{}*", " ".repeat(len + 2));
        println!("{}", "*".repeat(len + 4));
    }
}

struct Point {
    x: i32,
    y: i32,
}

impl OutlinePrint for Point {}

fn main() {
    let p = Point { x: 1, y: 3 };
    p.outline_print();
}

We get an error saying that Display is required but not implemented:

$ cargo run
   Compiling traits-example v0.1.0 (file:///projects/traits-example)
error[E0277]: `Point` doesn't implement `std::fmt::Display`
  --> src/main.rs:20:23
   |
20 | impl OutlinePrint for Point {}
   |                       ^^^^^ the trait `std::fmt::Display` is not implemented for `Point`
   |
note: required by a bound in `OutlinePrint`
  --> src/main.rs:3:21
   |
 3 | trait OutlinePrint: fmt::Display {
   |                     ^^^^^^^^^^^^ required by this bound in `OutlinePrint`

error[E0277]: `Point` doesn't implement `std::fmt::Display`
  --> src/main.rs:24:7
   |
24 |     p.outline_print();
   |       ^^^^^^^^^^^^^ the trait `std::fmt::Display` is not implemented for `Point`
   |
note: required by a bound in `OutlinePrint::outline_print`
  --> src/main.rs:3:21
   |
 3 | trait OutlinePrint: fmt::Display {
   |                     ^^^^^^^^^^^^ required by this bound in `OutlinePrint::outline_print`
 4 |     fn outline_print(&self) {
   |        ------------- required by a bound in this associated function

For more information about this error, try `rustc --explain E0277`.
error: could not compile `traits-example` (bin "traits-example") due to 2 previous errors

To fix this, we implement Display on Point and satisfy the constraint that OutlinePrint requires, like so:

Filename: src/main.rs
trait OutlinePrint: fmt::Display {
    fn outline_print(&self) {
        let output = self.to_string();
        let len = output.len();
        println!("{}", "*".repeat(len + 4));
        println!("*{}*", " ".repeat(len + 2));
        println!("* {output} *");
        println!("*{}*", " ".repeat(len + 2));
        println!("{}", "*".repeat(len + 4));
    }
}

struct Point {
    x: i32,
    y: i32,
}

impl OutlinePrint for Point {}

use std::fmt;

impl fmt::Display for Point {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "({}, {})", self.x, self.y)
    }
}

fn main() {
    let p = Point { x: 1, y: 3 };
    p.outline_print();
}

Then, implementing the OutlinePrint trait on Point will compile successfully, and we can call outline_print on a Point instance to display it within an outline of asterisks.

Implementing External Traits with the Newtype Pattern

In the “Implementing a Trait on a Type” section in Chapter 10, we mentioned the orphan rule that states we’re only allowed to implement a trait on a type if either the trait or the type, or both, are local to our crate. It’s possible to get around this restriction using the newtype pattern, which involves creating a new type in a tuple struct. (We covered tuple structs in the “Creating Different Types with Tuple Structs” section in Chapter 5.) The tuple struct will have one field and be a thin wrapper around the type for which we want to implement a trait. Then, the wrapper type is local to our crate, and we can implement the trait on the wrapper. Newtype is a term that originates from the Haskell programming language. There is no runtime performance penalty for using this pattern, and the wrapper type is elided at compile time.

As an example, let’s say we want to implement Display on Vec<T>, which the orphan rule prevents us from doing directly because the Display trait and the Vec<T> type are defined outside our crate. We can make a Wrapper struct that holds an instance of Vec<T>; then, we can implement Display on Wrapper and use the Vec<T> value, as shown in Listing 20-24.

Filename: src/main.rs
use std::fmt;

struct Wrapper(Vec<String>);

impl fmt::Display for Wrapper {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "[{}]", self.0.join(", "))
    }
}

fn main() {
    let w = Wrapper(vec![String::from("hello"), String::from("world")]);
    println!("w = {w}");
}
Listing 20-24: Creating a Wrapper type around Vec<String> to implement Display

The implementation of Display uses self.0 to access the inner Vec<T> because Wrapper is a tuple struct and Vec<T> is the item at index 0 in the tuple. Then, we can use the functionality of the Display trait on Wrapper.

The downside of using this technique is that Wrapper is a new type, so it doesn’t have the methods of the value it’s holding. We would have to implement all the methods of Vec<T> directly on Wrapper such that the methods delegate to self.0, which would allow us to treat Wrapper exactly like a Vec<T>. If we wanted the new type to have every method the inner type has, implementing the Deref trait on the Wrapper to return the inner type would be a solution (we discussed implementing the Deref trait in the “Treating Smart Pointers Like Regular References” section in Chapter 15). If we didn’t want the Wrapper type to have all the methods of the inner type—for example, to restrict the Wrapper type’s behavior—we would have to implement just the methods we do want manually.

This newtype pattern is also useful even when traits are not involved. Let’s switch focus and look at some advanced ways to interact with Rust’s type system.

Продвинутые типы

Advanced Types

The Rust type system has some features that we’ve so far mentioned but haven’t yet discussed. We’ll start by discussing newtypes in general as we examine why they are useful as types. Then, we’ll move on to type aliases, a feature similar to newtypes but with slightly different semantics. We’ll also discuss the ! type and dynamically sized types.

Type Safety and Abstraction with the Newtype Pattern

This section assumes you’ve read the earlier section “Implementing External Traits with the Newtype Pattern”. The newtype pattern is also useful for tasks beyond those we’ve discussed so far, including statically enforcing that values are never confused and indicating the units of a value. You saw an example of using newtypes to indicate units in Listing 20-16: Recall that the Millimeters and Meters structs wrapped u32 values in a newtype. If we wrote a function with a parameter of type Millimeters, we wouldn’t be able to compile a program that accidentally tried to call that function with a value of type Meters or a plain u32.

We can also use the newtype pattern to abstract away some implementation details of a type: The new type can expose a public API that is different from the API of the private inner type.

Newtypes can also hide internal implementation. For example, we could provide a People type to wrap a HashMap<i32, String> that stores a person’s ID associated with their name. Code using People would only interact with the public API we provide, such as a method to add a name string to the People collection; that code wouldn’t need to know that we assign an i32 ID to names internally. The newtype pattern is a lightweight way to achieve encapsulation to hide implementation details, which we discussed in the “Encapsulation that Hides Implementation Details” section in Chapter 18.

Type Synonyms and Type Aliases

Rust provides the ability to declare a type alias to give an existing type another name. For this we use the type keyword. For example, we can create the alias Kilometers to i32 like so:

fn main() {
    type Kilometers = i32;

    let x: i32 = 5;
    let y: Kilometers = 5;

    println!("x + y = {}", x + y);
}

Now the alias Kilometers is a synonym for i32; unlike the Millimeters and Meters types we created in Listing 20-16, Kilometers is not a separate, new type. Values that have the type Kilometers will be treated the same as values of type i32:

fn main() {
    type Kilometers = i32;

    let x: i32 = 5;
    let y: Kilometers = 5;

    println!("x + y = {}", x + y);
}

Because Kilometers and i32 are the same type, we can add values of both types and can pass Kilometers values to functions that take i32 parameters. However, using this method, we don’t get the type-checking benefits that we get from the newtype pattern discussed earlier. In other words, if we mix up Kilometers and i32 values somewhere, the compiler will not give us an error.

The main use case for type synonyms is to reduce repetition. For example, we might have a lengthy type like this:

Box<dyn Fn() + Send + 'static>

Writing this lengthy type in function signatures and as type annotations all over the code can be tiresome and error-prone. Imagine having a project full of code like that in Listing 20-25.

fn main() {
    let f: Box<dyn Fn() + Send + 'static> = Box::new(|| println!("hi"));

    fn takes_long_type(f: Box<dyn Fn() + Send + 'static>) {
        // --snip--
    }

    fn returns_long_type() -> Box<dyn Fn() + Send + 'static> {
        // --snip--
        Box::new(|| ())
    }
}
Listing 20-25: Using a long type in many places

A type alias makes this code more manageable by reducing the repetition. In Listing 20-26, we’ve introduced an alias named Thunk for the verbose type and can replace all uses of the type with the shorter alias Thunk.

fn main() {
    type Thunk = Box<dyn Fn() + Send + 'static>;

    let f: Thunk = Box::new(|| println!("hi"));

    fn takes_long_type(f: Thunk) {
        // --snip--
    }

    fn returns_long_type() -> Thunk {
        // --snip--
        Box::new(|| ())
    }
}
Listing 20-26: Introducing a type alias, Thunk, to reduce repetition

This code is much easier to read and write! Choosing a meaningful name for a type alias can help communicate your intent as well (thunk is a word for code to be evaluated at a later time, so it’s an appropriate name for a closure that gets stored).

Type aliases are also commonly used with the Result<T, E> type for reducing repetition. Consider the std::io module in the standard library. I/O operations often return a Result<T, E> to handle situations when operations fail to work. This library has a std::io::Error struct that represents all possible I/O errors. Many of the functions in std::io will be returning Result<T, E> where the E is std::io::Error, such as these functions in the Write trait:

use std::fmt;
use std::io::Error;

pub trait Write {
    fn write(&mut self, buf: &[u8]) -> Result<usize, Error>;
    fn flush(&mut self) -> Result<(), Error>;

    fn write_all(&mut self, buf: &[u8]) -> Result<(), Error>;
    fn write_fmt(&mut self, fmt: fmt::Arguments) -> Result<(), Error>;
}

The Result<..., Error> is repeated a lot. As such, std::io has this type alias declaration:

use std::fmt;

type Result<T> = std::result::Result<T, std::io::Error>;

pub trait Write {
    fn write(&mut self, buf: &[u8]) -> Result<usize>;
    fn flush(&mut self) -> Result<()>;

    fn write_all(&mut self, buf: &[u8]) -> Result<()>;
    fn write_fmt(&mut self, fmt: fmt::Arguments) -> Result<()>;
}

Because this declaration is in the std::io module, we can use the fully qualified alias std::io::Result<T>; that is, a Result<T, E> with the E filled in as std::io::Error. The Write trait function signatures end up looking like this:

use std::fmt;

type Result<T> = std::result::Result<T, std::io::Error>;

pub trait Write {
    fn write(&mut self, buf: &[u8]) -> Result<usize>;
    fn flush(&mut self) -> Result<()>;

    fn write_all(&mut self, buf: &[u8]) -> Result<()>;
    fn write_fmt(&mut self, fmt: fmt::Arguments) -> Result<()>;
}

The type alias helps in two ways: It makes code easier to write and it gives us a consistent interface across all of std::io. Because it’s an alias, it’s just another Result<T, E>, which means we can use any methods that work on Result<T, E> with it, as well as special syntax like the ? operator.

The Never Type That Never Returns

Rust has a special type named ! that’s known in type theory lingo as the empty type because it has no values. We prefer to call it the never type because it stands in the place of the return type when a function will never return. Here is an example:

fn bar() -> ! {
    // --snip--
    panic!();
}

This code is read as “the function bar returns never.” Functions that return never are called diverging functions. We can’t create values of the type !, so bar can never possibly return.

But what use is a type you can never create values for? Recall the code from Listing 2-5, part of the number-guessing game; we’ve reproduced a bit of it here in Listing 20-27.

use std::cmp::Ordering;
use std::io;

use rand::Rng;

fn main() {
    println!("Guess the number!");

    let secret_number = rand::thread_rng().gen_range(1..=100);

    println!("The secret number is: {secret_number}");

    loop {
        println!("Please input your guess.");

        let mut guess = String::new();

        // --snip--

        io::stdin()
            .read_line(&mut guess)
            .expect("Failed to read line");

        let guess: u32 = match guess.trim().parse() {
            Ok(num) => num,
            Err(_) => continue,
        };

        println!("You guessed: {guess}");

        // --snip--

        match guess.cmp(&secret_number) {
            Ordering::Less => println!("Too small!"),
            Ordering::Greater => println!("Too big!"),
            Ordering::Equal => {
                println!("You win!");
                break;
            }
        }
    }
}
Listing 20-27: A match with an arm that ends in continue

At the time, we skipped over some details in this code. In “The match Control Flow Construct” section in Chapter 6, we discussed that match arms must all return the same type. So, for example, the following code doesn’t work:

fn main() {
    let guess = "3";
    let guess = match guess.trim().parse() {
        Ok(_) => 5,
        Err(_) => "hello",
    };
}

The type of guess in this code would have to be an integer and a string, and Rust requires that guess have only one type. So, what does continue return? How were we allowed to return a u32 from one arm and have another arm that ends with continue in Listing 20-27?

As you might have guessed, continue has a ! value. That is, when Rust computes the type of guess, it looks at both match arms, the former with a value of u32 and the latter with a ! value. Because ! can never have a value, Rust decides that the type of guess is u32.

The formal way of describing this behavior is that expressions of type ! can be coerced into any other type. We’re allowed to end this match arm with continue because continue doesn’t return a value; instead, it moves control back to the top of the loop, so in the Err case, we never assign a value to guess.

The never type is useful with the panic! macro as well. Recall the unwrap function that we call on Option<T> values to produce a value or panic with this definition:

enum Option<T> {
    Some(T),
    None,
}

use crate::Option::*;

impl<T> Option<T> {
    pub fn unwrap(self) -> T {
        match self {
            Some(val) => val,
            None => panic!("called `Option::unwrap()` on a `None` value"),
        }
    }
}

In this code, the same thing happens as in the match in Listing 20-27: Rust sees that val has the type T and panic! has the type !, so the result of the overall match expression is T. This code works because panic! doesn’t produce a value; it ends the program. In the None case, we won’t be returning a value from unwrap, so this code is valid.

One final expression that has the type ! is a loop:

fn main() {
    print!("forever ");

    loop {
        print!("and ever ");
    }
}

Here, the loop never ends, so ! is the value of the expression. However, this wouldn’t be true if we included a break, because the loop would terminate when it got to the break.

Dynamically Sized Types and the Sized Trait

Rust needs to know certain details about its types, such as how much space to allocate for a value of a particular type. This leaves one corner of its type system a little confusing at first: the concept of dynamically sized types. Sometimes referred to as DSTs or unsized types, these types let us write code using values whose size we can know only at runtime.

Let’s dig into the details of a dynamically sized type called str, which we’ve been using throughout the book. That’s right, not &str, but str on its own, is a DST. In many cases, such as when storing text entered by a user, we can’t know how long the string is until runtime. That means we can’t create a variable of type str, nor can we take an argument of type str. Consider the following code, which does not work:

fn main() {
    let s1: str = "Hello there!";
    let s2: str = "How's it going?";
}

Rust needs to know how much memory to allocate for any value of a particular type, and all values of a type must use the same amount of memory. If Rust allowed us to write this code, these two str values would need to take up the same amount of space. But they have different lengths: s1 needs 12 bytes of storage and s2 needs 15. This is why it’s not possible to create a variable holding a dynamically sized type.

So, what do we do? In this case, you already know the answer: We make the type of s1 and s2 string slice (&str) rather than str. Recall from the “String Slices” section in Chapter 4 that the slice data structure only stores the starting position and the length of the slice. So, although &T is a single value that stores the memory address of where the T is located, a string slice is two values: the address of the str and its length. As such, we can know the size of a string slice value at compile time: It’s twice the length of a usize. That is, we always know the size of a string slice, no matter how long the string it refers to is. In general, this is the way in which dynamically sized types are used in Rust: They have an extra bit of metadata that stores the size of the dynamic information. The golden rule of dynamically sized types is that we must always put values of dynamically sized types behind a pointer of some kind.

We can combine str with all kinds of pointers: for example, Box<str> or Rc<str>. In fact, you’ve seen this before but with a different dynamically sized type: traits. Every trait is a dynamically sized type we can refer to by using the name of the trait. In the “Using Trait Objects to Abstract over Shared Behavior” section in Chapter 18, we mentioned that to use traits as trait objects, we must put them behind a pointer, such as &dyn Trait or Box<dyn Trait> (Rc<dyn Trait> would work too).

To work with DSTs, Rust provides the Sized trait to determine whether or not a type’s size is known at compile time. This trait is automatically implemented for everything whose size is known at compile time. In addition, Rust implicitly adds a bound on Sized to every generic function. That is, a generic function definition like this:

fn generic<T>(t: T) {
    // --snip--
}

is actually treated as though we had written this:

fn generic<T: Sized>(t: T) {
    // --snip--
}

By default, generic functions will work only on types that have a known size at compile time. However, you can use the following special syntax to relax this restriction:

fn generic<T: ?Sized>(t: &T) {
    // --snip--
}

A trait bound on ?Sized means “T may or may not be Sized,” and this notation overrides the default that generic types must have a known size at compile time. The ?Trait syntax with this meaning is only available for Sized, not any other traits.

Also note that we switched the type of the t parameter from T to &T. Because the type might not be Sized, we need to use it behind some kind of pointer. In this case, we’ve chosen a reference.

Next, we’ll talk about functions and closures!

Продвинутые функции и замыкания

Advanced Functions and Closures

This section explores some advanced features related to functions and closures, including function pointers and returning closures.

Function Pointers

We’ve talked about how to pass closures to functions; you can also pass regular functions to functions! This technique is useful when you want to pass a function you’ve already defined rather than defining a new closure. Functions coerce to the type fn (with a lowercase f), not to be confused with the Fn closure trait. The fn type is called a function pointer. Passing functions with function pointers will allow you to use functions as arguments to other functions.

The syntax for specifying that a parameter is a function pointer is similar to that of closures, as shown in Listing 20-28, where we’ve defined a function add_one that adds 1 to its parameter. The function do_twice takes two parameters: a function pointer to any function that takes an i32 parameter and returns an i32, and one i32 value. The do_twice function calls the function f twice, passing it the arg value, then adds the two function call results together. The main function calls do_twice with the arguments add_one and 5.

Filename: src/main.rs
fn add_one(x: i32) -> i32 {
    x + 1
}

fn do_twice(f: fn(i32) -> i32, arg: i32) -> i32 {
    f(arg) + f(arg)
}

fn main() {
    let answer = do_twice(add_one, 5);

    println!("The answer is: {answer}");
}
Listing 20-28: Using the fn type to accept a function pointer as an argument

This code prints The answer is: 12. We specify that the parameter f in do_twice is an fn that takes one parameter of type i32 and returns an i32. We can then call f in the body of do_twice. In main, we can pass the function name add_one as the first argument to do_twice.

Unlike closures, fn is a type rather than a trait, so we specify fn as the parameter type directly rather than declaring a generic type parameter with one of the Fn traits as a trait bound.

Function pointers implement all three of the closure traits (Fn, FnMut, and FnOnce), meaning you can always pass a function pointer as an argument for a function that expects a closure. It’s best to write functions using a generic type and one of the closure traits so that your functions can accept either functions or closures.

That said, one example of where you would want to only accept fn and not closures is when interfacing with external code that doesn’t have closures: C functions can accept functions as arguments, but C doesn’t have closures.

As an example of where you could use either a closure defined inline or a named function, let’s look at a use of the map method provided by the Iterator trait in the standard library. To use the map method to turn a vector of numbers into a vector of strings, we could use a closure, as in Listing 20-29.

fn main() {
    let list_of_numbers = vec![1, 2, 3];
    let list_of_strings: Vec<String> =
        list_of_numbers.iter().map(|i| i.to_string()).collect();
}
Listing 20-29: Using a closure with the map method to convert numbers to strings

Or we could name a function as the argument to map instead of the closure. Listing 20-30 shows what this would look like.

fn main() {
    let list_of_numbers = vec![1, 2, 3];
    let list_of_strings: Vec<String> =
        list_of_numbers.iter().map(ToString::to_string).collect();
}
Listing 20-30: Using the String::to_string function with the map method to convert numbers to strings

Note that we must use the fully qualified syntax that we talked about in the “Advanced Traits” section because there are multiple functions available named to_string.

Here, we’re using the to_string function defined in the ToString trait, which the standard library has implemented for any type that implements Display.

Recall from the “Enum Values” section in Chapter 6 that the name of each enum variant that we define also becomes an initializer function. We can use these initializer functions as function pointers that implement the closure traits, which means we can specify the initializer functions as arguments for methods that take closures, as seen in Listing 20-31.

fn main() {
    enum Status {
        Value(u32),
        Stop,
    }

    let list_of_statuses: Vec<Status> = (0u32..20).map(Status::Value).collect();
}
Listing 20-31: Using an enum initializer with the map method to create a Status instance from numbers

Here, we create Status::Value instances using each u32 value in the range that map is called on by using the initializer function of Status::Value. Some people prefer this style and some people prefer to use closures. They compile to the same code, so use whichever style is clearer to you.

Returning Closures

Closures are represented by traits, which means you can’t return closures directly. In most cases where you might want to return a trait, you can instead use the concrete type that implements the trait as the return value of the function. However, you can’t usually do that with closures because they don’t have a concrete type that is returnable; you’re not allowed to use the function pointer fn as a return type if the closure captures any values from its scope, for example.

Instead, you will normally use the impl Trait syntax we learned about in Chapter 10. You can return any function type, using Fn, FnOnce, and FnMut. For example, the code in Listing 20-32 will compile just fine.

#![allow(unused)]
fn main() {
fn returns_closure() -> impl Fn(i32) -> i32 {
    |x| x + 1
}
}
Listing 20-32: Returning a closure from a function using the impl Trait syntax

However, as we noted in the “Inferring and Annotating Closure Types” section in Chapter 13, each closure is also its own distinct type. If you need to work with multiple functions that have the same signature but different implementations, you will need to use a trait object for them. Consider what happens if you write code like that shown in Listing 20-33.

Filename: src/main.rs
fn main() {
    let handlers = vec![returns_closure(), returns_initialized_closure(123)];
    for handler in handlers {
        let output = handler(5);
        println!("{output}");
    }
}

fn returns_closure() -> impl Fn(i32) -> i32 {
    |x| x + 1
}

fn returns_initialized_closure(init: i32) -> impl Fn(i32) -> i32 {
    move |x| x + init
}
Listing 20-33: Creating a Vec<T> of closures defined by functions that return impl Fn types

Here we have two functions, returns_closure and returns_initialized_closure, which both return impl Fn(i32) -> i32. Notice that the closures that they return are different, even though they implement the same type. If we try to compile this, Rust lets us know that it won’t work:

$ cargo build
   Compiling functions-example v0.1.0 (file:///projects/functions-example)
error[E0308]: mismatched types
  --> src/main.rs:2:44
   |
 2 |     let handlers = vec![returns_closure(), returns_initialized_closure(123)];
   |                                            ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ expected opaque type, found a different opaque type
...
 9 | fn returns_closure() -> impl Fn(i32) -> i32 {
   |                         ------------------- the expected opaque type
...
13 | fn returns_initialized_closure(init: i32) -> impl Fn(i32) -> i32 {
   |                                              ------------------- the found opaque type
   |
   = note: expected opaque type `impl Fn(i32) -> i32`
              found opaque type `impl Fn(i32) -> i32`
   = note: distinct uses of `impl Trait` result in different opaque types

For more information about this error, try `rustc --explain E0308`.
error: could not compile `functions-example` (bin "functions-example") due to 1 previous error

The error message tells us that whenever we return an impl Trait, Rust creates a unique opaque type, a type where we cannot see into the details of what Rust constructs for us, nor can we guess the type Rust will generate to write ourselves. So, even though these functions return closures that implement the same trait, Fn(i32) -> i32, the opaque types Rust generates for each are distinct. (This is similar to how Rust produces different concrete types for distinct async blocks even when they have the same output type, as we saw in “The Pin Type and the Unpin Trait” in Chapter 17.) We have seen a solution to this problem a few times now: We can use a trait object, as in Listing 20-34.

fn main() {
    let handlers = vec![returns_closure(), returns_initialized_closure(123)];
    for handler in handlers {
        let output = handler(5);
        println!("{output}");
    }
}

fn returns_closure() -> Box<dyn Fn(i32) -> i32> {
    Box::new(|x| x + 1)
}

fn returns_initialized_closure(init: i32) -> Box<dyn Fn(i32) -> i32> {
    Box::new(move |x| x + init)
}
Listing 20-34: Creating a Vec<T> of closures defined by functions that return Box<dyn Fn> so that they have the same type

This code will compile just fine. For more about trait objects, refer to the section “Using Trait Objects To Abstract over Shared Behavior” in Chapter 18.

Next, let’s look at macros!

Макросы

Macros

We’ve used macros like println! throughout this book, but we haven’t fully explored what a macro is and how it works. The term macro refers to a family of features in Rust—declarative macros with macro_rules! and three kinds of procedural macros:

  • Custom #[derive] macros that specify code added with the derive attribute used on structs and enums
  • Attribute-like macros that define custom attributes usable on any item
  • Function-like macros that look like function calls but operate on the tokens specified as their argument

We’ll talk about each of these in turn, but first, let’s look at why we even need macros when we already have functions.

The Difference Between Macros and Functions

Fundamentally, macros are a way of writing code that writes other code, which is known as metaprogramming. In Appendix C, we discuss the derive attribute, which generates an implementation of various traits for you. We’ve also used the println! and vec! macros throughout the book. All of these macros expand to produce more code than the code you’ve written manually.

Metaprogramming is useful for reducing the amount of code you have to write and maintain, which is also one of the roles of functions. However, macros have some additional powers that functions don’t have.

A function signature must declare the number and type of parameters the function has. Macros, on the other hand, can take a variable number of parameters: We can call println!("hello") with one argument or println!("hello {}", name) with two arguments. Also, macros are expanded before the compiler interprets the meaning of the code, so a macro can, for example, implement a trait on a given type. A function can’t, because it gets called at runtime and a trait needs to be implemented at compile time.

The downside to implementing a macro instead of a function is that macro definitions are more complex than function definitions because you’re writing Rust code that writes Rust code. Due to this indirection, macro definitions are generally more difficult to read, understand, and maintain than function definitions.

Another important difference between macros and functions is that you must define macros or bring them into scope before you call them in a file, as opposed to functions you can define anywhere and call anywhere.

Declarative Macros for General Metaprogramming

The most widely used form of macros in Rust is the declarative macro. These are also sometimes referred to as “macros by example,” “macro_rules! macros,” or just plain “macros.” At their core, declarative macros allow you to write something similar to a Rust match expression. As discussed in Chapter 6, match expressions are control structures that take an expression, compare the resultant value of the expression to patterns, and then run the code associated with the matching pattern. Macros also compare a value to patterns that are associated with particular code: In this situation, the value is the literal Rust source code passed to the macro; the patterns are compared with the structure of that source code; and the code associated with each pattern, when matched, replaces the code passed to the macro. This all happens during compilation.

To define a macro, you use the macro_rules! construct. Let’s explore how to use macro_rules! by looking at how the vec! macro is defined. Chapter 8 covered how we can use the vec! macro to create a new vector with particular values. For example, the following macro creates a new vector containing three integers:

#![allow(unused)]
fn main() {
let v: Vec<u32> = vec![1, 2, 3];
}

We could also use the vec! macro to make a vector of two integers or a vector of five string slices. We wouldn’t be able to use a function to do the same because we wouldn’t know the number or type of values up front.

Listing 20-35 shows a slightly simplified definition of the vec! macro.

Filename: src/lib.rs
#[macro_export]
macro_rules! vec {
    ( $( $x:expr ),* ) => {
        {
            let mut temp_vec = Vec::new();
            $(
                temp_vec.push($x);
            )*
            temp_vec
        }
    };
}
Listing 20-35: A simplified version of the vec! macro definition

Note: The actual definition of the vec! macro in the standard library includes code to pre-allocate the correct amount of memory up front. That code is an optimization that we don’t include here, to make the example simpler.

The #[macro_export] annotation indicates that this macro should be made available whenever the crate in which the macro is defined is brought into scope. Without this annotation, the macro can’t be brought into scope.

We then start the macro definition with macro_rules! and the name of the macro we’re defining without the exclamation mark. The name, in this case vec, is followed by curly brackets denoting the body of the macro definition.

The structure in the vec! body is similar to the structure of a match expression. Here we have one arm with the pattern ( $( $x:expr ),* ), followed by => and the block of code associated with this pattern. If the pattern matches, the associated block of code will be emitted. Given that this is the only pattern in this macro, there is only one valid way to match; any other pattern will result in an error. More complex macros will have more than one arm.

Valid pattern syntax in macro definitions is different from the pattern syntax covered in Chapter 19 because macro patterns are matched against Rust code structure rather than values. Let’s walk through what the pattern pieces in Listing 20-29 mean; for the full macro pattern syntax, see the Rust Reference.

First, we use a set of parentheses to encompass the whole pattern. We use a dollar sign ($) to declare a variable in the macro system that will contain the Rust code matching the pattern. The dollar sign makes it clear this is a macro variable as opposed to a regular Rust variable. Next comes a set of parentheses that captures values that match the pattern within the parentheses for use in the replacement code. Within $() is $x:expr, which matches any Rust expression and gives the expression the name $x.

The comma following $() indicates that a literal comma separator character must appear between each instance of the code that matches the code in $(). The * specifies that the pattern matches zero or more of whatever precedes the *.

When we call this macro with vec![1, 2, 3];, the $x pattern matches three times with the three expressions 1, 2, and 3.

Now let’s look at the pattern in the body of the code associated with this arm: temp_vec.push() within $()* is generated for each part that matches $() in the pattern zero or more times depending on how many times the pattern matches. The $x is replaced with each expression matched. When we call this macro with vec![1, 2, 3];, the code generated that replaces this macro call will be the following:

{
    let mut temp_vec = Vec::new();
    temp_vec.push(1);
    temp_vec.push(2);
    temp_vec.push(3);
    temp_vec
}

We’ve defined a macro that can take any number of arguments of any type and can generate code to create a vector containing the specified elements.

To learn more about how to write macros, consult the online documentation or other resources, such as “The Little Book of Rust Macros” started by Daniel Keep and continued by Lukas Wirth.

Procedural Macros for Generating Code from Attributes

The second form of macros is the procedural macro, which acts more like a function (and is a type of procedure). Procedural macros accept some code as an input, operate on that code, and produce some code as an output rather than matching against patterns and replacing the code with other code as declarative macros do. The three kinds of procedural macros are custom derive, attribute-like, and function-like, and all work in a similar fashion.

When creating procedural macros, the definitions must reside in their own crate with a special crate type. This is for complex technical reasons that we hope to eliminate in the future. In Listing 20-36, we show how to define a procedural macro, where some_attribute is a placeholder for using a specific macro variety.

Filename: src/lib.rs
use proc_macro::TokenStream;

#[some_attribute]
pub fn some_name(input: TokenStream) -> TokenStream {
}
Listing 20-36: An example of defining a procedural macro

The function that defines a procedural macro takes a TokenStream as an input and produces a TokenStream as an output. The TokenStream type is defined by the proc_macro crate that is included with Rust and represents a sequence of tokens. This is the core of the macro: The source code that the macro is operating on makes up the input TokenStream, and the code the macro produces is the output TokenStream. The function also has an attribute attached to it that specifies which kind of procedural macro we’re creating. We can have multiple kinds of procedural macros in the same crate.

Let’s look at the different kinds of procedural macros. We’ll start with a custom derive macro and then explain the small dissimilarities that make the other forms different.

Custom derive Macros

Let’s create a crate named hello_macro that defines a trait named HelloMacro with one associated function named hello_macro. Rather than making our users implement the HelloMacro trait for each of their types, we’ll provide a procedural macro so that users can annotate their type with #[derive(HelloMacro)] to get a default implementation of the hello_macro function. The default implementation will print Hello, Macro! My name is TypeName! where TypeName is the name of the type on which this trait has been defined. In other words, we’ll write a crate that enables another programmer to write code like Listing 20-37 using our crate.

Filename: src/main.rs
use hello_macro::HelloMacro;
use hello_macro_derive::HelloMacro;

#[derive(HelloMacro)]
struct Pancakes;

fn main() {
    Pancakes::hello_macro();
}
Listing 20-37: The code a user of our crate will be able to write when using our procedural macro

This code will print Hello, Macro! My name is Pancakes! when we’re done. The first step is to make a new library crate, like this:

$ cargo new hello_macro --lib

Next, in Listing 20-38, we’ll define the HelloMacro trait and its associated function.

Filename: src/lib.rs
pub trait HelloMacro {
    fn hello_macro();
}
Listing 20-38: A simple trait that we will use with the derive macro

We have a trait and its function. At this point, our crate user could implement the trait to achieve the desired functionality, as in Listing 20-39.

Filename: src/main.rs
use hello_macro::HelloMacro;

struct Pancakes;

impl HelloMacro for Pancakes {
    fn hello_macro() {
        println!("Hello, Macro! My name is Pancakes!");
    }
}

fn main() {
    Pancakes::hello_macro();
}
Listing 20-39: How it would look if users wrote a manual implementation of the HelloMacro trait

However, they would need to write the implementation block for each type they wanted to use with hello_macro; we want to spare them from having to do this work.

Additionally, we can’t yet provide the hello_macro function with default implementation that will print the name of the type the trait is implemented on: Rust doesn’t have reflection capabilities, so it can’t look up the type’s name at runtime. We need a macro to generate code at compile time.

The next step is to define the procedural macro. At the time of this writing, procedural macros need to be in their own crate. Eventually, this restriction might be lifted. The convention for structuring crates and macro crates is as follows: For a crate named foo, a custom derive procedural macro crate is called foo_derive. Let’s start a new crate called hello_macro_derive inside our hello_macro project:

$ cargo new hello_macro_derive --lib

Our two crates are tightly related, so we create the procedural macro crate within the directory of our hello_macro crate. If we change the trait definition in hello_macro, we’ll have to change the implementation of the procedural macro in hello_macro_derive as well. The two crates will need to be published separately, and programmers using these crates will need to add both as dependencies and bring them both into scope. We could instead have the hello_macro crate use hello_macro_derive as a dependency and re-export the procedural macro code. However, the way we’ve structured the project makes it possible for programmers to use hello_macro even if they don’t want the derive functionality.

We need to declare the hello_macro_derive crate as a procedural macro crate. We’ll also need functionality from the syn and quote crates, as you’ll see in a moment, so we need to add them as dependencies. Add the following to the Cargo.toml file for hello_macro_derive:

Filename: hello_macro_derive/Cargo.toml
[lib]
proc-macro = true

[dependencies]
syn = "2.0"
quote = "1.0"

To start defining the procedural macro, place the code in Listing 20-40 into your src/lib.rs file for the hello_macro_derive crate. Note that this code won’t compile until we add a definition for the impl_hello_macro function.

Filename: hello_macro_derive/src/lib.rs
use proc_macro::TokenStream;
use quote::quote;

#[proc_macro_derive(HelloMacro)]
pub fn hello_macro_derive(input: TokenStream) -> TokenStream {
    // Construct a representation of Rust code as a syntax tree
    // that we can manipulate.
    let ast = syn::parse(input).unwrap();

    // Build the trait implementation.
    impl_hello_macro(&ast)
}
Listing 20-40: Code that most procedural macro crates will require in order to process Rust code

Notice that we’ve split the code into the hello_macro_derive function, which is responsible for parsing the TokenStream, and the impl_hello_macro function, which is responsible for transforming the syntax tree: This makes writing a procedural macro more convenient. The code in the outer function (hello_macro_derive in this case) will be the same for almost every procedural macro crate you see or create. The code you specify in the body of the inner function (impl_hello_macro in this case) will be different depending on your procedural macro’s purpose.

We’ve introduced three new crates: proc_macro, syn, and quote. The proc_macro crate comes with Rust, so we didn’t need to add that to the dependencies in Cargo.toml. The proc_macro crate is the compiler’s API that allows us to read and manipulate Rust code from our code.

The syn crate parses Rust code from a string into a data structure that we can perform operations on. The quote crate turns syn data structures back into Rust code. These crates make it much simpler to parse any sort of Rust code we might want to handle: Writing a full parser for Rust code is no simple task.

The hello_macro_derive function will be called when a user of our library specifies #[derive(HelloMacro)] on a type. This is possible because we’ve annotated the hello_macro_derive function here with proc_macro_derive and specified the name HelloMacro, which matches our trait name; this is the convention most procedural macros follow.

The hello_macro_derive function first converts the input from a TokenStream to a data structure that we can then interpret and perform operations on. This is where syn comes into play. The parse function in syn takes a TokenStream and returns a DeriveInput struct representing the parsed Rust code. Listing 20-41 shows the relevant parts of the DeriveInput struct we get from parsing the struct Pancakes; string.

DeriveInput {
    // --snip--

    ident: Ident {
        ident: "Pancakes",
        span: #0 bytes(95..103)
    },
    data: Struct(
        DataStruct {
            struct_token: Struct,
            fields: Unit,
            semi_token: Some(
                Semi
            )
        }
    )
}
Listing 20-41: The DeriveInput instance we get when parsing the code that has the macro’s attribute in Listing 20-37

The fields of this struct show that the Rust code we’ve parsed is a unit struct with the ident (identifier, meaning the name) of Pancakes. There are more fields on this struct for describing all sorts of Rust code; check the syn documentation for DeriveInput for more information.

Soon we’ll define the impl_hello_macro function, which is where we’ll build the new Rust code we want to include. But before we do, note that the output for our derive macro is also a TokenStream. The returned TokenStream is added to the code that our crate users write, so when they compile their crate, they’ll get the extra functionality that we provide in the modified TokenStream.

You might have noticed that we’re calling unwrap to cause the hello_macro_derive function to panic if the call to the syn::parse function fails here. It’s necessary for our procedural macro to panic on errors because proc_macro_derive functions must return TokenStream rather than Result to conform to the procedural macro API. We’ve simplified this example by using unwrap; in production code, you should provide more specific error messages about what went wrong by using panic! or expect.

Now that we have the code to turn the annotated Rust code from a TokenStream into a DeriveInput instance, let’s generate the code that implements the HelloMacro trait on the annotated type, as shown in Listing 20-42.

Filename: hello_macro_derive/src/lib.rs
use proc_macro::TokenStream;
use quote::quote;

#[proc_macro_derive(HelloMacro)]
pub fn hello_macro_derive(input: TokenStream) -> TokenStream {
    // Construct a representation of Rust code as a syntax tree
    // that we can manipulate
    let ast = syn::parse(input).unwrap();

    // Build the trait implementation
    impl_hello_macro(&ast)
}

fn impl_hello_macro(ast: &syn::DeriveInput) -> TokenStream {
    let name = &ast.ident;
    let generated = quote! {
        impl HelloMacro for #name {
            fn hello_macro() {
                println!("Hello, Macro! My name is {}!", stringify!(#name));
            }
        }
    };
    generated.into()
}
Listing 20-42: Implementing the HelloMacro trait using the parsed Rust code

We get an Ident struct instance containing the name (identifier) of the annotated type using ast.ident. The struct in Listing 20-41 shows that when we run the impl_hello_macro function on the code in Listing 20-37, the ident we get will have the ident field with a value of "Pancakes". Thus, the name variable in Listing 20-42 will contain an Ident struct instance that, when printed, will be the string "Pancakes", the name of the struct in Listing 20-37.

The quote! macro lets us define the Rust code that we want to return. The compiler expects something different from the direct result of the quote! macro’s execution, so we need to convert it to a TokenStream. We do this by calling the into method, which consumes this intermediate representation and returns a value of the required TokenStream type.

The quote! macro also provides some very cool templating mechanics: We can enter #name, and quote! will replace it with the value in the variable name. You can even do some repetition similar to the way regular macros work. Check out the quote crate’s docs for a thorough introduction.

We want our procedural macro to generate an implementation of our HelloMacro trait for the type the user annotated, which we can get by using #name. The trait implementation has the one function hello_macro, whose body contains the functionality we want to provide: printing Hello, Macro! My name is and then the name of the annotated type.

The stringify! macro used here is built into Rust. It takes a Rust expression, such as 1 + 2, and at compile time turns the expression into a string literal, such as "1 + 2". This is different from format! or println!, which are macros that evaluate the expression and then turn the result into a String. There is a possibility that the #name input might be an expression to print literally, so we use stringify!. Using stringify! also saves an allocation by converting #name to a string literal at compile time.

At this point, cargo build should complete successfully in both hello_macro and hello_macro_derive. Let’s hook up these crates to the code in Listing 20-37 to see the procedural macro in action! Create a new binary project in your projects directory using cargo new pancakes. We need to add hello_macro and hello_macro_derive as dependencies in the pancakes crate’s Cargo.toml. If you’re publishing your versions of hello_macro and hello_macro_derive to crates.io, they would be regular dependencies; if not, you can specify them as path dependencies as follows:

[dependencies]
hello_macro = { path = "../hello_macro" }
hello_macro_derive = { path = "../hello_macro/hello_macro_derive" }

Put the code in Listing 20-37 into src/main.rs, and run cargo run: It should print Hello, Macro! My name is Pancakes!. The implementation of the HelloMacro trait from the procedural macro was included without the pancakes crate needing to implement it; the #[derive(HelloMacro)] added the trait implementation.

Next, let’s explore how the other kinds of procedural macros differ from custom derive macros.

Attribute-Like Macros

Attribute-like macros are similar to custom derive macros, but instead of generating code for the derive attribute, they allow you to create new attributes. They’re also more flexible: derive only works for structs and enums; attributes can be applied to other items as well, such as functions. Here’s an example of using an attribute-like macro. Say you have an attribute named route that annotates functions when using a web application framework:

#[route(GET, "/")]
fn index() {

This #[route] attribute would be defined by the framework as a procedural macro. The signature of the macro definition function would look like this:

#[proc_macro_attribute]
pub fn route(attr: TokenStream, item: TokenStream) -> TokenStream {

Here, we have two parameters of type TokenStream. The first is for the contents of the attribute: the GET, "/" part. The second is the body of the item the attribute is attached to: in this case, fn index() {} and the rest of the function’s body.

Other than that, attribute-like macros work the same way as custom derive macros: You create a crate with the proc-macro crate type and implement a function that generates the code you want!

Function-Like Macros

Function-like macros define macros that look like function calls. Similarly to macro_rules! macros, they’re more flexible than functions; for example, they can take an unknown number of arguments. However, macro_rules! macros can only be defined using the match-like syntax we discussed in the “Declarative Macros for General Metaprogramming” section earlier. Function-like macros take a TokenStream parameter, and their definition manipulates that TokenStream using Rust code as the other two types of procedural macros do. An example of a function-like macro is an sql! macro that might be called like so:

let sql = sql!(SELECT * FROM posts WHERE id=1);

This macro would parse the SQL statement inside it and check that it’s syntactically correct, which is much more complex processing than a macro_rules! macro can do. The sql! macro would be defined like this:

#[proc_macro]
pub fn sql(input: TokenStream) -> TokenStream {

This definition is similar to the custom derive macro’s signature: We receive the tokens that are inside the parentheses and return the code we wanted to generate.

Summary

Whew! Now you have some Rust features in your toolbox that you likely won’t use often, but you’ll know they’re available in very particular circumstances. We’ve introduced several complex topics so that when you encounter them in error message suggestions or in other people’s code, you’ll be able to recognize these concepts and syntax. Use this chapter as a reference to guide you to solutions.

Next, we’ll put everything we’ve discussed throughout the book into practice and do one more project!

Final Project: Building a Multithreaded Web Server

It’s been a long journey, but we’ve reached the end of the book. In this chapter, we’ll build one more project together to demonstrate some of the concepts we covered in the final chapters, as well as recap some earlier lessons.

For our final project, we’ll make a web server that says “Hello!” and looks like Figure 21-1 in a web browser.

Here is our plan for building the web server:

  1. Learn a bit about TCP and HTTP.
  2. Listen for TCP connections on a socket.
  3. Parse a small number of HTTP requests.
  4. Create a proper HTTP response.
  5. Improve the throughput of our server with a thread pool.
Screenshot of a web browser visiting the address 127.0.0.1:8080 displaying a webpage with the text content “Hello! Hi from Rust”

Figure 21-1: Our final shared project

Before we get started, we should mention two details. First, the method we’ll use won’t be the best way to build a web server with Rust. Community members have published a number of production-ready crates available at crates.io that provide more complete web server and thread pool implementations than we’ll build. However, our intention in this chapter is to help you learn, not to take the easy route. Because Rust is a systems programming language, we can choose the level of abstraction we want to work with and can go to a lower level than is possible or practical in other languages.

Second, we will not be using async and await here. Building a thread pool is a big enough challenge on its own, without adding in building an async runtime! However, we will note how async and await might be applicable to some of the same problems we will see in this chapter. Ultimately, as we noted back in Chapter 17, many async runtimes use thread pools for managing their work.

We’ll therefore write the basic HTTP server and thread pool manually so that you can learn the general ideas and techniques behind the crates you might use in the future.

Создание однопоточного веб-сервера

Building a Single-Threaded Web Server

We’ll start by getting a single-threaded web server working. Before we begin, let’s look at a quick overview of the protocols involved in building web servers. The details of these protocols are beyond the scope of this book, but a brief overview will give you the information you need.

The two main protocols involved in web servers are Hypertext Transfer Protocol (HTTP) and Transmission Control Protocol (TCP). Both protocols are request-response protocols, meaning a client initiates requests and a server listens to the requests and provides a response to the client. The contents of those requests and responses are defined by the protocols.

TCP is the lower-level protocol that describes the details of how information gets from one server to another but doesn’t specify what that information is. HTTP builds on top of TCP by defining the contents of the requests and responses. It’s technically possible to use HTTP with other protocols, but in the vast majority of cases, HTTP sends its data over TCP. We’ll work with the raw bytes of TCP and HTTP requests and responses.

Listening to the TCP Connection

Our web server needs to listen to a TCP connection, so that’s the first part we’ll work on. The standard library offers a std::net module that lets us do this. Let’s make a new project in the usual fashion:

$ cargo new hello
     Created binary (application) `hello` project
$ cd hello

Now enter the code in Listing 21-1 in src/main.rs to start. This code will listen at the local address 127.0.0.1:7878 for incoming TCP streams. When it gets an incoming stream, it will print Connection established!.

Filename: src/main.rs
use std::net::TcpListener;

fn main() {
    let listener = TcpListener::bind("127.0.0.1:7878").unwrap();

    for stream in listener.incoming() {
        let stream = stream.unwrap();

        println!("Connection established!");
    }
}
Listing 21-1: Listening for incoming streams and printing a message when we receive a stream

Using TcpListener, we can listen for TCP connections at the address 127.0.0.1:7878. In the address, the section before the colon is an IP address representing your computer (this is the same on every computer and doesn’t represent the authors’ computer specifically), and 7878 is the port. We’ve chosen this port for two reasons: HTTP isn’t normally accepted on this port, so our server is unlikely to conflict with any other web server you might have running on your machine, and 7878 is rust typed on a telephone.

The bind function in this scenario works like the new function in that it will return a new TcpListener instance. The function is called bind because, in networking, connecting to a port to listen to is known as “binding to a port.”

The bind function returns a Result<T, E>, which indicates that it’s possible for binding to fail, for example, if we ran two instances of our program and so had two programs listening to the same port. Because we’re writing a basic server just for learning purposes, we won’t worry about handling these kinds of errors; instead, we use unwrap to stop the program if errors happen.

The incoming method on TcpListener returns an iterator that gives us a sequence of streams (more specifically, streams of type TcpStream). A single stream represents an open connection between the client and the server. Connection is the name for the full request and response process in which a client connects to the server, the server generates a response, and the server closes the connection. As such, we will read from the TcpStream to see what the client sent and then write our response to the stream to send data back to the client. Overall, this for loop will process each connection in turn and produce a series of streams for us to handle.

For now, our handling of the stream consists of calling unwrap to terminate our program if the stream has any errors; if there aren’t any errors, the program prints a message. We’ll add more functionality for the success case in the next listing. The reason we might receive errors from the incoming method when a client connects to the server is that we’re not actually iterating over connections. Instead, we’re iterating over connection attempts. The connection might not be successful for a number of reasons, many of them operating system specific. For example, many operating systems have a limit to the number of simultaneous open connections they can support; new connection attempts beyond that number will produce an error until some of the open connections are closed.

Let’s try running this code! Invoke cargo run in the terminal and then load 127.0.0.1:7878 in a web browser. The browser should show an error message like “Connection reset” because the server isn’t currently sending back any data. But when you look at your terminal, you should see several messages that were printed when the browser connected to the server!

     Running `target/debug/hello`
Connection established!
Connection established!
Connection established!

Sometimes you’ll see multiple messages printed for one browser request; the reason might be that the browser is making a request for the page as well as a request for other resources, like the favicon.ico icon that appears in the browser tab.

It could also be that the browser is trying to connect to the server multiple times because the server isn’t responding with any data. When stream goes out of scope and is dropped at the end of the loop, the connection is closed as part of the drop implementation. Browsers sometimes deal with closed connections by retrying, because the problem might be temporary.

Browsers also sometimes open multiple connections to the server without sending any requests so that if they do later send requests, those requests can happen more quickly. When this occurs, our server will see each connection, regardless of whether there are any requests over that connection. Many versions of Chrome-based browsers do this, for example; you can disable that optimization by using private browsing mode or using a different browser.

The important factor is that we’ve successfully gotten a handle to a TCP connection!

Remember to stop the program by pressing ctrl-C when you’re done running a particular version of the code. Then, restart the program by invoking the cargo run command after you’ve made each set of code changes to make sure you’re running the newest code.

Reading the Request

Let’s implement the functionality to read the request from the browser! To separate the concerns of first getting a connection and then taking some action with the connection, we’ll start a new function for processing connections. In this new handle_connection function, we’ll read data from the TCP stream and print it so that we can see the data being sent from the browser. Change the code to look like Listing 21-2.

Filename: src/main.rs
use std::{
    io::{BufReader, prelude::*},
    net::{TcpListener, TcpStream},
};

fn main() {
    let listener = TcpListener::bind("127.0.0.1:7878").unwrap();

    for stream in listener.incoming() {
        let stream = stream.unwrap();

        handle_connection(stream);
    }
}

fn handle_connection(mut stream: TcpStream) {
    let buf_reader = BufReader::new(&stream);
    let http_request: Vec<_> = buf_reader
        .lines()
        .map(|result| result.unwrap())
        .take_while(|line| !line.is_empty())
        .collect();

    println!("Request: {http_request:#?}");
}
Listing 21-2: Reading from the TcpStream and printing the data

We bring std::io::BufReader and std::io::prelude into scope to get access to traits and types that let us read from and write to the stream. In the for loop in the main function, instead of printing a message that says we made a connection, we now call the new handle_connection function and pass the stream to it.

In the handle_connection function, we create a new BufReader instance that wraps a reference to the stream. The BufReader adds buffering by managing calls to the std::io::Read trait methods for us.

We create a variable named http_request to collect the lines of the request the browser sends to our server. We indicate that we want to collect these lines in a vector by adding the Vec<_> type annotation.

BufReader implements the std::io::BufRead trait, which provides the lines method. The lines method returns an iterator of Result<String, std::io::Error> by splitting the stream of data whenever it sees a newline byte. To get each String, we map and unwrap each Result. The Result might be an error if the data isn’t valid UTF-8 or if there was a problem reading from the stream. Again, a production program should handle these errors more gracefully, but we’re choosing to stop the program in the error case for simplicity.

The browser signals the end of an HTTP request by sending two newline characters in a row, so to get one request from the stream, we take lines until we get a line that is the empty string. Once we’ve collected the lines into the vector, we’re printing them out using pretty debug formatting so that we can take a look at the instructions the web browser is sending to our server.

Let’s try this code! Start the program and make a request in a web browser again. Note that we’ll still get an error page in the browser, but our program’s output in the terminal will now look similar to this:

$ cargo run
   Compiling hello v0.1.0 (file:///projects/hello)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.42s
     Running `target/debug/hello`
Request: [
    "GET / HTTP/1.1",
    "Host: 127.0.0.1:7878",
    "User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:99.0) Gecko/20100101 Firefox/99.0",
    "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8",
    "Accept-Language: en-US,en;q=0.5",
    "Accept-Encoding: gzip, deflate, br",
    "DNT: 1",
    "Connection: keep-alive",
    "Upgrade-Insecure-Requests: 1",
    "Sec-Fetch-Dest: document",
    "Sec-Fetch-Mode: navigate",
    "Sec-Fetch-Site: none",
    "Sec-Fetch-User: ?1",
    "Cache-Control: max-age=0",
]

Depending on your browser, you might get slightly different output. Now that we’re printing the request data, we can see why we get multiple connections from one browser request by looking at the path after GET in the first line of the request. If the repeated connections are all requesting /, we know the browser is trying to fetch / repeatedly because it’s not getting a response from our program.

Let’s break down this request data to understand what the browser is asking of our program.

Looking More Closely at an HTTP Request

HTTP is a text-based protocol, and a request takes this format:

Method Request-URI HTTP-Version CRLF
headers CRLF
message-body

The first line is the request line that holds information about what the client is requesting. The first part of the request line indicates the method being used, such as GET or POST, which describes how the client is making this request. Our client used a GET request, which means it is asking for information.

The next part of the request line is /, which indicates the uniform resource identifier (URI) the client is requesting: A URI is almost, but not quite, the same as a uniform resource locator (URL). The difference between URIs and URLs isn’t important for our purposes in this chapter, but the HTTP spec uses the term URI, so we can just mentally substitute URL for URI here.

The last part is the HTTP version the client uses, and then the request line ends in a CRLF sequence. (CRLF stands for carriage return and line feed, which are terms from the typewriter days!) The CRLF sequence can also be written as \r\n, where \r is a carriage return and \n is a line feed. The CRLF sequence separates the request line from the rest of the request data. Note that when the CRLF is printed, we see a new line start rather than \r\n.

Looking at the request line data we received from running our program so far, we see that GET is the method, / is the request URI, and HTTP/1.1 is the version.

After the request line, the remaining lines starting from Host: onward are headers. GET requests have no body.

Try making a request from a different browser or asking for a different address, such as 127.0.0.1:7878/test, to see how the request data changes.

Now that we know what the browser is asking for, let’s send back some data!

Writing a Response

We’re going to implement sending data in response to a client request. Responses have the following format:

HTTP-Version Status-Code Reason-Phrase CRLF
headers CRLF
message-body

The first line is a status line that contains the HTTP version used in the response, a numeric status code that summarizes the result of the request, and a reason phrase that provides a text description of the status code. After the CRLF sequence are any headers, another CRLF sequence, and the body of the response.

Here is an example response that uses HTTP version 1.1 and has a status code of 200, an OK reason phrase, no headers, and no body:

HTTP/1.1 200 OK\r\n\r\n

The status code 200 is the standard success response. The text is a tiny successful HTTP response. Let’s write this to the stream as our response to a successful request! From the handle_connection function, remove the println! that was printing the request data and replace it with the code in Listing 21-3.

Filename: src/main.rs
use std::{
    io::{BufReader, prelude::*},
    net::{TcpListener, TcpStream},
};

fn main() {
    let listener = TcpListener::bind("127.0.0.1:7878").unwrap();

    for stream in listener.incoming() {
        let stream = stream.unwrap();

        handle_connection(stream);
    }
}

fn handle_connection(mut stream: TcpStream) {
    let buf_reader = BufReader::new(&stream);
    let http_request: Vec<_> = buf_reader
        .lines()
        .map(|result| result.unwrap())
        .take_while(|line| !line.is_empty())
        .collect();

    let response = "HTTP/1.1 200 OK\r\n\r\n";

    stream.write_all(response.as_bytes()).unwrap();
}
Listing 21-3: Writing a tiny successful HTTP response to the stream

The first new line defines the response variable that holds the success message’s data. Then, we call as_bytes on our response to convert the string data to bytes. The write_all method on stream takes a &[u8] and sends those bytes directly down the connection. Because the write_all operation could fail, we use unwrap on any error result as before. Again, in a real application, you would add error handling here.

With these changes, let’s run our code and make a request. We’re no longer printing any data to the terminal, so we won’t see any output other than the output from Cargo. When you load 127.0.0.1:7878 in a web browser, you should get a blank page instead of an error. You’ve just handcoded receiving an HTTP request and sending a response!

Returning Real HTML

Let’s implement the functionality for returning more than a blank page. Create the new file hello.html in the root of your project directory, not in the src directory. You can input any HTML you want; Listing 21-4 shows one possibility.

Filename: hello.html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <title>Hello!</title>
  </head>
  <body>
    <h1>Hello!</h1>
    <p>Hi from Rust</p>
  </body>
</html>
Listing 21-4: A sample HTML file to return in a response

This is a minimal HTML5 document with a heading and some text. To return this from the server when a request is received, we’ll modify handle_connection as shown in Listing 21-5 to read the HTML file, add it to the response as a body, and send it.

Filename: src/main.rs
use std::{
    fs,
    io::{BufReader, prelude::*},
    net::{TcpListener, TcpStream},
};
// --snip--

fn main() {
    let listener = TcpListener::bind("127.0.0.1:7878").unwrap();

    for stream in listener.incoming() {
        let stream = stream.unwrap();

        handle_connection(stream);
    }
}

fn handle_connection(mut stream: TcpStream) {
    let buf_reader = BufReader::new(&stream);
    let http_request: Vec<_> = buf_reader
        .lines()
        .map(|result| result.unwrap())
        .take_while(|line| !line.is_empty())
        .collect();

    let status_line = "HTTP/1.1 200 OK";
    let contents = fs::read_to_string("hello.html").unwrap();
    let length = contents.len();

    let response =
        format!("{status_line}\r\nContent-Length: {length}\r\n\r\n{contents}");

    stream.write_all(response.as_bytes()).unwrap();
}
Listing 21-5: Sending the contents of hello.html as the body of the response

We’ve added fs to the use statement to bring the standard library’s filesystem module into scope. The code for reading the contents of a file to a string should look familiar; we used it when we read the contents of a file for our I/O project in Listing 12-4.

Next, we use format! to add the file’s contents as the body of the success response. To ensure a valid HTTP response, we add the Content-Length header, which is set to the size of our response body—in this case, the size of hello.html.

Run this code with cargo run and load 127.0.0.1:7878 in your browser; you should see your HTML rendered!

Currently, we’re ignoring the request data in http_request and just sending back the contents of the HTML file unconditionally. That means if you try requesting 127.0.0.1:7878/something-else in your browser, you’ll still get back this same HTML response. At the moment, our server is very limited and does not do what most web servers do. We want to customize our responses depending on the request and only send back the HTML file for a well-formed request to /.

Validating the Request and Selectively Responding

Right now, our web server will return the HTML in the file no matter what the client requested. Let’s add functionality to check that the browser is requesting / before returning the HTML file and to return an error if the browser requests anything else. For this we need to modify handle_connection, as shown in Listing 21-6. This new code checks the content of the request received against what we know a request for / looks like and adds if and else blocks to treat requests differently.

Filename: src/main.rs
use std::{
    fs,
    io::{BufReader, prelude::*},
    net::{TcpListener, TcpStream},
};

fn main() {
    let listener = TcpListener::bind("127.0.0.1:7878").unwrap();

    for stream in listener.incoming() {
        let stream = stream.unwrap();

        handle_connection(stream);
    }
}
// --snip--

fn handle_connection(mut stream: TcpStream) {
    let buf_reader = BufReader::new(&stream);
    let request_line = buf_reader.lines().next().unwrap().unwrap();

    if request_line == "GET / HTTP/1.1" {
        let status_line = "HTTP/1.1 200 OK";
        let contents = fs::read_to_string("hello.html").unwrap();
        let length = contents.len();

        let response = format!(
            "{status_line}\r\nContent-Length: {length}\r\n\r\n{contents}"
        );

        stream.write_all(response.as_bytes()).unwrap();
    } else {
        // some other request
    }
}
Listing 21-6: Handling requests to / differently from other requests

We’re only going to be looking at the first line of the HTTP request, so rather than reading the entire request into a vector, we’re calling next to get the first item from the iterator. The first unwrap takes care of the Option and stops the program if the iterator has no items. The second unwrap handles the Result and has the same effect as the unwrap that was in the map added in Listing 21-2.

Next, we check the request_line to see if it equals the request line of a GET request to the / path. If it does, the if block returns the contents of our HTML file.

If the request_line does not equal the GET request to the / path, it means we’ve received some other request. We’ll add code to the else block in a moment to respond to all other requests.

Run this code now and request 127.0.0.1:7878; you should get the HTML in hello.html. If you make any other request, such as 127.0.0.1:7878/something-else, you’ll get a connection error like those you saw when running the code in Listing 21-1 and Listing 21-2.

Now let’s add the code in Listing 21-7 to the else block to return a response with the status code 404, which signals that the content for the request was not found. We’ll also return some HTML for a page to render in the browser indicating the response to the end user.

Filename: src/main.rs
use std::{
    fs,
    io::{BufReader, prelude::*},
    net::{TcpListener, TcpStream},
};

fn main() {
    let listener = TcpListener::bind("127.0.0.1:7878").unwrap();

    for stream in listener.incoming() {
        let stream = stream.unwrap();

        handle_connection(stream);
    }
}

fn handle_connection(mut stream: TcpStream) {
    let buf_reader = BufReader::new(&stream);
    let request_line = buf_reader.lines().next().unwrap().unwrap();

    if request_line == "GET / HTTP/1.1" {
        let status_line = "HTTP/1.1 200 OK";
        let contents = fs::read_to_string("hello.html").unwrap();
        let length = contents.len();

        let response = format!(
            "{status_line}\r\nContent-Length: {length}\r\n\r\n{contents}"
        );

        stream.write_all(response.as_bytes()).unwrap();
    // --snip--
    } else {
        let status_line = "HTTP/1.1 404 NOT FOUND";
        let contents = fs::read_to_string("404.html").unwrap();
        let length = contents.len();

        let response = format!(
            "{status_line}\r\nContent-Length: {length}\r\n\r\n{contents}"
        );

        stream.write_all(response.as_bytes()).unwrap();
    }
}
Listing 21-7: Responding with status code 404 and an error page if anything other than / was requested

Here, our response has a status line with status code 404 and the reason phrase NOT FOUND. The body of the response will be the HTML in the file 404.html. You’ll need to create a 404.html file next to hello.html for the error page; again, feel free to use any HTML you want, or use the example HTML in Listing 21-8.

Filename: 404.html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <title>Hello!</title>
  </head>
  <body>
    <h1>Oops!</h1>
    <p>Sorry, I don't know what you're asking for.</p>
  </body>
</html>
Listing 21-8: Sample content for the page to send back with any 404 response

With these changes, run your server again. Requesting 127.0.0.1:7878 should return the contents of hello.html, and any other request, like 127.0.0.1:7878/foo, should return the error HTML from 404.html.

Refactoring

At the moment, the if and else blocks have a lot of repetition: They’re both reading files and writing the contents of the files to the stream. The only differences are the status line and the filename. Let’s make the code more concise by pulling out those differences into separate if and else lines that will assign the values of the status line and the filename to variables; we can then use those variables unconditionally in the code to read the file and write the response. Listing 21-9 shows the resultant code after replacing the large if and else blocks.

Filename: src/main.rs
use std::{
    fs,
    io::{BufReader, prelude::*},
    net::{TcpListener, TcpStream},
};

fn main() {
    let listener = TcpListener::bind("127.0.0.1:7878").unwrap();

    for stream in listener.incoming() {
        let stream = stream.unwrap();

        handle_connection(stream);
    }
}
// --snip--

fn handle_connection(mut stream: TcpStream) {
    // --snip--
    let buf_reader = BufReader::new(&stream);
    let request_line = buf_reader.lines().next().unwrap().unwrap();

    let (status_line, filename) = if request_line == "GET / HTTP/1.1" {
        ("HTTP/1.1 200 OK", "hello.html")
    } else {
        ("HTTP/1.1 404 NOT FOUND", "404.html")
    };

    let contents = fs::read_to_string(filename).unwrap();
    let length = contents.len();

    let response =
        format!("{status_line}\r\nContent-Length: {length}\r\n\r\n{contents}");

    stream.write_all(response.as_bytes()).unwrap();
}
Listing 21-9: Refactoring the if and else blocks to contain only the code that differs between the two cases

Now the if and else blocks only return the appropriate values for the status line and filename in a tuple; we then use destructuring to assign these two values to status_line and filename using a pattern in the let statement, as discussed in Chapter 19.

The previously duplicated code is now outside the if and else blocks and uses the status_line and filename variables. This makes it easier to see the difference between the two cases, and it means we have only one place to update the code if we want to change how the file reading and response writing work. The behavior of the code in Listing 21-9 will be the same as that in Listing 21-7.

Awesome! We now have a simple web server in approximately 40 lines of Rust code that responds to one request with a page of content and responds to all other requests with a 404 response.

Currently, our server runs in a single thread, meaning it can only serve one request at a time. Let’s examine how that can be a problem by simulating some slow requests. Then, we’ll fix it so that our server can handle multiple requests at once.

Переход от однопоточного сервера к многопоточному

From a Single-Threaded to a Multithreaded Server

Right now, the server will process each request in turn, meaning it won’t process a second connection until the first connection is finished processing. If the server received more and more requests, this serial execution would be less and less optimal. If the server receives a request that takes a long time to process, subsequent requests will have to wait until the long request is finished, even if the new requests can be processed quickly. We’ll need to fix this, but first we’ll look at the problem in action.

Simulating a Slow Request

We’ll look at how a slowly processing request can affect other requests made to our current server implementation. Listing 21-10 implements handling a request to /sleep with a simulated slow response that will cause the server to sleep for five seconds before responding.

Filename: src/main.rs
use std::{
    fs,
    io::{BufReader, prelude::*},
    net::{TcpListener, TcpStream},
    thread,
    time::Duration,
};
// --snip--

fn main() {
    let listener = TcpListener::bind("127.0.0.1:7878").unwrap();

    for stream in listener.incoming() {
        let stream = stream.unwrap();

        handle_connection(stream);
    }
}

fn handle_connection(mut stream: TcpStream) {
    // --snip--

    let buf_reader = BufReader::new(&stream);
    let request_line = buf_reader.lines().next().unwrap().unwrap();

    let (status_line, filename) = match &request_line[..] {
        "GET / HTTP/1.1" => ("HTTP/1.1 200 OK", "hello.html"),
        "GET /sleep HTTP/1.1" => {
            thread::sleep(Duration::from_secs(5));
            ("HTTP/1.1 200 OK", "hello.html")
        }
        _ => ("HTTP/1.1 404 NOT FOUND", "404.html"),
    };

    // --snip--

    let contents = fs::read_to_string(filename).unwrap();
    let length = contents.len();

    let response =
        format!("{status_line}\r\nContent-Length: {length}\r\n\r\n{contents}");

    stream.write_all(response.as_bytes()).unwrap();
}
Listing 21-10: Simulating a slow request by sleeping for five seconds

We switched from if to match now that we have three cases. We need to explicitly match on a slice of request_line to pattern-match against the string literal values; match doesn’t do automatic referencing and dereferencing, like the equality method does.

The first arm is the same as the if block from Listing 21-9. The second arm matches a request to /sleep. When that request is received, the server will sleep for five seconds before rendering the successful HTML page. The third arm is the same as the else block from Listing 21-9.

You can see how primitive our server is: Real libraries would handle the recognition of multiple requests in a much less verbose way!

Start the server using cargo run. Then, open two browser windows: one for http://127.0.0.1:7878 and the other for http://127.0.0.1:7878/sleep. If you enter the / URI a few times, as before, you’ll see it respond quickly. But if you enter /sleep and then load /, you’ll see that / waits until sleep has slept for its full five seconds before loading.

There are multiple techniques we could use to avoid requests backing up behind a slow request, including using async as we did Chapter 17; the one we’ll implement is a thread pool.

Improving Throughput with a Thread Pool

A thread pool is a group of spawned threads that are ready and waiting to handle a task. When the program receives a new task, it assigns one of the threads in the pool to the task, and that thread will process the task. The remaining threads in the pool are available to handle any other tasks that come in while the first thread is processing. When the first thread is done processing its task, it’s returned to the pool of idle threads, ready to handle a new task. A thread pool allows you to process connections concurrently, increasing the throughput of your server.

We’ll limit the number of threads in the pool to a small number to protect us from DoS attacks; if we had our program create a new thread for each request as it came in, someone making 10 million requests to our server could wreak havoc by using up all our server’s resources and grinding the processing of requests to a halt.

Rather than spawning unlimited threads, then, we’ll have a fixed number of threads waiting in the pool. Requests that come in are sent to the pool for processing. The pool will maintain a queue of incoming requests. Each of the threads in the pool will pop off a request from this queue, handle the request, and then ask the queue for another request. With this design, we can process up to N requests concurrently, where N is the number of threads. If each thread is responding to a long-running request, subsequent requests can still back up in the queue, but we’ve increased the number of long-running requests we can handle before reaching that point.

This technique is just one of many ways to improve the throughput of a web server. Other options you might explore are the fork/join model, the single-threaded async I/O model, and the multithreaded async I/O model. If you’re interested in this topic, you can read more about other solutions and try to implement them; with a low-level language like Rust, all of these options are possible.

Before we begin implementing a thread pool, let’s talk about what using the pool should look like. When you’re trying to design code, writing the client interface first can help guide your design. Write the API of the code so that it’s structured in the way you want to call it; then, implement the functionality within that structure rather than implementing the functionality and then designing the public API.

Similar to how we used test-driven development in the project in Chapter 12, we’ll use compiler-driven development here. We’ll write the code that calls the functions we want, and then we’ll look at errors from the compiler to determine what we should change next to get the code to work. Before we do that, however, we’ll explore the technique we’re not going to use as a starting point.

Spawning a Thread for Each Request

First, let’s explore how our code might look if it did create a new thread for every connection. As mentioned earlier, this isn’t our final plan due to the problems with potentially spawning an unlimited number of threads, but it is a starting point to get a working multithreaded server first. Then, we’ll add the thread pool as an improvement, and contrasting the two solutions will be easier.

Listing 21-11 shows the changes to make to main to spawn a new thread to handle each stream within the for loop.

Filename: src/main.rs
use std::{
    fs,
    io::{BufReader, prelude::*},
    net::{TcpListener, TcpStream},
    thread,
    time::Duration,
};

fn main() {
    let listener = TcpListener::bind("127.0.0.1:7878").unwrap();

    for stream in listener.incoming() {
        let stream = stream.unwrap();

        thread::spawn(|| {
            handle_connection(stream);
        });
    }
}

fn handle_connection(mut stream: TcpStream) {
    let buf_reader = BufReader::new(&stream);
    let request_line = buf_reader.lines().next().unwrap().unwrap();

    let (status_line, filename) = match &request_line[..] {
        "GET / HTTP/1.1" => ("HTTP/1.1 200 OK", "hello.html"),
        "GET /sleep HTTP/1.1" => {
            thread::sleep(Duration::from_secs(5));
            ("HTTP/1.1 200 OK", "hello.html")
        }
        _ => ("HTTP/1.1 404 NOT FOUND", "404.html"),
    };

    let contents = fs::read_to_string(filename).unwrap();
    let length = contents.len();

    let response =
        format!("{status_line}\r\nContent-Length: {length}\r\n\r\n{contents}");

    stream.write_all(response.as_bytes()).unwrap();
}
Listing 21-11: Spawning a new thread for each stream

As you learned in Chapter 16, thread::spawn will create a new thread and then run the code in the closure in the new thread. If you run this code and load /sleep in your browser, then / in two more browser tabs, you’ll indeed see that the requests to / don’t have to wait for /sleep to finish. However, as we mentioned, this will eventually overwhelm the system because you’d be making new threads without any limit.

You may also recall from Chapter 17 that this is exactly the kind of situation where async and await really shine! Keep that in mind as we build the thread pool and think about how things would look different or the same with async.

Creating a Finite Number of Threads

We want our thread pool to work in a similar, familiar way so that switching from threads to a thread pool doesn’t require large changes to the code that uses our API. Listing 21-12 shows the hypothetical interface for a ThreadPool struct we want to use instead of thread::spawn.

Filename: src/main.rs
use std::{
    fs,
    io::{BufReader, prelude::*},
    net::{TcpListener, TcpStream},
    thread,
    time::Duration,
};

fn main() {
    let listener = TcpListener::bind("127.0.0.1:7878").unwrap();
    let pool = ThreadPool::new(4);

    for stream in listener.incoming() {
        let stream = stream.unwrap();

        pool.execute(|| {
            handle_connection(stream);
        });
    }
}

fn handle_connection(mut stream: TcpStream) {
    let buf_reader = BufReader::new(&stream);
    let request_line = buf_reader.lines().next().unwrap().unwrap();

    let (status_line, filename) = match &request_line[..] {
        "GET / HTTP/1.1" => ("HTTP/1.1 200 OK", "hello.html"),
        "GET /sleep HTTP/1.1" => {
            thread::sleep(Duration::from_secs(5));
            ("HTTP/1.1 200 OK", "hello.html")
        }
        _ => ("HTTP/1.1 404 NOT FOUND", "404.html"),
    };

    let contents = fs::read_to_string(filename).unwrap();
    let length = contents.len();

    let response =
        format!("{status_line}\r\nContent-Length: {length}\r\n\r\n{contents}");

    stream.write_all(response.as_bytes()).unwrap();
}
Listing 21-12: Our ideal ThreadPool interface

We use ThreadPool::new to create a new thread pool with a configurable number of threads, in this case four. Then, in the for loop, pool.execute has a similar interface as thread::spawn in that it takes a closure that the pool should run for each stream. We need to implement pool.execute so that it takes the closure and gives it to a thread in the pool to run. This code won’t yet compile, but we’ll try so that the compiler can guide us in how to fix it.

Building ThreadPool Using Compiler-Driven Development

Make the changes in Listing 21-12 to src/main.rs, and then let’s use the compiler errors from cargo check to drive our development. Here is the first error we get:

$ cargo check
    Checking hello v0.1.0 (file:///projects/hello)
error[E0433]: failed to resolve: use of undeclared type `ThreadPool`
  --> src/main.rs:11:16
   |
11 |     let pool = ThreadPool::new(4);
   |                ^^^^^^^^^^ use of undeclared type `ThreadPool`

For more information about this error, try `rustc --explain E0433`.
error: could not compile `hello` (bin "hello") due to 1 previous error

Great! This error tells us we need a ThreadPool type or module, so we’ll build one now. Our ThreadPool implementation will be independent of the kind of work our web server is doing. So, let’s switch the hello crate from a binary crate to a library crate to hold our ThreadPool implementation. After we change to a library crate, we could also use the separate thread pool library for any work we want to do using a thread pool, not just for serving web requests.

Create a src/lib.rs file that contains the following, which is the simplest definition of a ThreadPool struct that we can have for now:

Filename: src/lib.rs
pub struct ThreadPool;

Then, edit the main.rs file to bring ThreadPool into scope from the library crate by adding the following code to the top of src/main.rs:

Filename: src/main.rs
use hello::ThreadPool;
use std::{
    fs,
    io::{BufReader, prelude::*},
    net::{TcpListener, TcpStream},
    thread,
    time::Duration,
};

fn main() {
    let listener = TcpListener::bind("127.0.0.1:7878").unwrap();
    let pool = ThreadPool::new(4);

    for stream in listener.incoming() {
        let stream = stream.unwrap();

        pool.execute(|| {
            handle_connection(stream);
        });
    }
}

fn handle_connection(mut stream: TcpStream) {
    let buf_reader = BufReader::new(&stream);
    let request_line = buf_reader.lines().next().unwrap().unwrap();

    let (status_line, filename) = match &request_line[..] {
        "GET / HTTP/1.1" => ("HTTP/1.1 200 OK", "hello.html"),
        "GET /sleep HTTP/1.1" => {
            thread::sleep(Duration::from_secs(5));
            ("HTTP/1.1 200 OK", "hello.html")
        }
        _ => ("HTTP/1.1 404 NOT FOUND", "404.html"),
    };

    let contents = fs::read_to_string(filename).unwrap();
    let length = contents.len();

    let response =
        format!("{status_line}\r\nContent-Length: {length}\r\n\r\n{contents}");

    stream.write_all(response.as_bytes()).unwrap();
}

This code still won’t work, but let’s check it again to get the next error that we need to address:

$ cargo check
    Checking hello v0.1.0 (file:///projects/hello)
error[E0599]: no function or associated item named `new` found for struct `ThreadPool` in the current scope
  --> src/main.rs:12:28
   |
12 |     let pool = ThreadPool::new(4);
   |                            ^^^ function or associated item not found in `ThreadPool`

For more information about this error, try `rustc --explain E0599`.
error: could not compile `hello` (bin "hello") due to 1 previous error

This error indicates that next we need to create an associated function named new for ThreadPool. We also know that new needs to have one parameter that can accept 4 as an argument and should return a ThreadPool instance. Let’s implement the simplest new function that will have those characteristics:

Filename: src/lib.rs
pub struct ThreadPool;

impl ThreadPool {
    pub fn new(size: usize) -> ThreadPool {
        ThreadPool
    }
}

We chose usize as the type of the size parameter because we know that a negative number of threads doesn’t make any sense. We also know we’ll use this 4 as the number of elements in a collection of threads, which is what the usize type is for, as discussed in the “Integer Types” section in Chapter 3.

Let’s check the code again:

$ cargo check
    Checking hello v0.1.0 (file:///projects/hello)
error[E0599]: no method named `execute` found for struct `ThreadPool` in the current scope
  --> src/main.rs:17:14
   |
17 |         pool.execute(|| {
   |         -----^^^^^^^ method not found in `ThreadPool`

For more information about this error, try `rustc --explain E0599`.
error: could not compile `hello` (bin "hello") due to 1 previous error

Now the error occurs because we don’t have an execute method on ThreadPool. Recall from the “Creating a Finite Number of Threads” section that we decided our thread pool should have an interface similar to thread::spawn. In addition, we’ll implement the execute function so that it takes the closure it’s given and gives it to an idle thread in the pool to run.

We’ll define the execute method on ThreadPool to take a closure as a parameter. Recall from the “Moving Captured Values Out of Closures” in Chapter 13 that we can take closures as parameters with three different traits: Fn, FnMut, and FnOnce. We need to decide which kind of closure to use here. We know we’ll end up doing something similar to the standard library thread::spawn implementation, so we can look at what bounds the signature of thread::spawn has on its parameter. The documentation shows us the following:

pub fn spawn<F, T>(f: F) -> JoinHandle<T>
    where
        F: FnOnce() -> T,
        F: Send + 'static,
        T: Send + 'static,

The F type parameter is the one we’re concerned with here; the T type parameter is related to the return value, and we’re not concerned with that. We can see that spawn uses FnOnce as the trait bound on F. This is probably what we want as well, because we’ll eventually pass the argument we get in execute to spawn. We can be further confident that FnOnce is the trait we want to use because the thread for running a request will only execute that request’s closure one time, which matches the Once in FnOnce.

The F type parameter also has the trait bound Send and the lifetime bound 'static, which are useful in our situation: We need Send to transfer the closure from one thread to another and 'static because we don’t know how long the thread will take to execute. Let’s create an execute method on ThreadPool that will take a generic parameter of type F with these bounds:

Filename: src/lib.rs
pub struct ThreadPool;

impl ThreadPool {
    // --snip--
    pub fn new(size: usize) -> ThreadPool {
        ThreadPool
    }

    pub fn execute<F>(&self, f: F)
    where
        F: FnOnce() + Send + 'static,
    {
    }
}

We still use the () after FnOnce because this FnOnce represents a closure that takes no parameters and returns the unit type (). Just like function definitions, the return type can be omitted from the signature, but even if we have no parameters, we still need the parentheses.

Again, this is the simplest implementation of the execute method: It does nothing, but we’re only trying to make our code compile. Let’s check it again:

$ cargo check
    Checking hello v0.1.0 (file:///projects/hello)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.24s

It compiles! But note that if you try cargo run and make a request in the browser, you’ll see the errors in the browser that we saw at the beginning of the chapter. Our library isn’t actually calling the closure passed to execute yet!

Note: A saying you might hear about languages with strict compilers, such as Haskell and Rust, is “If the code compiles, it works.” But this saying is not universally true. Our project compiles, but it does absolutely nothing! If we were building a real, complete project, this would be a good time to start writing unit tests to check that the code compiles and has the behavior we want.

Consider: What would be different here if we were going to execute a future instead of a closure?

Validating the Number of Threads in new

We aren’t doing anything with the parameters to new and execute. Let’s implement the bodies of these functions with the behavior we want. To start, let’s think about new. Earlier we chose an unsigned type for the size parameter because a pool with a negative number of threads makes no sense. However, a pool with zero threads also makes no sense, yet zero is a perfectly valid usize. We’ll add code to check that size is greater than zero before we return a ThreadPool instance, and we’ll have the program panic if it receives a zero by using the assert! macro, as shown in Listing 21-13.

Filename: src/lib.rs
pub struct ThreadPool;

impl ThreadPool {
    /// Create a new ThreadPool.
    ///
    /// The size is the number of threads in the pool.
    ///
    /// # Panics
    ///
    /// The `new` function will panic if the size is zero.
    pub fn new(size: usize) -> ThreadPool {
        assert!(size > 0);

        ThreadPool
    }

    // --snip--
    pub fn execute<F>(&self, f: F)
    where
        F: FnOnce() + Send + 'static,
    {
    }
}
Listing 21-13: Implementing ThreadPool::new to panic if size is zero

We’ve also added some documentation for our ThreadPool with doc comments. Note that we followed good documentation practices by adding a section that calls out the situations in which our function can panic, as discussed in Chapter 14. Try running cargo doc --open and clicking the ThreadPool struct to see what the generated docs for new look like!

Instead of adding the assert! macro as we’ve done here, we could change new into build and return a Result like we did with Config::build in the I/O project in Listing 12-9. But we’ve decided in this case that trying to create a thread pool without any threads should be an unrecoverable error. If you’re feeling ambitious, try to write a function named build with the following signature to compare with the new function:

pub fn build(size: usize) -> Result<ThreadPool, PoolCreationError> {

Creating Space to Store the Threads

Now that we have a way to know we have a valid number of threads to store in the pool, we can create those threads and store them in the ThreadPool struct before returning the struct. But how do we “store” a thread? Let’s take another look at the thread::spawn signature:

pub fn spawn<F, T>(f: F) -> JoinHandle<T>
    where
        F: FnOnce() -> T,
        F: Send + 'static,
        T: Send + 'static,

The spawn function returns a JoinHandle<T>, where T is the type that the closure returns. Let’s try using JoinHandle too and see what happens. In our case, the closures we’re passing to the thread pool will handle the connection and not return anything, so T will be the unit type ().

The code in Listing 21-14 will compile, but it doesn’t create any threads yet. We’ve changed the definition of ThreadPool to hold a vector of thread::JoinHandle<()> instances, initialized the vector with a capacity of size, set up a for loop that will run some code to create the threads, and returned a ThreadPool instance containing them.

Filename: src/lib.rs
use std::thread;

pub struct ThreadPool {
    threads: Vec<thread::JoinHandle<()>>,
}

impl ThreadPool {
    // --snip--
    /// Create a new ThreadPool.
    ///
    /// The size is the number of threads in the pool.
    ///
    /// # Panics
    ///
    /// The `new` function will panic if the size is zero.
    pub fn new(size: usize) -> ThreadPool {
        assert!(size > 0);

        let mut threads = Vec::with_capacity(size);

        for _ in 0..size {
            // create some threads and store them in the vector
        }

        ThreadPool { threads }
    }
    // --snip--

    pub fn execute<F>(&self, f: F)
    where
        F: FnOnce() + Send + 'static,
    {
    }
}
Listing 21-14: Creating a vector for ThreadPool to hold the threads

We’ve brought std::thread into scope in the library crate because we’re using thread::JoinHandle as the type of the items in the vector in ThreadPool.

Once a valid size is received, our ThreadPool creates a new vector that can hold size items. The with_capacity function performs the same task as Vec::new but with an important difference: It pre-allocates space in the vector. Because we know we need to store size elements in the vector, doing this allocation up front is slightly more efficient than using Vec::new, which resizes itself as elements are inserted.

When you run cargo check again, it should succeed.

Sending Code from the ThreadPool to a Thread

We left a comment in the for loop in Listing 21-14 regarding the creation of threads. Here, we’ll look at how we actually create threads. The standard library provides thread::spawn as a way to create threads, and thread::spawn expects to get some code the thread should run as soon as the thread is created. However, in our case, we want to create the threads and have them wait for code that we’ll send later. The standard library’s implementation of threads doesn’t include any way to do that; we have to implement it manually.

We’ll implement this behavior by introducing a new data structure between the ThreadPool and the threads that will manage this new behavior. We’ll call this data structure Worker, which is a common term in pooling implementations. The Worker picks up code that needs to be run and runs the code in its thread.

Think of people working in the kitchen at a restaurant: The workers wait until orders come in from customers, and then they’re responsible for taking those orders and filling them.

Instead of storing a vector of JoinHandle<()> instances in the thread pool, we’ll store instances of the Worker struct. Each Worker will store a single JoinHandle<()> instance. Then, we’ll implement a method on Worker that will take a closure of code to run and send it to the already running thread for execution. We’ll also give each Worker an id so that we can distinguish between the different instances of Worker in the pool when logging or debugging.

Here is the new process that will happen when we create a ThreadPool. We’ll implement the code that sends the closure to the thread after we have Worker set up in this way:

  1. Define a Worker struct that holds an id and a JoinHandle<()>.
  2. Change ThreadPool to hold a vector of Worker instances.
  3. Define a Worker::new function that takes an id number and returns a Worker instance that holds the id and a thread spawned with an empty closure.
  4. In ThreadPool::new, use the for loop counter to generate an id, create a new Worker with that id, and store the Worker in the vector.

If you’re up for a challenge, try implementing these changes on your own before looking at the code in Listing 21-15.

Ready? Here is Listing 21-15 with one way to make the preceding modifications.

Filename: src/lib.rs
use std::thread;

pub struct ThreadPool {
    workers: Vec<Worker>,
}

impl ThreadPool {
    // --snip--
    /// Create a new ThreadPool.
    ///
    /// The size is the number of threads in the pool.
    ///
    /// # Panics
    ///
    /// The `new` function will panic if the size is zero.
    pub fn new(size: usize) -> ThreadPool {
        assert!(size > 0);

        let mut workers = Vec::with_capacity(size);

        for id in 0..size {
            workers.push(Worker::new(id));
        }

        ThreadPool { workers }
    }
    // --snip--

    pub fn execute<F>(&self, f: F)
    where
        F: FnOnce() + Send + 'static,
    {
    }
}

struct Worker {
    id: usize,
    thread: thread::JoinHandle<()>,
}

impl Worker {
    fn new(id: usize) -> Worker {
        let thread = thread::spawn(|| {});

        Worker { id, thread }
    }
}
Listing 21-15: Modifying ThreadPool to hold Worker instances instead of holding threads directly

We’ve changed the name of the field on ThreadPool from threads to workers because it’s now holding Worker instances instead of JoinHandle<()> instances. We use the counter in the for loop as an argument to Worker::new, and we store each new Worker in the vector named workers.

External code (like our server in src/main.rs) doesn’t need to know the implementation details regarding using a Worker struct within ThreadPool, so we make the Worker struct and its new function private. The Worker::new function uses the id we give it and stores a JoinHandle<()> instance that is created by spawning a new thread using an empty closure.

Note: If the operating system can’t create a thread because there aren’t enough system resources, thread::spawn will panic. That will cause our whole server to panic, even though the creation of some threads might succeed. For simplicity’s sake, this behavior is fine, but in a production thread pool implementation, you’d likely want to use std::thread::Builder and its spawn method that returns Result instead.

This code will compile and will store the number of Worker instances we specified as an argument to ThreadPool::new. But we’re still not processing the closure that we get in execute. Let’s look at how to do that next.

Sending Requests to Threads via Channels

The next problem we’ll tackle is that the closures given to thread::spawn do absolutely nothing. Currently, we get the closure we want to execute in the execute method. But we need to give thread::spawn a closure to run when we create each Worker during the creation of the ThreadPool.

We want the Worker structs that we just created to fetch the code to run from a queue held in the ThreadPool and send that code to its thread to run.

The channels we learned about in Chapter 16—a simple way to communicate between two threads—would be perfect for this use case. We’ll use a channel to function as the queue of jobs, and execute will send a job from the ThreadPool to the Worker instances, which will send the job to its thread. Here is the plan:

  1. The ThreadPool will create a channel and hold on to the sender.
  2. Each Worker will hold on to the receiver.
  3. We’ll create a new Job struct that will hold the closures we want to send down the channel.
  4. The execute method will send the job it wants to execute through the sender.
  5. In its thread, the Worker will loop over its receiver and execute the closures of any jobs it receives.

Let’s start by creating a channel in ThreadPool::new and holding the sender in the ThreadPool instance, as shown in Listing 21-16. The Job struct doesn’t hold anything for now but will be the type of item we’re sending down the channel.

Filename: src/lib.rs
use std::{sync::mpsc, thread};

pub struct ThreadPool {
    workers: Vec<Worker>,
    sender: mpsc::Sender<Job>,
}

struct Job;

impl ThreadPool {
    // --snip--
    /// Create a new ThreadPool.
    ///
    /// The size is the number of threads in the pool.
    ///
    /// # Panics
    ///
    /// The `new` function will panic if the size is zero.
    pub fn new(size: usize) -> ThreadPool {
        assert!(size > 0);

        let (sender, receiver) = mpsc::channel();

        let mut workers = Vec::with_capacity(size);

        for id in 0..size {
            workers.push(Worker::new(id));
        }

        ThreadPool { workers, sender }
    }
    // --snip--

    pub fn execute<F>(&self, f: F)
    where
        F: FnOnce() + Send + 'static,
    {
    }
}

struct Worker {
    id: usize,
    thread: thread::JoinHandle<()>,
}

impl Worker {
    fn new(id: usize) -> Worker {
        let thread = thread::spawn(|| {});

        Worker { id, thread }
    }
}
Listing 21-16: Modifying ThreadPool to store the sender of a channel that transmits Job instances

In ThreadPool::new, we create our new channel and have the pool hold the sender. This will successfully compile.

Let’s try passing a receiver of the channel into each Worker as the thread pool creates the channel. We know we want to use the receiver in the thread that the Worker instances spawn, so we’ll reference the receiver parameter in the closure. The code in Listing 21-17 won’t quite compile yet.

Filename: src/lib.rs
use std::{sync::mpsc, thread};

pub struct ThreadPool {
    workers: Vec<Worker>,
    sender: mpsc::Sender<Job>,
}

struct Job;

impl ThreadPool {
    // --snip--
    /// Create a new ThreadPool.
    ///
    /// The size is the number of threads in the pool.
    ///
    /// # Panics
    ///
    /// The `new` function will panic if the size is zero.
    pub fn new(size: usize) -> ThreadPool {
        assert!(size > 0);

        let (sender, receiver) = mpsc::channel();

        let mut workers = Vec::with_capacity(size);

        for id in 0..size {
            workers.push(Worker::new(id, receiver));
        }

        ThreadPool { workers, sender }
    }
    // --snip--

    pub fn execute<F>(&self, f: F)
    where
        F: FnOnce() + Send + 'static,
    {
    }
}

// --snip--


struct Worker {
    id: usize,
    thread: thread::JoinHandle<()>,
}

impl Worker {
    fn new(id: usize, receiver: mpsc::Receiver<Job>) -> Worker {
        let thread = thread::spawn(|| {
            receiver;
        });

        Worker { id, thread }
    }
}
Listing 21-17: Passing the receiver to each Worker

We’ve made some small and straightforward changes: We pass the receiver into Worker::new, and then we use it inside the closure.

When we try to check this code, we get this error:

$ cargo check
    Checking hello v0.1.0 (file:///projects/hello)
error[E0382]: use of moved value: `receiver`
  --> src/lib.rs:26:42
   |
21 |         let (sender, receiver) = mpsc::channel();
   |                      -------- move occurs because `receiver` has type `std::sync::mpsc::Receiver<Job>`, which does not implement the `Copy` trait
...
25 |         for id in 0..size {
   |         ----------------- inside of this loop
26 |             workers.push(Worker::new(id, receiver));
   |                                          ^^^^^^^^ value moved here, in previous iteration of loop
   |
note: consider changing this parameter type in method `new` to borrow instead if owning the value isn't necessary
  --> src/lib.rs:47:33
   |
47 |     fn new(id: usize, receiver: mpsc::Receiver<Job>) -> Worker {
   |        --- in this method       ^^^^^^^^^^^^^^^^^^^ this parameter takes ownership of the value
help: consider moving the expression out of the loop so it is only moved once
   |
25 ~         let mut value = Worker::new(id, receiver);
26 ~         for id in 0..size {
27 ~             workers.push(value);
   |

For more information about this error, try `rustc --explain E0382`.
error: could not compile `hello` (lib) due to 1 previous error

The code is trying to pass receiver to multiple Worker instances. This won’t work, as you’ll recall from Chapter 16: The channel implementation that Rust provides is multiple producer, single consumer. This means we can’t just clone the consuming end of the channel to fix this code. We also don’t want to send a message multiple times to multiple consumers; we want one list of messages with multiple Worker instances such that each message gets processed once.

Additionally, taking a job off the channel queue involves mutating the receiver, so the threads need a safe way to share and modify receiver; otherwise, we might get race conditions (as covered in Chapter 16).

Recall the thread-safe smart pointers discussed in Chapter 16: To share ownership across multiple threads and allow the threads to mutate the value, we need to use Arc<Mutex<T>>. The Arc type will let multiple Worker instances own the receiver, and Mutex will ensure that only one Worker gets a job from the receiver at a time. Listing 21-18 shows the changes we need to make.

Filename: src/lib.rs
use std::{
    sync::{Arc, Mutex, mpsc},
    thread,
};
// --snip--

pub struct ThreadPool {
    workers: Vec<Worker>,
    sender: mpsc::Sender<Job>,
}

struct Job;

impl ThreadPool {
    // --snip--
    /// Create a new ThreadPool.
    ///
    /// The size is the number of threads in the pool.
    ///
    /// # Panics
    ///
    /// The `new` function will panic if the size is zero.
    pub fn new(size: usize) -> ThreadPool {
        assert!(size > 0);

        let (sender, receiver) = mpsc::channel();

        let receiver = Arc::new(Mutex::new(receiver));

        let mut workers = Vec::with_capacity(size);

        for id in 0..size {
            workers.push(Worker::new(id, Arc::clone(&receiver)));
        }

        ThreadPool { workers, sender }
    }

    // --snip--

    pub fn execute<F>(&self, f: F)
    where
        F: FnOnce() + Send + 'static,
    {
    }
}

// --snip--

struct Worker {
    id: usize,
    thread: thread::JoinHandle<()>,
}

impl Worker {
    fn new(id: usize, receiver: Arc<Mutex<mpsc::Receiver<Job>>>) -> Worker {
        // --snip--
        let thread = thread::spawn(|| {
            receiver;
        });

        Worker { id, thread }
    }
}
Listing 21-18: Sharing the receiver among the Worker instances using Arc and Mutex

In ThreadPool::new, we put the receiver in an Arc and a Mutex. For each new Worker, we clone the Arc to bump the reference count so that the Worker instances can share ownership of the receiver.

With these changes, the code compiles! We’re getting there!

Implementing the execute Method

Let’s finally implement the execute method on ThreadPool. We’ll also change Job from a struct to a type alias for a trait object that holds the type of closure that execute receives. As discussed in the “Type Synonyms and Type Aliases” section in Chapter 20, type aliases allow us to make long types shorter for ease of use. Look at Listing 21-19.

Filename: src/lib.rs
use std::{
    sync::{Arc, Mutex, mpsc},
    thread,
};

pub struct ThreadPool {
    workers: Vec<Worker>,
    sender: mpsc::Sender<Job>,
}

// --snip--

type Job = Box<dyn FnOnce() + Send + 'static>;

impl ThreadPool {
    // --snip--
    /// Create a new ThreadPool.
    ///
    /// The size is the number of threads in the pool.
    ///
    /// # Panics
    ///
    /// The `new` function will panic if the size is zero.
    pub fn new(size: usize) -> ThreadPool {
        assert!(size > 0);

        let (sender, receiver) = mpsc::channel();

        let receiver = Arc::new(Mutex::new(receiver));

        let mut workers = Vec::with_capacity(size);

        for id in 0..size {
            workers.push(Worker::new(id, Arc::clone(&receiver)));
        }

        ThreadPool { workers, sender }
    }

    pub fn execute<F>(&self, f: F)
    where
        F: FnOnce() + Send + 'static,
    {
        let job = Box::new(f);

        self.sender.send(job).unwrap();
    }
}

// --snip--

struct Worker {
    id: usize,
    thread: thread::JoinHandle<()>,
}

impl Worker {
    fn new(id: usize, receiver: Arc<Mutex<mpsc::Receiver<Job>>>) -> Worker {
        let thread = thread::spawn(|| {
            receiver;
        });

        Worker { id, thread }
    }
}
Listing 21-19: Creating a Job type alias for a Box that holds each closure and then sending the job down the channel

After creating a new Job instance using the closure we get in execute, we send that job down the sending end of the channel. We’re calling unwrap on send for the case that sending fails. This might happen if, for example, we stop all our threads from executing, meaning the receiving end has stopped receiving new messages. At the moment, we can’t stop our threads from executing: Our threads continue executing as long as the pool exists. The reason we use unwrap is that we know the failure case won’t happen, but the compiler doesn’t know that.

But we’re not quite done yet! In the Worker, our closure being passed to thread::spawn still only references the receiving end of the channel. Instead, we need the closure to loop forever, asking the receiving end of the channel for a job and running the job when it gets one. Let’s make the change shown in Listing 21-20 to Worker::new.

Filename: src/lib.rs
use std::{
    sync::{Arc, Mutex, mpsc},
    thread,
};

pub struct ThreadPool {
    workers: Vec<Worker>,
    sender: mpsc::Sender<Job>,
}

type Job = Box<dyn FnOnce() + Send + 'static>;

impl ThreadPool {
    /// Create a new ThreadPool.
    ///
    /// The size is the number of threads in the pool.
    ///
    /// # Panics
    ///
    /// The `new` function will panic if the size is zero.
    pub fn new(size: usize) -> ThreadPool {
        assert!(size > 0);

        let (sender, receiver) = mpsc::channel();

        let receiver = Arc::new(Mutex::new(receiver));

        let mut workers = Vec::with_capacity(size);

        for id in 0..size {
            workers.push(Worker::new(id, Arc::clone(&receiver)));
        }

        ThreadPool { workers, sender }
    }

    pub fn execute<F>(&self, f: F)
    where
        F: FnOnce() + Send + 'static,
    {
        let job = Box::new(f);

        self.sender.send(job).unwrap();
    }
}

struct Worker {
    id: usize,
    thread: thread::JoinHandle<()>,
}

// --snip--

impl Worker {
    fn new(id: usize, receiver: Arc<Mutex<mpsc::Receiver<Job>>>) -> Worker {
        let thread = thread::spawn(move || {
            loop {
                let job = receiver.lock().unwrap().recv().unwrap();

                println!("Worker {id} got a job; executing.");

                job();
            }
        });

        Worker { id, thread }
    }
}
Listing 21-20: Receiving and executing the jobs in the Worker instance’s thread

Here, we first call lock on the receiver to acquire the mutex, and then we call unwrap to panic on any errors. Acquiring a lock might fail if the mutex is in a poisoned state, which can happen if some other thread panicked while holding the lock rather than releasing the lock. In this situation, calling unwrap to have this thread panic is the correct action to take. Feel free to change this unwrap to an expect with an error message that is meaningful to you.

If we get the lock on the mutex, we call recv to receive a Job from the channel. A final unwrap moves past any errors here as well, which might occur if the thread holding the sender has shut down, similar to how the send method returns Err if the receiver shuts down.

The call to recv blocks, so if there is no job yet, the current thread will wait until a job becomes available. The Mutex<T> ensures that only one Worker thread at a time is trying to request a job.

Our thread pool is now in a working state! Give it a cargo run and make some requests:

$ cargo run
   Compiling hello v0.1.0 (file:///projects/hello)
warning: field `workers` is never read
 --> src/lib.rs:7:5
  |
6 | pub struct ThreadPool {
  |            ---------- field in this struct
7 |     workers: Vec<Worker>,
  |     ^^^^^^^
  |
  = note: `#[warn(dead_code)]` on by default

warning: fields `id` and `thread` are never read
  --> src/lib.rs:48:5
   |
47 | struct Worker {
   |        ------ fields in this struct
48 |     id: usize,
   |     ^^
49 |     thread: thread::JoinHandle<()>,
   |     ^^^^^^

warning: `hello` (lib) generated 2 warnings
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 4.91s
     Running `target/debug/hello`
Worker 0 got a job; executing.
Worker 2 got a job; executing.
Worker 1 got a job; executing.
Worker 3 got a job; executing.
Worker 0 got a job; executing.
Worker 2 got a job; executing.
Worker 1 got a job; executing.
Worker 3 got a job; executing.
Worker 0 got a job; executing.
Worker 2 got a job; executing.

Success! We now have a thread pool that executes connections asynchronously. There are never more than four threads created, so our system won’t get overloaded if the server receives a lot of requests. If we make a request to /sleep, the server will be able to serve other requests by having another thread run them.

Note: If you open /sleep in multiple browser windows simultaneously, they might load one at a time in five-second intervals. Some web browsers execute multiple instances of the same request sequentially for caching reasons. This limitation is not caused by our web server.

This is a good time to pause and consider how the code in Listings 21-18, 21-19, and 21-20 would be different if we were using futures instead of a closure for the work to be done. What types would change? How would the method signatures be different, if at all? What parts of the code would stay the same?

After learning about the while let loop in Chapter 17 and Chapter 19, you might be wondering why we didn’t write the Worker thread code as shown in Listing 21-21.

Filename: src/lib.rs
use std::{
    sync::{Arc, Mutex, mpsc},
    thread,
};

pub struct ThreadPool {
    workers: Vec<Worker>,
    sender: mpsc::Sender<Job>,
}

type Job = Box<dyn FnOnce() + Send + 'static>;

impl ThreadPool {
    /// Create a new ThreadPool.
    ///
    /// The size is the number of threads in the pool.
    ///
    /// # Panics
    ///
    /// The `new` function will panic if the size is zero.
    pub fn new(size: usize) -> ThreadPool {
        assert!(size > 0);

        let (sender, receiver) = mpsc::channel();

        let receiver = Arc::new(Mutex::new(receiver));

        let mut workers = Vec::with_capacity(size);

        for id in 0..size {
            workers.push(Worker::new(id, Arc::clone(&receiver)));
        }

        ThreadPool { workers, sender }
    }

    pub fn execute<F>(&self, f: F)
    where
        F: FnOnce() + Send + 'static,
    {
        let job = Box::new(f);

        self.sender.send(job).unwrap();
    }
}

struct Worker {
    id: usize,
    thread: thread::JoinHandle<()>,
}
// --snip--

impl Worker {
    fn new(id: usize, receiver: Arc<Mutex<mpsc::Receiver<Job>>>) -> Worker {
        let thread = thread::spawn(move || {
            while let Ok(job) = receiver.lock().unwrap().recv() {
                println!("Worker {id} got a job; executing.");

                job();
            }
        });

        Worker { id, thread }
    }
}
Listing 21-21: An alternative implementation of Worker::new using while let

This code compiles and runs but doesn’t result in the desired threading behavior: A slow request will still cause other requests to wait to be processed. The reason is somewhat subtle: The Mutex struct has no public unlock method because the ownership of the lock is based on the lifetime of the MutexGuard<T> within the LockResult<MutexGuard<T>> that the lock method returns. At compile time, the borrow checker can then enforce the rule that a resource guarded by a Mutex cannot be accessed unless we hold the lock. However, this implementation can also result in the lock being held longer than intended if we aren’t mindful of the lifetime of the MutexGuard<T>.

The code in Listing 21-20 that uses let job = receiver.lock().unwrap().recv().unwrap(); works because with let, any temporary values used in the expression on the right-hand side of the equal sign are immediately dropped when the let statement ends. However, while let (and if let and match) does not drop temporary values until the end of the associated block. In Listing 21-21, the lock remains held for the duration of the call to job(), meaning other Worker instances cannot receive jobs.

Корректное завершение работы и очистка ресурсов

Graceful Shutdown and Cleanup

The code in Listing 21-20 is responding to requests asynchronously through the use of a thread pool, as we intended. We get some warnings about the workers, id, and thread fields that we’re not using in a direct way that reminds us we’re not cleaning up anything. When we use the less elegant ctrl-C method to halt the main thread, all other threads are stopped immediately as well, even if they’re in the middle of serving a request.

Next, then, we’ll implement the Drop trait to call join on each of the threads in the pool so that they can finish the requests they’re working on before closing. Then, we’ll implement a way to tell the threads they should stop accepting new requests and shut down. To see this code in action, we’ll modify our server to accept only two requests before gracefully shutting down its thread pool.

One thing to notice as we go: None of this affects the parts of the code that handle executing the closures, so everything here would be the same if we were using a thread pool for an async runtime.

Implementing the Drop Trait on ThreadPool

Let’s start with implementing Drop on our thread pool. When the pool is dropped, our threads should all join to make sure they finish their work. Listing 21-22 shows a first attempt at a Drop implementation; this code won’t quite work yet.

Filename: src/lib.rs
use std::{
    sync::{Arc, Mutex, mpsc},
    thread,
};

pub struct ThreadPool {
    workers: Vec<Worker>,
    sender: mpsc::Sender<Job>,
}

type Job = Box<dyn FnOnce() + Send + 'static>;

impl ThreadPool {
    /// Create a new ThreadPool.
    ///
    /// The size is the number of threads in the pool.
    ///
    /// # Panics
    ///
    /// The `new` function will panic if the size is zero.
    pub fn new(size: usize) -> ThreadPool {
        assert!(size > 0);

        let (sender, receiver) = mpsc::channel();

        let receiver = Arc::new(Mutex::new(receiver));

        let mut workers = Vec::with_capacity(size);

        for id in 0..size {
            workers.push(Worker::new(id, Arc::clone(&receiver)));
        }

        ThreadPool { workers, sender }
    }

    pub fn execute<F>(&self, f: F)
    where
        F: FnOnce() + Send + 'static,
    {
        let job = Box::new(f);

        self.sender.send(job).unwrap();
    }
}

impl Drop for ThreadPool {
    fn drop(&mut self) {
        for worker in &mut self.workers {
            println!("Shutting down worker {}", worker.id);

            worker.thread.join().unwrap();
        }
    }
}

struct Worker {
    id: usize,
    thread: thread::JoinHandle<()>,
}

impl Worker {
    fn new(id: usize, receiver: Arc<Mutex<mpsc::Receiver<Job>>>) -> Worker {
        let thread = thread::spawn(move || {
            loop {
                let job = receiver.lock().unwrap().recv().unwrap();

                println!("Worker {id} got a job; executing.");

                job();
            }
        });

        Worker { id, thread }
    }
}
Listing 21-22: Joining each thread when the thread pool goes out of scope

First, we loop through each of the thread pool workers. We use &mut for this because self is a mutable reference, and we also need to be able to mutate worker. For each worker, we print a message saying that this particular Worker instance is shutting down, and then we call join on that Worker instance’s thread. If the call to join fails, we use unwrap to make Rust panic and go into an ungraceful shutdown.

Here is the error we get when we compile this code:

$ cargo check
    Checking hello v0.1.0 (file:///projects/hello)
error[E0507]: cannot move out of `worker.thread` which is behind a mutable reference
  --> src/lib.rs:52:13
   |
52 |             worker.thread.join().unwrap();
   |             ^^^^^^^^^^^^^ ------ `worker.thread` moved due to this method call
   |             |
   |             move occurs because `worker.thread` has type `JoinHandle<()>`, which does not implement the `Copy` trait
   |
note: `JoinHandle::<T>::join` takes ownership of the receiver `self`, which moves `worker.thread`
  --> /rustc/1159e78c4747b02ef996e55082b704c09b970588/library/std/src/thread/mod.rs:1921:17

For more information about this error, try `rustc --explain E0507`.
error: could not compile `hello` (lib) due to 1 previous error

The error tells us we can’t call join because we only have a mutable borrow of each worker and join takes ownership of its argument. To solve this issue, we need to move the thread out of the Worker instance that owns thread so that join can consume the thread. One way to do this is to take the same approach we took in Listing 18-15. If Worker held an Option<thread::JoinHandle<()>>, we could call the take method on the Option to move the value out of the Some variant and leave a None variant in its place. In other words, a Worker that is running would have a Some variant in thread, and when we wanted to clean up a Worker, we’d replace Some with None so that the Worker wouldn’t have a thread to run.

However, the only time this would come up would be when dropping the Worker. In exchange, we’d have to deal with an Option<thread::JoinHandle<()>> anywhere we accessed worker.thread. Idiomatic Rust uses Option quite a bit, but when you find yourself wrapping something you know will always be present in an Option as a workaround like this, it’s a good idea to look for alternative approaches to make your code cleaner and less error-prone.

In this case, a better alternative exists: the Vec::drain method. It accepts a range parameter to specify which items to remove from the vector and returns an iterator of those items. Passing the .. range syntax will remove every value from the vector.

So, we need to update the ThreadPool drop implementation like this:

Filename: src/lib.rs
#![allow(unused)]
fn main() {
use std::{
    sync::{Arc, Mutex, mpsc},
    thread,
};

pub struct ThreadPool {
    workers: Vec<Worker>,
    sender: mpsc::Sender<Job>,
}

type Job = Box<dyn FnOnce() + Send + 'static>;

impl ThreadPool {
    /// Create a new ThreadPool.
    ///
    /// The size is the number of threads in the pool.
    ///
    /// # Panics
    ///
    /// The `new` function will panic if the size is zero.
    pub fn new(size: usize) -> ThreadPool {
        assert!(size > 0);

        let (sender, receiver) = mpsc::channel();

        let receiver = Arc::new(Mutex::new(receiver));

        let mut workers = Vec::with_capacity(size);

        for id in 0..size {
            workers.push(Worker::new(id, Arc::clone(&receiver)));
        }

        ThreadPool { workers, sender }
    }

    pub fn execute<F>(&self, f: F)
    where
        F: FnOnce() + Send + 'static,
    {
        let job = Box::new(f);

        self.sender.send(job).unwrap();
    }
}

impl Drop for ThreadPool {
    fn drop(&mut self) {
        for worker in self.workers.drain(..) {
            println!("Shutting down worker {}", worker.id);

            worker.thread.join().unwrap();
        }
    }
}

struct Worker {
    id: usize,
    thread: thread::JoinHandle<()>,
}

impl Worker {
    fn new(id: usize, receiver: Arc<Mutex<mpsc::Receiver<Job>>>) -> Worker {
        let thread = thread::spawn(move || {
            loop {
                let job = receiver.lock().unwrap().recv().unwrap();

                println!("Worker {id} got a job; executing.");

                job();
            }
        });

        Worker { id, thread }
    }
}
}

This resolves the compiler error and does not require any other changes to our code. Note that, because drop can be called when panicking, the unwrap could also panic and cause a double panic, which immediately crashes the program and ends any cleanup in progress. This is fine for an example program, but it isn’t recommended for production code.

Signaling to the Threads to Stop Listening for Jobs

With all the changes we’ve made, our code compiles without any warnings. However, the bad news is that this code doesn’t function the way we want it to yet. The key is the logic in the closures run by the threads of the Worker instances: At the moment, we call join, but that won’t shut down the threads, because they loop forever looking for jobs. If we try to drop our ThreadPool with our current implementation of drop, the main thread will block forever, waiting for the first thread to finish.

To fix this problem, we’ll need a change in the ThreadPool drop implementation and then a change in the Worker loop.

First, we’ll change the ThreadPool drop implementation to explicitly drop the sender before waiting for the threads to finish. Listing 21-23 shows the changes to ThreadPool to explicitly drop sender. Unlike with the thread, here we do need to use an Option to be able to move sender out of ThreadPool with Option::take.

Filename: src/lib.rs
use std::{
    sync::{Arc, Mutex, mpsc},
    thread,
};

pub struct ThreadPool {
    workers: Vec<Worker>,
    sender: Option<mpsc::Sender<Job>>,
}
// --snip--

type Job = Box<dyn FnOnce() + Send + 'static>;

impl ThreadPool {
    /// Create a new ThreadPool.
    ///
    /// The size is the number of threads in the pool.
    ///
    /// # Panics
    ///
    /// The `new` function will panic if the size is zero.
    pub fn new(size: usize) -> ThreadPool {
        // --snip--

        assert!(size > 0);

        let (sender, receiver) = mpsc::channel();

        let receiver = Arc::new(Mutex::new(receiver));

        let mut workers = Vec::with_capacity(size);

        for id in 0..size {
            workers.push(Worker::new(id, Arc::clone(&receiver)));
        }

        ThreadPool {
            workers,
            sender: Some(sender),
        }
    }

    pub fn execute<F>(&self, f: F)
    where
        F: FnOnce() + Send + 'static,
    {
        let job = Box::new(f);

        self.sender.as_ref().unwrap().send(job).unwrap();
    }
}

impl Drop for ThreadPool {
    fn drop(&mut self) {
        drop(self.sender.take());

        for worker in self.workers.drain(..) {
            println!("Shutting down worker {}", worker.id);

            worker.thread.join().unwrap();
        }
    }
}

struct Worker {
    id: usize,
    thread: thread::JoinHandle<()>,
}

impl Worker {
    fn new(id: usize, receiver: Arc<Mutex<mpsc::Receiver<Job>>>) -> Worker {
        let thread = thread::spawn(move || {
            loop {
                let job = receiver.lock().unwrap().recv().unwrap();

                println!("Worker {id} got a job; executing.");

                job();
            }
        });

        Worker { id, thread }
    }
}
Listing 21-23: Explicitly dropping sender before joining the Worker threads

Dropping sender closes the channel, which indicates no more messages will be sent. When that happens, all the calls to recv that the Worker instances do in the infinite loop will return an error. In Listing 21-24, we change the Worker loop to gracefully exit the loop in that case, which means the threads will finish when the ThreadPool drop implementation calls join on them.

Filename: src/lib.rs
use std::{
    sync::{Arc, Mutex, mpsc},
    thread,
};

pub struct ThreadPool {
    workers: Vec<Worker>,
    sender: Option<mpsc::Sender<Job>>,
}

type Job = Box<dyn FnOnce() + Send + 'static>;

impl ThreadPool {
    /// Create a new ThreadPool.
    ///
    /// The size is the number of threads in the pool.
    ///
    /// # Panics
    ///
    /// The `new` function will panic if the size is zero.
    pub fn new(size: usize) -> ThreadPool {
        assert!(size > 0);

        let (sender, receiver) = mpsc::channel();

        let receiver = Arc::new(Mutex::new(receiver));

        let mut workers = Vec::with_capacity(size);

        for id in 0..size {
            workers.push(Worker::new(id, Arc::clone(&receiver)));
        }

        ThreadPool {
            workers,
            sender: Some(sender),
        }
    }

    pub fn execute<F>(&self, f: F)
    where
        F: FnOnce() + Send + 'static,
    {
        let job = Box::new(f);

        self.sender.as_ref().unwrap().send(job).unwrap();
    }
}

impl Drop for ThreadPool {
    fn drop(&mut self) {
        drop(self.sender.take());

        for worker in self.workers.drain(..) {
            println!("Shutting down worker {}", worker.id);

            worker.thread.join().unwrap();
        }
    }
}

struct Worker {
    id: usize,
    thread: thread::JoinHandle<()>,
}

impl Worker {
    fn new(id: usize, receiver: Arc<Mutex<mpsc::Receiver<Job>>>) -> Worker {
        let thread = thread::spawn(move || {
            loop {
                let message = receiver.lock().unwrap().recv();

                match message {
                    Ok(job) => {
                        println!("Worker {id} got a job; executing.");

                        job();
                    }
                    Err(_) => {
                        println!("Worker {id} disconnected; shutting down.");
                        break;
                    }
                }
            }
        });

        Worker { id, thread }
    }
}
Listing 21-24: Explicitly breaking out of the loop when recv returns an error

To see this code in action, let’s modify main to accept only two requests before gracefully shutting down the server, as shown in Listing 21-25.

Filename: src/main.rs
use hello::ThreadPool;
use std::{
    fs,
    io::{BufReader, prelude::*},
    net::{TcpListener, TcpStream},
    thread,
    time::Duration,
};

fn main() {
    let listener = TcpListener::bind("127.0.0.1:7878").unwrap();
    let pool = ThreadPool::new(4);

    for stream in listener.incoming().take(2) {
        let stream = stream.unwrap();

        pool.execute(|| {
            handle_connection(stream);
        });
    }

    println!("Shutting down.");
}

fn handle_connection(mut stream: TcpStream) {
    let buf_reader = BufReader::new(&stream);
    let request_line = buf_reader.lines().next().unwrap().unwrap();

    let (status_line, filename) = match &request_line[..] {
        "GET / HTTP/1.1" => ("HTTP/1.1 200 OK", "hello.html"),
        "GET /sleep HTTP/1.1" => {
            thread::sleep(Duration::from_secs(5));
            ("HTTP/1.1 200 OK", "hello.html")
        }
        _ => ("HTTP/1.1 404 NOT FOUND", "404.html"),
    };

    let contents = fs::read_to_string(filename).unwrap();
    let length = contents.len();

    let response =
        format!("{status_line}\r\nContent-Length: {length}\r\n\r\n{contents}");

    stream.write_all(response.as_bytes()).unwrap();
}
Listing 21-25: Shutting down the server after serving two requests by exiting the loop

You wouldn’t want a real-world web server to shut down after serving only two requests. This code just demonstrates that the graceful shutdown and cleanup is in working order.

The take method is defined in the Iterator trait and limits the iteration to the first two items at most. The ThreadPool will go out of scope at the end of main, and the drop implementation will run.

Start the server with cargo run and make three requests. The third request should error, and in your terminal, you should see output similar to this:

$ cargo run
   Compiling hello v0.1.0 (file:///projects/hello)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.41s
     Running `target/debug/hello`
Worker 0 got a job; executing.
Shutting down.
Shutting down worker 0
Worker 3 got a job; executing.
Worker 1 disconnected; shutting down.
Worker 2 disconnected; shutting down.
Worker 3 disconnected; shutting down.
Worker 0 disconnected; shutting down.
Shutting down worker 1
Shutting down worker 2
Shutting down worker 3

You might see a different ordering of Worker IDs and messages printed. We can see how this code works from the messages: Worker instances 0 and 3 got the first two requests. The server stopped accepting connections after the second connection, and the Drop implementation on ThreadPool starts executing before Worker 3 even starts its job. Dropping the sender disconnects all the Worker instances and tells them to shut down. The Worker instances each print a message when they disconnect, and then the thread pool calls join to wait for each Worker thread to finish.

Notice one interesting aspect of this particular execution: The ThreadPool dropped the sender, and before any Worker received an error, we tried to join Worker 0. Worker 0 had not yet gotten an error from recv, so the main thread blocked, waiting for Worker 0 to finish. In the meantime, Worker 3 received a job and then all threads received an error. When Worker 0 finished, the main thread waited for the rest of the Worker instances to finish. At that point, they had all exited their loops and stopped.

Congrats! We’ve now completed our project; we have a basic web server that uses a thread pool to respond asynchronously. We’re able to perform a graceful shutdown of the server, which cleans up all the threads in the pool.

Here’s the full code for reference:

Filename: src/main.rs
use hello::ThreadPool;
use std::{
    fs,
    io::{BufReader, prelude::*},
    net::{TcpListener, TcpStream},
    thread,
    time::Duration,
};

fn main() {
    let listener = TcpListener::bind("127.0.0.1:7878").unwrap();
    let pool = ThreadPool::new(4);

    for stream in listener.incoming().take(2) {
        let stream = stream.unwrap();

        pool.execute(|| {
            handle_connection(stream);
        });
    }

    println!("Shutting down.");
}

fn handle_connection(mut stream: TcpStream) {
    let buf_reader = BufReader::new(&stream);
    let request_line = buf_reader.lines().next().unwrap().unwrap();

    let (status_line, filename) = match &request_line[..] {
        "GET / HTTP/1.1" => ("HTTP/1.1 200 OK", "hello.html"),
        "GET /sleep HTTP/1.1" => {
            thread::sleep(Duration::from_secs(5));
            ("HTTP/1.1 200 OK", "hello.html")
        }
        _ => ("HTTP/1.1 404 NOT FOUND", "404.html"),
    };

    let contents = fs::read_to_string(filename).unwrap();
    let length = contents.len();

    let response =
        format!("{status_line}\r\nContent-Length: {length}\r\n\r\n{contents}");

    stream.write_all(response.as_bytes()).unwrap();
}
Filename: src/lib.rs
use std::{
    sync::{Arc, Mutex, mpsc},
    thread,
};

pub struct ThreadPool {
    workers: Vec<Worker>,
    sender: Option<mpsc::Sender<Job>>,
}

type Job = Box<dyn FnOnce() + Send + 'static>;

impl ThreadPool {
    /// Create a new ThreadPool.
    ///
    /// The size is the number of threads in the pool.
    ///
    /// # Panics
    ///
    /// The `new` function will panic if the size is zero.
    pub fn new(size: usize) -> ThreadPool {
        assert!(size > 0);

        let (sender, receiver) = mpsc::channel();

        let receiver = Arc::new(Mutex::new(receiver));

        let mut workers = Vec::with_capacity(size);

        for id in 0..size {
            workers.push(Worker::new(id, Arc::clone(&receiver)));
        }

        ThreadPool {
            workers,
            sender: Some(sender),
        }
    }

    pub fn execute<F>(&self, f: F)
    where
        F: FnOnce() + Send + 'static,
    {
        let job = Box::new(f);

        self.sender.as_ref().unwrap().send(job).unwrap();
    }
}

impl Drop for ThreadPool {
    fn drop(&mut self) {
        drop(self.sender.take());

        for worker in &mut self.workers {
            println!("Shutting down worker {}", worker.id);

            if let Some(thread) = worker.thread.take() {
                thread.join().unwrap();
            }
        }
    }
}

struct Worker {
    id: usize,
    thread: Option<thread::JoinHandle<()>>,
}

impl Worker {
    fn new(id: usize, receiver: Arc<Mutex<mpsc::Receiver<Job>>>) -> Worker {
        let thread = thread::spawn(move || {
            loop {
                let message = receiver.lock().unwrap().recv();

                match message {
                    Ok(job) => {
                        println!("Worker {id} got a job; executing.");

                        job();
                    }
                    Err(_) => {
                        println!("Worker {id} disconnected; shutting down.");
                        break;
                    }
                }
            }
        });

        Worker {
            id,
            thread: Some(thread),
        }
    }
}

We could do more here! If you want to continue enhancing this project, here are some ideas:

  • Add more documentation to ThreadPool and its public methods.
  • Add tests of the library’s functionality.
  • Change calls to unwrap to more robust error handling.
  • Use ThreadPool to perform some task other than serving web requests.
  • Find a thread pool crate on crates.io and implement a similar web server using the crate instead. Then, compare its API and robustness to the thread pool we implemented.

Summary

Well done! You’ve made it to the end of the book! We want to thank you for joining us on this tour of Rust. You’re now ready to implement your own Rust projects and help with other people’s projects. Keep in mind that there is a welcoming community of other Rustaceans who would love to help you with any challenges you encounter on your Rust journey.

Приложение

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

A — Ключевые слова

Приложение A: Ключевые слова

Следующие списки содержат ключевые слова, зарезервированные для текущего или будущего использования языком Rust. Поэтому они не могут использоваться в качестве идентификаторов (за исключением сырых идентификаторов (raw identifiers), о которых мы поговорим в разделе «Сырые идентификаторы»). Идентификаторы — это имена функций, переменных, параметров, полей структур, модулей, крейтов, констант, макросов, статических значений, атрибутов, типов, трейтов или времён жизни.

Ключевые слова, используемые в настоящее время

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

  • as: Выполняет примитивное приведение типов (casting), позволяет уточнить конкретный трейт, содержащий элемент, или переименовать элементы в выражениях use.
  • async: Возвращает Future вместо блокировки текущего потока.
  • await: Приостанавливает выполнение до тех пор, пока результат Future не станет готов.
  • break: Немедленно завершает цикл.
  • const: Определяет константные элементы или константные сырые указатели.
  • continue: Переходит к следующей итерации цикла.
  • crate: В пути модуля указывает на корень крейта.
  • dyn: Выполняет динамическую диспетчеризацию для объекта трейта.
  • else: Резервный вариант для конструкций управления потоком if и if let.
  • enum: Определяет перечисление.
  • extern: Связывает внешнюю функцию или переменную.
  • false: Логический литерал ложного значения.
  • fn: Определяет функцию или тип указателя на функцию.
  • for: Выполняет перебор элементов итератора, реализует трейт или определяет время жизни более высокого ранга.
  • if: Выполняет ветвление на основе результата условного выражения.
  • impl: Реализует встроенную (inherent) или трейтовую функциональность.
  • in: Часть синтаксиса цикла for.
  • let: Привязывает значение к переменной.
  • loop: Создаёт безусловный цикл.
  • match: Сопоставляет значение с шаблонами.
  • mod: Определяет модуль.
  • move: Заставляет замыкание получить владение всеми захваченными значениями.
  • mut: Обозначает изменяемость для ссылок, сырых указателей или привязок шаблонов.
  • pub: Обозначает публичную видимость для полей структур, блоков impl или модулей.
  • ref: Выполняет привязку по ссылке.
  • return: Возвращает значение из функции.
  • Self: Псевдоним типа для типа, который мы определяем или реализуем.
  • self: Текущий объект метода или текущий модуль.
  • static: Глобальная переменная или время жизни, существующее в течение выполнения всей программы.
  • struct: Определяет структуру.
  • super: Родительский модуль текущего модуля.
  • trait: Определяет трейт.
  • true: Логический литерал истинного значения.
  • type: Определяет псевдоним типа или связанный тип.
  • union: Определяет union; является ключевым словом только при использовании в объявлении union.
  • unsafe: Обозначает небезопасный код, функции, трейты или реализации.
  • use: Импортирует символы в область видимости.
  • where: Определяет ограничения для типа.
  • while: Выполняет цикл на основе результата выражения.

Ключевые слова, зарезервированные для будущего использования

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

  • abstract
  • become
  • box
  • do
  • final
  • gen
  • macro
  • override
  • priv
  • try
  • typeof
  • unsized
  • virtual
  • yield

Сырые идентификаторы

Сырые идентификаторы (raw identifiers) — это синтаксис, позволяющий использовать ключевые слова там, где это обычно запрещено. Для использования сырого идентификатора перед ключевым словом добавляется префикс r#.

Например, match является ключевым словом. Если попытаться скомпилировать следующую функцию, использующую match в качестве имени:

Имя файла: src/main.rs

fn match(needle: &str, haystack: &str) -> bool {
    haystack.contains(needle)
}

вы получите следующую ошибку:

error: expected identifier, found keyword `match`
 --> src/main.rs:4:4
  |
4 | fn match(needle: &str, haystack: &str) -> bool {
  |    ^^^^^ expected identifier, found keyword

Ошибка показывает, что ключевое слово match нельзя использовать в качестве идентификатора функции. Чтобы использовать match как имя функции, необходимо применить синтаксис сырого идентификатора:

Имя файла: src/main.rs

fn r#match(needle: &str, haystack: &str) -> bool {
    haystack.contains(needle)
}

fn main() {
    assert!(r#match("foo", "foobar"));
}

Этот код скомпилируется без ошибок. Обратите внимание на префикс r# у имени функции как при её определении, так и при вызове функции в main.

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

Кроме того, сырые идентификаторы позволяют использовать библиотеки, написанные для другой редакции Rust (edition), чем используется в вашем крейте. Например, try не является ключевым словом в редакции 2015, но становится таковым в редакциях 2018, 2021 и 2024. Если ваш проект зависит от библиотеки, написанной для редакции 2015 и содержащей функцию try, вам потребуется использовать синтаксис сырого идентификатора — в данном случае r#try — чтобы вызвать эту функцию из вашего кода в более новых редакциях.

См. Приложение E для получения дополнительной информации о редакциях Rust.

B — Операторы и символы

Приложение B: Операторы и символы

Это приложение содержит глоссарий синтаксиса Rust, включая операторы и другие символы, которые используются как самостоятельно, так и в контексте путей, дженериков (generics), ограничений трейтов (trait bounds), макросов, атрибутов, комментариев, кортежей и скобок.

Операторы

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

Таблица B-1: Операторы

ОператорПримерОбъяснениеПерегружаемый?
!ident!(...), ident!{...}, ident![...]Разворачивание макроса
!!exprПобитовое или логическое отрицаниеNot
!=expr != exprПроверка на неравенствоPartialEq
%expr % exprОстаток от деленияRem
%=var %= exprОстаток от деления с присваиваниемRemAssign
&&expr, &mut exprЗаимствование
&&type, &mut type, &'a type, &'a mut typeТип заимствованного указателя
&expr & exprПобитовое ИBitAnd
&=var &= exprПобитовое И с присваиваниемBitAndAssign
&&expr && exprЛогическое И с коротким замыканием
*expr * exprАрифметическое умножениеMul
*=var *= exprАрифметическое умножение с присваиваниемMulAssign
**exprРазыменованиеDeref
**const type, *mut typeСырой указатель
+trait + trait, 'a + traitСоставное ограничение типа
+expr + exprАрифметическое сложениеAdd
+=var += exprАрифметическое сложение с присваиваниемAddAssign
,expr, exprРазделитель аргументов и элементов
-- exprАрифметическое отрицаниеNeg
-expr - exprАрифметическое вычитаниеSub
-=var -= exprАрифметическое вычитание с присваиваниемSubAssign
->fn(...) -> type, |…| -> typeВозвращаемый тип функции или замыкания
.expr.identДоступ к полю
.expr.ident(expr, ...)Вызов метода
.expr.0, expr.1, and so onИндексация кортежа
...., expr.., ..expr, expr..exprЛитерал диапазона без включения правой границыPartialOrd
..=..=expr, expr..=exprЛитерал диапазона с включением правой границыPartialOrd
....exprСинтаксис обновления литерала структуры
..variant(x, ..), struct_type { x, .. }Шаблон «и всё остальное»
...expr...expr(Устарело, используйте ..=) Шаблон диапазона с включением границы
/expr / exprАрифметическое делениеDiv
/=var /= exprАрифметическое деление с присваиваниемDivAssign
:pat: type, ident: typeОграничения
:ident: exprИнициализация поля структуры
:'a: loop {...}Метка цикла
;expr;Завершение инструкции или элемента
;[...; len]Часть синтаксиса массива фиксированного размера
<<expr << exprСдвиг влевоShl
<<=var <<= exprСдвиг влево с присваиваниемShlAssign
<expr < exprСравнение «меньше чем»PartialOrd
<=expr <= exprСравнение «меньше либо равно»PartialOrd
=var = expr, ident = typeПрисваивание/эквивалентность
==expr == exprПроверка на равенствоPartialEq
=>pat => exprЧасть синтаксиса ветви match
>expr > exprСравнение «больше чем»PartialOrd
>=expr >= exprСравнение «больше либо равно»PartialOrd
>>expr >> exprСдвиг вправоShr
>>=var >>= exprСдвиг вправо с присваиваниемShrAssign
@ident @ patПривязка шаблона
^expr ^ exprПобитовое исключающее ИЛИBitXor
^=var ^= exprПобитовое исключающее ИЛИ с присваиваниемBitXorAssign
|pat | patАльтернативы шаблона
|expr | exprПобитовое ИЛИBitOr
|=var |= exprПобитовое ИЛИ с присваиваниемBitOrAssign
||expr || exprЛогическое ИЛИ с коротким замыканием
?expr?Распространение ошибок

Символы, не являющиеся операторами

Следующие таблицы содержат все символы, которые не функционируют как операторы; то есть они не ведут себя как вызов функции или метода.

Таблица B-2 показывает символы, которые используются самостоятельно и допустимы в различных местах.

Таблица B-2: Независимый синтаксис

СимволОбъяснение
'identИменованное время жизни или метка цикла
Digits immediately followed by u8, i32, f64, usize, and so onЧисловой литерал конкретного типа
"..."Строковый литерал
r"...", r#"..."#, r##"..."##, and so onСырой строковый литерал; escape-последовательности не обрабатываются
b"..."Байтовый строковый литерал; создаёт массив байтов вместо строки
br"...", br#"..."#, br##"..."##, and so onСырой байтовый литерал; комбинация сырого и байтового литералов
'...'Символьный литерал
b'...'ASCII-байтовый литерал
|…| exprЗамыкание
!Всегда пустой нижний тип для расходящихся функций
_«Игнорируемая» привязка шаблона; также используется для читаемости чисел

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

Таблица B-3: Синтаксис путей

СимволОбъяснение
ident::identПуть пространства имён
::pathПуть относительно корня крейта (то есть явно абсолютный путь)
self::pathПуть относительно текущего модуля (то есть явно относительный путь)
super::pathПуть относительно родительского модуля текущего модуля
type::ident, <type as trait>::identСвязанные константы, функции и типы
<type>::...Связанный элемент типа, который невозможно назвать напрямую (например, <&T>::..., <[T]>::... и т.д.)
trait::method(...)Уточнение вызова метода через указание трейта, который его определяет
type::method(...)Уточнение вызова метода через указание типа, для которого он определён
<type as trait>::method(...)Уточнение вызова метода через указание трейта и типа

Таблица B-4 показывает символы, которые используются в контексте параметров дженериков.

Таблица B-4: Дженерики

СимволОбъяснение
path<...>Указывает параметры дженерик-типа (например, Vec<u8>)
path::<...>, method::<...>Указывает параметры дженерик-типа, функции или метода в выражении; часто называется turbofish (например, "42".parse::<i32>())
fn ident<...> ...Определение дженерик-функции
struct ident<...> ...Определение дженерик-структуры
enum ident<...> ...Определение дженерик-перечисления
impl<...> ...Определение дженерик-реализации
for<...> typeОграничения времени жизни более высокого ранга
type<ident=type>Дженерик-тип, где один или несколько связанных типов имеют конкретные значения (например, Iterator<Item=T>)

Таблица B-5 показывает символы, которые используются в контексте ограничения параметров дженерик-типов через ограничения трейтов (trait bounds).

Таблица B-5: Ограничения трейтов

СимволОбъяснение
T: UДженерик-параметр T ограничен типами, реализующими U
T: 'aДженерик-тип T должен жить дольше времени жизни 'a (то есть тип не может транзитивно содержать ссылки со временем жизни короче 'a)
T: 'staticДженерик-тип T не содержит заимствованных ссылок, кроме 'static
'b: 'aВремя жизни 'b должно жить дольше времени жизни 'a
T: ?SizedРазрешает параметру дженерик-типа быть типом динамического размера
'a + trait, trait + traitСоставное ограничение типа

Таблица B-6 показывает символы, используемые в контексте вызова или определения макросов и задания атрибутов для элемента.

Таблица B-6: Макросы и атрибуты

СимволОбъяснение
#[meta]Внешний атрибут
#![meta]Внутренний атрибут
$identПодстановка макроса
$ident:kindМетапеременная макроса
$(...)...Повторение макроса
ident!(...), ident!{...}, ident![...]Вызов макроса

Таблица B-7 показывает символы, создающие комментарии.

Таблица B-7: Комментарии

СимволОбъяснение
//Однострочный комментарий
//!Внутренний однострочный doc-комментарий
///Внешний однострочный doc-комментарий
/*...*/Блочный комментарий
/*!...*/Внутренний блочный doc-комментарий
/**...*/Внешний блочный doc-комментарий

Таблица B-8 показывает контексты, в которых используются круглые скобки.

Таблица B-8: Круглые скобки

СимволОбъяснение
()Пустой кортеж (также известный как unit), как литерал и как тип
(expr)Выражение в круглых скобках
(expr,)Выражение кортежа с одним элементом
(type,)Тип кортежа с одним элементом
(expr, ...)Выражение кортежа
(type, ...)Тип кортежа
expr(expr, ...)Выражение вызова функции; также используется для инициализации кортежных struct и вариантов enum

Таблица B-9 показывает контексты, в которых используются фигурные скобки.

Таблица B-9: Фигурные скобки

КонтекстОбъяснение
{...}Блочное выражение
Type {...}Литерал структуры

Таблица B-10 показывает контексты, в которых используются квадратные скобки.

Таблица B-10: Квадратные скобки

КонтекстОбъяснение
[...]Литерал массива
[expr; len]Литерал массива, содержащий len копий expr
[type; len]Тип массива, содержащий len экземпляров type
expr[expr]Индексация коллекции; поддерживает перегрузку (Index, IndexMut)
expr[..], expr[a..], expr[..b], expr[a..b]Индексация коллекции с имитацией среза коллекции через Range, RangeFrom, RangeTo или RangeFull в качестве «индекса»

C — Автоматически выводимые трейты

Appendix C: Derivable Traits

In various places in the book, we’ve discussed the derive attribute, which you can apply to a struct or enum definition. The derive attribute generates code that will implement a trait with its own default implementation on the type you’ve annotated with the derive syntax.

In this appendix, we provide a reference of all the traits in the standard library that you can use with derive. Each section covers:

  • What operators and methods deriving this trait will enable
  • What the implementation of the trait provided by derive does
  • What implementing the trait signifies about the type
  • The conditions in which you’re allowed or not allowed to implement the trait
  • Examples of operations that require the trait

If you want different behavior from that provided by the derive attribute, consult the standard library documentation for each trait for details on how to manually implement them.

The traits listed here are the only ones defined by the standard library that can be implemented on your types using derive. Other traits defined in the standard library don’t have sensible default behavior, so it’s up to you to implement them in the way that makes sense for what you’re trying to accomplish.

An example of a trait that can’t be derived is Display, which handles formatting for end users. You should always consider the appropriate way to display a type to an end user. What parts of the type should an end user be allowed to see? What parts would they find relevant? What format of the data would be most relevant to them? The Rust compiler doesn’t have this insight, so it can’t provide appropriate default behavior for you.

The list of derivable traits provided in this appendix is not comprehensive: Libraries can implement derive for their own traits, making the list of traits you can use derive with truly open ended. Implementing derive involves using a procedural macro, which is covered in the “Custom derive Macros” section in Chapter 20.

Debug for Programmer Output

The Debug trait enables debug formatting in format strings, which you indicate by adding :? within {} placeholders.

The Debug trait allows you to print instances of a type for debugging purposes, so you and other programmers using your type can inspect an instance at a particular point in a program’s execution.

The Debug trait is required, for example, in the use of the assert_eq! macro. This macro prints the values of instances given as arguments if the equality assertion fails so that programmers can see why the two instances weren’t equal.

PartialEq and Eq for Equality Comparisons

The PartialEq trait allows you to compare instances of a type to check for equality and enables use of the == and != operators.

Deriving PartialEq implements the eq method. When PartialEq is derived on structs, two instances are equal only if all fields are equal, and the instances are not equal if any fields are not equal. When derived on enums, each variant is equal to itself and not equal to the other variants.

The PartialEq trait is required, for example, with the use of the assert_eq! macro, which needs to be able to compare two instances of a type for equality.

The Eq trait has no methods. Its purpose is to signal that for every value of the annotated type, the value is equal to itself. The Eq trait can only be applied to types that also implement PartialEq, although not all types that implement PartialEq can implement Eq. One example of this is floating-point number types: The implementation of floating-point numbers states that two instances of the not-a-number (NaN) value are not equal to each other.

An example of when Eq is required is for keys in a HashMap<K, V> so that the HashMap<K, V> can tell whether two keys are the same.

PartialOrd and Ord for Ordering Comparisons

The PartialOrd trait allows you to compare instances of a type for sorting purposes. A type that implements PartialOrd can be used with the <, >, <=, and >= operators. You can only apply the PartialOrd trait to types that also implement PartialEq.

Deriving PartialOrd implements the partial_cmp method, which returns an Option<Ordering> that will be None when the values given don’t produce an ordering. An example of a value that doesn’t produce an ordering, even though most values of that type can be compared, is the NaN floating point value. Calling partial_cmp with any floating-point number and the NaN floating-point value will return None.

When derived on structs, PartialOrd compares two instances by comparing the value in each field in the order in which the fields appear in the struct definition. When derived on enums, variants of the enum declared earlier in the enum definition are considered less than the variants listed later.

The PartialOrd trait is required, for example, for the gen_range method from the rand crate that generates a random value in the range specified by a range expression.

The Ord trait allows you to know that for any two values of the annotated type, a valid ordering will exist. The Ord trait implements the cmp method, which returns an Ordering rather than an Option<Ordering> because a valid ordering will always be possible. You can only apply the Ord trait to types that also implement PartialOrd and Eq (and Eq requires PartialEq). When derived on structs and enums, cmp behaves the same way as the derived implementation for partial_cmp does with PartialOrd.

An example of when Ord is required is when storing values in a BTreeSet<T>, a data structure that stores data based on the sort order of the values.

Clone and Copy for Duplicating Values

The Clone trait allows you to explicitly create a deep copy of a value, and the duplication process might involve running arbitrary code and copying heap data. See the “Variables and Data Interacting with Clone” section in Chapter 4 for more information on Clone.

Deriving Clone implements the clone method, which when implemented for the whole type, calls clone on each of the parts of the type. This means all the fields or values in the type must also implement Clone to derive Clone.

An example of when Clone is required is when calling the to_vec method on a slice. The slice doesn’t own the type instances it contains, but the vector returned from to_vec will need to own its instances, so to_vec calls clone on each item. Thus, the type stored in the slice must implement Clone.

The Copy trait allows you to duplicate a value by only copying bits stored on the stack; no arbitrary code is necessary. See the “Stack-Only Data: Copy” section in Chapter 4 for more information on Copy.

The Copy trait doesn’t define any methods to prevent programmers from overloading those methods and violating the assumption that no arbitrary code is being run. That way, all programmers can assume that copying a value will be very fast.

You can derive Copy on any type whose parts all implement Copy. A type that implements Copy must also implement Clone because a type that implements Copy has a trivial implementation of Clone that performs the same task as Copy.

The Copy trait is rarely required; types that implement Copy have optimizations available, meaning you don’t have to call clone, which makes the code more concise.

Everything possible with Copy you can also accomplish with Clone, but the code might be slower or have to use clone in places.

Hash for Mapping a Value to a Value of Fixed Size

The Hash trait allows you to take an instance of a type of arbitrary size and map that instance to a value of fixed size using a hash function. Deriving Hash implements the hash method. The derived implementation of the hash method combines the result of calling hash on each of the parts of the type, meaning all fields or values must also implement Hash to derive Hash.

An example of when Hash is required is in storing keys in a HashMap<K, V> to store data efficiently.

Default for Default Values

The Default trait allows you to create a default value for a type. Deriving Default implements the default function. The derived implementation of the default function calls the default function on each part of the type, meaning all fields or values in the type must also implement Default to derive Default.

The Default::default function is commonly used in combination with the struct update syntax discussed in the “Creating Instances from Other Instances with Struct Update Syntax” section in Chapter 5. You can customize a few fields of a struct and then set and use a default value for the rest of the fields by using ..Default::default().

The Default trait is required when you use the method unwrap_or_default on Option<T> instances, for example. If the Option<T> is None, the method unwrap_or_default will return the result of Default::default for the type T stored in the Option<T>.

D — Полезные инструменты разработки

Appendix D: Useful Development Tools

In this appendix, we talk about some useful development tools that the Rust project provides. We’ll look at automatic formatting, quick ways to apply warning fixes, a linter, and integrating with IDEs.

Automatic Formatting with rustfmt

The rustfmt tool reformats your code according to the community code style. Many collaborative projects use rustfmt to prevent arguments about which style to use when writing Rust: Everyone formats their code using the tool.

Rust installations include rustfmt by default, so you should already have the programs rustfmt and cargo-fmt on your system. These two commands are analogous to rustc and cargo in that rustfmt allows finer grained control and cargo-fmt understands conventions of a project that uses Cargo. To format any Cargo project, enter the following:

$ cargo fmt

Running this command reformats all the Rust code in the current crate. This should only change the code style, not the code semantics. For more information on rustfmt, see its documentation.

Fix Your Code with rustfix

The rustfix tool is included with Rust installations and can automatically fix compiler warnings that have a clear way to correct the problem that’s likely what you want. You’ve probably seen compiler warnings before. For example, consider this code:

Filename: src/main.rs

fn main() {
    let mut x = 42;
    println!("{x}");
}

Here, we’re defining the variable x as mutable, but we never actually mutate it. Rust warns us about that:

$ cargo build
   Compiling myprogram v0.1.0 (file:///projects/myprogram)
warning: variable does not need to be mutable
 --> src/main.rs:2:9
  |
2 |     let mut x = 0;
  |         ----^
  |         |
  |         help: remove this `mut`
  |
  = note: `#[warn(unused_mut)]` on by default

The warning suggests that we remove the mut keyword. We can automatically apply that suggestion using the rustfix tool by running the command cargo fix:

$ cargo fix
    Checking myprogram v0.1.0 (file:///projects/myprogram)
      Fixing src/main.rs (1 fix)
    Finished dev [unoptimized + debuginfo] target(s) in 0.59s

When we look at src/main.rs again, we’ll see that cargo fix has changed the code:

Filename: src/main.rs

fn main() {
    let x = 42;
    println!("{x}");
}

The variable x is now immutable, and the warning no longer appears.

You can also use the cargo fix command to transition your code between different Rust editions. Editions are covered in Appendix E.

More Lints with Clippy

The Clippy tool is a collection of lints to analyze your code so that you can catch common mistakes and improve your Rust code. Clippy is included with standard Rust installations.

To run Clippy’s lints on any Cargo project, enter the following:

$ cargo clippy

For example, say you write a program that uses an approximation of a mathematical constant, such as pi, as this program does:

Filename: src/main.rs
fn main() {
    let x = 3.1415;
    let r = 8.0;
    println!("the area of the circle is {}", x * r * r);
}

Running cargo clippy on this project results in this error:

error: approximate value of `f{32, 64}::consts::PI` found
 --> src/main.rs:2:13
  |
2 |     let x = 3.1415;
  |             ^^^^^^
  |
  = note: `#[deny(clippy::approx_constant)]` on by default
  = help: consider using the constant directly
  = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#approx_constant

This error lets you know that Rust already has a more precise PI constant defined, and that your program would be more correct if you used the constant instead. You would then change your code to use the PI constant.

The following code doesn’t result in any errors or warnings from Clippy:

Filename: src/main.rs
fn main() {
    let x = std::f64::consts::PI;
    let r = 8.0;
    println!("the area of the circle is {}", x * r * r);
}

For more information on Clippy, see its documentation.

IDE Integration Using rust-analyzer

To help with IDE integration, the Rust community recommends using rust-analyzer. This tool is a set of compiler-centric utilities that speak Language Server Protocol, which is a specification for IDEs and programming languages to communicate with each other. Different clients can use rust-analyzer, such as the Rust analyzer plug-in for Visual Studio Code.

Visit the rust-analyzer project’s home page for installation instructions, then install the language server support in your particular IDE. Your IDE will gain capabilities such as autocompletion, jump to definition, and inline errors.

E — Редакции Rust

Appendix E: Editions

In Chapter 1, you saw that cargo new adds a bit of metadata to your Cargo.toml file about an edition. This appendix talks about what that means!

The Rust language and compiler have a six-week release cycle, meaning users get a constant stream of new features. Other programming languages release larger changes less often; Rust releases smaller updates more frequently. After a while, all of these tiny changes add up. But from release to release, it can be difficult to look back and say, “Wow, between Rust 1.10 and Rust 1.31, Rust has changed a lot!”

Every three years or so, the Rust team produces a new Rust edition. Each edition brings together the features that have landed into a clear package with fully updated documentation and tooling. New editions ship as part of the usual six-week release process.

Editions serve different purposes for different people:

  • For active Rust users, a new edition brings together incremental changes into an easy-to-understand package.
  • For non-users, a new edition signals that some major advancements have landed, which might make Rust worth another look.
  • For those developing Rust, a new edition provides a rallying point for the project as a whole.

At the time of this writing, four Rust editions are available: Rust 2015, Rust 2018, Rust 2021, and Rust 2024. This book is written using Rust 2024 edition idioms.

The edition key in Cargo.toml indicates which edition the compiler should use for your code. If the key doesn’t exist, Rust uses 2015 as the edition value for backward compatibility reasons.

Each project can opt in to an edition other than the default 2015 edition. Editions can contain incompatible changes, such as including a new keyword that conflicts with identifiers in code. However, unless you opt in to those changes, your code will continue to compile even as you upgrade the Rust compiler version you use.

All Rust compiler versions support any edition that existed prior to that compiler’s release, and they can link crates of any supported editions together. Edition changes only affect the way the compiler initially parses code. Therefore, if you’re using Rust 2015 and one of your dependencies uses Rust 2018, your project will compile and be able to use that dependency. The opposite situation, where your project uses Rust 2018 and a dependency uses Rust 2015, works as well.

To be clear: Most features will be available on all editions. Developers using any Rust edition will continue to see improvements as new stable releases are made. However, in some cases, mainly when new keywords are added, some new features might only be available in later editions. You will need to switch editions if you want to take advantage of such features.

For more details, see The Rust Edition Guide. This is a complete book that enumerates the differences between editions and explains how to automatically upgrade your code to a new edition via cargo fix.

F — Переводы книги

Appendix F: Translations of the Book

For resources in languages other than English. Most are still in progress; see the Translations label to help or let us know about a new translation!

G — Как создаётся Rust и «Nightly Rust»

Appendix G - How Rust is Made and “Nightly Rust”

This appendix is about how Rust is made and how that affects you as a Rust developer.

Stability Without Stagnation

As a language, Rust cares a lot about the stability of your code. We want Rust to be a rock-solid foundation you can build on, and if things were constantly changing, that would be impossible. At the same time, if we can’t experiment with new features, we may not find out important flaws until after their release, when we can no longer change things.

Our solution to this problem is what we call “stability without stagnation”, and our guiding principle is this: you should never have to fear upgrading to a new version of stable Rust. Each upgrade should be painless, but should also bring you new features, fewer bugs, and faster compile times.

Choo, Choo! Release Channels and Riding the Trains

Rust development operates on a train schedule. That is, all development is done in the main branch of the Rust repository. Releases follow a software release train model, which has been used by Cisco IOS and other software projects. There are three release channels for Rust:

  • Nightly
  • Beta
  • Stable

Most Rust developers primarily use the stable channel, but those who want to try out experimental new features may use nightly or beta.

Here’s an example of how the development and release process works: let’s assume that the Rust team is working on the release of Rust 1.5. That release happened in December of 2015, but it will provide us with realistic version numbers. A new feature is added to Rust: a new commit lands on the main branch. Each night, a new nightly version of Rust is produced. Every day is a release day, and these releases are created by our release infrastructure automatically. So as time passes, our releases look like this, once a night:

nightly: * - - * - - *

Every six weeks, it’s time to prepare a new release! The beta branch of the Rust repository branches off from the main branch used by nightly. Now, there are two releases:

nightly: * - - * - - *
                     |
beta:                *

Most Rust users do not use beta releases actively, but test against beta in their CI system to help Rust discover possible regressions. In the meantime, there’s still a nightly release every night:

nightly: * - - * - - * - - * - - *
                     |
beta:                *

Let’s say a regression is found. Good thing we had some time to test the beta release before the regression snuck into a stable release! The fix is applied to the main branch, so that nightly is fixed, and then the fix is backported to the beta branch, and a new release of beta is produced:

nightly: * - - * - - * - - * - - * - - *
                     |
beta:                * - - - - - - - - *

Six weeks after the first beta was created, it’s time for a stable release! The stable branch is produced from the beta branch:

nightly: * - - * - - * - - * - - * - - * - * - *
                     |
beta:                * - - - - - - - - *
                                       |
stable:                                *

Hooray! Rust 1.5 is done! However, we’ve forgotten one thing: because the six weeks have gone by, we also need a new beta of the next version of Rust, 1.6. So after stable branches off of beta, the next version of beta branches off of nightly again:

nightly: * - - * - - * - - * - - * - - * - * - *
                     |                         |
beta:                * - - - - - - - - *       *
                                       |
stable:                                *

This is called the “train model” because every six weeks, a release “leaves the station”, but still has to take a journey through the beta channel before it arrives as a stable release.

Rust releases every six weeks, like clockwork. If you know the date of one Rust release, you can know the date of the next one: it’s six weeks later. A nice aspect of having releases scheduled every six weeks is that the next train is coming soon. If a feature happens to miss a particular release, there’s no need to worry: another one is happening in a short time! This helps reduce pressure to sneak possibly unpolished features in close to the release deadline.

Thanks to this process, you can always check out the next build of Rust and verify for yourself that it’s easy to upgrade to: if a beta release doesn’t work as expected, you can report it to the team and get it fixed before the next stable release happens! Breakage in a beta release is relatively rare, but rustc is still a piece of software, and bugs do exist.

Maintenance time

The Rust project supports the most recent stable version. When a new stable version is released, the old version reaches its end of life (EOL). This means each version is supported for six weeks.

Unstable Features

There’s one more catch with this release model: unstable features. Rust uses a technique called “feature flags” to determine what features are enabled in a given release. If a new feature is under active development, it lands on the main branch, and therefore, in nightly, but behind a feature flag. If you, as a user, wish to try out the work-in-progress feature, you can, but you must be using a nightly release of Rust and annotate your source code with the appropriate flag to opt in.

If you’re using a beta or stable release of Rust, you can’t use any feature flags. This is the key that allows us to get practical use with new features before we declare them stable forever. Those who wish to opt into the bleeding edge can do so, and those who want a rock-solid experience can stick with stable and know that their code won’t break. Stability without stagnation.

This book only contains information about stable features, as in-progress features are still changing, and surely they’ll be different between when this book was written and when they get enabled in stable builds. You can find documentation for nightly-only features online.

Rustup and the Role of Rust Nightly

Rustup makes it easy to change between different release channels of Rust, on a global or per-project basis. By default, you’ll have stable Rust installed. To install nightly, for example:

$ rustup toolchain install nightly

You can see all of the toolchains (releases of Rust and associated components) you have installed with rustup as well. Here’s an example on one of your authors’ Windows computer:

> rustup toolchain list
stable-x86_64-pc-windows-msvc (default)
beta-x86_64-pc-windows-msvc
nightly-x86_64-pc-windows-msvc

As you can see, the stable toolchain is the default. Most Rust users use stable most of the time. You might want to use stable most of the time, but use nightly on a specific project, because you care about a cutting-edge feature. To do so, you can use rustup override in that project’s directory to set the nightly toolchain as the one rustup should use when you’re in that directory:

$ cd ~/projects/needs-nightly
$ rustup override set nightly

Now, every time you call rustc or cargo inside of ~/projects/needs-nightly, rustup will make sure that you are using nightly Rust, rather than your default of stable Rust. This comes in handy when you have a lot of Rust projects!

The RFC Process and Teams

So how do you learn about these new features? Rust’s development model follows a Request For Comments (RFC) process. If you’d like an improvement in Rust, you can write up a proposal, called an RFC.

Anyone can write RFCs to improve Rust, and the proposals are reviewed and discussed by the Rust team, which is comprised of many topic subteams. There’s a full list of the teams on Rust’s website, which includes teams for each area of the project: language design, compiler implementation, infrastructure, documentation, and more. The appropriate team reads the proposal and the comments, writes some comments of their own, and eventually, there’s consensus to accept or reject the feature.

If the feature is accepted, an issue is opened on the Rust repository, and someone can implement it. The person who implements it very well may not be the person who proposed the feature in the first place! When the implementation is ready, it lands on the main branch behind a feature gate, as we discussed in the “Unstable Features” section.

After some time, once Rust developers who use nightly releases have been able to try out the new feature, team members will discuss the feature, how it’s worked out on nightly, and decide if it should make it into stable Rust or not. If the decision is to move forward, the feature gate is removed, and the feature is now considered stable! It rides the trains into a new stable release of Rust.