【MacでRust入門】Rust日本語版チュートリアルの要点まとめ ~ 第4回 簡単なゲームの作成
目次
こんにちは。
最近流行りのRustを初めてみよう!ということでチュートリアルを読み始めました。
内容を忘れないように & チュートリアルがTL;DRな方向けにブログ記事に要点をまとめていこうと思います。
前回はビルド&パッケージマネージャのCargoをまとめました。
今回は第4回です。前回までの知識といくつかの追加知識を使って、Rustで簡単なゲームを作ります。
この記事で学べること
数当てゲームの作成を通じてRustに関する以下の基本的な使い方を学びます。
- letによる変数宣言
- ユーザー入力
- メソッド
- 関連関数
- 外部クレート
- ループ
- matchによるエラー処理
数当てゲームのアルゴリズム
- プログラムが「答え」となる1~100の整数をランダムに生成
- プレイヤーが予想した数字を入力
- 入力された予想と答えを比較して、小さいか大きいかを表示
- 予想が当たるまで予想の入力と比較を繰り返す
- 予想が当たったらプログラムがメッセージを表示し、ゲーム終了。
プロジェクトの作成
前回学んだお決まりの手順。
$ cargo new guessing_game
$ cd guessing_game
一旦、このままビルドと実行をしておきます。デフォルトで生成されていたhello world文が出力されます。
$ cargo run
Hello, world!
このあと、src/main.rs
を編集していきます。このように、コード編集のたびにrunコマンドで実装・テストのサイクルを回していきます。
数あてゲームのRustコーディング 1. 予想の入出力
乱数の生成の前に、ユーザーが「予想」を入力する部分を作ります。main.rs
を以下に書き換えます。
use std::io;
fn main() {
println!("~~ 数を当てよう! ~~");
println!("入力してください: ");
let mut guess = String::new();
io::stdin()
.read_line(&mut guess)
.expect("行の読み込み失敗...");
println!("あなたの予想: {}", guess);
}
Rustの入出力プログラムの解説
use std::io;
- 1行目で、
std
という標準ライブラリの中のio
という入出力ライブラリをスコープ内で使用することを宣言- Rustはデフォルトでprelude(プレリュード)という標準ライブラリで定義されるアイテムのいくつかをスコープに取り込む。preludeにない場合は
use
で入れる。
- Rustはデフォルトでprelude(プレリュード)という標準ライブラリで定義されるアイテムのいくつかをスコープに取り込む。preludeにない場合は
fn main() {
println!("~~ 数を当てよう! ~~");
fn main(){}
とprintln!()
は第2回を参照
let mut guess = String::new();
let
で変数を作る。これはデフォルトでimmutable(イミュータブル、不変)- 変数名の前に
mut
で変数をmutable(ミュータブル、可変)にできる String::new
関数でString(文字列)型の新しいインスタンスを返す::
構文はnew
がString
型の関連関数であることを示す- 関連関数とは、ある「型」に実装される関数のこと
- つまり、この行では
guess
という値が可変な空の文字列を作った
io::stdin()
.read_line(&mut guess)
- 取り込んだ
io
モジュールのstdin
関数により、ユーザー入力の処理を可能にするStdin
インスタンスを返すuse
する代わりに、ここでstd::io::stdin
でもOK
.
でインスタンスが持つメソッドを呼び出す。read_line
メソッドはユーザーが標準入力(普通はキーボード)に入力したものを引数の文字列に追加する- 引数はmutableである必要がある
- 引数の
&
は引数が「参照」であることを示す- 参照により、同じデータにアクセスする際に何度もデータをメモリ上でコピーせずに済む
- 参照もデフォルトで不変なので、変数前に
mut
をつけることで可変にする
.expect("行の読み込み失敗...");
read_line
メソッドは引数の文字列に入力を入れるだけでなく、io::Result
という列挙型(enum)の一種であるResult型の値を返す- Result型はエラー処理のための列挙型で、列挙子は
Ok
(成功)とErr
(失敗)Ok
には正常に生成された値が入るErr
には処理に失敗した過程や理由が入るmatch
式で列挙子がOk/Errのどちらかに応じて違う処理を行える
- Result型の
expect
メソッドにより、Err
のときにプログラムを中止して引数のメッセージを表示
println!("あなたの予想: {}", guess);
pringln!
の{}
はプレースホルダーで、2つめ以降の引数に与えた変数を順番に表示する
プログラムのテスト
ここまででcargo run
すると、キーボード入力を表示するプログラムが実行できます。
~~ 数を当てよう! ~~
入力してください:
12
あなたの予想: 12
数あてゲームのRustコーディング 2. 乱数(ランダムな整数)の生成
Rustで乱数を生成するにはrandクレートを使います。
クレートでRustに機能を追加する方法
Cargo.toml
の[dependencies]
セクションに以下をコピペします。
rand = "0.8.3"
0.8.3
は^0.8.3
の省略で、0.8.3以上0.9.0未満の任意のバージョンです。
これでcargo build
でビルドすると、Cargoが依存関係のあるパッケージの最新バージョンをレジストリ(オープンソースのCrates.ioのデータのコピー)から取得してきます。パッケージを追加した最初は少し時間がかかりますが、一度コンパイル済であると次からは再コンパイルしないのでビルドも速くなります。(依存関係が変わらなければ、コードを変更してもビルドは速いです)
また、最初ビルドしたときの依存関係のバージョンをCargo.lock
に書き込むため、後にレジストリ側のバージョンがアップデートされたとしても、明示的にバージョンを更新するまでは再現性のあるビルドを行います。バージョン更新はcargo update
でできます。
クレートを使ってRustで乱数を生成する
rand
が依存関係に追加されたら、src/main.rs
を更新する形で以下を書き加えます。
//...
use rand::Rng;
fn main() {
// ...
let secret_number = rand::thread_rng().gen_range(1..101);
println!("答えは: {}", secret_number);
// ...
Rustの乱数生成プログラムの解説
use rand::Rng;
Rng
は乱数生成のためのメソッドを定義している。use
でスコープに追加- このようにクレートから
::
で呼び出せるものはトレイトと呼ばれる
- このようにクレートから
let secret_number = rand::thread_rng().gen_range(1..101);
rand::thread_rng
関数でOSからシード値を取得した乱数生成器を返すgen_range
メソッドで、引数の範囲式で定義された範囲内の乱数を生成する- 範囲式は
下限..上限
という形式で、上限値は含まない。(100を入れたければ101を書く) 1..=100
とも書ける
- 範囲式は
println!("答えは: {}", secret_number);
これはテスト版のみの行で、乱数が生成されることを確認するためのコードです。
クレートのトレイトやメソッドを調べる方法
ちなみに、cargo doc --open
というコマンドで依存関係にあるクレートのドキュメントをローカルでビルド(html生成)してブラウザで開けます。これによって、クレートの中のどのトレイトを使うか、どのメソッドや関数を使うかといった情報を探せます。
プログラムのテスト
再びcargo run
でここまでの実装をテストしてみます。
~~ 数を当てよう! ~~
答えは: 28
入力してください:
12
あなたの予想: 12
答えが見えているのに間違えるという。
何回かcargo run
すると、答えが変わっていることがわかります。
数あてゲームのRustコーディング 3. 予想と答えの比較
予想と答えを生成できるようになったので、これらを比較する処理を追加します。
//...
use std::cmp::Ordering;
fn main() {
// ...
match guess.cmp(&secret_number) {
Ordering::Less => println!("小さすぎます"),
Ordering::Greater => println!("大きすぎます"),
Ordering::Equal => println!("やったね! ~ THE END ~"),
}
}
Rustの比較プログラムの解説
use std::cmp::Ordering;
std::cmp::Ordering
という型はLess
,Greater
,Equal
という列挙子を持つenum
match guess.cmp(&secret_number) {
Ordering::Less => println!("小さすぎます"),
Ordering::Greater => println!("大きすぎます"),
Ordering::Equal => println!("やったね! ~ THE END ~"),
}
cmp
メソッドで比較対象の参照を引数にとり2つの値の比較を行う- たとえば、入力値
guess
が乱数secret_number
より大きければOrdering::Greater
を返す
- たとえば、入力値
match
式でcmp
のResultであるOrdering
の列挙子に応じて異なる処理を実行match
式の中身は複数のアームで構成され、各アームは「マッチさせるパターン=>
マッチ時の実行コード」
Rustの型不一致によるコンパイルエラーをシャドーイングで解決する
上記のコードをcargo run
すると型の不一致によるコンパイルエラーが出ます。
これはguess
がString
型であるのに対し、secret_number
が数値型(i32
型)なので比較できないためです。
そこでコード内のユーザー入力部分io::stdin()...
の後ろに以下のコードを追記します。
let guess: u32 = guess.trim().parse().expect("Please type a number!");
- 同じ変数名を別に宣言することをシャドーイング(Shadowing, Shadowする)という
- 同じ変数名が再利用できる
guess.trim().pars()
のguess
は元の入力値を入れた変数trim
メソッドは先頭末尾の空白を削除- ユーザー入力後のEnterで
\n
という改行コードが入るため、これを消す
- ユーザー入力後のEnterで
- 文字列の
parse
メソッドにより、文字列を解析して数値にする - 新たに宣言した変数
guess
の:
は型注釈で、この型がu32
(符号なし32ビット整数)であることを示す- これにより、
parse
で変換された結果もu32
になり、cmp
で比較されたsecret_number
の型もu32
に推論されるので、比較を行うことができるようになる
- これにより、
parse
はResult
も返す。変換できないエラー値が入力されたらプログラムを中止させるように、expect
でErr
を返したときの挙動を定義する
プログラムのテスト
再びcargo run
すると、無事動きました。
~~ 数を当てよう! ~~
答えは: 40
入力してください:
12
あなたの予想: 12
小さすぎます
数あてゲームのRustコーディング 4. 予想と答えが一致するまでループ
次に、予想と答えが一致するまで入力処理と比較をループするようにします。
// ...
loop {
println!("入力してください: ");
// ...
match guess.cmp(&secret_number) {
// ...
Ordering::Equal => {
println!("やったね! ~ THE END ~");
break;
}
}
}
}
Rustのループプログラムの解説
loop {}
で囲った部分は無限ループするbreak
を書くとループを抜ける
数あてゲームのRustコーディング 5. 不正入力のエラー処理
最後に、エラーハンドリングでプログラムを洗練させます。
現状だと数値以外(文字列など)をユーザーが入力したらプログラムが中断されてしまうので、これを中断ではなく継続して再び入力を促すように修正します。これによって正解にたどり着くまで終了しないゲームになります。
let guess: u32 = match guess.trim().parse() {
Ok(num) => num,
Err(_) => {
println!("それは数字じゃないです");
continue;
}
};
Rustエラー処理プログラムの解説
expect
でErr
時の挙動を書くとそこで中断されてしまうので、match
式に変更parse
はResult
を返すので、列挙子Ok
とErr
のアームを書くOk
のとき、つまり数値に変換できた場合は、Ok
に格納された値num
を返す。Err
のとき、格納された値が何であれ、continue
を実行するcontinue
はloop {}
内を一旦抜けて次のループに移るErr
の格納値は使わないので_
で表現する。
最終テスト & 完成!
お決まりのcargo run
でテストします。
無事完成...! と思いきや、テスト用の答え表示を消すのを忘れていました。
println!("答えは:
の行を消して完成です!
~~ 数を当てよう! ~~
入力してください:
23
あなたの予想: 23
大きすぎます
入力してください:
12
あなたの予想: 12
小さすぎます
入力してください:
15
あなたの予想: 15
小さすぎます
入力してください:
20
あなたの予想: 20
小さすぎます
入力してください:
21
あなたの予想: 21
やったね! ~ THE END ~
試しにやってみたら、最初から結構近いとこ狙えてました。
まとめ
というわけで、今回はRustを使って「数あてゲーム」を作ってみました。
Rustにクレートを追加する方法や、変数宣言、ユーザー入力、ループ、エラー処理の基本が実装できました。
要点をまとめると言いつつ長編になってしまいましたね。(オリジナルはもっと長い。)
次回はRustプログラミングに出てくる変数、型、関数、といった基本的な概念を深堀りしていきます。
それでは〜
Rust オススメの参考書籍
▼ 入門者向け。Pythonとの比較あり。Kindleでも読める。
手を動かして考えればよくわかる 高効率言語 Rust 書きかた・作りかた
▼ 中級者向け。オライリーならではの詳解。硬派な方向け。
参考リンク
Rustチュートリアル (日本語版 非公式) https://doc.rust-jp.rs/book-ja/