【MacでRust入門】Rust日本語版チュートリアルの要点まとめ ~ 第4回 簡単なゲームの作成

2022-08-16
Main Image

目次

こんにちは。

最近流行りの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という入出力ライブラリをスコープ内で使用することを宣言
fn main() {
    println!("~~ 数を当てよう! ~~");
  • fn main(){}println!()第2回を参照
    let mut guess = String::new();
  • letで変数を作る。これはデフォルトでimmutable(イミュータブル、不変)
  • 変数名の前にmutで変数をmutable(ミュータブル、可変)にできる
  • String::new関数でString(文字列)型の新しいインスタンスを返す
  • ::構文はnewString型の関連関数であることを示す
    • 関連関数とは、ある「型」に実装される関数のこと
  • つまり、この行では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すると型の不一致によるコンパイルエラーが出ます。

これはguessString型であるのに対し、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という改行コードが入るため、これを消す
  • 文字列のparseメソッドにより、文字列を解析して数値にする
  • 新たに宣言した変数guess:は型注釈で、この型がu32(符号なし32ビット整数)であることを示す
    • これにより、parseで変換された結果もu32になり、cmpで比較されたsecret_numberの型もu32に推論されるので、比較を行うことができるようになる
  • parseResultも返す。変換できないエラー値が入力されたらプログラムを中止させるように、expectErrを返したときの挙動を定義する

プログラムのテスト

再び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エラー処理プログラムの解説

  • expectErr時の挙動を書くとそこで中断されてしまうので、match式に変更
  • parseResultを返すので、列挙子OkErrのアームを書く
  • Okのとき、つまり数値に変換できた場合は、Okに格納された値numを返す。
  • Errのとき、格納された値が何であれ、continueを実行する
    • continueloop {}内を一旦抜けて次のループに移る
    • Errの格納値は使わないので_で表現する。

最終テスト & 完成!

お決まりのcargo runでテストします。

無事完成...! と思いきや、テスト用の答え表示を消すのを忘れていました。

println!("答えは:の行を消して完成です!

~~ 数を当てよう! ~~
入力してください: 
23
あなたの予想: 23
大きすぎます
入力してください: 
12
あなたの予想: 12
小さすぎます
入力してください: 
15
あなたの予想: 15
小さすぎます
入力してください: 
20
あなたの予想: 20
小さすぎます
入力してください: 
21
あなたの予想: 21
やったね! ~ THE END ~

試しにやってみたら、最初から結構近いとこ狙えてました。

まとめ

というわけで、今回はRustを使って「数あてゲーム」を作ってみました。

Rustにクレートを追加する方法や、変数宣言、ユーザー入力、ループ、エラー処理の基本が実装できました。

要点をまとめると言いつつ長編になってしまいましたね。(オリジナルはもっと長い。)

次回はRustプログラミングに出てくる変数、型、関数、といった基本的な概念を深堀りしていきます。

それでは〜

Rust オススメの参考書籍

▼ 入門者向け。Pythonとの比較あり。Kindleでも読める。

手を動かして考えればよくわかる 高効率言語 Rust 書きかた・作りかた

▼ 中級者向け。オライリーならではの詳解。硬派な方向け。

プログラミングRust 第2版

参考リンク

Rustチュートリアル (日本語版 非公式) https://doc.rust-jp.rs/book-ja/

ads【オススメ】未経験からプログラマーへ転職できる【GEEK JOBキャンプ】
▼ Amazonオススメ商品
ディスプレイライト デスクライト BenQ ScreenBar モニター掛け式
スマートLEDフロアライト 間接照明 Alexa/Google Home対応

Author

Penta

都内で働くITエンジニアもどき。好きなものは音楽・健康・貯金・シンプルでミニマルな暮らし。AWSクラウドやデータサイエンスを勉強中。学んだことや体験談をのんびり書いてます。TypeScript / Next.js / React / Python / AWS / インデックス投資 / 高配当株投資 More profile

Location : Tokyo, JPN

Contact : Twitter@penguinchord

Recommended Posts

Copy Right / Penguin Chord, ペンギンコード (penguinchord.com) 2022 / Twitter@penguinchord