Сравнение попытки угадать с секретным числом
Теперь, когда у нас есть ввод пользователя и случайное число, мы можем сравнить их. Этот шаг показан в Листинге 2-4. Обратите внимание, что этот код еще не скомпилируется, как мы объясним.
Имя файла: src/main.rs
use rand::Rng;
1 use std::cmp::Ordering;
use std::io;
fn main() {
--snip--
println!("You guessed: {guess}");
2 match guess.3 cmp(&secret_number) {
Ordering::Less => println!("Too small!"),
Ordering::Greater => println!("Too big!"),
Ordering::Equal => println!("You win!"),
}
}
Листинг 2-4: Обработка возможных возвращаемых значений при сравнении двух чисел
Во - первых, мы добавляем еще одну инструкцию use
[1], подключая тип std::cmp::Ordering
из стандартной библиотеки в область видимости. Тип Ordering
- это еще один перечисление (enum) и имеет варианты Less
, Greater
и Equal
. Эти три результата могут быть при сравнении двух значений.
Затем мы добавляем пять новых строк внизу, которые используют тип Ordering
. Метод cmp
[3] сравнивает два значения и может быть вызван на любом типе, который может быть сравниваемым. Он принимает ссылку на то, с чем вы хотите сравнивать: здесь он сравнивает guess
с secret_number
. Затем он возвращает вариант перечисления Ordering
, которое мы подключили к области видимости с помощью инструкции use
. Мы используем выражение match
[2], чтобы определить, что делать дальше, в зависимости от того, какой вариант Ordering
был возвращен из вызова cmp
с значениями в guess
и secret_number
.
Выражение match
состоит из ветвей (arms). Ветвь состоит из шаблона (pattern), который нужно сопоставить, и кода, который должен выполняться, если значение, переданное в match
, соответствует шаблону этой ветви. Rust берет значение, переданное в match
, и последовательно просматривает каждый шаблон ветви. Шаблоны и конструкция match
- это мощные особенности Rust: они позволяют вам выражать различные ситуации, которые может встретить ваш код, и обеспечивают обработку всех этих ситуаций. Эти особенности будут рассмотрены подробнее в главе 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
! Связанный с этой ветвью код будет выполнен, и на экран будет выведено Too big!
. Выражение match
заканчивается после первого успешного сопоставления, поэтому в этом сценарии оно не посмотрит на последнюю ветвь.
Однако код в Листинге 2-4 еще не скомпилируется. Попробуем его:
$ cargo build
Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
error[E0308]: mismatched types
--> src/main.rs:22:21
|
22 | match guess.cmp(&secret_number) {
| ^^^^^^^^^^^^^^ expected struct `String`, found integer
|
= note: expected reference `&String`
found reference `&{integer}`
Основная причина ошибки заключается в том, что есть несовместимые типы. Rust имеет сильную статическую систему типов. Однако он также имеет вывод типов (type inference). Когда мы написали let mut guess = String::new()
, Rust смог вывести, что guess
должен быть String
, и не заставил нас указывать тип. С другой стороны, secret_number
- это числовой тип. Некоторые числовые типы в Rust могут иметь значение от 1 до 100: i32
(32 - битное число), u32
(беззнаковое 32 - битное число), i64
(64 - битное число) и другие. Если не указано иначе, Rust по умолчанию использует i32
, который является типом secret_number
, если вы не добавите информацию о типе в другом месте, которая заставит Rust вывести другой числовой тип. Причина ошибки в том, что Rust не может сравнить строку и числовой тип.
В конечном итоге, мы хотим преобразовать String
, которое программа читает в качестве ввода, в реальный числовой тип, чтобы мы могли сравнить его численно с секретным числом. Мы делаем это, добавив эту строку в тело функции main
:
Имя файла: src/main.rs
use std::io;
use rand::Rng;
use std::cmp::Ordering;
fn main() {
println!("Guess the number!");
let secret_number = rand::thread_rng().gen_range(1..=100);
println!("The secret number is: {secret_number}");
println!("Please input your guess.");
let mut guess = String::new();
io::stdin()
.read_line(&mut guess)
.expect("Failed to read line");
let guess: u32 = guess
.trim()
.parse()
.expect("Please type a number!");
println!("You guessed: {guess}");
match guess.cmp(&secret_number) {
Ordering::Less => println!("Too small!"),
Ordering::Greater => println!("Too big!"),
Ordering::Equal => println!("You win!"),
}
}
Мы создаем переменную с именем guess
. Но подождите, у программы уже есть переменная с именем guess
? Да, но к счастью, Rust позволяет нам "затенять" (shadow) предыдущее значение guess
новым. Затенение (shadowing) позволяет нам повторно использовать имя переменной guess
, вместо того чтобы заставить нас создавать два уникальных имени переменной, например, guess_str
и guess
. Мы рассмотрим это более подробно в главе 3, но на данный момент просто запомните, что эта особенность часто используется, когда вы хотите преобразовать значение из одного типа в другой тип.
Мы связываем эту новую переменную с выражением guess.trim().parse()
. guess
в выражении ссылается на исходную переменную guess
, которая содержала ввод в виде строки. Метод trim
на экземпляре String
удалит все пробелы в начале и в конце, что необходимо сделать, чтобы сравнить строку с u32
, который может содержать только числовые данные. Пользователь должен нажать Enter, чтобы удовлетворить read_line
и ввести свою догадку, что добавляет символ новой строки в строку. Например, если пользователь вводит 5
и нажимает Enter, guess
выглядит так: 5\n
. \n
представляет "новую строку" (На Windows нажатие Enter приводит к возврату каретки и новой строке, \r\n
). Метод trim
удаляет \n
или \r\n
, оставляя только 5
.
Метод parse
на строках преобразует строку в другой тип. Здесь мы используем его, чтобы преобразовать из строки в число. Мы должны сказать Rust точный числовой тип, который мы хотим, используя let guess: u32
. Двоеточие (:
) после guess
говорит Rust, что мы будем аннотировать тип переменной. Rust имеет несколько встроенных числовых типов; u32
, который мы видим здесь, - это беззнаковое, 32 - битное целое число. Это хороший выбор по умолчанию для небольшого положительного числа. Вы узнаете о других числовых типах в главе 3.
此外,аннотация u32
в этом примере программы и сравнение с secret_number
означает, что Rust также будет выводить, что secret_number
должен быть u32
. Теперь сравнение будет между двумя значениями одного типа!
Метод parse
будет работать только с символами, которые могут логически быть преобразованы в числа, и поэтому может легко вызывать ошибки. Например, если строка содержала A👍%
, невозможно было бы преобразовать это в число. Поскольку это может привести к ошибке, метод parse
возвращает тип Result
, так же, как и метод read_line
(обсуждается ранее в разделе "Обработка потенциальной ошибки с Result"). Мы будем обрабатывать это Result
так же, используя метод expect
снова. Если parse
возвращает вариант Err
из Result
, потому что он не может создать число из строки, вызов expect
упадет с игрой и выведет сообщение, которое мы передаем ему. Если parse
может успешно преобразовать строку в число, он вернет вариант Ok
из Result
, и expect
вернет число, которое мы хотим из значения Ok
.
Теперь запустим программу:
$ cargo run
Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
Finished dev [unoptimized + debuginfo] target(s) in 0.43s
Running `target/debug/guessing_game`
Guess the number!
The secret number is: 58
Please input your guess.
76
You guessed: 76
Too big!
Отлично! Даже несмотря на то, что перед догадкой были добавлены пробелы, программа все равно поняла, что пользователь угадал 76. Запустите программу несколько раз, чтобы проверить разное поведение при разных видах ввода: угадать число правильно, угадать число, которое слишком большое, и угадать число, которое слишком маленькое.
Теперь у нас работает большая часть игры, но пользователь может сделать только одну попытку. Изменим это, добавив цикл!