Rust: Знакомимся с языком на примере «Угадай-ки»

https://doc.rust-lang.org/book/second-edition/ch02-00-guessing-game-tutorial.html
  • Перевод
  • Tutorial

Давайте познакомимся с Rust, поработав над маленьким проектом! Мы покажем основные концепты Rust на реальном примере. Вы узнаете о let, match, методах, ассоциированных функциях, подключении сторонних библиотек и много о чем другом. Мы реализуем классическую задачу: игра “угадай-ка”.


  1. Программа генерирует случайное число от 1 до 100.
  2. После этого просит игрока ввести его догадку.
  3. После этого программа уведомляет игрока:
    • если он угадал число, то игра заканчивается
    • если нет:
      • пишет, меньше предложенное число чем загаданное или больше.
      • переходит к шагу 2.

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


Для того чтобы создать новый проект:


$ cargo new guessing_game --bin
$ cd guessing_game

Первая команда cargo new принимает название программы (guessing_game) как первый аргумент. --bin велит Cargo подготовить проект для написания программы (в отличие от библиотеки). Посмотрим, что у нас в конфигурационном файле проекта — Cargo.toml


[package]
name = "guessing_game"
version = "0.1.0"
authors = ["Your Name <you@example.com>"]
[dependencies]

Если сведения об авторе, которые Cargo получил из вашей системы, неправильные, исправьте конфигурационный файл и сохраните. По умолчанию в новом проекте содержится "Hello, world".
src/main.rs


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

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


$ 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 очень удобна, когда вы часто последовательно повторяете фазы компиляции, сборки и запуска.


Обработка догадки


Первая часть нашей программы будет просить пользователя ввести его догадку, обрабатывать его ввод и проверять, что он корректен (valid). Для начала давайте дадим пользователю ввести его догадку: Здесь и далее все изменения вносятся в src/main.rs:


// Код принятия догадки и его вывода в stdout
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);
}

Код несет в себе много информации, так что рассмотрим его постепенно. Чтобы получить введенную догадку от пользователя и вывести ее в stdout, мы должны внести io библиотеку в глобальную область видимости (scope). io является частью стандартной библиотеки (здесь и далее std):


use std::io;

По умолчанию Rust привносит в область видимости программы только несколько типов (prelude). Если в prelude нет нужного вам типа, то следует внести его в область видимости вручную написанием оператора use. Использование std::io дает нам множество функций для работы с вводом-выводом (IO), включая возможность считывать ввод пользователя. Функцияmain (как и в C и в C++) является точкой входа (entry point) в программу.


fn main() {

fn позволяет объявить новую функцию, () указывает на то, что функция не принимает параметров, { предваряет тело функции. println! — макрос (macro), который выводит строку на консоль:


println!("Guess the number!"); // Тут все очевидно.
println!("Please input your guess.");

Храним значения в переменных


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


let mut guess = String::new();

Обратите внимание оператор let, который нужен для создания переменных (variables). Можно и так:


let foo = bar;

Код создает переменную с именем foo и привязывает (bind) к значению bar. В Rust по умолчанию переменные неизменяемые. Что сделать их изменяемыми, рядом с let до имени переменной добавьте mut.


let foo = 5; // неизменяемая (immutable)
let mut bar = 5; // mutable(изменяемая)

Теперь вы знаете, что let mut guess создает изменяемую переменную guess. С правой стороны знака = находится значение, к которому "привяжется" guess — это результат вызова String::new, функции возвращающей новую строку. String — это строковый тип (подобный string в C++ и StringBuilder в Java), который может расти, содержит закодированный в UTF-8 текст. :: в ::new указывает на то, что new это ассоциированная функция (associated function) при типе String. Ассоциированная функция реализована над типом, в данном случае над String, а над конкретным экземпляром типа. Некоторые языки, например C++, называют такие функции статическими (static) методами. Функция new создает новый пустой экземпляр String. Вы увидите, что new реализована над многими типами, потому что используется для обозначения функции, которая создает новое значение какого-либо типа. Обобщим: let mut guess = String::new(); создала новую изменяемую переменную, которая "привязана" (bound) к новому пустому экземпляру String (пустой строке). Мы подключили функции для работы с IO посредством use std::io. Теперь вызовем ассоциированную функцию stdin:


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

Если бы мы не писали use std::io в начале программы, то мы могли бы записать обращение к этой функции в виде std::io::stdin. Функция stdin возвращает экземпляр std::io::Stdin, который является типом, который представляет собой указатель (handle) на стандартный поток ввода (standard input). Здесь им является пользовательская клавиатура. Следующая часть нашего кода, .read_line(&mut guess), вызывает read_line на только что полученном указателе (handle) на stdin, чтобы считать ввод пользователя. Мы также посылаем единственный аргумент: &mut guess. read_line принимает от пользователя то, что приходит в stdin и помещает этот ввод в строку, поэтому принимает ссылку на строковую переменную. Этот строковой аргумент должен быть изменяемым, чтобы метод мог изменять значение этой строки, добавляя туда пользовательский ввод. & указывает на то, что данный аргумент (&mut guess) является ссылкой (reference), которая позволяет разным частям кода обращаться к одной области данных, не копируя значение из этой области данных много раз. Одной из сильных сторон Rust является то, что со ссылками легко (и при этом не повреждая память) обращаться. Пока что нам достаточно знать, что ссылки, как и переменные, по умолчанию являются неизменяемыми. Поэтому мы пишем &mut guess (создаем изменяемую ссылку), а не &guess (создаем неизменяемую ссылку).


.expect("Failed to read line");

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


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

читается несколько тяжелее.


Обработка завершения с ошибкой (используем Result)


read_line не только считывает строку, но и возвращает значение, здесь это значение типа io::Result. Rust имеет несколько типов, называющихся Result в разных библиотечных модулях.


  • общий Result
  • специфические, например io::Result
    Типы Result являются перечислениями (enumerations) — реализация Algebraic Data Type. В С/С++ аналогом является tagged union. Значение Result может быть представлено вариантами Ok и Err. Ok указывает на то, что операция прошла успешно, содержит возвращенное значение. Err — указывает на то, что операция завершилась с ошибкой, содержит внутри сведения об ошибке. К значениям типа Result применимы Result-специфичные методы, например, expect. Если объект типа Result принял значение Err, то expect выведет переданное ему сообщение об ошибке и аварийно завершит работу программы. Если принял значение Ok, то expect возвратит значение, которое находится внутри Ok. Вызываемая нами функция read_line возвращает (в случае успешного завершения) в Ok количество байт, которое ввел пользователь. Если мы не вызовем expect, то программа выдаст предупреждение при компиляции:

$ 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

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


Печать значений посредством println!


println!("You guessed: {}", guess); // печатает значение, которое ввел пользователь

{} является "заполнителем" (placeholder), указывает на то, что в полученной строке будет значение соответствующей этому заполнителю переменной. Чтобы напечатать несколько значений, используйте несколько заполнителей: первый заполнитель ({}) соответствует первому выводимому значению, второй — второму и т.д. Например, так:


let x = 5;
let y = 10;
println!("x = {} and y = {}", x, y);
// выведет:
// x = 5 and y = 10

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


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

Программа запросила число, мы ввели, программа вывела это число — все работает. Идем дальше.


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


Задумаем число (от 1 до 100), которое пользователь будет пытаться угадать. Это должно быть случайное число, чтобы в игру можно быть играть больше одного раза. У Rust в std(стандартной библиотеке) нет модуля для работы со случайными числами, поэтому используем стороннюю библиотеку (крейт (crate) в терминах Rust) rand.


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


Крейт — это всего лишь пакет кода на Rust. Мы с вами пишем исполняемый крейт (binary crate) — обычную программу в виде исполняемого (executable) бинарного файла. rand — библиотечный крейт (library crate), подобен .o и .so файлам в C — содержит функции, которые может подключить любой другой крейт. Cargo очень удобно использовать для установки сторонних крейтов. Для того чтобы начать использовать крейт rand, мы должны внести соответствующие изменения в Cargo.toml, чтобы он включал rand как зависимость (dependency). Внесем изменения в секцию [dependencies]


[dependencies]
rand = "0.3.14"

В файле Cargo.toml все что следует после заголовка секции продолжается до того, пока не начнется новая секция. Секция [dependencies] — это то место, где вы указываете Cargo крейты, от которых зависит код вашего проекта, и какие именно версии вам нужны. В данном случае мы указали, что нам для сборки проекта требуется крейт rand со спецификатором версии 0.3.14. Cargo понимает модель семантического назначения версий (Semantic Versioning), который является одним из стандартов присваивания имен разным версиям программ. Число 0.3.14 (на самом деле это краткая форма записи ^0.3.14) означает, что подходит любая версия, у которого открытый (public) API совместим с версией 0.3.14. Соберем, но уже с новой зависимостью:


$ 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 2.53 secs

Cargo скачивает зависимости из репозитория (registry) — Crates.io. Crates.io — это место, где Rust-разработчики выкладывают свои open-source проекты. После того, как Cargo.toml скачивает индекс, он проверяет то, есть ли какие-либо зависимости, которые еще не скачаны. Если есть, то скачивает их. Кроме того он еще скачивает libc, ибо крейт rand зависит от него. Это значит, что транзитивные зависимости разрешаются (dependency resolution) автоматически. После скачивания зависимостей rustc (компилятор Rust) компилирует их, потом компилирует саму программу, используя предварительно скомпилированные зависимости. Если вы запустите cargo build снова, то не увидите никаких сообщений. Cargo знает, что необходимые зависимости уже скачаны и скомпилированы и знает, что вы не меняли секцию [dependencies] в Cargo.toml. Cargo также знает, что вы не меняли свой код, поэтому он заново не перекомпилируется. Cargo завершается, так как нет никакой работы, которую ему бы требовалось выполнить. Если открыть src/main.rs и внести изменения, то cargo build проведет компиляцию кода нашего проекта заново, но не компиляцию зависимостей, ибо они не менялись (incremental compilation).


Cargo.lock способствует сборке воспроизводимых (reproducible builds) программ


Cargo имеет средства, которые позволяют получать возобновляемые сборки, он будет использовать только те версии зависимостей, которые вы указали, до тех пор, пока вы не укажете другие версии зависимостей. Что будет, если появится крейт rand версии v0.3.15, содержащий важное исправление (bug fix), и регрессию (regression), которая "ломает" ваш код? Для того чтобы решить эту проблему, используется файл Cargo.lock, который был создан при первом запуске cago build и теперь находится в корневой директории проекта. Когда вы собираете проект в первый раз, Cargo выясняет номера зависимостей, которые соответствуют заданным требованиям (в Cargo.toml) и записывает сведения о них в Cargo.lock. Когда вы собираете ваш проект во второй и последующий разы, Cargo видит, что Cargo.lock уже существует и использует версии, которые там указаны, а не выводит их заново, тратя время на анализ зависимостей. Это дает нам возможность получать воспроизводимые сборки. Другие словами, вам проект будет использовать крейт rand версии 0.3.14, пока вы явно не произведете обновление.


Обновление крейта до свежей версии


Когда вы хотите обновить крейт, Cargo предоставляет нам команду update, которая:


  • игнорирует Cargo.lock и выясняет последние версии зависимостей, которые удовлетворяют заданным вами требованиям в Cargo.toml.
  • если это удается, Cargo пишет эти номера версий обновленный зависимостей в Cargo.lock
    По умолчанию Cargo будет использовать версии > 0.3.0 и < 0.4.0. Если авторы rand выпустят две новые версии, например, 0.3.15 и 0.4.0, то вы увидите следующее при обновлении зависимостей (cargo update):

$ cargo update
   Updating registry `https://github.com/rust-lang/crates.io-index`
   Updating rand v0.3.14 -> v0.3.15

После этого вы заметите, что версия rand в Сargo.lock изменилась на 0.3.15. Если же вы хотите использовать rand версии 0.4.0 или любой другой 0.4.x версии, то вы должны будете обновить Cargo.toml, чтобы он выглядел следующим образом:


[dependencies]
rand = "0.4.0"

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


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


Перейдем к использованию rand, для этого обновим src/main.rs


extern crate rand;
use std::io;
use rand::Rng;

fn main() {
   println!("Guess the number!");
   let secret_number = rand::thread_rng().gen_range(1, 101);
   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);
}

Мы добавили extern crate rand;, что указывает rustc на то, что нужно подключить стороннюю библиотеку. Это также подобно use rand, ибо теперь у нас появилась возможность вызывать функции из rand посредством написания rand::. Также мы добавили use rand::Rng. Rng — типаж, который определяет методы, которые будут реализованы генераторами случайных чисел. Это типаж должен быть в области видимости, чтобы мы могли использовать его методы. Также мы добавили две строчки в середине. Функция rand::thread_rng возвратит генератор случайных чисел, который является локальным для текущего потока (thread), будучи предварительно инициализированным (seeded) операционной системой. Далее мы вызываем gen_range у генератора. Этот метод определен в типаже Rng, который мы предварительно внесли в область видимости оператором use rand::Rng. gen_range принимает два числа и возвращает случайное число, которое находится между ними. Диапазон включает в себя нижнюю границу и не включает верхнюю, поэтому мы должны указать числа 1 и 101, чтобы получить число от 1 до 100. Чтобы ознакомиться со всеми возможностями крейта, нужно прочитать его документацию. Еще одной возможностью Cargo является то, что вы можете "собрать" документацию, вызвав команду cargo doc --open, после чего документация будет открыта в браузере (после сборки). Если вас интересует другая функциональность крейта rand, то выберите пункт rand в панели слева. Вторая строка, которую мы добавили, печатает секретное число. Пока что оставим это так, это удобно для проверки работы программы, в финальной версии программы ее уже не будет. Запустим пару раз:


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

Программа должна каждый раз выводить разные случайные числа от 1 до 100.


Сравниваем догадку с секретным числом


После того как мы сгенерировали случайное число и получили догадку от пользователя, мы можем сравнить их. Внесем изменения в src/main.rs


extern crate rand;

use std::io;
use std::cmp::Ordering;
use rand::Rng;

fn main() {
   println!("Guess the number!");
   let secret_number = rand::thread_rng().gen_range(1, 101);
   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!"),
   }
}

Первым незнакомым элементом здесь является еще одно использование оператора use, который привносит в область видимости тип std::cmp::Ordering из std. Ordering — еще одно перечисление, оно подобно Result, но имеет другие варианты:


  • Less
  • Greater
  • Equal
    Это три возможных результата, которые мы можем получить при сравнении двух чисел. Также мы добавили код, использующий тип Ordering:

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

Метод cmp сравнивает два числа, он может быть вызван применительно к любым двум сущностям, которые сравнимы между собой. Он получает ссылку на то, с чем вы хотите сравнить данный элемент: в данном случае сравнивается guess и secret_number. cmp возвращает вариант Ordering (мы внесли его в область видимости ранее посредством оператора use). Мы также используем сопоставление match для того, чтобы, в зависимости от результатов сравнения, решить, как следует поступить далее. Сопоставлениеmatch состоит из веток (arms). Ветка состоит из шаблона и кода, который должен быть выполнен, если сопоставляемое выражение соответствует шаблону ветки. Rust последовательно сопоставляет выражение c шаблонами в ветках match, и после того, как будет найдено соответствие, код справа от подошедшего шаблона исполняется. Давай рассмотрим пример возможного взаимодействия с программой. Скажем, пользователь предложил число 50 как догадку, а секретным (задуманным) числом является 38. Когда код сравнивает 50 и 38, cmp метод возвращает Ordering::Greater, потому что 50 > 38. Ordering::Greater — это значение, которое принимает match. match смотрит на шаблон первой ветки, Ordering::Less, но значение Ordering::Greater не сопоставимо (в данном случае потому что не равно) c Ordering::Less, поэтому игнорирует код в этой ветке и переходит к сопоставлению со следующими шаблонами. Шаблон следующей ветки, Ordering::Greater сопоставим с Ordering::Greater (который мы передали match). Соответствующий код в данной ветке исполняется и печатает Too big!. Выражение match завершает сопоставление выражения с шаблонами, потому что соответствие уже найдено. Однако данный код содержит ошибку, поэтому не может быть скомпилирован:


$ 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 — это язык со строгой статической типизацией, имеющий, однако, выведение типов. Когда мы написали let guess = String::new(), Rust смог вывести, что переменная guess должна иметь тип String, и не заставил нас писать этот тип вручную. С другой стороны, secret_number — числовая переменная. Переменные некоторых типов могут принимать значения от 1 до 100. Сюда относятся следующие типы:


  • i32 — 32-битное число (может принимать отрицательные значения)
  • u32 — 32-битное число (принимает только неотрицательные целые значения)
  • i64 — 64-битное число (может принимать отрицательные значения)
    и другие. Rust по умолчанию использует тип i32, и переменная secret_number имеет данный тип (если только мы не укажем другой тип вручную, что будет указанием компилятору, что мы хотим, чтобы он сделал secret_number переменной другого, указанного нами типа). Причиной ошибки является то, что Rust не может сравнить переменную строкового типа с переменной числового типа. В конечном итоге мы хотим преобразовать переменную типа String, которую программа считывает со стандартного потока ввода, в число, чтобы мы могли сравнить догадку пользователя с загаданным числом. Мы может достичь этого добавлением следующего кода в main:

extern crate rand;

use std::io;
use std::cmp::Ordering;
use rand::Rng;

fn main() {
   println!("Guess the number!");
   let secret_number = rand::thread_rng().gen_range(1, 101);
   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!"),
   }
}

Здесь был добавлен код:


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

Мы создали переменную с именем guess. Постойте, разве программа не имеет этой переменной? Имеет, но Rust позволяет затенять (shadow), предыдущую переменную новой. Эта возможность часто используется в схожих ситуациях, когда вы хотите преобразовать значение одного типа в значение другого типа. Затенение (shadowing) позволяет заново использовать имя (identifier), не создавая две различные переменные, наподобие guess_str и guess. Мы привязываем (bind) guess к выражению guess.trim().parse(). guess в данном выражении относится к строковой переменной guess, которую мы задали ранее. Метод trim у переменной типа String удаляет пробелы в начале и конце строки. u32 может содержать только цифры, но пользователь может нажать на ENTER, чтобы завершить ввод (и передать его read_line). Когда пользователь нажимает ENTER, символ переноса строки добавляется в ее конец. Например, когда пользователь нажимает на клавишу 5 и потом на ENTER, guess принимает значение 5\n. \n — строковое представление символа перевода строки, который вводится нажатием на ENTER. Метод trim удаляет \n, поэтому остается 5. Метод parse у типа String, преобразовывает строку в число. Так как данный метод может преобразовывать строки в числа разных типов, мы должны указать Rust точный тип новой переменной, в данном случае кодом let guess: u32. (:) после guess, указывает Rust на то, что мы хотим указать (annotate) конкретный тип новой переменной. Rust включает в себя несколько встроенных типов данных, например, u32, который представляет собой неотрицательное целое 32-битное число. Данный тип часто является хорошим выбором для небольших положительных чисел. В добавок назначение u32 как типа для guess и сравнение guess с secret_number неявно означает, что Rust выведет для переменной secret_number тип u32. В итоге сравнение будет происходить между переменными одного и того же типа (u32). Функция parse может легко завершиться с ошибкой. Если, к примеру, строка содержит в себе подстроку A%, то она не может быть преобразована в число. Из-за того, что строка может быть непреобразовываемой, parse возвращает значение типа Result, подобно тому, как это делает read_line. Мы поступим с Result так же: вызовем метод expect. Если parse возвращает вариант Err из-за того, что не может преобразовать строку в число, то вызов expect приведет к аварийному завершению игры, предварительно показав пользователю переданное функции expect сообщение. Если же строка была успешно преобразована методом parse в число, то parse возвратит вариант Ok, и expect возвратит нам извлеченное из Ok число. Попробуем еще раз:


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

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


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

Предлагайте ваши догадки несколько раз


Ключевое слово loop позволяет нам создать бесконечный цикл. Добавим:


extern crate rand;

use std::io;
use std::cmp::Ordering;
use rand::Rng;

fn main() {
   println!("Guess the number!");
   let secret_number = rand::thread_rng().gen_range(1, 101);
   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);
       match guess.cmp(&secret_number) {
           Ordering::Less => println!("Too small!"),
           Ordering::Greater => println!("Too big!"),
           Ordering::Equal => println!("You win!"),
       }
   }
}

Как вы можете видеть, мы поместили все в цикл. Запустим:


$ cargo run
  Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
    Running `target/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 'Please type a number!: ParseIntError { kind: InvalidDigit }', src/libcore/result.rs:785
note: Run with `RUST_BACKTRACE=1` for a backtrace.
error: Process didn't exit successfully: `target/debug/guess` (exit code: 101)

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


Завершение игры при правильной догадке


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


extern crate rand;

use std::io;
use std::cmp::Ordering;
use rand::Rng;

fn main() {
   println!("Guess the number!");
   let secret_number = rand::thread_rng().gen_range(1, 101);
   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);
       match guess.cmp(&secret_number) {
           Ordering::Less => println!("Too small!"),
           Ordering::Greater => println!("Too big!"),
           Ordering::Equal => {
               println!("You win!");
               break;
           }
       }
   }
}

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


Обработка неправильного ввода


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


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

Переход от expect к match меняет поведение программы. Теперь программа не завершается аварийно, а обрабатывает ошибку. Если метод parse может успешно преобразовать строку в число, то он возвращает Ok и сопоставление match, в свою очередь, возвращает извлеченное из Ok число (num), которое parse в этот Ok завернул. После этого данное число окажется присвоенным новой переменной guess. Если же метод parse не может успешно преобразовать строку в число, то он возвращает вариант Err, который содержит в себе сведения о произошедшей ошибке. Значение Err не может быть успешно сопоставлено с шаблоном Ok(num) в первой ветке выражения match, но оно успешно сопоставляется с шаблоном Err(_) во второй ветке. _ является шаблоном, который успешно сопоставляется с любым выражением, в нашем примере Err(_) будет успешно сопоставлен с любой ошибкой внутри варианта Err. После успешного сопоставления будет выполнен код во второй ветке matchcontinue, что приведет к переходу к следующей итерации цикла. Теперь программа работает должным образом:


$ cargo run
  Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
    Running `target/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!

Отлично. Осталось удалить из программы код печати задуманного числа.


extern crate rand;

use std::io;
use std::cmp::Ordering;
use rand::Rng;

fn main() {
   println!("Guess the number!");
   let secret_number = rand::thread_rng().gen_range(1, 101);
   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;
           }
       }
   }
}

Заключение


Здесь были представлены основные концепции языка Rust:


  • let — объявление переменной с выведением типа (type inference)
    на этапе компиляции
  • match — сопоставление выражения с шаблонами в стиле функционального программирования
  • методы
  • ассоциированные функции. Подобны static методам в С++
  • использование сторонних библиотек (crates)
  • +23
  • 5,6k
  • 1
Поделиться публикацией

Комментарии 1

    +8
    О, на хабре уже публикуют ранее переведенные и опубликованные куски книги по Rust?
    Печально это :(

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

    Самое читаемое