LoginSignup
20
17

More than 5 years have passed since last update.

[翻訳]Rustを学ぼう - 数字当てゲーム

Last updated at Posted at 2015-06-07

Rustドキュメントの日本語訳は Rustドキュメント翻訳プロジェクト に移行しました。

http://rust-lang-ja.github.io/the-rust-programming-language-ja/1.6/book/guessing-game.html にてRust 1.6原文に追従した版があります。


Rust 1.0時点の Learn Rust - Guessing Game より訳出。

訳者注:Qiita Markdownソースには原文をコメント形式<!--/-->で残してあります。誤訳修正や訳出改善の編集リクエストを歓迎します。


数字当てゲーム(Guessing Game)

私たちの最初のプロジェクトとして、プログラミング入門の古典問題:数当てゲームを実装していきます。動作はこんな感じです:プログラムは1から100までの間でランダムな整数を一つ生成し、ユーザが推測した値を入力させます。推測値が入力されると、プログラムが選んだ数字よりも高いか低いかを教えます。正しい値に到達したら、そこでゲームクリアです。いい感じでしょ?

セットアップ(Set up)

では新しいプロジェクトを準備しましょう。プロジェクト用のディレクトリに行ってください。ディレクトリ構造やhello_worldでのCargo.tomlの作り方を覚えていますか?Cargoはそのためのコマンドを持っています。次の通りにやってみて:

$ cd ~/projects
$ cargo new guessing_game --bin
$ cd guessing_game

ここではcargo newにプロジェクトの名前を渡し、ライブラリではなくバイナリを作るために--binフラグを指定しました。

生成されたCargo.tomlを確認してみましょう:

[package]

name = "guessing_game"
version = "0.0.1"
authors = ["Your Name <you@example.com>"]

Cargoはこの情報をあなたの環境から取得しています。間違っていたら直しておいてください。

最後に、Cargoは‘Hello, world!’を生成してくれます。src/main.rsを確認してみます:

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

さっそくCargoが用意したものをコンパイルしましょう:

$ cargo build
   Compiling guessing_game v0.0.1 (file:///home/you/projects/guessing_game)

よろしい!ではもう一度src/main.rsを開いてください。私たちのコードは全部このファイルに書いていきます。

先に進む前に、もう一つCargoのコマンドrunを説明しておきましょうか。cargo runcargo buildと似ていますが、出来上がったファイルの実行まで行います。では試してみます:

$ cargo run
   Compiling guessing_game v0.0.1 (file:///home/you/projects/guessing_game)
     Running `target/debug/guessing_game`
Hello, world!

素晴らしい!プロジェクトをすばやくイテレーションする場合は、runコマンドが便利です。私たちのゲームは、次ステップへ進む前にはイテレーション毎で確認する必要があるため、まさにその手のプロジェクトといえます。

推測値を処理する(Processing a Guess)

では始めましょう!数当てゲームで最初にすべきことは、プレイヤーに推測値を入力させることです。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)
        .ok()
        .expect("Failed to read line");

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

盛りだくさんですね!少しづつ見ていきましょうか。

use std::io;

私たちはユーザ入力を受け付けて、さらに出力として結果を表示しなければなりません。ということで、標準ライブラリにあるioライブラリが必要になります。Rustでは、すべてのプログラムに「プレリュード(prelude)」に含まれるものしかインポートしません。あるライブラリ機能がプレリュードに含まれないときは、直接useする必要があります。

fn main() {

すでに見たとおり、main()関数がプログラムのエントリポイントになります。fn構文は新しい関数を宣言し、()は引数が無いことを表します。そして{で関数の本体が開始します。戻り値型を明記しなかったため、ここでは空のタプル(tuple)()とみなされます。

    println!("Guess the number!");

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

println!()が画面に文字列(string)を表示するマクロ(macro)であることは前に学びました。

    let mut guess = String::new();

さあ面白いところにきました!この1行には様々な要素がふくまれています。最初に気づくのは、これが「変数束縛(variable bindings)」に用いられるlet文(let statement)であることです。次の形式をとります:

let foo = bar;

これはfooという名前で新しい束縛を作り、そこにbarの値を結びつけます(bind)。多くのプログラミング言語では「変数(variable)」と呼ばれますが、Rustの変数束縛にはいくつかの秘策が隠されているのです。

例えば、変数束縛はデフォルトでimmutableです。私たちの例でmutを使うのはこのため: immutableではなく、mutableな束縛とするためです。letはその左側に名前をとるのではなく、実は「パターン(pattern)」を書くことができます。パターンは後ほど使いましょう。今は簡単な使い方で十分です:

let foo = 5; // immutable.
let mut bar = 5; // mutable

あと、//から行末まではコメントになります。Rustはコメント(comments)部分を全部無視します。

let mut guessが名前guessのmutable束縛を導入すると理解したところで、=の逆側にあるString::new()が意味するところを見ていく必要があります。

Stringは文字列型であり、標準ライブラリで提供されます。Stringは可変長であり、テキストをUTF-8エンコーディングしたものです。

::new()構文は::を使いますが、これはある型に「関連する関数(associated function)」となっているためです1。つまり、String型の特定のインスタンスではなく、String型自身に関連付けられています。プログラミング言語によってはこれを「静的メソッド(static method)」と呼ぶものもあります。

この関数は新しい空のStringを生成するため、new()という名前を持ちます。その型の新しい値を作る一般的な名前のため、多くの型でnew()関数を見つけられるでしょう。

では次に行きましょう:

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

これまた沢山!ちょっとづつ見ていきましょう。最初の行は2つのパートから成ります。まずはこちら:

io::stdin()

プログラムの冒頭行でstd::iouseしたのを覚えていますか?ここでは関連する関数(associated function)を呼び出しているのです。もしuse std::ioがなかったら、この行はstd::io::stdin()と書かなければなりませんでした。

この関数は端末の標準入力へのハンドルを返します。具体的にはstd::flag_io::Stdinを返します。

続くパートでは、ユーザ入力の取得にこのハンドルを使います:

.read_line(&mut guess)

ここでは、ハンドルに対してread_line()メソッドを呼び出します。メソッド(method)は関連する関数(associated function)と似ていますが、その型自身ではなく、型の特定インスタンスに対してのみ有効です。またread_line()には引数も1つ: &mut guessを渡しています。

上でguessに束縛したやり方を覚えていますか?mutableな束縛としましたね。しかし、read_lineは引数としてStringを取りません: &mut Stringを取ります。Rustには、あるデータへの複数個の参照ができて、コピー回数を減らせる、「参照(references)」という機能があります。参照を安全かつ簡単に扱えることはRustの主要なセールスポイントの1つですが、参照は複雑な機能です。もっとも、今回のプログラムを完成させるにはその詳細まで理解する必要はありません。今知っておくべきは、let束縛と同じく参照もデフォルトでimmutableとなる点です。なので、&guessではなく&mut guessと書く必要があります。

どうしてread_line()が文字列へのmutable参照をとるのでしょう?この関数の仕事はユーザが標準入力にタイプしたものを取得し、文字列へと変換することです。なので関数は文字列を引数として取り、入力を追加するためにはmutableとする必要があるのです。

ただし、コードのこの行はまだ終わっていません。テキストの1行ではありますが、コードの1論理行の最初のパートにすぎません:

        .ok()
        .expect("Failed to read line");

.foo()構文でメソッドを呼び出すとき、新しい行や空白から始めることがあります。これは長い行を分割するのに役立ちます。次のようにも書くこともできます:

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

これでは読むのが大変です。それで3つのメソッド呼び出しを3行に分けたのです。すでにread_line()には言及しましたが、ok()expect()はどうでしょう?ええと、read_line()はユーザ入力を渡した&mut Stringへ代入することは説明した通りです。しかしその戻り値: この場合はio::Resultも返しているのです。Rust標準ライブラリにはResultという名前の型がいくつかあります: ジェネリックなResultと、io::Resultのようなサブラリブラリ用の具体的な型です。

これらResult型の目的は、エラーハンドリング情報をエンコードすることです。Result型の値は、任意の型と同様に、メソッドを定義しています。この場合はio::Resultok()メソッドを持ち、その意味は「我々はこの値が成功を意味すると仮定する。そうでなければエラー情報を捨てればよい。」となります。なぜ捨ててしまうのでしょう?基本的なプログラムでは、なんらかの問題が生じたときは処理続行不可能を意味するため、一般エラーを表示できれば十分でしょう。ok()メソッドは、もう一つのメソッドexpect()を定義する値を返します。expect()メソッド呼び出しには値を取り、もし成功を意味していなければ、その値はpanic!メッセージとして渡されます。このpanic!はプログラムをクラッシュさせ、メッセージを表示します。

これら2つのメソッド呼び出しをやめても、プログラムはコンパイルできますが、警告がなされるでしょう:

$ cargo build
   Compiling guessing_game v0.1.0 (file:///home/you/projects/guessing_game)
src/main.rs:10:5: 10:39 warning: unused result which must be used,
#[warn(unused_must_use)] on by default
src/main.rs:10     io::stdin().read_line(&mut guess);
                   ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Rustは私たちがResult値を使っていないと警告しています。この警告はio::Resultへの特殊なアノテーションによるものです。Rustは、エラー発生の可能性を扱っていないケースを教えてくれるのです。エラーを抑制する正しい方法は、エラーハンドリング処理を実際に書くことです。幸運にも、問題が起きた場合はクラッシュさせたいだけですから、今回は2つの小さなメソッド利用で十分です。エラーからどうにかして回復できるのならば、何かしらの対処が必要ですが、それは将来のプロジェクトのためにとっておきましょう。

この最初の例では、あと1行を残すのみとなりました:

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

ここでは入力を保存しておいた文字列を表示します。{}はプレースホルダーであり、対応させるguessを引数として渡します。複数の{}がある場合は、複数個の引数を渡すことになります:

let x = 5;
let y = 10;

println!("x and y: {} and {}", x, y);

簡単ですね。

何にせよ、ツアーは以上です。ここまででcargo runで実行できます:

$ cargo run
   Compiling guessing_game v0.1.0 (file:///home/you/projects/guessing_game)
     Running `target/debug/guessing_game`
Guess the number!
Please input your guess.
6
You guessed: 6

オーライ!最初のパートが完了です: キーボードからの入力を取得し、それを表示し返すことができました。

秘密の数字を生成する(Generating a secret number)

次は、秘密の数字を生成する必要があります。Rust標準ライブラリはまだ乱数生成機能を含んでいません。ただし、Rustチームがrandクレートを提供しています。「クレート(crate)」はRustコードのパッケージです。私たちは実行可能な「バイナリ・クレート(binary crate)」をビルドしてきました。randは「ライブラリ・クレート(library crate)」であり、他プログラムから利用されることを目的としたコードを含みます。

外部クレートの利用ではCargoの真価が発揮されます。randを利用したコードを書く前に、私たちのCargo.tomlを変更する必要があります。ファイルを開いて、次の数行を下部に追記してください:

[dependencies]

rand="0.3.0"

Cargo.toml[dependencies]セクションは[package]と似ています: 次のセクションが始まるまでの後続内容は該当セクションに属します。Cargoは依存(dependencies)セクションを、依存先の外部クレートと要求バージョンの指定に利用します。ここでは、私たちはバージョン0.3.0を利用します。Cargoはバージョン番号付与の標準セマンティック・バージョニングを理解します。最新版を使いたいなら、*やバージョン範囲の指定が可能です。Cargoのドキュメントに詳細があります。

では、コードを一切変更していませんが、プロジェクトをビルドしましょう:

$ cargo build
    Updating registry `https://github.com/rust-lang/crates.io-index`
 Downloading rand v0.3.8
 Downloading libc v0.1.6
   Compiling libc v0.1.6
   Compiling rand v0.3.8
   Compiling guessing_game v0.1.0 (file:///home/you/projects/guessing_game)

(当然ですが、違うバージョンになるかもしれません。)

新しい出力がいっぱい!今回は外部の依存を指定したため、Crates.ioよりコピーされるレジストリから、Cargoが全ての最新バージョンを取ってきます。

レジストリを更新したのち、Cargoは[dependencies]をチェックして足りないものをダウンロードします。ここではrandへの依存のみを記述しましたが、libcのコピーも取得しています。これはrandlibcに依存しているためです。ダウンロードが終わると、それらをコンパイルし、その後に私たちのプロジェクトをコンパイルします。

もう一度cargo buildを実行すると、異なる出力が得られるでしょう:

$ cargo build

そうです、出力なし!プロジェクトがビルド済みで、また全ての依存先もビルド済みであり、何もしなくて良いことをCargoは知っています。することがなければ、単に終了します。再びsrc/main.rsを開いて、つまらない変更を行って、もう一度保存すれば、次の1行が出力されるでしょう:

$ cargo build
   Compiling guessing_game v0.1.0 (file:///home/you/projects/guessing_game)

Cargoにはrandの任意バージョン0.3.xが欲しいと伝えたので、執筆時点での最新バージョンv0.3.8が取得されました。しかし翌週に、重要なバグ修正が行われたバージョンv0.3.9がリリースされたらどうなるでしょう?確かにバグ修正は重要ですが、0.3.9が私たちのコードを壊すレグレッションを起こすとしたら?

プロジェクトのディレクトリ以下で見つかるCargo.lockファイルが、この問題への答えです。初めてプロジェクトをビルドするとき、Cargoは基準に適合する全バージョンを解決し、それらをCargo.lockファイルに書き出します。以降プロジェクトをビルドする際には、CargoはCargo.lockファイルがあれば読み取って、再びバージョン解決処理を行う代わりに、指定されたバージョンを使います。これにより反復可能なビルドが自動的に達成されます。言い換えると、明示的にアップグレードしない限りは0.3.8のままです。誰かとコードを共有する場合には、lockファイルの恩恵があるでしょう。

v0.3.9を使いたい場合はどうすれば?Cargoにはもう一つのコマンドupdateがあり、その意味は「lockファイルを無視して、基準に合う最新バージョンを解決せよ。上手くいったら、それらのバージョン情報をlockファイルに書き出せ。」となります。ただし、デフォルトでは、Cargoはバージョン0.3.0以上かつ0.4.0未満だけを検索します。0.4.xに移行したければ、直接Cargo.tomlを更新しなければなりません。そうすれば、次回cargo buildしたときに、Cargoはインデックスを更新してrandの要件を再評価するでしょう。

Cargoそのエコシステムについては、もっと言うべきことがあります。とはいえ今は、ここまでが知っておくべき全てです。Cargoによってライブラリ再利用が本当に簡単になるため、Rust使い(Rustaceans)は多くのサブパッケージで組み上げられる小さなプロジェクトを書く傾向があります。

それではrandを実際に使っていきましょう。これが次のステップです:

extern crate rand;

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)
        .ok()
        .expect("failed to read line");

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

最初に行ったのは冒頭行の変更です。今はextern crate randになっています。[dependencies]randを宣言しておいたので、Rustにその利用を教えるためにextern crateとできます。これはuse rand;とも同じ意味であり、rand::プレフィクスを付ければrandクレートの要素を使えるようになります。

続いて、もう一つのuse行: use rand::Rngを追加しました。すぐにメソッドを使う予定なので、そのとき要求されるRngをスコープに入れておきましょう。基本的な考え方は次の通りです: 「トレイト(traits)」と呼ばれる何かに対してメソッドが定義され、そのメソッドを動かすには、トレイトがスコープ内に存在している必要があります。より詳細を知りたければ、トレイト(traits)の節を読んでください。

間には、2つの行を追加しました:

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

    println!("The secret number is: {}", secret_number);

実行中のスレッド(thread)に対してローカルな、乱数生成器のコピーを得るためにrand::thread_rng()関数を使います。上でuse rand::Rngとしたため、gen_range()メソッドが有効になっています。このメソッドは2つの引数をとり、それらの間の数字を生成します。下限値は含みます(inclusive)が、上限値は範囲に含まれない(exclusive)ため、1から100までの間の数字を得るには1101が必要になります。

2行目では単に秘密の数字を表示します。確認が簡単になりますから、プログラム開発のあいだは便利です。ただし最終バージョンでは削除する予定です。最初から答えを表示してしまってはゲームになりませんからね!

新しいプログラムを何回か実行してみます:

$ cargo run
   Compiling guessing_game v0.1.0 (file:///home/you/projects/guessing_game)
     Running `target/debug/guessing_game`
Guess the number!
The secret number is: 7
Please input your guess.
4
You guessed: 4
$ cargo run
     Running `target/debug/guessing_game`
Guess the number!
The secret number is: 83
Please input your guess.
5
You guessed: 5

素晴らしい!次は: 推測値と秘密の数字を比較しましょう。

推測値を比較する(Comparing guesses)

ここではユーザの入力を取得して、ランダムな数字と推測値を比較しましょう。まだ上手く動きませんが、下記が次ステップです:

extern crate rand;

use std::io;
use std::cmp::Ordering;
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)
        .ok()
        .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という型をスコープにいれます。それから、後半部にはその型を使った5行を追加します:

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

cmp()メソッドは比較可能な型に対して呼ぶことができ、引数には比較対象オブジェクトの参照をとります。そして前でuse済みのOrdering型を返します。ここではOrderingの種類を判定するのにmatch文を使います。Orderingenumです。これは'enumeration(列挙)'の短縮表記で、次のようなものです:

enum Foo {
    Bar,
    Baz,
}

この定義では、Foo型はFoo::BarまたはFoo::Bazのみを値として持ちます。::は、固有のenum変数の名前空間を指定するのに使います。

Ordering列挙型(enum)では3つの値: Less, Equal, Greaterを取りえます。match文は型の値をとり、それぞれの候補値から'arm(アーム; 腕)'を伸ばしていきます。Orderingは3種類の値をもつため、3つのarmを書きます2:

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

LessならToo small!を、GreaterならToo big!を、EqualならYou win!を表示します。matchはとても便利で、Rustではよく用いられます。

ところで、このコードはまだ上手く動かないと言いました。試してみましょうか:

$ cargo build
   Compiling guessing_game v0.1.0 (file:///home/you/projects/guessing_game)
src/main.rs:28:21: 28:35 error: mismatched types:
 expected `&collections::string::String`,
    found `&_`
(expected struct `collections::string::String`,
    found integral variable) [E0308]
src/main.rs:28     match guess.cmp(&secret_number) {
                                   ^~~~~~~~~~~~~~
error: aborting due to previous error
Could not compile `guessing_game`.

ヒェー!大きな間違いがあったようです。このでの問題は‘mismatched types(型の不整合)’です。Rustは強い静的な型システムを持ちます。一方で型推論も持っています。let guess = String::new()と書いたとき、RustはguessString型であると推論できるため、私たちは型を明示せずに済みました。そしてsecret_numberの候補には、1から100の間の整数値を保持できる型: 32ビットの数値型i32、符号無し32ビット数値型u32、64ビットの数値型i64などがあります。これまでは気にしていませんでしたが、Rustはi32をデフォルトとします。しかし、ここでは、Rustはguesssecret_numberを比較する方法が分かりません。同じ型同士で比べる必要があります。つまり、比較のためには入力から読み取ったStringを本当の数値型に変換したいのです。これは3行の追加で可能です。新しいプログラムは次の通り:

extern crate rand;

use std::io;
use std::cmp::Ordering;
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)
        .ok()
        .expect("failed to read line");

    let guess: u32 = guess.trim().parse()
        .ok()
        .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!"),
    }
}

新しいのは次の3行:

    let guess: u32 = guess.trim().parse()
        .ok()
        .expect("Please type a number!");

ちょっと待って、もうguess変数は使っていましたよね?その通り、ただRustでは前のguessを新しい変数で'shadow(覆い隠す)'ことができます。最初guessStringだったものを、u32に変換したいというような、まさにこの手の状況で良く用いられます。Shadowing(覆い隠し)によって名前guessが再利用でき、guess_strguessといった2つの名前をいちいち付けなくて済みます。

guessを以前も書いたような式に束縛します:

guess.trim().parse()

さらにok().expect()の呼び出しが続きます。ここで、guessString型で入力を保持している古い方のguessを指しています。Stringtrim()メソッドは、文字列前/後にある空白文字を全て取り除きます。read_line()では必ずreturnキーを入力させるため、この処理は重要です。つまり5とタイプしてからreturnを入力すると、guess5\nのようになっているのです。ここで\nはエンターキーによる'newline'を表します。trim()でこれを取り除き、文字列を5だけにするのです。文字列型のparse()メソッドは、文字列を数値へと解析(parse)します。解析される数値型は複数候補があるため、Rustには欲しい正確な数値型を教える必要があります。よってlet guess: u32とします。guess直後のコロン(:)はRustに型の注釈を与えています。u32は符号なし(unsigned)の32bit整数型です。Rustは数種類の組込み値型を提供しますが、ここではu32を選びました。小さな正数に対しては、良いデフォルトの選択です。

read_line()と同様に、parse()呼び出しもエラーの可能性があります。文字列がA👍%を含んだらどうなるでしょう?これらは数値へと変換できません。そこで、read_line()のときと同じ方法をとります: ok()expect()メソッドを使って、エラー時はクラッシュさせます。

ではプログラムを動かしてみましょう!

$ cargo run
   Compiling guessing_game v0.0.1 (file:///home/you/projects/guessing_game)
     Running `target/guessing_game`
Guess the number!
The secret number is: 58
Please input your guess.
  76
You guessed: 76
Too big!

イイネ!推測値の前に空白を含めてみましたが、ちゃんと推測値76として扱われています。プログラムを何度か実行して、小さな数を入れてみるなどして数当てが動いているか確認してください。

これでゲーム処理の大部分が出来あがりましたが、まだ数当てを1回しか行えません。繰り返し処理を加えていきましょう!

繰り返し処理(Looping)

loopキーワードで無限ループできます。さっそく追加しましょう:

extern crate rand;

use std::io;
use std::cmp::Ordering;
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);

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

        let mut guess = String::new();

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

        let guess: u32 = guess.trim().parse()
            .ok()
            .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!"),
        }
    }
}

では試します。ちょっと待った、無限ループさせるだけ?parse()のときの議論を忘れた?数値以外の答えが入力されたら、returnして終了しましょう。やってみます:

$ cargo run
   Compiling guessing_game v0.0.1 (file:///home/you/projects/guessing_game)
     Running `target/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!'

おや!quitで本当に終了しました。他の数値でない入力も同様です。んー、これはあまりイケてないですね。まずは、ゲームに勝利したときだけ終了させましょう:

extern crate rand;

use std::io;
use std::cmp::Ordering;
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);

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

        let mut guess = String::new();

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

        let guess: u32 = guess.trim().parse()
            .ok()
            .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!");
                break;
            }
        }
    }
}

You win!の次行にbreakを追加したので、勝利したときにループを抜けるようになります。ループを抜けるとmain()終端に到達するため、プログラムの終了となります。もう1箇所調整をしましょう: 数値以外でプログラム終了するのではなく、単に入力を無視したいです。次のようにして実現可能です:

extern crate rand;

use std::io;
use std::cmp::Ordering;
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);

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

        let mut guess = String::new();

        io::stdin().read_line(&mut guess)
            .ok()
            .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 guess: u32 = match guess.trim().parse() {
    Ok(num) => num,
    Err(_) => continue,
};

このok().expect()からmatch文への切り替えは、「エラー発生でクラッシュ」から「実際のエラーハンドリング処理」に移行する一般的なやり方です。parse()により返されたResultOrdering同様に列挙型(enum)なのですが、こちらはそれぞれが状況に関連したデータを保持しています: Okは成功に、Errは失敗を表します。それぞれ追加の情報: 成功時には解析された数値を、失敗時はエラー型を含んでいます。ここでは、Ok(num)matchしたときは、Ok値が保持する値をnumに取り出して、右辺にそれを置いて値を戻しています。Errの場合には、エラーの種類を気しないため、名前の代わりに_を使っています。これによりエラーを無視して、continueloopの次イテレーションへと進ませます。

これで上手くいくはず!レッツ・トライ:

$ cargo run
   Compiling guessing_game v0.0.1 (file:///home/you/projects/guessing_game)
     Running `target/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!

最高!数当てゲームを完成させるため、最後の微調整が残っています。何だかわかった?その通り、秘密の数字を出力したらダメですよね。確認中は便利でしたが、ゲームとしては台無しです。こちらが最終的なコードです:

extern crate rand;

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

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)
            .ok()
            .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;
            }
        }
    }
}

完成!(Complete!)

ようやく、数当てゲームの作成に成功しました!おめでとう!

このプロジェクトではたくさんの要素を説明しました: letmatch、メソッド、関連する関数(associated functions)、外部クレート利用など。次のプロジェクトでも色々見ていきましょう。



  1. 訳注:日本語圏では用語が安定していない(と思う)ので、あえて"associated function"を直訳調の「関連する関数」とした。もうちょいマシな対訳語が欲しいところ。 

  2. 訳注:'arm'とはmatch式中の => を指している。日本語訳だと意味が取りにくいように思えたので、そのまま'arm'表記とした。 

20
17
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
20
17