数当てゲームをプログラミングする
実践的なプロジェクトに一緒に取り組みながら、Rust に飛び込んでいきましょう!この
章では、実際のプログラムの中でそれらをどのように使うかを示しながら、Rust の一般的な
概念をいくつか紹介します。let、match、メソッド、関連
関数、外部クレートなどについて学びます!以降の章では、これらの考え方をさらに詳しく
見ていきます。この章では、まず基礎を
練習するだけです。
初心者向けプログラミングの定番問題である数当てゲームを実装します。仕組みは こうです。プログラムは 1 から 100 までのランダムな整数を生成します。次に、 プレイヤーに予想を入力するよう促します。予想が入力されると、その予想が 小さすぎるか大きすぎるかをプログラムが示します。予想が 正しければ、ゲームは祝福のメッセージを表示して終了します。
新しいプロジェクトをセットアップする
新しいプロジェクトをセットアップするには、1 章で作成した projects ディレクトリに移動し、次のように Cargo を使って新しいプロジェクトを作成します。
$ cargo new guessing_game
$ cd guessing_game
最初のコマンド cargo new は、プロジェクト名 (guessing_game)
を最初の引数として受け取ります。2 つ目のコマンドは、新しいプロジェクトの
ディレクトリに移動します。
生成された Cargo.toml ファイルを見てみましょう。
ファイル名: Cargo.toml
[package]
name = "guessing_game"
version = "0.1.0"
edition = "2024"
[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` profile [unoptimized + debuginfo] target(s) in 0.08s
Running `target/debug/guessing_game`
Hello, world!
このゲームでもそうするように、プロジェクトを素早く反復し、
次の反復に進む前に毎回すばやくテストしたいときには、run コマンドが役立ちます。
src/main.rs ファイルをもう一度開いてください。このファイルに これから書くコードをすべて記述します。
予想を処理する
数当てゲームプログラムの最初の部分では、ユーザー入力を受け取り、その 入力を処理し、期待した形式になっているかを確認します。まずは、 プレイヤーが予想を入力できるようにします。リスト 2-1 のコードを 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}");
}
このコードには多くの情報が含まれているので、1 行ずつ見ていきましょう。ユーザー
入力を取得し、その結果を出力として表示するには、入出力ライブラリ io
をスコープに導入する必要があります。io ライブラリは、std として知られる
標準ライブラリに含まれています。
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}");
}
Rust ではデフォルトで、標準ライブラリで定義された項目の集合が あらゆるプログラムのスコープに導入されます。この集合は prelude と呼ばれ、 その内容は標準ライブラリのドキュメントで確認できます。
使いたい型が prelude に含まれていない場合は、その型を
use 文で明示的にスコープに導入する必要があります。std::io ライブラリ
を使うと、ユーザー入力を受け付ける機能を含む、いくつもの便利な機能を
利用できます。
1 章で見たとおり、main 関数はプログラムの
エントリポイントです。
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}");
}
fn 構文は新しい関数を宣言し、丸かっこ () は
引数がないことを示し、波かっこ { は関数本体の始まりを示します。
また 1 章で学んだように、println! は文字列を
画面に表示するマクロです。
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}");
}
このコードは、どのようなゲームなのかを説明し、ユーザーに入力 を求めるプロンプトを表示しています。
変数で値を保存する
次に、ユーザー入力を保存するための 変数 を次のように作成します。
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}");
}
ここからプログラムが面白くなってきます!この短い 1 行には多くのことが
詰まっています。変数を作成するには let 文を使います。別の例を見てみましょう。
let apples = 5;
この行は、apples という名前の新しい変数を作成し、それを値 5
に束縛します。Rust では、変数はデフォルトで不変です。つまり、いったん変数
に値を与えると、その値は変わりません。この概念については、
3 章の「変数と可変性」
節で詳しく説明します。変数を可変にするには、変数名の前に
mut を付けます。
let apples = 5; // 不変
let mut bananas = 5; // 可変
注:
//構文は、行末まで続くコメントを開始します。 Rust はコメント内のものをすべて無視します。コメントについては、 3 章でさらに詳しく説明します。
数当てゲームのプログラムに戻ると、let mut guess が
guess という名前の可変変数を導入することがわかりました。等号 (=)
は、その時点で何かをその変数に束縛したいことを Rust に伝えます。等号の右側に
あるのは guess に束縛される値であり、それは
String の新しいインスタンスを返す関数 String::new を呼び出した
結果です。String は標準
ライブラリが提供する文字列型で、拡張可能な UTF-8 エンコードのテキストです。
::new の行にある :: 構文は、new が String 型の関連
関数であることを示しています。関連関数 とは、型に対して
実装された関数のことで、この場合は String です。この new 関数は新しい
空の文字列を作成します。何らかの新しい値を作る関数の名前として new は
一般的なので、多くの型に new 関数があるのを目にするでしょう。
つまり、let mut guess = String::new(); という行は、現在
String の新しい空インスタンスに束縛されている可変変数を
作成したのです。ふう!
ユーザー入力を受け取る
プログラムの最初の行で、use std::io; を使って標準
ライブラリの入出力機能を取り込んだことを思い出してください。次に、
io モジュールの stdin 関数を呼び出し、ユーザー
入力を扱えるようにします。
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}");
}
プログラムの先頭で use std::io; によって io モジュールをインポートして
いなかったとしても、この関数呼び出しを std::io::stdin と書くことで、この関数を
使うことはできます。stdin 関数は
std::io::Stdin のインスタンスを返します。これは、
端末の標準入力へのハンドルを表す型です。
次に、.read_line(&mut guess) という行は、ユーザーから入力を受け取るために、
標準入力ハンドルに対して read_line メソッドを呼び出しています。また、read_line に
&mut guess を引数として渡して、ユーザー入力をどの文字列に格納するかを指定して
います。read_line の完全な役割は、ユーザーが標準入力に入力した内容をすべて文字列に
追記することです(内容を上書きはしません)。そのため、その文字列を引数として
渡します。このメソッドが文字列の内容を変更できるようにするため、文字列引数は
可変である必要があります。
& は、この引数が 参照 であることを示します。参照を使うと、データを何度も
メモリにコピーしなくても、コードの複数の部分から同じデータにアクセスできます。
参照は複雑な機能ですが、参照を安全かつ簡単に使えることは Rust の大きな利点の
ひとつです。このプログラムを完成させるために、その詳細をたくさん知っている必要は
ありません。今のところ知っておくべきなのは、変数と同じく、参照もデフォルトでは
不変だということだけです。したがって、可変にするには &guess ではなく
&mut guess と書く必要があります。(第4章で参照についてさらに詳しく説明
します。)
Result を使った起こりうる失敗の処理
まだこのコード行について見ています。ここではテキスト上では3行目を説明していますが、 これは論理的には依然として1行のコードの一部であることに注意してください。次の 部分はこのメソッドです。
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}");
}
このコードは次のように書くこともできました。
io::stdin().read_line(&mut guess).expect("Failed to read line");
しかし、1つの長い行は読みにくいため、分割したほうがよいです。.method_name()
構文でメソッドを呼び出すときは、改行やその他の空白を入れて長い行を分割すると、
読みやすくなることがよくあります。では、この行が何をしているのかを説明しましょう。
前に述べたように、read_line はユーザーが入力した内容を、渡された文字列に
入れますが、同時に Result 値も返します。Result は、しばしば enum と呼ばれる 列挙型 で、
複数の取りうる状態のいずれかになりうる型です。可能な各状態を バリアント と
呼びます。
第6章 では enum をより詳しく扱います。これらの
Result 型の目的は、エラー処理に関する情報をエンコードすることです。
Result のバリアントは Ok と Err です。Ok バリアントは操作が成功した
ことを示し、正常に生成された値を含みます。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: this `Result` may be an `Err` variant, which should be handled
= note: `#[warn(unused_must_use)]` on by default
help: use `let _ = ...` to ignore the resulting value
|
10 | let _ = io::stdin().read_line(&mut guess);
| +++++++
warning: `guessing_game` (bin "guessing_game") generated 1 warning
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.59s
Rust は、read_line から返された Result 値を使っていないことを警告しており、
プログラムが起こりうるエラーを処理していないことを示しています。
警告を抑制する正しい方法は、実際にエラー処理のコードを書くことです。しかし今回の
場合は、問題が発生したときにこのプログラムをクラッシュさせたいだけなので、
expect を使えます。エラーから回復する方法については、第
9章 で学びます。
println! のプレースホルダーで値を表示する
閉じ中かっこを除けば、これまでのコードで説明する行はあと1行だけです。
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}");
}
この行は、今やユーザー入力を含んでいる文字列を表示します。{} という
中かっこの組はプレースホルダーです。{} を、値をその場所に保持しておく小さな
カニのはさみだと考えてください。変数の値を表示するときは、その変数名を中かっこの
中に入れられます。式を評価した結果を表示するときは、フォーマット文字列の中に空の
中かっこを置き、その後に、各空の中かっこプレースホルダーに表示する式を、同じ順序で
コンマ区切りでフォーマット文字列の後ろに並べます。1回の println! 呼び出しで
変数と式の評価結果を表示する例は、次のようになります。
#![allow(unused)]
fn main() {
let x = 5;
let y = 10;
println!("x = {x} and y + 2 = {}", y + 2);
}
このコードは x = 5 and y + 2 = 12 と表示します。
最初の部分をテストする
数当てゲームの最初の部分をテストしてみましょう。cargo run を使って実行します。
$ cargo run
Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
Finished `dev` profile [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 チームはその機能を備えた rand
crate を提供しています。
クレートで機能を増やす
クレートは Rust のソースコードファイルの集まりだということを思い出してください。
これまで作ってきたプロジェクトは、実行可能ファイルであるバイナリクレートです。
rand crate はライブラリクレートで、他のプログラムで使うことを意図したコードを
含んでおり、単独では実行できません。
外部クレートの調整こそ、Cargo が真価を発揮するところです。rand を使うコードを書く前に、Cargo.toml ファイルを変更して、rand クレートを依存関係として含める必要があります。今すぐそのファイルを開き、Cargo が作成した [dependencies] セクションヘッダーの下、末尾に次の行を追加してください。ここで示すものとまったく同じように、このバージョン番号付きで rand を指定してください。そうしないと、このチュートリアルのコード例が動作しない可能性があります。
ファイル名: Cargo.toml
[dependencies]
rand = "0.8.5"
Cargo.toml ファイルでは、ヘッダーの後に続くものはすべてそのセクションの一部であり、別のセクションが始まるまでそのセクションが続きます。[dependencies] では、プロジェクトが依存する外部クレートと、それらのクレートに必要なバージョンを Cargo に伝えます。この場合、rand クレートをセマンティックバージョン指定子 0.8.5 で指定します。Cargo は セマンティックバージョニング(しばしば SemVer と呼ばれます)を理解します。これはバージョン番号の記述方法に関する標準です。指定子 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
Locking 15 packages to latest Rust 1.85.0 compatible versions
Adding rand v0.8.5 (available: v0.9.0)
Compiling proc-macro2 v1.0.93
Compiling unicode-ident v1.0.17
Compiling libc v0.2.170
Compiling cfg-if v1.0.0
Compiling byteorder v1.5.0
Compiling getrandom v0.2.15
Compiling rand_core v0.6.4
Compiling quote v1.0.38
Compiling syn v2.0.98
Compiling zerocopy-derive v0.7.35
Compiling zerocopy v0.7.35
Compiling ppv-lite86 v0.2.20
Compiling rand_chacha v0.3.1
Compiling rand v0.8.5
Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 2.48s
表示されるバージョン番号は異なるかもしれません(ただし、SemVer のおかげで、どれもコードと互換性があります!)。また、行の内容も異なる場合があり(オペレーティングシステムによります)、行の順序も異なることがあります。
外部の依存関係を含めると、Cargo はその依存関係が必要とするすべての最新バージョンを レジストリ から取得します。これは Crates.io のデータのコピーです。Crates.io は、Rust エコシステムの人々が、他の人が使えるようにオープンソースの Rust プロジェクトを公開する場所です。
レジストリを更新した後、Cargo は [dependencies] セクションを確認し、まだダウンロードしていない、そこに列挙されたクレートをダウンロードします。この場合、依存関係として列挙したのは rand だけですが、Cargo は rand が動作するために依存している他のクレートも取得しました。クレートをダウンロードした後、Rust はそれらをコンパイルし、そのうえで依存関係を利用できる状態でプロジェクトをコンパイルします。
変更を何も加えずにすぐもう一度 cargo build を実行すると、Finished の行以外の出力はありません。Cargo は依存関係をすでにダウンロードしてコンパイル済みであり、Cargo.toml ファイル内でもそれらについて何も変更していないことを認識しています。また、コードについても何も変更していないことを認識しているので、それも再コンパイルしません。やることが何もないので、単に終了します。
src/main.rs ファイルを開いてちょっとした変更を加え、保存してから再びビルドすると、出力は 2 行だけになります。
$ cargo build
Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.13s
これらの行は、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.999.0 という 2 つの新しいバージョンをリリースしていた場合、cargo update を実行すると次のように表示されます。
$ cargo update
Updating crates.io index
Locking 1 package to latest Rust 1.85.0 compatible version
Updating rand v0.8.5 -> v0.8.6 (available: v0.999.0)
Cargo は 0.999.0 リリースを無視します。この時点で、現在使用している rand クレートの
バージョンが 0.8.6 であることを示す変更が Cargo.lock ファイルにも見られる
はずです。rand のバージョン 0.999.0、または 0.999.x 系列の任意の
バージョンを使うには、代わりに Cargo.toml ファイルを次のように更新する
必要があります(以下の例では rand 0.8 を使っている前提なので、実際にはこの変更を
行わないでください):
[dependencies]
rand = "0.999.0"
次に cargo build を実行すると、Cargo は利用可能なクレートのレジストリを
更新し、指定した新しいバージョンに従って rand の要件を再評価します。
Cargo と そのエコシステム については、 第14章でさらに詳しく説明しますが、今のところ知っておく必要があるのはこれだけです。 Cargo によってライブラリの再利用がとても簡単になるため、Rustacean は 複数のパッケージを組み合わせて構成される、より小さなプロジェクトを 書けます。
乱数を生成する
rand を使って、当てる対象となる数を生成してみましょう。次の手順は、
リスト 2-3 に示すように src/main.rs を更新することです。
use std::io;
use rand::Rng;
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");
println!("You guessed: {guess}");
}
まず、use rand::Rng; という行を追加します。Rng トレイトは、乱数
ジェネレーターが実装するメソッドを定義しており、それらのメソッドを使うには
このトレイトがスコープ内になければなりません。トレイトについては第10章で
詳しく扱います。
次に、途中に 2 行追加します。1 行目では、使用する特定の乱数
ジェネレーターを返す rand::thread_rng 関数を呼び出します。これは、現在
実行中のスレッドにローカルであり、オペレーティングシステムによってシードされる
ものです。次に、乱数ジェネレーターに対して gen_range
メソッドを呼び出します。このメソッドは、use rand::Rng; 文によって
スコープに導入した Rng トレイトで定義されています。
gen_range メソッドは、引数として範囲式を受け取り、その範囲内の
乱数を生成します。ここで使用している範囲式は start..=end
という形を取り、下限と上限の両方を含みます。そのため、1 から 100 までの数を
要求するには 1..=100 を指定する必要があります。
注: どのトレイトを使い、クレートのどのメソッドや関数を呼び出せばよいかが 自然にわかるわけではないので、各クレートにはその使い方を説明する ドキュメントがあります。Cargo のもう 1 つの便利な機能は、
cargo doc --openコマンドを実行すると、依存関係が提供するすべてのドキュメントを ローカルでビルドしてブラウザーで開いてくれることです。たとえば、randクレートのほかの機能に興味があるなら、cargo doc --openを実行して、 左側のサイドバーにあるrandをクリックしてください。
2 行目の新しい行では、秘密の数を表示します。これはプログラムを開発している 間、テストできるようにするために便利ですが、最終版では削除します。 起動した瞬間にプログラムが答えを表示してしまっては、あまりゲームになりません!
プログラムを何回か実行してみましょう:
$ cargo run
Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.02s
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` profile [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 に示します。ただし、後で説明するように、このコードはまだ コンパイルできないことに注意してください。
use std::cmp::Ordering;
use std::io;
use rand::Rng;
fn main() {
// --snip--
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");
println!("You guessed: {guess}");
match guess.cmp(&secret_number) {
Ordering::Less => println!("Too small!"),
Ordering::Greater => println!("Too big!"),
Ordering::Equal => println!("You win!"),
}
}
まず、もう 1 つ use 文を追加し、標準ライブラリから
std::cmp::Ordering という型をスコープに導入します。Ordering 型も
別の enum であり、Less、Greater、Equal というバリアントを
持っています。これらは 2 つの値を比較したときにあり得る 3 つの結果です。
次に、末尾に Ordering 型を使う 5 行を新しく追加します。
cmp メソッドは 2 つの値を比較し、比較可能なものなら何に対してでも
呼び出せます。これは比較対象への参照を受け取ります。ここでは、
guess と secret_number を比較しています。そして、use 文で
スコープに導入した Ordering enum のバリアントを返します。guess と
secret_number の値で cmp を呼び出した結果として Ordering の
どのバリアントが返されたかに基づいて、次に何をするかを決めるために
match 式を使います。
match 式は アーム で構成されます。アームは、照合対象となる パターン と、
match に渡された値がそのアームのパターンに当てはまった場合に実行される
コードで構成されます。Rust は match に渡された値を受け取り、
各アームのパターンを順番に調べます。パターンと match 構文は
強力な Rust の機能です。これらにより、コードが遭遇しうるさまざまな状況を
表現でき、それらすべてを確実に処理できます。これらの機能については、
それぞれ第6章と第19章で詳しく扱います。
ここで使っている match 式の例を見ていきましょう。たとえば、
ユーザーの予想が 50 で、今回ランダムに生成された秘密の数が 38 だとします。
コードが 50 と 38 を比較すると、50 は 38 より大きいため、cmp
メソッドは Ordering::Greater を返します。match 式は
Ordering::Greater の値を受け取り、各アームのパターンのチェックを
始めます。まず最初のアームのパターン Ordering::Less を見て、
値 Ordering::Greater は Ordering::Less に一致しないので、その
アームのコードは無視して次のアームに進みます。次のアームのパターンは
Ordering::Greater で、これは Ordering::Greater に一致します!
そのアームに関連付けられたコードが実行され、画面に Too big! と
表示します。match 式は最初に成功した一致の後で終了するため、
この状況では最後のアームは調べません。
しかし、リスト 2-4 のコードはまだコンパイルできません。試してみましょう:
$ cargo build
Compiling libc v0.2.86
Compiling getrandom v0.2.2
Compiling cfg-if v1.0.0
Compiling ppv-lite86 v0.2.10
Compiling rand_core v0.6.2
Compiling rand_chacha v0.3.0
Compiling rand v0.8.5
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 `&String`, found `&{integer}`
| |
| arguments to this method are incorrect
|
= note: expected reference `&String`
found reference `&{integer}`
note: method defined here
--> /rustc/1159e78c4747b02ef996e55082b704c09b970588/library/core/src/cmp.rs:979:8
For more information about this error, try `rustc --explain E0308`.
error: could not compile `guessing_game` (bin "guessing_game") due to 1 previous error
エラーの核心は、型の不一致 があるということです。Rust には強力な静的型システムがあります。しかし、型推論も備えています。let mut guess = String::new() と書いたとき、Rust は guess が String であるべきだと推論できたので、型を書かなくてもよかったのです。一方、secret_number は数値型です。Rust の数値型のうち、1 から 100 までの値を取りうるものには、32ビット整数の i32、符号なし32ビット整数の u32、64ビット整数の i64 などがあります。特に指定しない限り、Rust は i32 をデフォルトとするため、ほかの場所で Rust が別の数値型を推論するような型情報を追加しない限り、secret_number の型は i32 になります。エラーの理由は、Rust では文字列型と数値型を比較できないからです。
最終的には、プログラムが入力として読み取った String を数値型に変換して、秘密の数と数値として比較できるようにしたいわけです。そのために、main 関数本体に次の行を追加します:
ファイル名: src/main.rs
use std::cmp::Ordering;
use std::io;
use rand::Rng;
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.");
// --snip--
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 という名前の変数を作成します。ですが、ちょっと待ってください。プログラムにはすでに guess という名前の変数があるのではないでしょうか。あります。しかし、ありがたいことに Rust では、新しい値で以前の guess の値をシャドーイングできます。シャドーイング を使うと、たとえば guess_str と guess のように 2 つの別々の変数を作らなくても、guess という変数名を再利用できます。これについては 第3章 で詳しく扱いますが、今は、この機能がある値をある型から別の型へ変換したいときによく使われる、ということだけ知っておいてください。
この新しい変数を、guess.trim().parse() という式に束縛します。この式の guess は、入力を文字列として保持していた元の guess 変数を指しています。String インスタンスの trim メソッドは、先頭と末尾のあらゆる空白を取り除きます。これは、文字列を u32 に変換する前に必要な処理です。というのも、u32 には数値データしか入れられないからです。ユーザーは read_line を完了させて予想を入力するために enter を押す必要があり、その結果、文字列に改行文字が追加されます。たとえば、ユーザーが 5 と入力して enter を押すと、guess は次のようになります: 5\n。\n は「改行」を表します。(Windows では、enter を押すとキャリッジリターンと改行、つまり \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 メソッドは read_line メソッドと同じように Result 型を返します(これは前の 「Result による起こりうる失敗の処理」 で説明しました)。この Result も、再び expect メソッドを使って同じように扱います。文字列から数値を作れず parse が Err の Result バリアントを返した場合、expect の呼び出しによってゲームはクラッシュし、与えたメッセージが表示されます。parse が文字列を数値にうまく変換できた場合は、Result の Ok バリアントを返し、expect は Ok の値から欲しい数値を返します。
では、ここでプログラムを実行してみましょう:
$ cargo run
Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.26s
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::cmp::Ordering;
use std::io;
use rand::Rng;
fn main() {
println!("Guess the number!");
let secret_number = rand::thread_rng().gen_range(1..=100);
// --snip--
println!("The secret number is: {secret_number}");
loop {
println!("Please input your guess.");
// --snip--
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` profile [unoptimized + debuginfo] target(s) in 0.23s
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 src/main.rs:28:47:
Please type a number!: ParseIntError { kind: InvalidDigit }
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
quit と入力するとゲームは終了しますが、お気づきのとおり、数値以外の
ほかの入力をしても同様に終了してしまいます。控えめに言っても、これは最適
ではありません。正しい数を当てたときにもゲームが終了するようにしたいです。
正しく当てたら終了する
break 文を追加して、ユーザーが勝ったときにゲームが終了するようにして
みましょう。
ファイル名: src/main.rs
use std::cmp::Ordering;
use std::io;
use rand::Rng;
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}");
// --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 の
最後の部分だからです。
無効な入力の処理
ゲームの動作をさらに洗練させるために、ユーザーが数値以外を入力したときに
プログラムをクラッシュさせるのではなく、数値以外の入力を無視してユーザー
が予想を続けられるようにしてみましょう。それは、リスト 2-5 に示すように、
guess を String から u32 に変換している行を変更することで実現できます。
use std::cmp::Ordering;
use std::io;
use rand::Rng;
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();
// --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--
match guess.cmp(&secret_number) {
Ordering::Less => println!("Too small!"),
Ordering::Greater => println!("Too big!"),
Ordering::Equal => {
println!("You win!");
break;
}
}
}
}
expect 呼び出しから match 式に切り替えることで、エラー時にクラッシュ
するのではなく、エラーを処理するようにしています。parse は Result
型を返し、Result は Ok と Err というバリアントを持つ enum である
ことを思い出してください。ここでは、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` profile [unoptimized + debuginfo] target(s) in 0.13s
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!
すばらしいですね! あと最後に小さな調整を 1 つ加えれば、数当てゲームは
完成です。プログラムがまだ秘密の数を表示していることを思い出してください。
これはテストには便利でしたが、ゲームとしては台無しです。秘密の数を出力し
ている println! を削除しましょう。リスト 2-6 に最終的なコードを示し
ます。
use std::cmp::Ordering;
use std::io;
use rand::Rng;
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;
}
}
}
}
これで、数当てゲームを無事に作り上げることができました。おめでとうござい ます!
まとめ
このプロジェクトは、let、match、関数、外部 crate の利用など、多くの
新しい Rust の概念を実際に手を動かしながら導入する方法でした。これから
数章にわたって、これらの概念をさらに詳しく学んでいきます。第 3 章では、
変数、データ型、関数など、ほとんどのプログラミング言語にある概念を扱い、
それらを Rust でどう使うかを示します。第 4 章では、Rust を他の言語と
異なるものにしている機能である所有権を掘り下げます。第 5 章では構造体と
メソッド構文について説明し、第 6 章では enum の仕組みを説明します。