数当てゲームのプログラミング
ハンズオン形式のプロジェクトに一緒に取り組むことで、Rustの世界に飛び込んでみましょう!
この章ではRustの一般的な概念を、実際のプログラムでの使い方を示しながら紹介します。
let
、match
、メソッド、関連関数、外部クレートの使いかたなどについて学びます!
これらについての詳細は後続の章で取り上げますので、この章では基本的なところを練習します。
プログラミング初心者向けの定番問題である「数当てゲーム」を実装してみましょう。 これは次のように動作します。 プログラムは1から100までのランダムな整数を生成します。 そして、プレーヤーに予想(した数字)を入力するように促します。 予想が入力されると、プログラムはその予想が小さすぎるか大きすぎるかを表示します。 予想が当たっているなら、お祝いのメッセージを表示し、ゲームを終了します。
新規プロジェクトの立ち上げ
新しいプロジェクトを立ち上げましょう。 第1章で作成したprojectsディレクトリに移動し、以下のようにCargoを使って新規プロジェクトを作成します。
$ cargo new guessing_game
$ cd guessing_game
最初のコマンドcargo new
は、第1引数としてプロジェクト名 (guessing_game
) を取ります。
2番目のコマンドは新規プロジェクトのディレクトリに移動します。
生成されたCargo.tomlファイルを見てみましょう。
ファイル名:Cargo.toml
[package]
name = "guessing_game"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at 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!"); }
さて、cargo run
コマンドを使って、この「Hello, world!」プログラムのコンパイルと実行を一気に行いましょう。
$ 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); // 次のように予想しました: {}
}
このコードには多くの情報が詰め込まれています。
行ごとに見ていきましょう。
ユーザ入力を受け付け、結果を出力するためには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
ライブラリをuse
すると、ユーザ入力を受け付ける機能など(入出力に関する)多くの便利な機能が利用できるようになります。
第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); // 次のように予想しました: {}
}
プログラムが少し興味深いものになってきました。
この小さな行の中でいろいろなことが起きています。
let
文を使って変数を作っています。
別の例も見てみましょう。
let apples = 5;
この行ではapples
という名前の新しい変数を作成し5
という値に束縛しています。
Rustでは変数はデフォルトで不変(immutable)になります。
この概念については第3章の「変数と可変性」の節で詳しく説明します。
変数を可変(mutable)にするには、変数名の前にmut
をつけます。
let apples = 5; // immutable
// 不変
let mut bananas = 5; // mutable
// 可変
注:
//
構文は行末まで続くコメントを開始し、Rustはコメント内のすべて無視します。 コメントについては第3章で詳しく説明します。
数当てゲームのプログラムに戻りましょう。
ここまでの話でlet mut guess
がguess
という名前の可変変数を導入することがわかったと思います。
等号記号(=
)はRustに、いまこの変数を何かに束縛したいことを伝えます。
等号記号の右側にはguess
が束縛される値があります。
これはString::new
関数を呼び出すことで得られた値で、この関数はString
型の新しいインスタンスを返します。
String
は標準ライブラリによって提供される文字列型で、サイズが拡張可能な、UTF-8でエンコードされたテキスト片になります。
::new
の行にある::
構文はnew
がString
型の関連関数であることを示しています。
関連関数とは、ある型(ここではString
)に対して実装される関数のことです。
この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行目になりますが、まだ一つの論理的な行の一部分に過ぎません。 次の部分はこのメソッドです。
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");
しかし、長い行は読みづらいので分割したほうがよいでしょう。
.method_name()
構文でメソッドを呼び出すとき、長い行を改行と空白で分割するのが賢明なことがよくあります。
それでは、この行(expect()
メソッド)が何をするのか説明します。
前述したように、read_line
メソッドは渡された文字列にユーザが入力したものを入れます。
しかし、同時に値(この場合はio::Result
)も返します。
Rustの標準ライブラリにはResult
という名前の型がいくつかあります。
汎用のResult
と、io::Result
といったサブモジュール用の特殊な型などです。
これらのResult
型は列挙型になります。
列挙型はenumとも呼ばれ、取りうる値として決まった数の列挙子(variant)を持ちます。
列挙型はよくmatch
と一緒に使われます。
これは条件式の一種で、評価時に、列挙型の値がどの列挙子であるかに基づいて異なるコードを実行できるという便利なものです。
enumについては第6章で詳しく説明します。
これらのResult
型の目的は、エラー処理に関わる情報を符号化(エンコード)することです。
Result
の列挙子はOk
かErr
です。
Ok
列挙子は処理が成功したことを示し、Ok
の中には正常に生成された値が入っています。
Err
列挙子は処理が失敗したことを意味し、Err
には処理が失敗した過程や理由についての情報が含まれています。
Result
型の値にも、他の型と同様にメソッドが定義されています。
io::Result
のインスタンスにはexpect
メソッドがありますので、これを呼び出せます。
このio::Result
インスタンスがErr
の値の場合、expect
メソッドはプログラムをクラッシュさせ、引数として渡されたメッセージを表示します。
read_line
メソッドがErr
を返したら、それはおそらく基礎となるオペレーティング・システムに起因するものでしょう。
もしこのio::Result
オブジェクトがOk
値の場合、expect
メソッドはOk
列挙子が保持する戻り値を取り出して、その値だけを返してくれます。
こうして私たちはその値を使うことができるわけです。
今回の場合、その値はユーザ入力のバイト数になります。
もしexpect
メソッドを呼び出さなかったら、コンパイルはできるものの警告が出るでしょう。
$ cargo build
Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
warning: unused `Result` that must be used
(警告: 使用されなければならない`std::result::Result`が使用されていません)
--> 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行だけです。
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組目は2番目の値、というように続いていきます。
一回のprintln!
の呼び出しで複数の値を表示するなら次のようになります。
#![allow(unused)] fn main() { let x = 5; let y = 10; println!("x = {} and y = {}", x, y); }
このコードはx = 5 and y = 10
と表示するでしょう。
最初の部分をテストする
数当てゲームの最初の部分をテストしてみましょう。
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の開発チームがこの機能を持つrand
クレートを提供してくれています。
クレートを使用して機能を追加する
クレートはRustソースコードを集めたものであることを思い出してください。
私たちがここまで作ってきたプロジェクトはバイナリクレートであり、これは実行可能ファイルになります。
rand
クレートはライブラリクレートです。
他のプログラムで使用するためのコードが含まれており、単独で実行することはできません。
Cargoがその力を発揮するのは外部クレートと連携するときです。
rand
を使ったコードを書く前に、Cargo.tomlファイルを編集してrand
クレートを依存関係に含める必要があります。
そのファイルを開いて、Cargoが作ってくれた[dependencies]
セクションヘッダの下に次の行を追加してください。
バージョンナンバーを含め、ここに書かれている通り正確にrand
を指定してください。
そうしないと、このチュートリアルのコード例が動作しないかもしれません。
ファイル名:Cargo.toml
rand = "0.8.3"
Cargo.tomlファイルでは、ヘッダに続くものはすべて、他のセクションが始まるまで続くセクションの一部になります。
(訳注:Cargo.tomlファイル内には複数のセクションがあり、各セクションは[ ]
で囲まれたヘッダ行から始まります)
[dependecies]
はプロジェクトが依存する外部クレートと必要とするバージョンをCargoに伝えます。
今回はrand
クレートを0.8.3
というセマンティックバージョン指定子で指定します。
Cargoはセマンティックバージョニング(SemVerと呼ばれることもあります)を理解しており、これはバージョンナンバーを記述するための標準です。
0.8.3
という数字は実際には^0.8.3
の省略記法で、0.8.3
以上0.9.0
未満の任意のバージョンを意味します。
Cargoはこれらのバージョンを、バージョン0.8.3
と互換性のある公開APIを持つものとみなします。
この仕様により、この章のコードが引き続きコンパイルできるようにしつつ、最新のパッチリリースを取得できるようになります。
0.9.0以降のバージョンは、以下の例で使用しているものと同じAPIを持つことを保証しません。
さて、コードを一切変えずに、次のリスト2-2のようにプロジェクトをビルドしてみましょう。
$ cargo build
Updating crates.io index
(crates.ioインデックスを更新しています)
Downloaded rand v0.8.3
(rand v0.8.3をダウンロードしています)
Downloaded libc v0.2.86
Downloaded getrandom v0.2.2
Downloaded cfg-if v1.0.0
Downloaded ppv-lite86 v0.2.10
Downloaded rand_chacha v0.3.0
Downloaded rand_core v0.6.2
Compiling rand_core v0.6.2
(rand_core v0.6.2をコンパイルしています)
Compiling libc v0.2.86
Compiling getrandom v0.2.2
Compiling cfg-if v1.0.0
Compiling ppv-lite86 v0.2.10
Compiling rand_chacha v0.3.0
Compiling rand v0.8.3
Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
(guessing_game v0.1.0をコンパイルしています)
Finished dev [unoptimized + debuginfo] target(s) in 2.53s
もしかしたら異なるバージョンナンバー(とはいえ、SemVerのおかげですべてのコードに互換性があります)や、 異なる行(オペレーティングシステムに依存します)が表示されるかもしれません。 また、行の順序も違うかもしれません。
外部依存を持つようになると、Cargoはその依存関係が必要とするすべてについて最新のバージョンをレジストリから取得します。 レジストリとはCrates.ioのデータのコピーです。 Crates.ioは、Rustのエコシステムにいる人たちがオープンソースのRustプロジェクトを投稿し、他の人が使えるようにする場所です。
レジストリの更新後、Cargoは[dependencies]
セクションにリストアップされているクレートをチェックし、まだ取得していないものがあればダウンロードします。
ここでは依存関係としてrand
だけを書きましたが、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.4が出て、そのバージョンには重要なバグ修正が含まれていますが、同時にあなたのコードを破壊するリグレッションも含まれているとします。
これに対応するため、Rustはcargo build
を最初に実行したときにCargo.lockファイルを作成します。
(いまのguessing_gameディレクトリにもあるはずです)
プロジェクトを初めてビルドするとき、Cargoは条件に合うすべての依存関係のバージョンを計算しCargo.lockファイルに書き込みます。
次にプロジェクトをビルドすると、CargoはCargo.lockファイルが存在することを確認し、バージョンを把握するすべての作業を再び行う代わりに、そこで指定されているバージョンを使います。
これにより再現性のあるビルドを自動的に行えます。
言い換えれば、Cargo.lockファイルのおかげで、あなたが明示的にアップグレードするまで、プロジェクトは0.8.3
を使い続けます。
クレートを更新して新バージョンを取得する
クレートを本当にアップグレードしたくなったときのために、Cargoはupdate
コマンドを提供します。
このコマンドはCargo.lockファイルを無視して、Cargo.tomlファイル内の全ての指定に適合する最新バージョンを算出します。
成功したらCargoはそれらのバージョンをCargo.lockファイルに記録します。
ただし、デフォルトでCargoは0.8.3
以上、0.9.0
未満のバージョンのみを検索します。
もしrand
クレートの新しいバージョンとして0.8.4
と0.9.0
の二つがリリースされていたなら、cargo update
を実行したときに以下のようなメッセージが表示されるでしょう。
$ cargo update
Updating crates.io index
(crates.ioインデックスを更新しています)
Updating rand v0.8.3 -> v0.8.4
(randクレートをv0.8.3 -> v0.8.4に更新しています)
Cargoは0.9.0
リリースを無視します。
またそのとき、Cargo.lockファイルが変更され、rand
クレートの現在使用中のバージョンが0.8.4
になったことにも気づくでしょう。
そうではなく、rand
のバージョン0.9.0
か、0.9.x
系のどれかを使用するには、Cargo.tomlファイルを以下のように変更する必要があります。
[dependencies]
rand = "0.9.0"
次にcargo build
コマンドを実行したとき、Cargoは利用可能なクレートのレジストリを更新し、あなたが指定した新しいバージョンに従ってrand
の要件を再評価します。
Cargoとそのエコシステムについては、まだ伝えたいことが山ほどありますが、それらについては第14章で説明します。 いまのところは、これだけ知っていれば十分です。 Cargoはライブラリの再利用をとても簡単にしてくれるので、Rustaceanが数多くのパッケージから構成された小さなプロジェクトを書くことが可能になっています。
乱数を生成する
rand
クレートを使って予想する数字を生成しましょう。
次のステップはsrc/main.rsファイルをリスト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..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);
}
まずuse rand::Rng
という行を追加します。
Rng
トレイトは乱数生成器が実装すべきメソッドを定義しており、それらのメソッドを使用するには、このトレイトがスコープ内になければなりません。
トレイトについて詳しくは第10章で解説します。
次に、途中に2行を追加しています。
最初の行ではrand::thread_rng
関数を呼び出して、これから使う、ある特定の乱数生成器を取得しています。
なお、この乱数生成器は現在のスレッドに固有で、オペレーティングシステムからシード値を得ています。
そして、この乱数生成器のgen_range
メソッドを呼び出しています。
このメソッドはuse rand::Rng
文でスコープに導入したRng
トレイトで定義されています。
gen_range
メソッドは範囲式を引数にとり、その範囲内の乱数を生成してくれます。
ここで使っている範囲式の種類は開始..終了
という形式で、下限値は含みますが上限値は含みません。
そのため、1から100までの数をリクエストするには1..101
と指定する必要があります。
あるいは、これと同等の1..=100
という範囲を渡すこともできます。
注:クレートのどのトレイトを
use
するかや、どのメソッドや関数を呼び出すかを知るために、各クレートにはその使い方を説明したドキュメントが用意されています。 Cargoのもう一つの素晴らしい機能は、cargo doc --open
コマンドを走らせると、すべての依存クレートが提供するドキュメントをローカルでビルドして、ブラウザで開いてくれることです。 たとえばrand
クレートの他の機能に興味があるなら、cargo doc --open
コマンドを実行して、左側のサイドバーにあるrand
をクリックしてください。
コードに追加した2行目は秘密の数字を表示します。 これはプログラムを開発している間のテストに便利ですが、最終版からは削除する予定です。 プログラムが始まってすぐに答えが表示されたらゲームになりませんからね!
試しにプログラムを何回か走らせてみてください。
$ 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;
use std::cmp::Ordering;
use std::io;
fn main() {
// --snip--
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
という型をスコープに導入しています。
Ordering
もenumの一つでLess
、Greater
、Equal
という列挙子を持っています。
これらは二つの値を比較したときに得られる3種類の結果です。
match guess.cmp(&secret_number) {
Ordering::Less => println!("Too small!"),
Ordering::Greater => println!("Too big!"),
Ordering::Equal => println!("You win!"),
}
それからOrdering
型を使用する新しい5行をいちばん下に追加してしています。
cmp
メソッドは二つの値の比較を行い、比較できるものになら何に対しても呼び出せます。
比較対象への参照をとり、ここではguess
とsecret_number
を比較しています。
そしてuse
文でスコープに導入したOrdering
列挙型の列挙子を返します。
ここではmatch
式を使用しており、guess
とsecret_number
の値に対してcmp
を呼んだ結果返されたOrdering
の列挙子に基づき、次の動作を決定しています。
match
式は複数のアーム(腕)で構成されます。
各アームはマッチさせるパターンと、match
に与えられた値がそのアームのパターンにマッチしたときに実行されるコードで構成されます。
Rustはmatch
に与えられた値を受け取って、各アームのパターンを順に照合していきます。
パターンとmatch
式はRustの強力な機能で、コードか遭遇する可能性のあるさまざまな状況を表現し、それらすべてを確実に処理できるようにします。
これらの機能については、それぞれ第6章と第18章で詳しく説明します。
ここで使われている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.3
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
| (構造体`std::string::String`を予期したけど、整数型変数が見つかりました)
|
= note: expected reference `&String`
found reference `&{integer}`
error[E0283]: type annotations needed for `{integer}`
--> src/main.rs:8:44
|
8 | let secret_number = rand::thread_rng().gen_range(1..101);
| ------------- ^^^^^^^^^ cannot infer type for type `{integer}`
| |
| consider giving `secret_number` a type
|
= note: multiple `impl`s satisfying `{integer}: SampleUniform` found in the `rand` crate:
- impl SampleUniform for i128;
- impl SampleUniform for i16;
- impl SampleUniform for i32;
- impl SampleUniform for i64;
and 8 more
note: required by a bound in `gen_range`
--> /Users/carolnichols/.cargo/registry/src/github.com-1ecc6299db9ec823/rand-0.8.3/src/rng.rs:129:12
|
129 | T: SampleUniform,
| ^^^^^^^^^^^^^ required by this bound in `gen_range`
help: consider specifying the type arguments in the function call
|
8 | let secret_number = rand::thread_rng().gen_range::<T, R>(1..101);
| ++++++++
Some errors have detailed explanations: E0283, E0308.
For more information about an error, try `rustc --explain E0283`.
error: could not compile `guessing_game` due to 2 previous errors (先の2つのエラーのため、`guessing_game`をコンパイルできませんでした)
このエラーの核心は型の不一致があると述べていることです。
Rustは強い静的型システムを持ちますが、型推論も備えています。
let guess = String::new()
と書いたとき、Rustはguess
がString
型であるべきと推論したので、私たちはその型を書かずに済みました。
一方でsecret_number
は数値型です。
Rustのいくつかの数値型は1から100までの値を表現でき、それらの型には32ビット数値のi32
、符号なしの32ビット数値のu32
、64ビット数値のi64
などがあります。
Rustのデフォルトはi32
型で、型情報をどこかに追加してRustに異なる数値型だと推論させない限りsecret_number
の型はこれになります。
エラーの原因はRustが文字列と数値型を比較できないためです。
最終的にはプログラムが入力として読み込んだString
を実数型に変換し、秘密の数字と数値として比較できるようにしたいわけです。
そのためにはmain
関数の本体に次の行を追加します。
ファイル名: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..101);
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
の前の値を新しい値で覆い隠す(shadowする)ことが許されているのです。
シャドーイング(shadowing)は、guess_str
とguess
のような重複しない変数を二つ作る代わりに、guess
という変数名を再利用させてくれるのです。
これについては第3章で詳しく説明しますが、今のところ、この機能はある型から別の型に値を変換するときによく使われることを知っておいてください。
この新しい変数をguess.trim().parse()
という式に束縛しています。
式の中にあるguess
は、入力が文字列として格納されたオリジナルのguess
変数を指しています。
String
インスタンスのtrim
メソッドは文字列の先頭と末尾の空白をすべて削除します。
これは数値データのみを表現できる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
型であるべきだと推論しています。
つまり、いまでは二つの同じ型の値を比較することになるわけです!
parse
メソッドは論理的に数値に変換できる文字にしか使えないので、よくエラーになります。
たとえば文字列にA👍%
が含まれていたら数値に変換する術はありません。
解析に失敗する可能性があるため、parse
メソッドはread_line
メソッドと同様にResult
型を返します
(「Result
型で失敗の可能性を扱う」で説明しました)
今回もexpect
メソッドを使用してResult
型を同じように扱います。
parse
メソッドが文字列から数値を作成できなかったためにResult
型のErr
列挙子を返したら、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 rand::Rng;
use std::cmp::Ordering;
use std::io;
fn main() {
println!("Guess the number!");
let secret_number = rand::thread_rng().gen_range(1..101);
// --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 [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
(スレッド'main'は'数字を入力してください!:ParseIntError { kind: InvalidDigit }', src/libcore/result.rs:785でパニックしました)
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
(注:`RUST_BACKTRACE=1`で走らせるとバックトレースを見れます)
quit
と入力すればゲームが終了しますが、数字以外の入力でもそうなります。
これは控えめに言っても最適ではありません。
私たちは正しい数字が予想されたときにゲームが停止するようにしたいのです。
正しい予想をした後に終了する
break
文を追加して、ユーザが勝ったらゲームが終了するようにプログラムしましょう。
ファイル名: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..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);
// --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のように、String
からu32
にguess
を変換する行を変えることで実現できます。
ファイル名: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..101);
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
の列挙子を持つ列挙型であることを思い出してください。
ここではmatch
式を、cmp
メソッドから返されるOrdering
を処理したときと同じように使っています。
もし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..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;
}
}
}
}
まとめ
数当てゲームを無事に作り上げることができました。 おめでとうございます!
このプロジェクトではハンズオンを通して、let
、match
、メソッド、関連関数、外部クレートの使いかたなど、多くの新しいRustの概念に触れました。
以降の章では、これらの概念についてより詳しく学びます。
第3章では変数、データ型、関数など多くのプログラミング言語が持つ概念を取り上げ、Rustでの使い方を説明します。
第4章ではRustを他の言語とは異なるものに特徴づける、所有権について説明します。
第5章では構造体とメソッドの構文について説明し、第6章では列挙型がどのように動くのかについて説明します。