トラック
/
Rust
Rust
/
演習
/
ロールプレイングゲーム
ロールプレイングゲーム

ロールプレイングゲーム

学習演習

はじめに

ヌル参照

これまでに他のプログラミング言語(C/C++・Python・Java・Ruby・Lispなど)を使ったことがあれば、nullやnilに出会ったことがあるでしょう。 nullやnilは、ある変数に値がないことをこれらの言語が示すための方法です。 しかしそのせいで、nullを指す変数をうっかり使ってしまうという、簡単で(しかもよくある)間違いが起こりやすくなります。 ご想像のとおり、存在しない関数を呼び出そうとしたり、存在しない値にアクセスしようとしたりすると、さまざまなバグやクラッシュにつながります。 nullの生みの親は、それを自身の「10億ドルの過ち」と呼んだほどです。

Option型

こうした問題を避けるため、Rustはヌル参照を使いません。 しかし、ある変数に値がないことを安全に示す方法は依然として必要です。 そこで登場するのがOptionです。 値を持たない変数を用意する代わりに、Rustの変数はOption列挙型を使うことができます。 この列挙型には2つのバリアントがあります。Rustにおけるnull相当のNoneと、任意の型の値TをとるSome(T)です。

次のようになります。

enum Option<T> {
    None,
    Some(T),
}

Optionは、ヌル参照が引き起こしうる問題と自分の間に挟まる安全層のようなものだと考えてみてください。しかも、ヌル参照が持つ概念的な便利さはそのまま残っています。

Optionを使う

変数にNoneを設定するのは、わりと簡単です。

let nothing: Option<u32> = None;  // Variable nothing is set to None

ただし、Option型に値を持たせたい場合、その値を直接代入することはできません。 Option型の変数と、たとえばi32型の変数は同じものではありません。 Someを使う必要があります。

let wrong_way: Option<i32> = -4; // This will not work

let right_way: Option<i32> = Some(-4); // This will work
let another_right_way = Some(-4); // Compiler infers that this is Option<i32>

同じ理由で、次のコードも動作しません。

let number = 47;
let option_number = Some(15);

let compile_error = number + option_number; // Cannot add an i32 and an Option<i32> - they are of different types

Someに含まれる値を取り出したい場合は、まずそれが存在するかどうかを確認する必要があります。

let mut some_words = Some("choose something to say"); // some_words set to something

match some_words {
    Some(str) => println!("Here, we will {}", str),
    None => println!("I've got nothing to say"),
} // Prints "Here, we will choose something to say"

some_words = None; // some_words now set to None

// exactly the same match block as above
match some_words {
    Some(str) => println!("Here, we will {}", str),
    None => println!("I've got nothing to say"),
} // Prints "I've got nothing to say"

match以外にも、Optionに含まれる値を確認したり取り出したりするための道具がRustには用意されていますが、matchにはもう慣れているはずです。

さらに、これはRustがヌル参照の代わりにOptionを使う理由を示す例だと考えてください。 要点は、Option変数がSomeなのかNoneなのかを必ず_確認_しなければならないということです(Someであれば、中に含まれる値を取り出して使ってかまいません)。 それ以外のことをすると、プログラムはコンパイルできません。コンパイラーがnullから守ってくれているのです。

説明

ロールプレイングゲームの実装に取り組んでいます。プレイヤーのキャラクターは、次のように表されます。

pub struct Player {
    health: u32,
    mana: Option<u32>,
    level: u32,
}

このゲームのプレイヤーが呪文を唱えられるようになるには、まずレベル10に到達してマナプールを解放する必要があります。それまでは、プレイヤーのマナはNoneです。

このゲームでは、復活の仕組みと呪文詠唱の仕組みという2つの機能を実装します。

reviveメソッドは、プレイヤーが本当に死んでいるか(体力が0になっているか)を確認し、死んでいる場合は、体力100の新しいプレイヤーのインスタンスを返す必要があります。 プレイヤーのレベルが10以上の場合、マナ100の状態でも復活させます。 プレイヤーのレベルが10未満の場合、マナはNoneになります。reviveメソッドは、プレイヤーのレベルをそのまま保つ必要があります。

let dead_player = Player { health: 0, mana: None, level: 2 };
dead_player.revive()
// Returns Player { health: 100, mana: None, level: 2 }

体力が1以上あるプレイヤーに対してreviveメソッドを呼び出した場合、メソッドはNoneを返す必要があります。

let alive_player = Player { health: 1, mana: Some(15), level: 11 };
alive_player.revive()
// Returns None

cast_spellメソッドは、プレイヤーへの可変参照と、その呪文に必要なマナの量を表す入力mana_costを受け取ります。戻り値は、唱えた呪文が与えるダメージ量です。呪文の詠唱に成功した場合、これは常に呪文のマナ消費量の2倍になります。

  • プレイヤーがマナプールを利用できない場合、呪文を唱えようとすると、呪文のマナ消費量の分だけ体力が減ります。返されるダメージは0でなければなりません。

    let not_a_wizard_yet = Player { health: 79, mana: None, level: 9 };
    assert_eq!(not_a_wizard_yet.cast_spell(5), 0)
    assert_eq!(not_a_wizard_yet.health, 74);
    assert_eq!(not_a_wizard_yet.mana, None);
    
  • プレイヤーにマナプールはあるもののマナが足りない場合、メソッドはマナプールに影響を与えず、代わりに0を返します。

    let low_mana_wizard = Player { health: 93, mana: Some(3), level: 12 };
    assert_eq!(low_mana_wizard.cast_spell(10), 0);
    assert_eq!(low_mana_wizard.health, 93);
    assert_eq!(low_mana_wizard.mana, Some(3));
    
  • それ以外の場合は、プレイヤーのマナプールからmana_costが差し引かれ、それに応じたダメージ量が返されます。

    let wizard = Player { health: 123, mana: Some(30), level: 18 };
    assert_eq!(wizard.cast_spell(10), 20);
    assert_eq!(wizard.health, 123);
    assert_eq!(wizard.mana, Some(20));
    

楽しんでくださいね!

GitHubで編集する リンクは新しいウィンドウまたはタブで開きます
Rust Exercism

ロールプレイングゲームを始める準備はできましたか?

Exercismに登録すれば、99個の演習、そして本物の人間によるメンタリングとともに、Rustを学んでマスターできます。すべて無料です。