автордың кітабын онлайн тегін оқу Программирование на Rust
Переводчик А. Логунов
Литературный редактор И. Кизилова
Художник В. Мостипан
Корректоры С. Беляева, Н. Сидорова
Стив Клабник, Кэрол Николс
Программирование на Rust. — СПб.: Питер, 2020.
ISBN 978-5-4461-1656-0
© ООО Издательство "Питер", 2020
Все права защищены. Никакая часть данной книги не может быть воспроизведена в какой бы то ни было форме без письменного разрешения владельцев авторских прав.
Вступление
Не сразу очевидно, но язык программирования Rust всецело строится на расширении возможностей: независимо от того, какой код вы пишете сейчас, Rust наделяет вас возможностями достичь большего, уверенно программировать в более широком спектре областей, чем раньше.
Возьмем, например, работу «на системном уровне», которая завязана на низкоуровневые детали управления памятью, представления данных и конкурентности. Традиционно на эту сферу программирования смотрят как на доступную только избранным, которые посвятили годы обучения, чтобы избежать ее печально известных ловушек. И даже те, кто работает в этой области, действуют весьма осторожно, чтобы не подвергать свой код уязвимостям, аварийным сбоям и повреждениям.
Язык Rust разрушает эти барьеры, устраняя старые ловушки и предоставляя дружественный, безупречный набор инструментов, который поможет вам на этом пути. Программисты, которым нужно погрузиться в более низкоуровневое управление, смогут сделать это с помощью языка Rust, не беря на себя привычный риск сбоев или дыр в безопасности и не изучая тонкости переменчивой цепочки инструментов. Более того, данный язык разработан так, чтобы естественным образом направлять вас к надежному коду, который является эффективным с точки зрения скорости и использования памяти.
Программисты, которые уже работают с низкоуровневым кодом, могут использовать Rust для повышения своих амбиций. Например, введение в Rust конкурентности является операцией с относительной невысокой степенью риска: компилятор будет отлавливать типичные ошибки за вас. И вы можете заняться более активной оптимизацией в коде и быть уверенным, что случайно не внесете сбои или уязвимости.
Но Rust не ограничивается программированием низкоуровневых систем. Он является выразительным и эргономичным настолько, что делает приятным написание CLI-приложений, веб-серверов и многих других видов кода — позже в книге вы найдете простые примеры того и другого. Работа с Rust позволяет накапливать навыки, применимые в одной области, и использовать их в другой сфере. Вы можете усвоить язык Rust, написав веб-приложение, а затем применить те же навыки в разработке кода для Raspberry Pi.
Эта книга всеобъемлюще охватывает потенциал языка Rust, наделяя его пользователей расширенными возможностями. Этот доступный текст был задуман для того, чтобы помочь вам повысить не только уровень знаний о языке Rust, но и ваши достижения и уверенность в себе как программиста в целом. Так что погружайтесь, готовьтесь учиться, и добро пожаловать в сообщество языка Rust!
Николас Мацакис и Аарон Турон
Предисловие
Авторы исходят из того, что вы используете язык Rust 1.31.0 или более позднюю версию с опцией edition="2018" во всех проектах Cargo.toml, где применяются идиомы Rust в редакции 2018 года. См. раздел «Установка» для получения информации об установке или обновлении Rust, а также приложение Д, где можно найти сведения о редакциях языка.
Редакция 2018 года языка Rust включает в себя ряд улучшений, которые делают Rust эргономичнее и проще в освоении. Настоящая версия книги содержит ряд изменений, отражающих эти улучшения:
• Глава 7 «Управление растущими проектами с помощью пакетов, упаковок и модулей» была в основном переписана. Система модулей и характер работы путей в редакции 2018 года были сделаны более согласованными.
• В главе 10 появились новые разделы, озаглавленные «Типажи в качестве параметров» и «Возвращение типов, реализующих типажи», которые объясняют новый синтаксис impl Trait.
• В главе 11 есть новый раздел под названием «Использование типа Result<T, E> в тестах», где показаны способы написания тестов, в которых используется оператор ?.
• Раздел «Продвинутые сроки жизни» в главе 19 был удален, поскольку улучшения компилятора сделали конструкции в этом разделе еще более редкими.
• Предыдущее приложение Г «Макрокоманды» было расширено за счет процедурных макрокоманд и перенесено в раздел «Макрокоманды» главы 19.
• Приложение А «Ключевые слова» также объясняет новое языковое средство под названием «сырые идентификаторы», которое позволяет взаимодействовать коду, написанному в редакции 2015 года, c редакцией 2018 года.
• Приложение Г теперь называется «Полезные инструменты разработки», в нем рассказывается о недавно выпущенных инструментах, которые помогают вам писать код Rust.
• В книге мы исправили ряд мелких ошибок и неточных формулировок. Спасибо читателям, которые о них сообщили!
Обратите внимание, что любой код в первом варианте этой книги, который компилировался, будет продолжать компилироваться без опции edition="2018" в Cargo.toml проекта, даже после того как вы обновите используемую вами версию компилятора Rust. Яркий пример гарантии обратной совместимости Rust в действии!
Благодарности
Мы хотели бы поблагодарить всех, кто работал над Rust, за создание удивительного языка, о котором стоит написать книгу. Мы благодарны всем участникам сообщества языка Rust за их радушие и создание среды, достойной того, чтобы пригласить туда еще больше людей.
Мы особенно благодарны всем, кто читал первые редакции этой книги онлайн и давал отзывы, отчеты об ошибках и запросы на включение внесенных изменений. Особая благодарность Эдуарду-Михаю Буртеску и Алексу Крайтону за научное редактирование и Карен Рустад Тельва за обложку. Спасибо нашей команде из издательства No Starch — Биллу Поллоку, Лиз Чедвик и Джанель Людовиз — за то, что они усовершенствовали эту книгу и издали ее.
Стив хотел бы поблагодарить Кэрол за то, что она была замечательным соавтором. Без нее эта книга была бы гораздо менее качественной и написание заняло бы гораздо больше времени. Дополнительная благодарность Эшли Уильямс, которая оказала невероятную поддержку на всех этапах.
Кэрол хотела бы поблагодарить Стива за то, что он пробудил в ней интерес к Rust и дал возможность поработать над этой книгой. Она благодарна своей семье за постоянную любовь и поддержку, в особенности мужу Джейку Гулдингу и дочери Вивиан.
Об авторах
Стив Клабник возглавляет команду по документированию Rust и является одним из ключевых разработчиков языка. Часто выступает с лекциям и пишет много открытого исходного кода. Ранее работал над такими проектами, как Ruby и Ruby on Rails.
Кэрол Николс является членом команды разработчиков Rust Core и соучредителем Integer 32, LLC, первой в мире консалтинговой компании по разработке ПО, ориентированной на Rust. Николс является организатором конференции «Ржавый пояс» (Rust Belt) по языку Rust.
Введение
Добро пожаловать в язык программирования Rust! Он помогает писать более быстрое и надежное программное обеспечение. Высокоуровневая эргономика и низкоуровневое управление часто противоречат друг другу в дизайне языка программирования, но Rust бросает вызов этому конфликту. Обеспечивая сбалансированное сочетание мощных технических возможностей и великолепного опыта разработки программ, язык Rust дает возможность контролировать низкоуровневые детали (например использование памяти) без каких-либо хлопот, традиционно связанных с таким контролем.
Кому подойдет язык Rust
Rust идеально подходит многим людям по целому ряду причин. Давайте взглянем на несколько наиболее важных групп.
Команды разработчиков
Rust оказывается продуктивным инструментом для совместной работы больших команд разработчиков с различным уровнем знаний в области программирования. Низкоуровневый код подвержен множеству едва уловимых багов, которые в большинстве других языков можно обнаружить только путем тщательного тестирования и скрупулезного анализа кода опытными разработчиками. В Rust компилятор играет роль привратника, отказываясь компилировать код с такими неуловимыми багами, включая ошибки конкурентности. Работая бок о бок с компилятором, команда может сосредоточиться не на отлове багов, а на логике программы.
Rust также привносит современные инструменты разработчика в мир системного программирования:
• Включенный в комплект менеджер зависимостей и инструмент сборки Cargo делает добавление, компиляцию и управление зависимостями, безболезненными и согласованными во всей экосистеме Rust.
• Инструмент форматирования исходного кода Rustfmt обеспечивает согласованный стиль написания кода для всех разработчиков.
• Rust Language Server поддерживает интеграцию со средой разработки (IDE), обеспечивая автодополнение кода и построчные сообщения об ошибках.
Используя эти и другие инструменты в экосистеме Rust, разработчики могут продуктивно писать код на системном уровне.
Студенты
Rust предназначен студентам и тем, кто заинтересован в изучении системных понятий. Используя Rust, многие люди узнали о разработке операционных систем. Сообщество является очень гостеприимным и с удовольствием отвечает на вопросы студентов. Благодаря таким проектам, как эта книга, коллективы разработчиков на языке Rust хотят сделать системные понятия доступнее для большего числа людей, в особенности для новичков в программировании.
Компании
Сотни компаний, крупных и малых, используют Rust в производстве для различных задач. Эти задачи включают инструменты командной строки, веб-службы, инструменты DevOps, встраиваемые устройства, аудио- и видеоанализ и транскодирование, криптовалюты, биоинформатику, поисковые системы, приложения Интернета вещей, машинное обучение и даже основные компоненты веб-браузера Firefox.
Разработчики открытого исходного кода
Язык Rust предназначен людям, которые хотят развивать Rust, сообщество, инструменты разработчика и библиотеки. Мы хотели бы, чтобы вы внесли свой вклад в развитие языка.
Люди, ценящие скорость и стабильность
Язык Rust предназначен тем, кто жаждет скорости и стабильности в языке. Под скоростью мы подразумеваем скорость программ, которые вы можете создавать с помощью языка Rust, и скорость, с которой язык Rust позволяет вам их писать. Проверки компилятора языка Rust обеспечивают стабильность за счет добавления функциональности и рефакторинга. Это контрастирует с хрупким привычным кодом на языках, где подобных проверок нет, — и такое положение дел разработчики часто боятся изменять. Стремясь к нулевым по стоимости абстракциям, более высокоуровневым языковым средствам, которые компилируются в более низкоуровневый код так же быстро, как код, написанный вручную, язык Rust стремится сделать безопасный код в том числе и быстрым.
Разработчики Rust также надеются оказывать поддержку многим другим пользователям. Перечисленные здесь лица являются лишь частью наиболее заинтересованных в языке. В целом главная цель Rust — устранить компромиссы, которые программисты принимали на протяжении десятилетий, обеспечив безопасность и производительность, скорость и эргономику. Дайте Rust шанс и посмотрите, подходят ли вам его возможности.
Для кого эта книга
Мы предполагаем, что вы писали код на другом языке программирования, но не делаем никаких допущений относительно того, на каком именно. Мы постарались сделать этот материал доступным для тех, кто имеет широкий спектр навыков программирования. Мы не будем тратить время на разговоры о том, что такое программирование. Если вы в программировании абсолютный новичок, то для начала прочтите введение в программирование.
Как пользоваться этой книгой
В общем-то, авторы этой книги исходят из того, что вы читаете ее последовательно, от начала до конца. Последующие главы строятся на понятиях предыдущих глав, и в начальных главах мы можем не углубляться в детали по конкретной теме; обычно мы возвращаемся к этой теме в дальнейшем.
В этой книге вы найдете два типа глав: концептуальные и проектные. В концептуальных главах вы будете усваивать тот или иной аспект языка. Мы вместе будем создавать небольшие программы, применяя то, что вы уже усвоили. Главы 2, 12 и 20 посвящены разработке проектов, остальные главы — концептуальные.
В главе 1 рассказано, как установить Rust, написать программу «Hello, World!» и использовать пакетный менеджер и инструмент Cargo. Глава 2 представляет собой практическое введение в язык Rust. Здесь мы рассмотрим понятия с точки зрения высокоуровневого языка, а в последующих главах приведем дополнительные подробности. Если вы хотите сразу же приступить к практике, то сможете это сделать. Можно даже пропустить главу 3, в которой рассматриваются средства языка Rust, аналогичные средствам других языков программирования, сразу перейти к главе 4 и узнать о системе владения в Rust. Но если вы дотошны и предпочитаете разбирать каждую деталь, прежде чем переходить к следующей, то можете пропустить главу 2, перейти к главе 3, а затем вернуться к главе 2, когда захотите поработать над проектом. Так вы сможете применить знания, которые освоили.
В главе 5 обсуждаются структуры и методы, а в главе 6 рассматриваются перечисления, выражения match и конструкция управления потоком iflet. Вы будете использовать структуры и перечисления для создания в языке Rust настраиваемых типов.
В главе 7 вы узнаете о системе модулей и правилах конфиденциальности для выстраивания организационной структуры вашего кода и его публичном интерфейсе программирования приложений (API). В главе 8 обсуждаются некоторые часто встречающиеся структуры сбора данных, обеспечиваемые стандартной библиотекой, такие как векторы, строки и хеш-отображения. В главе 9 изучаются философия и методы обработки ошибок.
В главе 10 мы погрузимся в обобщения, типажи и жизненные циклы, которые дают вам возможность определять код, применимый к нескольким типам. Глава 11 полностью посвящена тестированию, которое даже несмотря на гарантии безопасности языка Rust является необходимым для обеспечения правильной логики программы. В главе 12 мы построим собственную реализацию подмножества функциональности инструмента командной строки grep, которая ищет текст внутри файлов. Для этого мы воспользуемся многими понятиями, которые обсуждаются в предыдущих главах.
В главе 13 рассматриваются замыкания и итераторы — средства, которые восходят к функциональным языкам программирования. В главе 14 мы изучим Cargo подробнее и расскажем о лучших практических приемах обмена библиотеками с другими разработчиками. В главе 15 обсуждаются умные указатели, которые обеспечивает стандартная библиотека, и типажи, которые гарантируют их функциональность.
В главе 16 мы рассмотрим разные модели конкурентного программирования и поговорим о том, как Rust помогает вам безбоязненно программировать в множестве потоков исполнения. Глава 17 обращается к сопоставлению идиом Rust с принципами объектно-ориентированного программирования, с которыми вы, возможно, знакомы.
Глава 18 представляет собой справочный материал о паттернах и сопоставлении с паттернами, которые являются мощными способами выражения идей во всех программах на языке Rust. Глава 19 содержит ряд дополнительных тем, представляющих интерес, включая небезопасный код Rust, макрокоманды и другие сведения о типажах, типах, функциях и замыканиях.
В главе 20 мы осуществим проект, в котором выполним реализацию низкоуровневого многопоточного сервера!
Наконец, несколько приложений в конце книги содержат полезную информацию о языке в справочном формате. В приложении А приводятся ключевые слова языка Rust, в приложении Б рассказывается об операторах и символах языка Rust, в приложении В рассматриваются генерируемые типажи, предусмотренные стандартной библиотекой, в приложении Г приводятся некоторые полезные инструменты разработчика, а в приложении Д даются пояснения по поводу редакций языка Rust.
Просто невозможно прочитать эту книгу неправильно: если вы хотите пропустить что-то, пропускайте! В случае если вы почувствуете какую-то путаницу, то, возможно, вам придется вернуться к предыдущим главам. Короче, делайте все, что вам подходит.
Важная часть процесса усвоения языка Rust — научиться читать сообщения об ошибках, выводимые на экран компилятором: они будут направлять вас к рабочему коду. В связи с этим мы приведем много примеров, которые не компилируются вместе с сообщением об ошибке, которое компилятор покажет вам в каждой ситуации. Знайте, что если вы введете и запустите выполнение случайного примера, то он может не скомпилироваться! Обязательно прочтите окружающий текст, чтобы увидеть, что пример, который вы пытаетесь выполнить, является неизбежно ошибочным. В большинстве ситуаций мы будем вести вас к правильной версии любого кода, который не компилируется.
Ресурсы
Эта книга распространяется по лицензии открытого исходного кода и текста. Если вы нашли ошибку, то, пожалуйста, не стесняйтесь добавить сообщение о проблеме или отправить запрос на включение внесенных изменений на GitHub по адресу https://github.com/rust-lang/book/. Более подробную информацию смотрите в CONTRIBUTING.md по адресу https://github.com/rust-lang/book/blob/master/CONTRIBUTING.md.
Исходный код примеров из этой книги, список опечаток и другая информация доступны по адресу https://www.nostarch.com/Rust2018/.
От издательства
Ваши замечания, предложения, вопросы отправляйте по адресу comp@piter.com (издательство «Питер», компьютерная редакция).
Мы будем рады узнать ваше мнение!
На веб-сайте издательства www.piter.com вы найдете подробную информацию о наших книгах.
1. Начало работы
Давайте начнем наше «ржавое»1 путешествие по языку Rust! Здесь есть чему поучиться, но любое путешествие нужно с чего-то начать. В этой главе мы обсудим следующее:
• Установка языка Rust в Linux, macOS и Windows.
• Написание программы, которая выводит Hello, World!.
• Использование cargo, пакетного менеджера и системы сборки языка Rust.
Установка
Первый шаг — это установка языка Rust. Мы скачаем Rust через rustup, инструмент командной строки для управления версиями языка Rust и связанными инструментами. Для скачивания вам понадобится подключение к интернету.
Примечание
Если по какой-либо причине вы предпочитаете не использовать инструмент rustup, то, пожалуйста, обратитесь к веб-странице установки языка Rust по адресу https://www.rust-lang.org/tools/install/, чтобы ознакомиться с другими вариантами.
Следующие шаги позволяют установить последнюю стабильную версию компилятора языка Rust. Гарантии стабильности Rust обеспечивают, что все примеры из книги, которые компилируются, будут продолжать компилироваться с более новыми версиями языка Rust. Выходные данные могут немного отличаться от версии к версии, поскольку язык Rust часто улучшает сообщения об ошибках и предупреждения. Другими словами, любая новая стабильная версия языка Rust, которую вы устанавливаете с помощью этих инструкций, должна работать с содержимым этой книги.
Обозначения командной строки
В этой главе и на протяжении всей книги мы покажем несколько команд, используемых в терминале. Все командные строки, которые вы должны вводить в терминале, начинаются с символа $. Вам не нужно вводить сам символ — он указывает на начало каждой команды. Строки, которые не начинаются с $, обычно показывают выходные данные предыдущей команды. В дополнение к этому, в примерах, относящихся к PowerShell, вместо $ будет использоваться символ >.
Установка инструмента rustup в Linux или macOS
Если вы используете Linux или macOS, то откройте терминал и введите следующую команду:
$ curl https://sh.rustup.rs -sSf | sh
Указанная команда скачивает скрипт и запускает установку инструмента rustup, который устанавливает последнюю стабильную версию языка Rust. Возможно, вам будет предложено ввести пароль. Если установка прошла успешно, то появится следующая строчка («Rust установлен. Отлично!»):
Rust is installed now. Great!
Если хотите, то можете самостоятельно скачать этот скрипт и проверить его перед запуском.
Скрипт установки автоматически добавит язык Rust в системный путь после следующего входа в систему. Если вместо перезагрузки терминала вы хотите сразу же начать использовать язык Rust, то в командной строке выполните следующую команду, чтобы добавить язык Rust в системный путь вручную:
$ source $HOME/.cargo/env
Как вариант, вы можете добавить следующую строку в свой профиль ~/.bash_profile:
$ export PATH="$HOME/.cargo/bin:$PATH"
В дополнение к этому вам понадобится какой-то редактор связей (линкер). Скорее всего, он уже установлен, но когда вы пытаетесь скомпилировать программу Rust и видите ошибки, которые говорят о том, что не получается исполнить редактор связей, это означает, что в вашей системе редактор связей не установлен и вам нужно установить его вручную. Компиляторы C, как правило, поставляются вместе с нужным редактором связей. Сверьтесь с документацией вашей платформы и выясните, как устанавливать компилятор C. Кроме того, некоторые распространенные пакеты Rust зависят от кода C, и им требуется компилятор C. Поэтому, возможно, стоит установить его сейчас.
Установка инструмента rustup в Windows
В случае с Windows перейдите в раздел https://www.rust-lang.org/tools/install/ и следуйте инструкциям по установке языка Rust. В какой-то момент установки вы получите сообщение, объясняющее, что вам также понадобятся инструменты сборки C++ для Visual Studio 2013 или более поздней версии. Самый простой способ приобрести инструменты сборки — загрузить их для Visual Studio 2019 по адресу https://www.visualstudio.com/downloads/#build-tools-for-visual-studio-2019 в разделе «Другие инструменты и платформы».
В остальной части этой книги используются команды, которые работают как в интерпретаторе командной строки cmd.exe, так и в оболочке PowerShell. Если будут определенные различия, то мы объясним, какой из этих инструментов использовать.
Обновление и деинсталляция
После того как вы инсталлировали язык Rust инструментом rustup, его легко можно обновить до последней версии. Из командной строки выполните следующий ниже скрипт обновления:
$ rustup update
Для того чтобы деинсталлировать язык Rust и инструмент rustup, выполните следующий ниже скрипт деинсталляции из командной строки:
$ rustup self uninstall
Устранение неисправностей
Для того чтобы проверить правильность установки языка Rust, откройте оболочку и введите следующую команду:
$ rustc ––version
Вы должны увидеть номер версии, хеш фиксации и дату фиксации последней стабильной версии, выпущенной в следующем формате:
rustc x.y.z (abcabcabc yyyy-mm-dd)
Если вы видите эту информацию, значит, вы успешно инсталлировали язык Rust! Если вы не видите эту информацию и находитесь в Windows, то проверьте, что язык Rust содержится в вашей системной переменной %PATH%. Если все правильно, а Rust по-прежнему не работает, то вот несколько вариантов, где можно получить помощь. Самый простой — это канал #beginners на официальном веб-сайте языка Rust в мессенджере Discord по адресу https://discord.gg/rust-lang. Там вы можете пообщаться с другими растианами (растианин, на англ. rustacean, произносится как «растейшен», — это наше забавное самоназвание), которые вам непременно помогут. Другие замечательные ресурсы включают форум пользователей Users по адресу https://users.rust-lang.org/ и на веб-сайте Stack Overflow по адресу http://stackoverflow.com/questions/tagged/rust/.
Локальная документация
Установщик также содержит локальную копию документации, благодаря чему вы можете читать ее офлайн. Выполните команду rustupdoc, и локальная копия документации откроется в браузере.
Всякий раз, когда тип или функция предусмотрены стандартной библиотекой и вы не уверены, что она делает или как ее использовать, то для ответов на эти вопросы используйте документацию об интерфейсе программирования приложений (API).
Здравствуй, Мир!
Теперь, когда вы установили язык Rust, давайте напишем первую программу Rust. Традиционно при изучении нового языка пишется небольшая программа, которая выводит на экране текст Hello,World!, и поэтому поступим так же.
Примечание
Авторы книги исходят из базового знакомства с командной строкой. Язык Rust не предъявляет особых требований к редактированию или инструментам, а также к тому, где находится ваш код, поэтому, если вместо командной строки вы предпочитаете использовать интегрированную среду разработки (IDE), то, пожалуйста, используйте свою любимую IDE. Многие IDE теперь имеют некоторую степень поддержки языка Rust; для получения подробностей перепроверьте документацию интегрированной среды разработки. В последнее время команда разработчиков языка Rust сосредоточилась на обеспечении отличной поддержки IDE, и в этом направлении был достигнут значительный прогресс!
Создание каталога проектов
Вы начнете с создания каталога для хранения кода Rust. Неважно, где располагается код, но для упражнений и проектов из этой книги мы предлагаем создать каталог проектов projects в вашем домашнем каталоге и хранить там все проекты.
Откройте терминал и введите следующие команды, чтобы создать каталог projects и каталог для проекта Hello, world! в каталоге проектов.
Для Linux, macOS и оболочки PowerShell в Windows введите следующее:
$ mkdir ~/projects
$ cd ~/projects
$ mkdir hello_world
$ cd hello_world
Для интерпретатора командной строки cmd в Windows введите:
> 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.
Листинг 1.1. Программа, которая выводит Hello, World!
main.rs
fn main() {
println!("Hello, World!");
}
Сохраните файл и вернитесь в окно вашего терминала. В Linux или macOS введите следующие команды для компиляции и выполнения файла:
$ rustc main.rs
$ ./main
Hello, World!
В Windows вместо команды ./main введите команду .\main.exe:
> rustc main.rs
> .\main.exe
Hello, World!
Независимо от вашей операционной системы в терминале должен быть выведен строковый литерал Hello, World!. Если вы не видите этих данных, то обратитесь к разделу «Устранение неполадок» для получения справки.
Если Hello,World! все-таки напечаталось, то примите наши поздравления! Вы официально написали программу на языке Rust. И значит, вы стали программистом на языке Rust — добро пожаловать!
Анатомия программы на языке Rust
Давайте подробно рассмотрим, что только что произошло в программе Hello, World!. Вот первый элемент пазла:
fn main() {
}
Эти строки кода определяют функцию на языке Rust. Функция main является особенной: это всегда первый код, который выполняется в каждой исполняемой программе Rust. Первая строка кода объявляет функцию с именем main, которая не имеет параметров и ничего не возвращает. Если бы имелись параметры, то они бы вошли внутрь скобок, ().
Также обратите внимание на то, что тело функции заключено в фигурные скобки, {}. В Rust они требуются вокруг всех тел функций. Правильно размещать открывающую фигурную скобку на той же строке кода, что и объявление функции, добавив один пробел между ними.
На момент написания этой книги инструмент автоматического форматирования под названием rustfmt находился на стадии разработки. Если в своих проектах Rust вы хотите придерживаться стандартного стиля, то rustfmt отформатирует код в определенном стиле. Коллектив Rust планирует в итоге включить этот инструмент в стандартный дистрибутив Rust, подобно rustc. Поэтому в зависимости от того, когда вы читаете эту книгу, он уже может быть установлен на вашем компьютере. Для получения более подробной информации ознакомьтесь с документацией онлайн.
Внутри функции main находится следующий код:
println!("Hello, World!");
Эта строка кода выполняет всю работу в этой маленькой программе: она выводит текст на экране. Здесь следует отметить четыре важные детали. Во-первых, стиль языка Rust предусматривает не табуляцию, а отступ из четырех пробелов.
Во-вторых, инструкция println! вызывает макрокоманду языка Rust. Если бы вместо этого она вызвала функцию, то мы бы ввели ее как println (без !). Мы обсудим макрокоманды языка Rust подробнее в главе 19. Пока же вам просто нужно знать: использование ! означает, что вы вызываете макрокоманду вместо обычной функции.
В-третьих, вы видите строковый литерал "Hello,world!". Мы передаем его в качестве аргумента макрокоманды println!, и этот строковый литерал выводится на экран.
В-четвертых, мы заканчиваем строку кода точкой с запятой (;). Она указывает на то, что это выражение закончено и готово начаться следующее. Большинство строк кода Rust заканчиваются точкой с запятой.
Компиляция и выполнение являются отдельными шагами
Вы выполнили только что созданную программу, поэтому давайте рассмотрим каждый шаг в этом процессе.
Перед выполнением программы Rust вам необходимо ее скомпилировать с помощью компилятора языка Rust, введя команду rustc и передав ей имя вашего исходного файла, как показано ниже:
$ rustc main.rs
Если у вас есть опыт работы с C или C++, то вы заметите, что команда похожа на gcc или clang. После успешной компиляции Rust выводит двоичный исполняемый файл.
В Linux, macOS и PowerShell в Windows вы увидите исполняемый файл, введя в командной строке команду ls. В Linux и macOS вы увидите два файла. С помощью PowerShell в Windows вы увидите те же три файла, что и при использовании интерпретатора командной строки cmd.
$ ls
main main.rs
С помощью интерпретатора командной строки cmd в Windows можно ввести следующее:
> dir /B %= опция /B говорит о том, что нужно показывать только имена файлов =%
main.exe
main.pdb
main.rs
Здесь вы видите файл исходного кода с расширением .rs, исполняемый файл (main.exe в Windows, но main на всех других платформах) и при использовании Windows файл, содержащий отладочную информацию с расширением .pdb. Таким образом, вы выполняете файл main, или main.exe, вот так:
$ ./main # или .\main.exe в Windows
Если бы программа main.rs была вашей программой Hello, World!, то в терминале было бы выведено Hello,World!.
Если вы лучше знакомы с динамическим языком, таким как Ruby, Python или JavaScript, то вы, возможно, не привыкли к компиляции и выполнению программы как к отдельным шагам. Rust — это язык с предварительным компилированием (Ahead-of-Time-компилированием), то есть вы можете компилировать программу и передавать исполняемый файл кому-то другому и он может выполнять его даже без установки языка Rust. Если же вы дадите кому-то файл .rb, .py или .js, то этот кто-то должен иметь установленную реализацию соответственно языка Ruby, Python или JavaScript. Но на этих языках для компиляции и выполнения программы вам потребуется только одна команда. В дизайне языков все является компромиссом.
Компиляция только с помощью rustc отлично подходит для простых программ, но по мере развития вашего проекта вам захочется управлять всеми вариантами и упрощать распространение вашего кода. Далее мы познакомим вас с инструментом Cargo, который поможет писать реальные программы языка Rust.
Здравствуй, Cargo!
Cargo — это система сборки и пакетный менеджер языка Rust. Большинство растиан используют этот инструмент для управления проектами Rust, потому что Cargo выполняет много работы за вас, в частности построение кода, скачивание библиотек, от которых зависит код, и построение этих библиотек. (Библиотеки, которые нужны коду, мы называем зависимостями.)
Самые простые программы Rust, как та, которую мы уже написали, не имеют никаких зависимостей. Поэтому, если бы мы построили проект Hello, World! с помощью пакетного менеджера Cargo, то он бы задействовал только ту часть Cargo, которая занимается построением вашего кода. При написании более сложных программ Rust вы будете добавлять зависимости, и если вы начнете проект с помощью Cargo, то добавлять зависимости будет намного проще.
Поскольку подавляющее большинство проектов Rust используют пакетный менеджер Cargo, в других главах этой книги мы исходим из того, что вы тоже используете Cargo. Если вы пользовались официальными установщиками, описанными в разделе «Установка», то Cargo устанавливается в комплекте с языком Rust. Если же вы инсталлировали Rust каким-то другим способом, то проверьте наличие установленного Cargo, введя в терминал следующее:
$ cargo --version
Если вы видите номер версии, то она у вас есть! Если же вы видите ошибку, например commandnotfound («команда не найдена»), то обратитесь к документации вашего метода установки, чтобы выяснить, как установить 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 внутри. Он также инициализировал новый репозиторий Git вместе с файлом .gitignore.
Примечание
Git — это распространенная система управления версиями. С помощью флага --vcs вы можете изменить команду cargonew, чтобы использовать другую систему управления версиями или вообще не использовать никакой системы. Выполните команду cargonew --help, чтобы увидеть имеющиеся варианты.
Откройте файл Cargo.toml в любом текстовом редакторе. Указанный файл должен быть похожим на код в листинге 1.2.
Листинг 1.2. Содержимое Cargo.toml, сгенерированное командой cargo new
Cargo.toml
[package]
name = "hello_cargo"
version = "0.1.0"
authors = ["Ваше имя <you@example.com>"]
edition = "2018"
[dependencies]
Этот файл имеет формат TOML («очевидный минимальный язык Тома», от англ. Tom’s Obvious, Minimal Language), который является конфигурационным форматом Cargo.
Первая строка файла, [package], является заголовком раздела, который указывает на то, что последующие инструкции настраивают пакет. По мере добавления информации в этот файл мы будем добавлять и другие разделы.
Следующие четыре строки задают информацию о конфигурации, необходимую для компиляции программы: название, версия, имя автора и используемая редакция языка Rust. Cargo получает информацию о вашем имени и электронной почте из вашей среды, поэтому, если эта информация неверная, исправьте ее сейчас и сохраните файл. Мы поговорим о ключе edition в приложении Д в конце книги.
Последняя строка, [dependencies], является началом раздела, в котором вы перечисляете все зависимости проекта. В языке Rust пакеты с исходным кодом называются упаковками (crate). Для этого проекта нам не нужны другие упаковки, но они нам понадобятся в первом проекте в главе 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 и его выполнение
Теперь давайте посмотрим, чем отличается ситуация, когда мы создаем и выполняем программу 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). Вы можете выполнить исполняемый файл с помощью следующей команды:
$ ./target/debug/hello_cargo # или .\target\debug\hello_cargo.exe в Windows
Hello, world!
Если все сделано правильно, то терминал должен вывести Hello,World!. Выполнение команды cargobuild в первый раз также приводит к тому, что Cargo создает новый файл на верхнем уровне — Cargo.lock. Этот файл отслеживает точные версии зависимостей в проекте. Данный проект не имеет зависимостей, поэтому указанный файл немного разрежен. Вам никогда не придется изменять этот файл вручную, Cargo управляет его содержимым за вас.
Мы только что построили проект с помощью команды cargobuild и выполнили его с помощью команды ./target/debug/hello_cargo, но мы также можем использовать команду cargorun для компиляции кода и последующего выполнения результирующего исполняемого файла, и все это в одной команде:
$ cargo run
Finished dev [unoptimized + debuginfo] target(s) in 0.0 secs
Running `target/debug/hello_cargo`
Hello, World!
Обратите внимание, что на этот раз мы не увидели данных, указывающих на то, что 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 также предоставляет команду cargocheck. Эта команда быстро проверяет ваш код, чтобы убедиться в его компилируемости, но не создает исполняемый файл:
$ cargo check
Checking hello_cargo v0.1.0 (file:///projects/hello_cargo)
Finished dev [unoptimized + debuginfo] target(s) in 0.32 secs
Вы, наверное, захотите иметь исполняемый файл? Часто команда cargocheck выполняется намного быстрее, чем команда cargobuild, потому что она пропускает этап порождения исполняемого файла. Если вы непрерывно проверяете свою работу во время написания кода, то использование команды cargocheck ускорит этот процесс! По этой причине многие растиане периодически выполняют команду cargocheck, когда пишут программу, чтобы убедиться, что она компилируется. Затем, когда они готовы использовать исполняемый файл, они выполняют команду cargobuild.
Давайте повторим то, что мы уже узнали о Cargo:
• Мы создаем проект, используя команды cargobuild или cargocheck.
• Мы создаем и выполняем проект в один прием, используя команду cargorun.
• Вместо сохранения результата построения в том же каталоге, что и наш код, Cargo сохраняет его в каталоге target/debug.
Дополнительным преимуществом использования пакетного менеджера Cargo является то, что команды одинаковы независимо от того, в какой операционной системе вы работаете. Таким образом, в этой книге мы больше не будем предоставлять инструкции конкретно для Linux и macOS по сравнению с Windows.
Сборка для релиза
Когда ваш проект будет окончательно готов к релизу, вы можете использовать команду cargobuild--release для его компиляции с оптимизациями. Эта команда создаст исполняемый файл в target/release вместо target/debug. Оптимизации ускорят работу кода Rust, но их включение увеличивает время, необходимое для компиляции программы. Вот почему существуют два разных профиля: один для разработки, когда вы хотите перестраивать программу быстро и часто, а другой — для построения окончательной программы для пользователя, которая не будет перестраиваться повторно и будет работать как можно быстрее. Если вы проводите сравнительный анализ времени выполнения вашего кода, то обязательно выполните команду cargobuild--release и сравните с исполняемым файлом в target/release.
Cargo как общепринятое средство
В случае с простыми проектами у Cargo нет какой-то большой ценности по сравнению с компилятором rustc. Но когда ваши программы усложнятся, он докажет свою ценность. Имея сложные проекты, состоящие из множества упаковок, гораздо проще передать обязанности по координированию сборки пакетному менеджеру Cargo.
Несмотря на то что проект hello_cargo прост, он теперь использует бóльшую часть реального инструментария, который вы будете применять в остальной части вашей Rust-овской карьеры. По сути дела, работая над любыми существующими проектами, вы можете использовать следующие команды, чтобы получать полную копию кода с помощью Git, переходить в каталог этого проекта и выполнять его построение:
$ git clone someurl.com/некийпроект
$ cd некийпроект
$ cargo build
Для получения дополнительной информации о пакетном менеджере Cargo ознакомьтесь с его документацией по адресу https://doc.rust-lang.org/cargo/.
Итоги
Неплохое начало путешествия по языку Rust! В этой главе вы узнали, как:
• Устанавливать последнюю стабильную версию языка Rust с помощью инструмента rustup.
• Обновлять язык Rust до более новой версии.
• Открывать локально установленную документацию.
• Написать и выполнить программу Hello, World!, используя компилятор rustc напрямую.
• Создавать и выполнять новый проект с использованием обозначений пакетного менеджера Cargo.
Сейчас самое подходящее время создать программу посерьезней, чтобы привыкнуть к чтению и написанию кода на языке Rust. И поэтому в главе 2 мы создадим программу игры-угадайки. Если вы предпочитаете начать с изучения того, как в Rust работают распространенные концепции программирования, то обратитесь к главе 3, а затем вернитесь к главе 2.
1 Язык программирования Rust получил свое название от грибов семейства ржавчинные (англ. rust fungi; отсюда и перевод слова rusty как «ржавый»), а также от слова «robust» («надежный»). — Здесь и далее примеч. пер.
Давайте начнем наше «ржавое»1 путешествие по языку Rust! Здесь есть чему поучиться, но любое путешествие нужно с чего-то начать. В этой главе мы обсудим следующее:
Язык программирования Rust получил свое название от грибов семейства ржавчинные (англ. rust fungi; отсюда и перевод слова rusty как «ржавый»), а также от слова «robust» («надежный»). — Здесь и далее примеч. пер.
2. Программирование игры-угадайки
Давайте же сразу перейдем к языку Rust и разработаем вместе практический проект! Эта глава познакомит вас с несколькими распространенными понятиями Rust и покажет, как их использовать в реальной программе. Вы узнаете о let, match, методах, связанных функциях, использовании внешних упаковок и многом другом! В последующих главах мы изучим эти идеи подробнее. В этой главе вы будете заниматься основами языка.
Мы выполним реализацию классической задачи для начинающих программистов — игру-угадайку. Вот как она работает: программа сгенерирует случайное целое число от 1 до 100. Затем предложит игроку отгадать число и ввести его значение. После ввода числа-догадки программа сообщит, является ли оно слишком маленьким либо слишком большим. Если игрок правильно отгадает число, то игра покажет поздравительное сообщение и завершит работу.
Настройка нового проекта
Для настройки нового проекта перейдите в каталог проектов projects, созданный в главе 1, и сделайте новый проект с помощью Cargo:
$ cargo new guessing_game
$ cd guessing_game
Первая команда cargonew берет имя проекта (guessing_game) в качестве первого аргумента. Вторая команда меняет каталог нового проекта.
Взгляните на сгенерированный файл Cargo.toml:
Cargo.toml
[package]
name = "guessing_game"
version = "0.1.0"
authors = ["Ваше имя <you@example.com>"]
edition = "2018"
[dependencies]
Если сведения об авторе, полученные из вашей среды, неверны, то исправьте их в файле и сохраните его снова.
Как вы увидели в главе 1, команда cargonew генерирует программу «Hello, World!». Проверьте файл src/main.rs:
src/main.rs
fn main() {
println!("Hello, World!");
}
Теперь давайте скомпилируем эту программу «Hello, World!» и выполним ее, используя команду cargorun:
$ cargo run
Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
Finished dev [unoptimized + debuginfo] target(s) in 1.50 secs
Running `target/debug/guessing_game`
Hello, World!
Команда run бывает очень кстати, когда требуются быстрые итерации по проекту, как в этой игре, где нужно будет оперативно тестировать каждую итерацию перед переходом к следующей.
Снова откройте файл src/main.rs. Вы будете писать весь код в этот файл.
Обработка загаданного числа
В первой части программы игры на угадывание пользователю нужно будет ввести данные, программа обработает эти данные и проверит, что они в корректной форме. Вначале мы даем игроку ввести загаданное число. Наберите код из листинга 2.1 в файл src/main.rs.
Листинг 2.1. Код, который получает от пользователя загаданное число и выводит его
src/main.rs
use std::io;
fn main() {
println!("Угадайте число!");
println!("Пожалуйста, введите свою догадку.");
let mut guess = String::new();
io::stdin().read_line(&mut guess)
.expect("Не получилось прочитать строку");
println!("Вы загадали: {}", guess);
}
Этот код содержит много информации, поэтому давайте посмотрим на каждую строку отдельно. Для получения данных от пользователя и затем распечатки результата нам нужно ввести библиотеку io (библиотеку ввода-вывода) в область видимости. Библиотека io берется из стандартной библиотеки (носящей название std):
use std::io;
По умолчанию Rust в прелюдии вводит в область видимости каждой программы всего несколько типов. Если тип, который вы хотите использовать, не находится в прелюдии, то вы должны ввести этот тип в область видимости с помощью инструкции use. Использование библиотеки std::io обеспечивает вас рядом полезных средств, включая способность принимать данные от пользователя.
Как вы видели в главе 1, функция main является точкой входа в программу:
fn main() {
Синтаксис fn объявляет новую функцию, круглые скобки () указывают на отсутствие параметров, а фигурная скобка { начинает тело функции.
Как вы также узнали из главы 1, println! является макрокомандой, которая выводит строковый литерал на экране:
println!("Угадайте число!");
println!("Пожалуйста, введите свою догадку.");
Этот код выводит подсказку с указанием названия игры и запрашивает у пользователя данные.
Хранение значений с помощью переменных
Далее мы создадим место для хранения введенных пользователем данных. Это делается так:
let mut guess = String::new();
Теперь программа становится интересной! В этой маленькой строке кода много чего происходит. Обратите внимание на инструкцию let, которая используется для создания переменной. Вот еще один пример:
let foo = bar;
Эта строка кода создает новую переменную с именем foo и привязывает ее к значению переменной bar. В Rust переменные по умолчанию являются неизменяемыми. Мы подробно обсудим это понятие в разделе «Переменные и изменяемость» (с. 61). В следующем примере показано, как использовать ключевое слово mut перед именем переменной, чтобы сделать переменную изменяемой:
let foo = 5; // неизменяемая
let mut bar = 5; // изменяемая
Примечание
Синтаксис // начинает комментарий, который продолжается до конца строки кода. Все, что находится в комментариях, которые обсуждаются в главе 3 подробнее, язык Rust игнорирует.
Давайте вернемся к программе игры-угадайки. Теперь вы знаете, что letmutguess вводит в программу изменяемую переменную с именем guess. По другую сторону знака равенства (=) находится значение, к которому привязана переменная guess, являющееся результатом вызова функции String::new, возвращающей новый экземпляр типа String. String — это строковый тип, предусмотренный стандартной библиотекой, а именно наращиваемый фрагмент текста в кодировке UTF-8.
Синтаксис :: в строке кода ::new указывает на то, что new является функцией, связанной с типом String. Связанная функция реализуется в типе, в данном случае в типе String, а не в конкретном экземпляре типа String. В некоторых языках она называется статическим методом.
Функция new создает новую пустую строку. Вы найдете функцию new во многих типах, потому что так принято называть функцию, которая делает новое значение какого-либо рода.
Подводя итог, строка кода letmutguess=String::new(); создала изменяемую переменную, которая в данный момент привязана к новому пустому экземпляру типа String. Фух!
Напомним, что мы включили в состав функциональность ввода-вывода из стандартной библиотеки, указав std::io; в первой строке программы. Теперь мы вызовем функцию stdin из модуля io:
io::stdin().read_line(&mut guess)
.expect("Не получилось прочитать строку");
Если бы мы не разместили в начале программы строку usestd::io, то мы бы могли записать вызов этой функции как std::io::stdin. Функция stdin возвращает экземпляр std::io::Stdin, то есть тип, который представляет собой дескриптор стандартного ввода данных для терминала.
Следующая часть кода .read_line(&mutguess) вызывает метод read_line, заданный для дескриптора стандартного ввода данных для получения данных от пользователя. Мы также передаем в метод read_line один аргумент: &mutguess.
Работа метода read_line состоит в том, чтобы брать все, что пользователь набирает в стандартном вводе данных, и помещать это в экземпляр типа String, поэтому он берет этот строковый экземпляр в качестве аргумента. Указанный аргумент должен быть изменяемым, чтобы этот метод мог изменять содержимое строкового экземпляра путем добавления вводимых пользователем данных.
Символ & указывает на то, что этот аргумент является ссылкой, которая дает возможность многочисленным частям кода обращаться к одному фрагменту данных без многократного копирования этих данных в память. Ссылки являются сложным языковым средством, и одно из главных преимуществ Rust состоит в том, что в нем можно совершенно безопасно и легко использовать ссылки. Для того чтобы завершить программу, не нужно знать многих подробностей. На данный момент вам нужно лишь знать, что, как и переменные, ссылки по умолчанию являются неизменяемыми. Следовательно, чтобы сделать ее изменяемой, нужно написать &mutguess вместо &guess. (В главе 4 ссылки будут объяснены подробнее.)
Обработка потенциального сбоя с помощью типа Result
Мы еще не закончили с этой строкой кода. Хотя темой нашего обсуждения до сих пор была одна-единственная строка текста, она является лишь первой частью одной логической строки кода. Вторая часть заключается в следующем методе:
.expect("Не получилось прочитать строку");
Когда вы вызываете метод с помощью синтаксиса .foo(), часто бывает разумно добавить новую строку и другие пробелы с целью разбиения длинных строк кода. Мы могли бы написать этот код следующим образом:
io::stdin().read_line(&mut guess).expect("Не получилось прочитать строку");
Однако одну длинную строку кода трудно читать, поэтому лучше всего ее разделить: две строки кода для двух вызовов метода. Теперь давайте рассмотрим, что делает эта строка кода.
Как уже упоминалось ранее, метод read_line помещает то, что пользователь набирает, в переменную типа String, которую мы ему передаем, но также возвращает значение — в данном случае io::Result. Стандартная библиотека языка Rust имеет ряд типов с именем Result: обобщенный тип Result, а также специальные версии для подмодулей, такие как тип io::Result.
Типы Result — это перечисления, часто кратко именуемые enum. Перечисление — это тип, который имеет фиксированное множество значений, и эти значения называются вариантами перечисления. В главе 6 перечисления будут рассмотрены подробнее.
Для типа Result вариантами являются Ok и Err. Вариант Ok указывает на то, что операция была успешной, а внутри Ok находится успешно сгенерированное значение. Вариант Err означает, что операция была неуспешной, и Err содержит информацию о том, как или почему операция не сработала.
Целью этих типов Result является кодирование информации об обработке ошибок. Значения типа Result, как и значения любого типа, задают методы. Экземпляр io::Result имеет метод expect, который вы можете вызывать. Если этот экземпляр io::Result является значением Err, то expect вызовет аварийный сбой программы и покажет сообщение, которое вы передали в метод expect в качестве аргумента. Если метод read_line возвращает Err, то это, скорее всего, будет результатом ошибки, исходящей из базовой операционной системы. Если этот экземпляр io::Result является значением Ok, то метод expect возьмет возвращаемое значение, которое содержит Ok, и вернет вам только это значение, которое вы сможете использовать. В данном случае указанное значение является числом в байтах, введенным пользователем в стандартный ввод данных.
Если вы не вызовете метод expect, то программа будет компилирована, но вы получите предупреждение2:
$ cargo build
Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
warning: unused `std::result::Result` which must be used
--> src/main.rs:10:5
|
10 | io::stdin().read_line(&mut guess);
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
= note: #[warn(unused_must_use)] on by default
Язык Rust предупреждает, что вы не использовали значение типа Result, возвращенное из метода read_line, указывая на то, что программа не обработала возможную ошибку.
Правильный способ блокировать это предупреждение — фактически написать обработку ошибок, но поскольку вы хотите, чтобы эта программа просто завершала свою работу аварийно, когда возникает проблема, то можете использовать метод expect. О восстановлении после ошибок вы узнаете в главе 9.
Печать значений с помощью заполнителей макрокоманды println!
Помимо закрывающих фигурных скобок осталось обсудить еще одну строку кода, а именно:
println!("Вы загадали: {}", guess);
Эта строка кода выводит строковую переменную, в которой мы сохранили введенные пользователем данные. Фигурные скобки {} являются заполнителем: думайте о {} как о маленьких клешнях краба, которые удерживают значение на месте. С помощью фигурных скобок можно вывести более одного значения: первый набор фигурных скобок содержит первое значение, приводимое после форматной строки, второй набор содержит второе значение и так далее. Вывод более одного значения в одном вызове макрокоманды println! выглядел бы вот так:
let x = 5;
let y = 10;
println!("x = {} и y = {}", x, y);
Этот код будет печатать x = 5 и y = 10.
Тестирование первой части
Давайте проверим первую часть игры на угадывание. Запустите ее с помощью команды cargorun:
$ cargo run
Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
Finished dev [unoptimized + debuginfo] target(s) in 1.50 secs
Running `target/debug/guessing_game`
Угадайте число!
Пожалуйста, введите свою догадку.
6
Вы загадали: 6
На этом этапе первая часть игры закончена: мы получаем ввод с клавиатуры, а затем выводим его.
Генерирование секретного числа
Далее нам нужно сгенерировать секретное число, которое пользователь попытается угадать. Секретное число всегда должно быть разным, чтобы в игру было интересно играть несколько раз. Давайте использовать случайное число от 1 до 100, чтобы не слишком усложнять игру. Rust пока еще не содержит функциональность случайных чисел в стандартной библиотеке. Тем не менее команда разработчиков языка Rust все же предлагает упаковку rand по адресу https://crates.io/crates/rand/.
Использование упаковки для получения большей функциональности
Напомним, что упаковка — это множество файлов исходного кода Rust3. Проект, который мы до сих пор строили, представляет собой двоичную, то есть исполняемую, упаковку. Упаковка rand является библиотечной, то есть содержит код, предназначенный для использования в других программах.
Пакетный менеджер Cargo проявляет себя во всей красе там, где он использует внешние упаковки. Прежде чем мы сможем написать код, который задействует упаковку rand, нам нужно модифицировать файл Cargo.toml, включив упаковку rand в качестве зависимости. Теперь откройте этот файл и добавьте следующую строку в конец файла под заголовком раздела [dependencies], которую Cargo создал за вас:
Cargo.toml
[dependencies]
rand = "0.3.14"
В файле Cargo.toml все, что следует за заголовком, является частью раздела, который продолжается до тех пор, пока не начнется другой. Раздел [dependencies] — это то место, где вы сообщаете Cargo, от каких внешних упаковок зависит ваш проект и какие версии этих упаковок вам требуются. В данном случае мы укажем упаковку rand с помощью семантического спецификатора версии 0.3.14. Cargo понимает семантическое управление версиями (иногда именуемое SemVer), которое является стандартом для написания номеров версий. На самом деле число 0.3.14 — это сокращение для ^0.3.14, означающее «любая версия с публичным API, совместимым с версией 0.3.14».
Теперь, не внося никаких изменений в код, давайте соберем проект, как показано в листинге 2.2.
Листинг 2.2. Результат выполнения команды cargo build после добавления упаковки rand в качестве зависимости
$ cargo build
Updating registry `https://github.com/rust-lang/crates.io-index`
Downloading rand v0.3.14
Downloading libc v0.2.14
Compiling libc v0.2.14
Compiling rand v0.3.14
Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
Finished dev [unoptimized + debuginfo] target(s) in 1.50 secs
Вы видите разные номера версий (но все они будут совместимы с кодом, благодаря SemVer!), причем выводимые на экран строки могут находиться в другом порядке.
Теперь, когда у нас есть внешняя зависимость, Cargo извлекает последние версии всего этого из реестра, то есть копии данных из https://crates.io/. Crates.io — это то место, где участники экосистемы языка Rust размещают свои проекты с открытым исходным кодом Rust для их использования другими разработчиками.
После обновления реестра пакетный менеджер Cargo проверяет раздел [dependencies] и скачивает все упаковки, которых у вас еще нет. В данном случае, несмотря на то что мы указали в качестве зависимости только rand, Cargo также захватил копию libc, потому что в своей работе rand зависит от libc. После скачивания упаковок язык Rust компилирует их, а затем компилирует проект с имеющимися зависимостями.
Если вы сразу же выполните команду cargobuild снова, не внося никаких изменений, то на выходе вы не получите никакого результата, кроме строчки Finished. Cargo знает, что он уже скачал и скомпилировал зависимости и вы ничего не изменили в файле Cargo.toml. Cargo также знает, что вы ничего не изменили в коде, поэтому он не перекомпилирует и его. Он просто завершает работу, ничего не делая.
Если вы откроете файл src/main.rs, внесете незначительное изменение, а затем сохраните его и соберете снова, то увидите только две результирующие строчки:
$ cargo build
Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
Finished dev [unoptimized + debuginfo] target(s) in 1.50 secs
Эти строчки показывают, что Cargo обновляет сборку, используя только крошечное изменение в файле src/main.rs. Ваши зависимости не изменились, поэтому Cargo знает, что он может использовать повторно те упаковки, которые он уже скачал и скомпилировал для них. Он просто перестраивает вашу часть кода.
Обеспечение воспроизводимых сборок с помощью файла Cargo.lock
Cargo имеет механизм, который дает возможность перестройки одного и того же артефакта всякий раз, когда вы или кто-либо другой выполняете сборку вашего кода: Cargo будет использовать только те версии зависимостей, которые вы указали, до тех пор, пока вы не укажете иное. Например, что произойдет, если на следующей неделе выйдет версия 0.3.15 упаковки rand, содержащая важное исправление ошибок, а также регрессию, которая нарушит работу вашего кода?
Ответом на этот вопрос является файл Cargo.lock, который был создан при первом выполнении команды cargobuild и теперь находится в каталоге guessing_game. Когда вы создаете проект в первый раз, Cargo выясняет все версии зависимостей, которые соответствуют критериям, а затем записывает их в файл Cargo.lock. Когда вы будете выполнять сборку проекта в будущем, Cargo будет видеть, что файл Cargo.lock существует, и использовать указанные там версии, а не делать всю работу по выяснению версий снова. Это позволяет вам автоматически иметь воспроизводимую сборку. Другими словами, ваш проект будет оставаться в версии 0.3.14 до тех пор, пока вы не обновите его благодаря файлу Cargo.lock.
Обновление упаковки до новой версии
Когда вы хотите обновить упаковку, Cargo предоставляет еще одну команду, update, которая будет игнорировать файл Cargo.lock и выяснит все последние версии, которые соответствуют вашим спецификациям в Cargo.toml. Если она сработает, то Cargo запишет эти версии в файл Cargo.lock.
Но по умолчанию Cargo будет искать только версии больше 0.3.0 и меньше 0.4.0. Если упаковка rand выпустила две новые версии, 0.3.15 и 0.4.0, то вы увидите следующее ниже, выполнив команду cargoupdate:
$ cargo update
Updating registry `https://github.com/rust-lang/crates.io-index`
Updating rand v0.3.14 -> v0.3.15
На этом этапе вы также заметите изменение в файле Cargo.lock, отметив, что версия упаковки rand, которую вы сейчас используете, равна 0.3.15.
Если бы вы захотели использовать rand версии 0.4.0 или любую версию в серии 0.4.x, то вам следовало бы обновить файл Cargo.toml так, чтобы он выглядел следующим образом:
Cargo.toml
[dependencies]
rand = "0.4.0"
В следующий раз, когда вы выполните команду cargobuild, Cargo обновит реестр имеющихся упаковок и пересмотрит потребности rand в соответствии с новой версией, которую вы указали.
О пакетном менеджере Cargo и его экосистеме мы еще много чего расскажем в главе 14, но пока это все, что вам нужно знать. Cargo очень упрощает повторное использование библиотек, и поэтому растиане могут писать небольшие проекты, которые собираются из многочисленных пакетов.
Генерирование случайного числа
Теперь, когда вы добавили упаковку rand в файл Cargo.toml, приступим к ее использованию. Следующим шагом является обновление src/main.rs, как показано в листинге 2.3.
Листинг 2.3. Добавление кода генерирования случайного числа
use std::io;
use rand::Rng;
fn main() {
println!("Угадайте число!");
let secret_number = rand::thread_rng().gen_range(1, 101);
println!("Секретное число равно {}", secret_number);
println!("Пожалуйста, введите свою догадку.");
let mut guess = String::new();
io::stdin().read_line(&mut guess)
.expect("Не получилось прочитать строку");
println!("Вы загадали: {}", guess);
}
Сначала мы добавляем строку кода use: userand::Rng. Типаж Rng определяет методы, которые реализуются генераторами случайных чисел, и указанный типаж должен быть в области видимости, благодаря чему мы можем использовать эти методы. В главе 10 типажи будут рассмотрены подробно.
Далее мы добавляем еще две строки кода в середине . Функция rand::thread_rng даст нам конкретный генератор случайных чисел, который мы намерены использовать — он является локальным для текущего потока и инициализируется операционной системой. Затем мы вызываем метод gen_range на указанном генераторе случайных чисел. Этот метод определен типажом Rng, который мы ввели в область видимости с помощью инструкции userand::Rng. Метод gen_range берет два числа в качестве аргументов и генерирует случайное число между ними. Результат включает в себя нижнюю границу, но исключает верхнюю, поэтому для того, чтобы запросить число между 1 и 100, нам нужно указать 1 и 101.
Примечание
Вы не просто узнаете, какие типажи использовать и какие функции и методы вызывать из упаковки. Инструкции по использованию упаковки содержатся в сопроводительной документации к ней. Еще одно приятное свойство Cargo заключается в том, что вы можете выполнить команду cargodoc--open, которая выведет документацию, порождаемую всеми вашими зависимостями, локально и откроет ее в браузере. К примеру, если вас интересует прочая функциональность упаковки rand, то выполните cargodoc--open и нажмите rand на боковой панели слева.
Вторая строка кода, добавленная нами в середине программы, выводит секретное число. Это полезно при разработке программы и позволяет ее тестировать, но мы удалим эту строку из окончательной версии. Игра никуда не годится, если программа выводит ответ, как только запускается!
Попробуйте выполнить программу несколько раз:
$ cargo run
Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
Finished dev [unoptimized + debuginfo] target(s) in 1.50 secs
Running `target/debug/guessing_game`
Угадайте число!
Секретное число равно 7
Пожалуйста, введите свою догадку.
4
Вы загадали: 4
$ cargo run
Running `target/debug/guessing_game`
Угадайте число!
Секретное число равно 83
Пожалуйста, введите свою догадку.
5
Вы загадали: 5
Вы должны получить разные случайные числа, и все они должны быть от 1 до 100. Отлично!
Сравнение загаданного числа с секретным числом
Теперь, когда у нас есть данные, введенные пользователем, и случайное число, мы можем их сравнить. Этот шаг показан в листинге 2.4. Обратите внимание, что этот код пока что не компилируется, и позже мы объясним почему.
Листинг 2.4. Обработка возможных результатов сравнения двух чисел
src/main.rs
use std::io;
use std::cmp::Ordering;
use rand::Rng;
fn main() {
// --пропуск--
println!("Вы загадали: {}", guess);
match guess.cmp(&secret_number) {
Ordering::Less => println!("Слишком малое число!"),
Ordering::Greater => println!("Слишком большое число!"),
Ordering::Equal => println!("Вы выиграли!"),
}
}
Здесь первый новый фрагмент — это еще одна инструкция use, вводящая тип с именем std::cmp::Ordering в область видимости из стандартной библиотеки. Как и Result, Ordering («упорядочение») — это еще одно перечисление, но вариантами для перечисления Ordering являются Less («меньше»), Greater («больше») и Equal («равно»), являющиеся тремя возможными результатами, когда вы сравниваете два значения.
Затем мы добавляем внизу пять новых строк кода, которые используют тип Ordering. Метод cmp сравнивает два значения и может быть вызван для всего, что можно сравнить. Он ссылается на то, с чем вы хотите сравнить. Здесь он сравнивает загаданное число guess с секретным числом secret_number. Затем он возвращает вариант перечисления Ordering, который мы ввели в область видимости с помощью инструкции use. Мы используем выражение match, чтобы решить, что делать дальше, основываясь на том, какой вариант упорядочения Ordering был возвращен из вызова метода cmp со значениями в guess и secret_number.
Выражение match состоит из ветвей. Рукав (arm) состоит из шаблона и кода, который должен быть исполнен, если значение, заданное в начале выражения match, совпадает с шаблоном ветви. Язык Rust берет значение, переданное в match, и по очереди просматривает шаблон каждого рукава. Конструкция match и шаблоны являются мощными средствами. Они позволяют выражать разнообразные ситуации, с которыми может столкнуться код, и гарантируют, что каждая ситуация будет обработана. Мы подробно разберем эти средства в главах 6 и 18 соответственно.
Давайте посмотрим, что произойдет с используемым здесь выражением match. Будем считать, что пользователь загадал 50, а случайно сгенерированное секретное число на этот раз равно 38. Когда код сравнивает 50 и 38, то метод cmp возвращает Ordering::Greater, потому что 50 больше 38. Выражение match получает значение Ordering::Greater и начинает проверять шаблон каждой ветви. Выражение смотрит на шаблон первой ветви Ordering::Less и видит, что значение Ordering::Greater не совпадает с Ordering::Less, поэтому оно игнорирует код в этом рукаве и переходит к следующему. Паттерн следующего рукава Ordering::Greater действительно совпадает с Ordering::Greater! Связанный код в ветви исполнится и выведет Слишкомбольшоечисло!. Выражение match завершает свою работу, потому что нет необходимости проверять последнюю ветвь в этом сценарии.
Однако код в листинге 2.4 пока что не компилируется. Давайте попробуем это сделать4:
$ cargo build
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 struct `std::string::String`,
found integral variable
|
= note: expected type `&std::string::String`
= note: found type `&{integer}`
error: aborting due to previous error
Could not compile `guessing_game`.
Ошибка заключается в том, что типы не совпадают. В Rust имеется сильная статическая система типов. Однако в нем также есть логический вывод типов. Когда мы написали letmutguess=String::new(), Rust смог логически вывести, что переменная guess должна иметь тип String, и не заставил нас указать тип. С другой стороны, переменная secret_number имеет числовой тип. Значение от 1 до 100 могут иметь несколько числовых типов: i32, 32-битное число; u32, беззнаковое 32-битное число; i64, 64-битное число и другие. По умолчанию Rust использует тип i32, который является типом переменной secret_number, при условии, что вы не добавляете информацию о типе в другом месте, что заставит компилятор логически вывести другой числовой тип. Причина ошибки здесь заключается в том, что Rust не может сравнить строковый и числовой типы.
В итоге мы хотим конвертировать значение типа String, которое программа читает на входе, в числовой тип, чтобы численно сравнить его с секретным числом. Мы можем это сделать, добавив следующие две строки кода в тело функции main:
src/main.rs
// --пропуск--
let mut guess = String::new();
io::stdin().read_line(&mut guess)
.expect("Не получилось прочитать строку");
let guess: u32 = guess.trim().parse()
.expect("Пожалуйста, наберите число!");
println!("Вы загадали: {}", guess);
match guess.cmp(&secret_number) {
Ordering::Less => println!("Слишком малое число!"),
Ordering::Greater => println!("Слишком большое число!"),
Ordering::Equal => println!("Вы выиграли!"),
}
}
Мы создаем переменную с именем guess. Но подождите, у программы ведь уже есть переменная с именем guess? Да, есть, но Rust позволяет нам затенить предыдущее значение переменной guess новым. Это языковое средство часто используется в ситуациях, когда требуется конвертировать значение из одного типа в другой. Затенение позволяет нам использовать имя переменной guess повторно, а не создавать две уникальные переменные, как, например, guess_str и guess. (В главе 3 затенение рассматривается подробнее.)
Мы связываем переменную guess с выражением guess.trim().parse(). Переменная guess в указанном выражении относится к исходной переменной guess, которая была экземпляром типа String, содержащим в себе входные данные. Метод trim для типа String устраняет любые пробелы в начале и конце. Несмотря на то что тип u32 может содержать только числовые символы, пользователь должен нажать клавишу ENTER и тем самым выполнить метод read_line. Когда пользователь нажимает ENTER, символ новой строки добавляется в конец строкового значения. Например, если пользователь набирает 5 и нажимает ENTER, то переменная guess выглядит следующим образом: 5\n. \n обозначает «новую строку», то есть результат нажатия клавиши ENTER. Метод trim исключает \n, давая в результате только 5.
Метод parse, определенный для типа String, делает разбор строкового значения и преобразует его в какое-то число. Поскольку этот метод извлекает различные типы чисел, нужно указать точный тип числа, который мы хотим, используя letguess:u32. Двоеточие (:) после guess говорит о том, что мы аннотируем тип переменной. В Rust имеется несколько встроенных числовых типов: u32, который мы рассматриваем, является беззнаковым 32-битным целочисленным типом. Это хороший выбор по умолчанию для малого положительного числа. О других типах чисел вы узнаете в главе 3. Кроме того, аннотация u32 в этом примере программы и сравнение с переменной secret_number означают, что Rust логически выведет, что переменная secret_number тоже должна иметь тип u32. И, таким образом, теперь будут сравниваться два значения одного и того же типа!
Вызов метода parse легко может стать причиной ошибки. Если бы, например, строка содержала A☺%, то было бы невозможно конвертировать ее в число. Поскольку метод parse может не сработать, он возвращает тип Result во многом так же, как и метод read_line (тип Result обсуждался в разделе «Обработка потенциального сбоя с помощью типа Result»). Мы будем обращаться с этим типом точно так же, снова используя метод expect. Если parse возвращает вариант типа Result, равный Err, так как он не смог создать число из строки, то вызов метода expect завершит игру аварийно и выведет сообщение, заданное нами. Если метод parse выполнит успешную конвертацию строки в число, то он вернет вариант типа Result, равный Ok, а метод expect вернет число, которое мы хотим получить из значения Ok.
Давайте прямо сейчас выполним эту программу!
$ cargo run
Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
Finished dev[unoptimized + debuginfo] target(s) in 1.50 secs
Running `target/debug/guessing_game`
Угадайте число!
Секретное число равно 58
Пожалуйста, введите свою догадку.
76
Вы загадали: 76
Слишком большое число!
Здорово! Несмотря на то что перед загаданным числом были пробелы, программа все равно выяснила, что пользователь загадал 76. Выполните программу несколько раз, чтобы проверить разное поведение с разными входными данными: загадайте правильное число, слишком большое и слишком маленькое.
Сейчас у нас работает большая часть игры, но пользователь может вводить только одно загаданное число. Давайте это изменим, добавив цикл!
Вывод нескольких загаданных чисел с помощью цикличности
Ключевое слово loop создает бесконечный цикл. Сейчас мы его добавим, предоставив пользователям больше шансов угадать число:
src/main.rs
// --пропуск--
println!("Секретное число равно {}", secret_number);
loop {
println!("Пожалуйста, введите свою догадку.");
// --пропуск--
match guess.cmp(&secret_number) {
Ordering::Less => println!("Слишком малое число!"),
Ordering::Greater => println!("Слишком большое число!"),
Ordering::Equal => println!("Вы выиграли!"),
}
}
}
Как видите, мы перенесли в цикл все операции — и просьбу ввести загаданное число, и все остальное. Обязательно сделайте отступы между строками кода внутри цикла еще на четыре пробела и выполните программу снова. Обратите внимание, что возникла новая проблема, потому что программа делает именно то, что мы ей сказали: бесконечно запрашивает следующую догадку! Похоже, что пользователь просто не сможет выйти из игры!
Пользователь всегда может прервать программу, нажав Ctrl-C. Но есть и другой способ избежать этого ненасытного монстра, как упоминалось при обсуждении метода parse в разделе «Сравнение загаданного числа с секретным числом»: если пользователь введет нечисловой ответ, то программа завершится аварийно. Пользователь может этим воспользоваться для того, чтобы выйти из программы, как показано здесь:
$ cargo run
Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
Finished dev [unoptimized + debuginfo] target(s) in 1.50 secs
Running `target/debug/guessing_game`
Угадайте число!
Секретное число равно 59
Пожалуйста, введите свою догадку.
45
Вы загадали: 45
Слишком малое число!
Пожалуйста, введите свою догадку.
60
Вы загадали: 60
Слишком большое число!
Пожалуйста, введите свою догадку.
59
Вы загадали: 59
Вы выиграли!
Пожалуйста, введите свою догадку.
выйти
thread 'main' panicked at 'Please type a number!: ParseIntError { kind:
InvalidDigit }', src/libcore/result.rs:785
note: Run with `RUST_BACKTRACE=1` for a backtrace.
Набрав quit, вы фактически выходите из игры, но так же произойдет и при вводе любых других нечисловых данных. Мягко говоря, неразумно. Мы хотим, чтобы игра автоматически останавливалась, когда игрок угадывает правильное число.
Выход из игры после правильно угаданного числа
Давайте запрограммируем игру так, чтобы она завершалась, когда пользователь выигрывает, добавив для этого инструкцию break:
src/main.rs
// --пропуск--
match guess.cmp(&secret_number) {
Ordering::Less => println!("Слишком малое число!"),
Ordering::Greater => println!("Слишком большое число!"),
Ordering::Equal => {
println!("Вы выиграли!");
break;
}
}
}
}
Добавление строки кода с инструкцией break после сообщения Вывыиграли! побуждает программу выйти из цикла, когда пользователь правильно угадывает секретное число. Выход из цикла также означает выход из программы, потому что цикл является последней частью функции main.
Обработка ввода недопустимых данных
Чтобы уточнить, как поведет себя игра, вместо аварийного завершения программы, когда пользователь вводит нечисловое значение, давайте сделаем так, чтобы игра игнорировала нечисловое значение, предоставив пользователю возможность продолжить угадывать. Мы можем сделать это, изменив строку кода, в которой переменная guess конвертируется из типа String в тип u32, как показано в листинге 2.5.
Листинг 2.5. Игнорирование нечислового загаданного числа и запрос следующего загаданного числа вместо аварийного завершения программы
src/main.rs
// --пропуск--
io::stdin().read_line(&mut guess)
.expect("Не получилось прочитать строку");
let guess: u32 = match guess.trim().parse() {
Ok(num) => num,
Err(_) => continue,
};
println!("Вы загадали: {}", guess);
// --пропуск--
Переключаясь с вызова метода 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(_) во втором рукаве. Подчеркивание _ является всеохватывающим значением. В данном примере мы хотим, чтобы совпали все значения Err, независимо от того, какая информация у них внутри. Поэтому программа выполнит код второго рукава continue, который говорит программе перейти к следующей итерации цикла и запросить еще одно загаданное число. Таким образом, по сути, программа игнорирует все ошибки, с которыми может столкнуться метод parse!
Теперь в программе все должно работать как следует. Давайте испытаем ее:
$ cargo run
Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
Finished dev [unoptimized + debuginfo] target(s) in 1.50 secs
Running `target/debug/guessing_game`
Угадайте число!
Секретное число равно 61
Пожалуйста, введите свою догадку.
10
Вы загадали: 10
Слишком малое число!
Пожалуйста, введите свою догадку.
99
Вы загадали: 99
Слишком большое число!
Пожалуйста, введите свою догадку.
foo
Пожалуйста, введите свою догадку.
61
Вы загадали: 61
Вы выиграли!
Потрясающе! Благодаря одной крошечной финальной доработке мы завершим игру-угадайку! Напомним, что программа по-прежнему выводит секретное число. Это пригодилось для тестирования, но портит игру. Давайте удалим макрокоманду println!, которая выводит секретное число. В листинге 2.6 показан окончательный код.
Листинг 2.6. Полный код игры-угадайки
src/main.rs
use std::io;
use std::cmp::Ordering;
use rand::Rng;
fn main() {
println!("Угадайте число!");
let secret_number = rand::thread_rng().gen_range(1, 101);
loop {
println!("Пожалуйста, введите свою догадку.");
let mut guess = String::new();
io::stdin().read_line(&mut guess)
.expect("Не получилось прочитать строку");
let guess: u32 = match guess.trim().parse() {
Ok(num) => num,
Err(_) => continue,
};
println!("Вы загадали: {}", guess);
match guess.cmp(&secret_number) {
Ordering::Less => println!("Слишком малое число!"),
Ordering::Greater => println!("Слишком большое число!"),
Ordering::Equal => {
println!("Вы выиграли!");
break;
}
}
}
}
Итоги
Итак, вы успешно создали игру-угадайку. Примите поздравления!
Этот проект на практике познакомил вас со многими новыми понятиями языка Rust: let, match, методами, связанными функциями, использованием внешних упаковок и многим другим. В следующих главах вы узнаете об этих понятиях подробнее. В главе 3 рассказывается о понятиях, которые есть в большинстве языков программирования, таких как переменные, типы данных и функции, и показывается, как их использовать в Rust. В главе 4 рассматривается идея владения — средство, отличающее Rust от других языков. В главе 5 обсуждаются структуры и синтаксис методов, а в главе 6 объясняется принцип работы перечислений.
2предупреждение: неиспользованное значение типа `std::result::Result`, которое должно быть использовано
3 В модульной системе Rust упаковки (crate также переводится как «ящик, корзина, тара») занимают промежуточное положение между модулями и пакетами: пакет состоит из одной или нескольких исполняемых или библиотечных упаковок, а упаковки — из одного или нескольких модулей. См. https://doc.rust-lang.org/book/ch07-01-packages-and-crates.html.
4ошибка[E0308]: несовпадающие типы
предупреждение: неиспользованное значение типа `std::result::Result`, которое должно быть использовано
В модульной системе Rust упаковки (crate также переводится как «ящик, корзина, тара») занимают промежуточное положение между модулями и пакетами: пакет состоит из одной или нескольких исполняемых или библиотечных упаковок, а упаковки — из одного или нескольких модулей. См. https://doc.rust-lang.org/book/ch07-01-packages-and-crates.html.
ошибка[E0308]: несовпадающие типы
Однако код в листинге 2.4 пока что не компилируется. Давайте попробуем это сделать4:
Если вы не вызовете метод expect, то программа будет компилирована, но вы получите предупреждение2:
Напомним, что упаковка — это множество файлов исходного кода Rust3. Проект, который мы до сих пор строили, представляет собой двоичную, то есть исполняемую, упаковку. Упаковка rand является библиотечной, то есть содержит код, предназначенный для использования в других программах.
