予想ゲームのプログラミング

RustRustBeginner
今すぐ練習

This tutorial is from open-source community. Access the source code

💡 このチュートリアルは英語版からAIによって翻訳されています。原文を確認するには、 ここをクリックしてください

はじめに

予想ゲームのプログラミングへようこそ。この実験は、Rust Bookの一部です。LabExでRustのスキルを練習することができます。

この実験では、Rustで予想ゲームを実装します。このプログラムはランダムな数を生成し、プレーヤーにそれを予想するよう促します。予想が低すぎるか高すぎるかに関するフィードバックを提供し、正解した場合にはプレーヤーに祝いを送ります。


Skills Graph

%%%%{init: {'theme':'neutral'}}%%%% flowchart RL rust(("Rust")) -.-> rust/DataStructuresandEnumsGroup(["Data Structures and Enums"]) rust(("Rust")) -.-> rust/BasicConceptsGroup(["Basic Concepts"]) rust(("Rust")) -.-> rust/DataTypesGroup(["Data Types"]) rust(("Rust")) -.-> rust/FunctionsandClosuresGroup(["Functions and Closures"]) rust/BasicConceptsGroup -.-> rust/variable_declarations("Variable Declarations") rust/BasicConceptsGroup -.-> rust/mutable_variables("Mutable Variables") rust/DataTypesGroup -.-> rust/integer_types("Integer Types") rust/DataTypesGroup -.-> rust/string_type("String Type") rust/FunctionsandClosuresGroup -.-> rust/function_syntax("Function Syntax") rust/FunctionsandClosuresGroup -.-> rust/expressions_statements("Expressions and Statements") rust/DataStructuresandEnumsGroup -.-> rust/method_syntax("Method Syntax") subgraph Lab Skills rust/variable_declarations -.-> lab-100386{{"予想ゲームのプログラミング"}} rust/mutable_variables -.-> lab-100386{{"予想ゲームのプログラミング"}} rust/integer_types -.-> lab-100386{{"予想ゲームのプログラミング"}} rust/string_type -.-> lab-100386{{"予想ゲームのプログラミング"}} rust/function_syntax -.-> lab-100386{{"予想ゲームのプログラミング"}} rust/expressions_statements -.-> lab-100386{{"予想ゲームのプログラミング"}} rust/method_syntax -.-> lab-100386{{"予想ゲームのプログラミング"}} end

予想ゲームのプログラミング

一緒に手を動かしながらRustに挑戦しましょう!この章では、実際のプログラムでRustの概念をどのように使うかを示すことで、いくつかの一般的なRustの概念を紹介します。letmatch、メソッド、関連関数、外部クレートなどについて学びます!次の章では、これらのアイデアを詳細に探ります。この章では、基本を練習するだけです。

私たちは、古典的な初心者向けのプログラミング問題である予想ゲームを実装します。以下がその仕組みです。プログラムは1から100までのランダムな整数を生成します。そして、プレーヤーに予想を入力するよう促します。予想が入力された後、プログラムは予想が低すぎるか高すぎるかを示します。予想が正解した場合、ゲームは祝辞を表示して終了します。

新しいプロジェクトのセットアップ

新しいプロジェクトをセットアップするには、第1章で作成したprojectディレクトリに移動し、Cargoを使って新しいプロジェクトを作成します。次のようにします。

cargo new guessing_game
cd guessing_game

最初のコマンドcargo newは、プロジェクト名(guessing_game)を第一引数として取ります。第二のコマンドは新しいプロジェクトのディレクトリに移動します。

生成されたCargo.tomlファイルを見てみましょう。

ファイル名:Cargo.toml

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

## 詳細はこちらを参照ください
https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]

第1章で見たように、cargo newはあなたに「Hello, world!」プログラムを生成します。src/main.rsファイルを見てみましょう。

ファイル名:src/main.rs

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

ここで、この「Hello, world!」プログラムをコンパイルして、cargo runコマンドを使って同じ手順で実行しましょう。

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

このゲームで行うように、プロジェクトを迅速に反復する必要がある場合、runコマンドは便利です。次の反復に移る前に、各反復を迅速にテストできます。

src/main.rsファイルを再開します。このファイルにすべてのコードを書きます。

予想の処理

予想ゲームプログラムの最初の部分は、ユーザー入力を求め、その入力を処理し、入力が期待される形式であることを確認します。まずは、プレーヤーに予想を入力できるようにしましょう。リスト2-1のコードをsrc/main.rsに入力してください。

ファイル名:src/main.rs

use std::io;

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

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

    let mut guess = String::new();

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

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

リスト2-1:ユーザーから予想を取得して表示するコード

このコードにはたくさんの情報が含まれているので、1行ずつ見ていきましょう。ユーザー入力を取得し、その結果を出力として表示するには、io入出力ライブラリをスコープに入れる必要があります。ioライブラリは、stdと呼ばれる標準ライブラリにあります。

use std::io;

デフォルトでは、Rustには標準ライブラリに定義された一連の項目があり、これらはすべてのプログラムのスコープに入ります。このセットは「プレリュード」と呼ばれ、その中身はhttps://doc.rust-lang.org/std/prelude/index.htmlで確認できます。

使用したい型がプレリュードにない場合、use文を使ってその型を明示的にスコープに入れる必要があります。std::ioライブラリを使用することで、ユーザー入力を受け付ける機能など、多くの便利な機能が利用できます。

第1章で見たように、main関数はプログラムのエントリポイントです。

fn main() {

fn構文は新しい関数を宣言します。丸括弧()はパラメータがないことを示し、波括弧{は関数の本体を始めます。

第1章で学んだように、println!は文字列を画面に表示するマクロです。

println!("Guess the number!");

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

このコードは、ゲームの内容を示すプロンプトを表示し、ユーザーからの入力を求めています。

変数を使った値の格納

次に、ユーザー入力を格納するための「変数」を作成しましょう。次のようになります。

let mut guess = String::new();

これでプログラムが面白くなってきました!この短い行にはたくさんのことが起こっています。変数を作成するにはlet文を使います。もう一つ例を見てみましょう。

let apples = 5;

この行は、applesという名前の新しい変数を作成し、それを値5にバインドします。Rustでは、変数はデフォルトで不変です。つまり、変数に値を与えると、その値は変更されません。この概念については、「変数と可変性」で詳細に説明します。変数を可変にするには、変数名の前にmutを追加します。

let apples = 5; // 不変
let mut bananas = 5; // 可変

注://構文はコメントを始め、その行の終わりまで続きます。Rustはコメント内のすべてを無視します。第3章でコメントについてもっと詳しく説明します。

予想ゲームのプログラムに戻ります。これで、let mut guessguessという名前の可変変数を導入することがわかりました。等号(=)は、Rustに対して、今変数に何かをバインドしたいことを伝えます。等号の右側は、guessがバインドされる値で、String::newを呼び出した結果です。String::newは、Stringの新しいインスタンスを返す関数です。Stringは、標準ライブラリによって提供される文字列型で、拡張可能なUTF-8エンコードのテキストです。

::new行の::構文は、newString型の関連関数であることを示しています。「関連関数」とは、特定の型に対して実装された関数のことで、この場合ではString型です。このnew関数は、新しい空の文字列を作成します。多くの型にnew関数がありますが、それはある種の新しい値を作成する関数の一般的な名前だからです。

要するに、let mut guess = String::new();行は、可変変数を作成し、それを現在、Stringの新しい空のインスタンスにバインドしています。なるほど!

ユーザー入力の受け取り

プログラムの最初の行でuse std::io;を使って標準ライブラリの入出力機能を取り込んだことを思い出してください。ここでは、ioモジュールのstdin関数を呼び出して、ユーザー入力を処理します。

io::stdin()
 .read_line(&mut guess)

もしプログラムの最初にuse std::io;ioライブラリをインポートしていなかった場合でも、この関数呼び出しをstd::io::stdinと書くことでまだ使うことができます。stdin関数はstd::io::Stdinのインスタンスを返します。これは、端末の標準入力に対するハンドルを表す型です。

次に、.read_line(&mut guess)の行は、標準入力ハンドルのread_lineメソッドを呼び出して、ユーザーからの入力を取得します。また、read_lineに引数として&mut guessを渡して、ユーザー入力を格納する文字列の場所を伝えています。read_lineの主な役割は、ユーザーが標準入力に入力したものをすべて取得し、それを文字列に追加すること(内容を上書きしない)です。したがって、その文字列を引数として渡します。文字列の引数は可変でなければなりません。なぜなら、メソッドが文字列の内容を変更できるようにするためです。

&は、この引数が「参照」であることを示しています。これにより、コードの複数の部分が同じデータを複数回メモリにコピーする必要なくアクセスできるようになります。参照は複雑な機能ですが、Rustの大きな利点の一つは、参照を使うことがどれほど安全で簡単であるかということです。このプログラムを完成させるには、それらの詳細をたくさん知る必要はありません。今のところ、必要なのは、変数と同じように、参照はデフォルトで不変であることだけを知ることです。したがって、可変にするには&mut guessと書く必要があります(第4章で参照についてもっと詳しく説明します)。

Result を使った潜在的なエラーの処理

まだこのコード行に取り組んでいます。今は3行目のコードについて話していますが、1つの論理的なコード行の一部であることに注意してください。次の部分はこのメソッドです。

.expect("Failed to read line");

このコードを次のように書くこともできました。

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

しかし、1行が長くなると読みにくくなるので、分割するのが良いでしょう。.method_name()構文でメソッドを呼び出す際に、改行やその他の空白を入れて長い行を分割するのは、多くの場合賢明です。では、この行が何をするか見てみましょう。

前述の通り、read_lineはユーザーが入力したものを渡された文字列に入れますが、Result値を返します。Resultは「列挙型」と呼ばれるもので、通常は「enum」と略されます。これは、複数の可能な状態のいずれかになり得る型です。それぞれの可能な状態を「バリアント」と呼びます。

第6章で列挙型についてもっと詳しく説明します。これらのResult型の目的は、エラー処理情報をエンコードすることです。

ResultのバリアントはOkErrです。Okバリアントは操作が成功したことを示し、Okの中には成功して生成された値があります。Errバリアントは操作が失敗したことを意味し、Errには操作がどのように、またはなぜ失敗したかに関する情報が含まれています。

Result型の値は、他の型の値と同様に、それに定義されたメソッドがあります。Resultのインスタンスには、呼び出すことができるexpectメソッドがあります。このResultのインスタンスがErr値である場合、expectはプログラムをクラッシュさせ、expectに引数として渡したメッセージを表示します。read_lineメソッドがErrを返す場合、それはおそらく基礎となるオペレーティングシステムからのエラーの結果でしょう。このResultのインスタンスがOk値である場合、expectOkが保持している戻り値を取り、その値だけを返してくれます。それで、その値を使うことができます。この場合、その値はユーザー入力のバイト数です。

expectを呼び出さない場合、プログラムはコンパイルされますが、警告が表示されます。

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

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

Rustは、read_lineから返されたResult値を使っていないことを警告しており、これはプログラムが潜在的なエラーを処理していないことを示しています。

警告を抑制する正しい方法は、実際にエラー処理コードを書くことですが、この場合、問題が発生したときにこのプログラムをクラッシュさせたいだけなので、expectを使うことができます。第9章でエラーから回復する方法について学びます。

println! プレースホルダを使った値の表示

これまでのコードで、閉じ括弧を除いて、もう1行だけ説明する必要があります。

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

この行は、現在ユーザー入力を含む文字列を表示します。波括弧のセット{}はプレースホルダです。{}を小さなガニのはさみと考えてください。これが値を保持するためのものです。変数の値を表示する際、変数名を波括弧の中に入れることができます。式の評価結果を表示する際は、フォーマット文字列の中に空の波括弧を置き、その後に、同じ順序で各空の波括弧プレースホルダに表示する式のコンマ区切りのリストを続けます。println!の1回の呼び出しで変数と式の結果を表示するには、次のようになります。

let x = 5;
let y = 10;

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

このコードはx = 5 and y = 12を表示します。

最初の部分のテスト

予想ゲームの最初の部分をテストしてみましょう。cargo runを使って実行します。

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

この時点で、ゲームの最初の部分は完了です。キーボードから入力を受け取り、それを表示しています。

秘密の数字の生成

次に、ユーザーが予想しようとする秘密の数字を生成する必要があります。秘密の数字は毎回異なるようにして、ゲームを何度も楽しくプレイできるようにします。ゲームがあまり難しくないように、1から100までの乱数を使います。Rustの標準ライブラリにはまだ乱数機能が含まれていません。ただし、Rustチームはhttps://crates.io/crates/randに、そのような機能を備えたrandクレートを提供しています。

機能を増やすためのクレートの使用

クレートはRustのソースコードファイルのコレクションであることを覚えておいてください。これまでに構築してきたプロジェクトは「バイナリクレート」であり、実行可能なものです。randクレートは「ライブラリクレート」であり、他のプログラムで使用するためのコードが含まれており、独自で実行することはできません。

Cargoによる外部クレートのコーディネーションこそが、Cargoの本当の強みです。randを使用するコードを書く前に、Cargo.tomlファイルを変更して、randクレートを依存関係として追加する必要があります。今すぐそのファイルを開き、Cargoが自動的に作成した[dependencies]セクションヘッダの下の末尾に次の行を追加しましょう。このバージョン番号の通りにrandを正確に指定してください。そうしないと、このチュートリアルのコード例が機能しなくなる場合があります。

ファイル名:Cargo.toml

[dependencies]
rand = "0.8.5"

Cargo.tomlファイルでは、ヘッダの後に続くすべてのものは、そのセクションの一部であり、別のセクションが始まるまで続きます。[dependencies]では、プロジェクトがどの外部クレートに依存しているか、およびそれらのクレートのどのバージョンが必要かをCargoに伝えます。この場合、セマンティックバージョニング(時々「SemVer」と呼ばれます)を指定してrandクレートを指定しています。これは、バージョン番号を記述するための標準です。指定子0.8.5は実際には^0.8.5の省略形であり、少なくとも0.8.5以上0.9.0未満の任意のバージョンを意味します。

Cargoはこれらのバージョンがバージョン0.8.5と互換性のあるパブリックAPIを持っていると考えており、この仕様により、本章のコードとまだコンパイルできる最新のパッチリリースが得られます。0.9.0以上の任意のバージョンは、以下の例で使用されているAPIと同じものであることは保証されていません。

では、コードを変更せずに、リスト2-2に示すようにプロジェクトをビルドしてみましょう。

$ cargo build
    Updating crates.io index
  Downloaded rand v0.8.5
  Downloaded libc v0.2.127
  Downloaded getrandom v0.2.7
  Downloaded cfg-if v1.0.0
  Downloaded ppv-lite86 v0.2.16
  Downloaded rand_chacha v0.3.1
  Downloaded rand_core v0.6.3
   Compiling rand_core v0.6.3
   Compiling libc v0.2.127
   Compiling getrandom v0.2.7
   Compiling cfg-if v1.0.0
   Compiling ppv-lite86 v0.2.16
   Compiling rand_chacha v0.3.1
   Compiling rand v0.8.5
   Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
    Finished dev [unoptimized + debuginfo] target(s) in 2.53s

リスト2-2:randクレートを依存関係として追加した後のcargo buildの実行結果

バージョン番号が異なる場合があります(ただし、SemVerのおかげですべてコードと互換性があります!)、行も異なります(オペレーティングシステムによって異なります)、行の順序も異なる場合があります。

外部依存関係を含めると、Cargoはレジストリからその依存関係が必要とするすべてのものの最新バージョンを取得します。これは、https://crates.ioのCrates.ioからのデータのコピーです。Crates.ioは、Rustエコシステムの人々が他の人が使用できるようにオープンソースのRustプロジェクトを投稿する場所です。

レジストリを更新した後、Cargoは[dependencies]セクションを確認し、既にダウンロードされていないリストにあるクレートをすべてダウンロードします。この場合、依存関係としてrandのみをリストに記載しましたが、Cargoはrandが動作するために依存している他のクレートも取得しました。クレートをダウンロードした後、Rustはそれらをコンパイルし、その後、利用可能な依存関係を使ってプロジェクトをコンパイルします。

何も変更せずにすぐにcargo buildを再度実行すると、Finished行以外には何も出力されません。Cargoは既に依存関係をダウンロードしてコンパイルしていることを知っており、Cargo.tomlファイルで依存関係に関する何も変更を加えていません。Cargoはまた、コードに関する何も変更を加えていないことも知っているので、それも再コンパイルしません。何もすることがないので、単に終了します。

src/main.rsファイルを開き、些細な変更を加えて保存してから再ビルドすると、出力は2行だけになります。

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

これらの行は、Cargoがsrc/main.rsファイルに対する些細な変更でのみビルドを更新することを示しています。依存関係は変更されていないので、Cargoはそれらに対して既にダウンロードしてコンパイルしたものを再利用できることを知っています。

Cargo.lockファイルを使った再現可能なビルドの確保

Cargoには、あなた自身または他の誰があなたのコードをビルドしたときに、同じアーティファクトを再ビルドできるようにするメカニズムがあります。Cargoは、あなたがそう指示しない限り、指定した依存関係のバージョンのみを使用します。たとえば、来週randクレートのバージョン0.8.6がリリースされ、そのバージョンには重要なバグ修正が含まれていますが、同時にあなたのコードを破壊するリグレッションも含まれているとしましょう。これを処理するために、Rustは最初にcargo buildを実行したときにCargo.lockファイルを作成します。そのため、今ではguessing_gameディレクトリにこのファイルがあります。

最初にプロジェクトをビルドするとき、Cargoは基準に合致する依存関係のすべてのバージョンを特定し、それらをCargo.lockファイルに書き込みます。これからプロジェクトをビルドするとき、CargoはCargo.lockファイルが存在することに気付き、そこに指定されたバージョンを使用して、再びバージョンを特定するすべての作業を行わなくて済むようになります。これにより、自動的に再現可能なビルドが可能になります。言い換えると、Cargo.lockファイルのおかげで、明示的にアップグレードするまで、あなたのプロジェクトは0.8.5のままです。Cargo.lockファイルは再現可能なビルドにとって重要なので、通常、プロジェクトのコードの残りとともにソースコントロールに登録されます。

新しいバージョンを取得するためのクレートの更新

クレートを更新したい場合、Cargoにはupdateコマンドが用意されています。これはCargo.lockファイルを無視し、Cargo.toml内のあなたの仕様に合致するすべての最新バージョンを特定します。その後、CargoはそれらのバージョンをCargo.lockファイルに書き込みます。それ以外の場合、デフォルトでは、Cargoは0.8.5より大きく0.9.0未満のバージョンのみを探します。randクレートが0.8.6と0.9.0の2つの新しいバージョンをリリースしていた場合、cargo updateを実行すると次のようになります。

$ cargo update
Updating crates.io index
Updating rand v0.8.5 - > v0.8.6

Cargoは0.9.0のリリースを無視します。この時点で、あなたはまたCargo.lockファイルに変更があることに気付きます。それは、あなたが今使用しているrandクレートのバージョンが0.8.6であることを示しています。randバージョン0.9.0や0.9.xシリーズの任意のバージョンを使用するには、代わりにCargo.tomlファイルをこのように更新する必要があります。

[dependencies]
rand = "0.9.0"

次にcargo buildを実行すると、Cargoは利用可能なクレートのレジストリを更新し、指定した新しいバージョンに基づいてrandの要件を再評価します。

Cargoとそのエコシステムについてはもっとたくさん話すことができますが、第14章で議論します。今のところ、これだけで十分です。Cargoはライブラリを再利用することを非常に簡単にしてくれるので、Rustaceansは多数のパッケージから組み立てられたより小さなプロジェクトを書くことができます。

乱数の生成

では、予想するための数を生成するためにrandを使ってみましょう。次のステップは、リスト2-3に示すようにsrc/main.rsを更新することです。

ファイル名:src/main.rs

use std::io;
1 use rand::Rng;

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

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

  3 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}");
}

リスト2-3:乱数を生成するコードの追加

まず、use rand::Rng;という行を追加します[1]。Rngトレイトは、乱数生成器が実装するメソッドを定義しており、これらのメソッドを使用するためにはこのトレイトがスコープ内にある必要があります。第10章ではトレイトについて詳細に説明します。

次に、途中に2行を追加します。最初の行で[2]、rand::thread_rng関数を呼び出します。この関数は、使用する特定の乱数生成器を返します。これは、現在の実行スレッド固有のもので、オペレーティングシステムによってシードが設定されます。その後、乱数生成器のgen_rangeメソッドを呼び出します。このメソッドは、use rand::Rng;文によってスコープ内に持ち込んだRngトレイトによって定義されています。gen_rangeメソッドは、範囲式を引数として取り、その範囲内の乱数を生成します。ここで使用している範囲式の形式はstart..=endで、下限と上限の両方が含まれます。したがって、1から100の間の数を要求するには1..=100を指定する必要があります。

注:どのトレイトを使用するか、またクレートからどのメソッドや関数を呼ぶかをすぐに知ることはできません。したがって、各クレートには使用方法の説明が含まれたドキュメントがあります。Cargoのもう一つの便利な機能は、cargo doc --openコマンドを実行すると、すべての依存関係によって提供されるドキュメントがローカルにビルドされ、ブラウザで開かれることです。たとえば、randクレートの他の機能に興味がある場合は、cargo doc --openを実行して、左側のサイドバーのrandをクリックしてください。

2番目の新しい行で[3]、秘密の数を表示します。これは、プログラムを開発中にテストするために便利ですが、最終バージョンからは削除します。プログラムが始まるとすぐに答えを表示すると、あまりゲームになりません!

このプログラムを何回か実行してみてください:

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

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

異なる乱数が得られるはずで、それらはすべて1から100の間の数であるはずです。素晴らしい仕事です!

予想値と秘密の数を比較する

これでユーザー入力と乱数があるので、それらを比較することができます。そのステップをリスト2-4に示します。ただし、説明するように、このコードはまだコンパイルされません。

ファイル名: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:2つの数を比較したときの返り値を処理する

まず、もう一つのuse文[1]を追加します。これにより、標準ライブラリからstd::cmp::Orderingという型がスコープ内に持ち込まれます。Ordering型は別の列挙型であり、LessGreaterEqualの各バリアントを持っています。これらは2つの値を比較したときに考えられる3つの結果です。

次に、底部に5行の新しいコードを追加します。これらはOrdering型を使用しています。cmpメソッド[3]は2つの値を比較し、比較可能な値に対して呼び出すことができます。比較対象の値への参照を引数に取ります。ここではguesssecret_numberを比較しています。そして、use文によってスコープ内に持ち込んだOrdering列挙型のバリアントを返します。cmp関数にguesssecret_numberの値を渡して返されたOrderingのバリアントに基づいて、次に何をするかを決定するためにmatch式[2]を使用します。

match式は「アーム」で構成されています。アームは、一致させる「パターン」と、matchに渡された値がそのアームのパターンに合致した場合に実行するコードで構成されています。Rustはmatchに渡された値を取り、順番に各アームのパターンを調べます。パターンとmatch構文は強力なRustの機能です。これらは、コードが遭遇する可能性のあるさまざまな状況を表現し、それらすべてを処理することを確実にします。これらの機能については、それぞれ第6章と第18章で詳細に説明されます。

ここで使用しているmatch式の例を見てみましょう。ユーザーが50を予想し、今回ランダムに生成された秘密の数が38だったとします。

コードが50と38を比較すると、cmpメソッドは50が38より大きいためOrdering::Greaterを返します。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は強力な静的型システムを持っています。ただし、型推論も備えています。let mut guess = String::new()と書いたとき、RustはguessString型であると推論し、型を明示的に書く必要はありませんでした。一方、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は既存のguessの値を新しい値で上書きする(シャドーイング)ことを許可しています。シャドーイングにより、guess_strguessのように2つの一意の変数を作成する代わりに、guessという変数名を再利用できます。これについては第3章で詳しく説明しますが、ここでは、値をある型から別の型に変換したい場合によく使用されることを知っておいてください。

この新しい変数をguess.trim().parse()という式にバインドします。式内のguessは、入力を文字列として含む元のguess変数を指します。Stringインスタンスのtrimメソッドは、先頭と末尾の空白を削除します。これは、u32(数値データのみを含む)と文字列を比較するために必要です。ユーザーはread_lineを満たすためにエンターキーを押して予想値を入力します。これにより、文字列に改行文字が追加されます。たとえば、ユーザーが5と入力してエンターキーを押すと、guessはこのようになります:5\n\nは「改行」を表します。(Windowsでは、エンターキーを押すと復帰車と改行、\r\nが入ります。)trimメソッドは\nまたは\r\nを削除し、結果として5になります。

文字列のparseメソッドは、文字列を別の型に変換します。ここでは、文字列を数値に変換するために使用します。let guess: u32を使用して、Rustに変換したい正確な数値型を伝えます。guessの後のコロン(:)は、Rustに変数の型を注釈することを示しています。Rustにはいくつかの組み込み数値型があります。ここで見られるu32は、符号なし32ビット整数です。小さな正の数には適したデフォルトの選択肢です。第3章で他の数値型について学びます。

また、このサンプルプログラムにおけるu32の注釈とsecret_numberとの比較は、Rustがsecret_numberもまたu32であると推論することを意味します。したがって、今では同じ型の2つの値が比較されるようになりました!

parseメソッドは、論理的に数値に変換できる文字のみに対して機能し、したがってエラーを引き起こしやすくなります。たとえば、文字列にA👍%が含まれていた場合、それを数値に変換することはできません。失敗する可能性があるため、parseメソッドはResult型を返します。これは、read_lineメソッドと同じように(「Resultを使った潜在的な失敗の処理」の前半で説明した通り)。同じようにexpectメソッドを使ってこのResultを処理します。parseが文字列から数値を作成できなかったためにErr Resultバリアントを返した場合、expect呼び出しはゲームをクラッシュさせ、与えられたメッセージを表示します。parseが文字列から数値を正常に変換できた場合、ResultOkバリアントを返し、expectOk値から必要な数値を返します。

では、今プログラムを実行してみましょう:

$ 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を予想したことを正しく判断しました。正しく予想する、予想値が高すぎる、予想値が低すぎるなど、さまざまな入力に対する異なる動作を確認するために、プログラムを何回か実行してみてください。

これでゲームの大部分は機能していますが、ユーザーは1回の予想しかできません。ループを追加することでそれを変更しましょう!

ループを使って複数回の予想を可能にする

loopキーワードは無限ループを作成します。ユーザーに数を予想する機会をもう少し与えるためにループを追加しましょう。

ファイル名: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}");

    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!"),
        }
    }
}

ご覧の通り、予想入力プロンプト以降のすべてのコードをループ内に移動しました。ループ内の行をそれぞれさらに4つのスペースでインデントし、再度プログラムを実行してください。すると、プログラムは永久に別の予想を求め続けます。これは実際には新しい問題を引き起こします。ユーザーがゲームを終了できないように見えます!

ユーザーは常にキーボードショートカットのctrl-Cを使ってプログラムを中断することができます。しかし、「予想値と秘密の数を比較する」のparseに関する議論で言及されているように、この貪欲なモンスターから抜け出す別の方法もあります。ユーザーが数字以外の答えを入力した場合、プログラムはクラッシュします。これを利用してユーザーがゲームを終了できるようにすることができます。以下のようになります。

$ cargo run
   Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
    Finished dev [unoptimized + debuginfo] target(s) in 1.50s
     Running `target/debug/guessing_game`
Guess the number!
The secret number is: 59
Please input your guess.
45
You guessed: 45
Too small!
Please input your guess.
60
You guessed: 60
Too big!
Please input your guess.
59
You guessed: 59
You win!
Please input your guess.
quit
thread 'main' panicked at 'Please type a number!: ParseIntError
{ kind: InvalidDigit }', src/main.rs:28:47
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

quitと入力するとゲームが終了しますが、ご覧の通り、他の数字以外の入力を行っても同じようにゲームが終了します。少なくとも言えば、これは最適ではありません。正解の数字を予想したときにもゲームが停止するようにしたいです。

正解を予想した後にゲームを終了する

break文を追加することで、ユーザーが勝利したときにゲームを終了するようにプログラムを作成しましょう。

ファイル名:src/main.rs

--snip--

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

You win!の後にbreak行を追加することで、ユーザーが秘密の数を正しく予想したときにプログラムがループを抜けます。ループを抜けることは、プログラムを終了することも意味します。なぜなら、ループはmainの最後の部分だからです。

無効な入力を処理する

ゲームの動作をさらに改善するために、ユーザーが数字以外を入力したときにプログラムがクラッシュする代わりに、ゲームが数字以外を無視してユーザーが続けて予想できるようにしましょう。これは、guessStringからu32に変換する行を変更することで行うことができます。リスト2-5に示すようになります。

ファイル名:src/main.rs

--snip--

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

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

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

--snip--

リスト2-5:数字以外の予想を無視し、プログラムをクラッシュさせる代わりに別の予想を求める

エラー発生時にクラッシュする代わりにエラーを処理するために、expect呼び出しからmatch式に切り替えます。覚えておいてください。parseResult型を返し、ResultOkErrのバリアントを持つ列挙型です。ここでは、cmpメソッドのOrdering結果と同じようにmatch式を使用しています。

parseが文字列を正常に数値に変換できた場合、それは結果の数値を含むOk値を返します。そのOk値は最初のアームのパターンに一致し、match式はparseが生成してOk値の中に入れたnum値をそのまま返します。その数値は、作成している新しいguess変数の正しい場所に最終的に入ります。

parseが文字列を数値に変換できなかった場合、それはエラーに関する詳細な情報を含むErr値を返します。Err値は最初のmatchアームのOk(num)パターンと一致しませんが、2番目のアームのErr(_)パターンと一致します。アンダースコア_は全てをキャッチする値です。この例では、どんな情報が入っていようとすべてのErr値と一致させたいと言っています。したがって、プログラムは2番目のアームのコードであるcontinueを実行します。これは、プログラムにloopの次の反復に移り、別の予想を求めるように指示します。したがって、実際には、プログラムはparseが遭遇するすべてのエラーを無視します!

これで、プログラムのすべての部分が期待通りに動作するはずです。試してみましょう:

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

素晴らしい!最後の小さな調整を加えることで、予想ゲームを完成させます。プログラムはまだ秘密の数を表示していることを思い出してください。これはテストには便利でしたが、ゲームを台無しにしてしまいます。秘密の数を出力するprintln!を削除しましょう。リスト2-6に最終コードを示します。

ファイル名:src/main.rs

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

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

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

    loop {
        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;
            }
        }
    }
}

リスト2-6:完成した予想ゲームのコード

この時点で、あなたは成功裏に予想ゲームを作成しました。おめでとうございます!

まとめ

おめでとうございます!あなたは「予想ゲームのプログラミング」の実験を完了しました。あなたの技術を向上させるために、LabExでさらに多くの実験を行って練習してください。