はじめに
予想ゲームのプログラミングへようこそ。この実験は、Rust Bookの一部です。LabExでRustのスキルを練習することができます。
この実験では、Rustで予想ゲームを実装します。このプログラムはランダムな数を生成し、プレーヤーにそれを予想するよう促します。予想が低すぎるか高すぎるかに関するフィードバックを提供し、正解した場合にはプレーヤーに祝いを送ります。
This tutorial is from open-source community. Access the source code
💡 このチュートリアルは英語版からAIによって翻訳されています。原文を確認するには、 ここをクリックしてください
予想ゲームのプログラミングへようこそ。この実験は、Rust Bookの一部です。LabExでRustのスキルを練習することができます。
この実験では、Rustで予想ゲームを実装します。このプログラムはランダムな数を生成し、プレーヤーにそれを予想するよう促します。予想が低すぎるか高すぎるかに関するフィードバックを提供し、正解した場合にはプレーヤーに祝いを送ります。
一緒に手を動かしながらRustに挑戦しましょう!この章では、実際のプログラムでRustの概念をどのように使うかを示すことで、いくつかの一般的なRustの概念を紹介します。let
、match
、メソッド、関連関数、外部クレートなどについて学びます!次の章では、これらのアイデアを詳細に探ります。この章では、基本を練習するだけです。
私たちは、古典的な初心者向けのプログラミング問題である予想ゲームを実装します。以下がその仕組みです。プログラムは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 guess
がguess
という名前の可変変数を導入することがわかりました。等号(=
)は、Rustに対して、今変数に何かをバインドしたいことを伝えます。等号の右側は、guess
がバインドされる値で、String::new
を呼び出した結果です。String::new
は、String
の新しいインスタンスを返す関数です。String
は、標準ライブラリによって提供される文字列型で、拡張可能なUTF-8エンコードのテキストです。
::new
行の::
構文は、new
がString
型の関連関数であることを示しています。「関連関数」とは、特定の型に対して実装された関数のことで、この場合では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章で参照についてもっと詳しく説明します)。
まだこのコード行に取り組んでいます。今は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
のバリアントはOk
とErr
です。Ok
バリアントは操作が成功したことを示し、Ok
の中には成功して生成された値があります。Err
バリアントは操作が失敗したことを意味し、Err
には操作がどのように、またはなぜ失敗したかに関する情報が含まれています。
Result
型の値は、他の型の値と同様に、それに定義されたメソッドがあります。Result
のインスタンスには、呼び出すことができるexpect
メソッドがあります。このResult
のインスタンスがErr
値である場合、expect
はプログラムをクラッシュさせ、expect
に引数として渡したメッセージを表示します。read_line
メソッドがErr
を返す場合、それはおそらく基礎となるオペレーティングシステムからのエラーの結果でしょう。このResult
のインスタンスがOk
値である場合、expect
はOk
が保持している戻り値を取り、その値だけを返してくれます。それで、その値を使うことができます。この場合、その値はユーザー入力のバイト数です。
expect
を呼び出さない場合、プログラムはコンパイルされますが、警告が表示されます。
$ cargo build
Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
warning: unused `Result` that must be used
--> src/main.rs:10:5
|
10 | io::stdin().read_line(&mut guess);
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
= note: `#[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章でエラーから回復する方法について学びます。
これまでのコードで、閉じ括弧を除いて、もう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には、あなた自身または他の誰があなたのコードをビルドしたときに、同じアーティファクトを再ビルドできるようにするメカニズムがあります。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
型は別の列挙型であり、Less
、Greater
、Equal
の各バリアントを持っています。これらは2つの値を比較したときに考えられる3つの結果です。
次に、底部に5行の新しいコードを追加します。これらはOrdering
型を使用しています。cmp
メソッド[3]は2つの値を比較し、比較可能な値に対して呼び出すことができます。比較対象の値への参照を引数に取ります。ここではguess
とsecret_number
を比較しています。そして、use
文によってスコープ内に持ち込んだOrdering
列挙型のバリアントを返します。cmp
関数にguess
とsecret_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は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は既存のguess
の値を新しい値で上書きする(シャドーイング)ことを許可しています。シャドーイングにより、guess_str
やguess
のように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
が文字列から数値を正常に変換できた場合、Result
のOk
バリアントを返し、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を予想したことを正しく判断しました。正しく予想する、予想値が高すぎる、予想値が低すぎるなど、さまざまな入力に対する異なる動作を確認するために、プログラムを何回か実行してみてください。
これでゲームの大部分は機能していますが、ユーザーは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
の最後の部分だからです。
ゲームの動作をさらに改善するために、ユーザーが数字以外を入力したときにプログラムがクラッシュする代わりに、ゲームが数字以外を無視してユーザーが続けて予想できるようにしましょう。これは、guess
をString
から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
式に切り替えます。覚えておいてください。parse
はResult
型を返し、Result
はOk
とErr
のバリアントを持つ列挙型です。ここでは、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でさらに多くの実験を行って練習してください。