これまでに別のプログラミング言語(C/C++、Python、Java、Ruby、Lispなど)を使ったことがあれば、nullやnilに出会ったことがあるでしょう。
これらの言語では、nullやnilを使って、特定の変数に値がないことを示します。
しかしそのせいで、nullの変数をうっかり使ってしまうという、簡単で(しかもよくある)間違いを犯しやすくなります。
ご想像のとおり、存在しない関数を呼び出そうとしたり、存在しない値にアクセスしようとしたりすると、さまざまなバグやクラッシュにつながります。
nullの生みの親は、それを自ら'10億ドルの過ち'と呼んだほどです。
Option型こうした問題を避けるために、Cairoの変数ではOption列挙型を使って、値がないことを安全に表せます。
この列挙型には2つのバリアントがあります。Cairoにおけるnull相当のNoneと、任意の型の値TをとるSome(T)です。
次のようになっています:
enum Option<T> {
None,
Some(T),
}
Optionは、null参照が引き起こしかねない問題から守ってくれる安全の層のようなものだと考えるとよいでしょう。それでいて、null参照の概念としての有用さはそのまま残っています。
Optionの使い方変数にNoneを設定するのは、わりと簡単です:
let nothing: Option<u32> = Option::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> = Option::Some(-4); // This will work
let another_right_way = Option::Some(-4); // Compiler infers that this is Option<i32>
同じ理由で、次のコードも動きません:
let number = 47;
let option_number = Option::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 = Option::Some("choose something to say"); // some_words set to something
match some_words {
Option::Some(str) => println!("Here, we will {}", str),
Option::None => println!("I've got nothing to say"),
} // Prints, "Here, we will choose something to say"
some_words = Option::None; // some_words now set to None
// Exactly the same match block as above
match some_words {
Option::Some(str) => println!("Here, we will {}", str),
Option::None => println!("I've got nothing to say"),
} // Prints, "I've got nothing to say"
Cairoには、Optionに含まれる値を確認したり取り出したりするために、match以外にも使えるツールがあります。ただし、matchにはもう慣れているはずです。
これは、Cairoがnull参照ではなくOptionを使う理由を示す例だと考えてください。
要点はこうです。Optionの変数がSomeなのか(その場合は、中に含まれる値を取り出して使ってかまいません)、それともNoneなのかを、必ず確認しなければなりません。
そうしないと、プログラムはコンパイルされません。コンパイラーがnullから守ってくれます。
ロールプレイングゲームを実装しています。 プレイヤーのキャラクターは次のように表されます。
pub struct Player {
health: u32,
mana: Option<u32>,
level: u32,
}
このゲームのプレイヤーは、レベル10に達するまではマナプールを使えません。マナプールは、呪文を唱えるために使います。
それまでは、PlayerのマナはOption::Noneです。
このゲームでは、2つの機能に取り組みます。復活の仕組みと、呪文詠唱の仕組みです。
reviveメソッドは、Playerが本当に死んでいるかどうか(体力が0になっているか)を確認し、死んでいれば、体力が100の新しいPlayerインスタンスを返す必要があります。
reviveメソッドは、Playerのレベルを保持する必要があります。
Playerのレベルが10以上であれば、マナ100で復活させます。
Playerのレベルが10未満であれば、マナはOption::Noneになります。
let dead_player = Player { health: 0, mana: Option::None, level: 2 };
dead_player.revive()
// Returns Player { health: 100, mana: Option::None, level: 2 }
体力が1以上のPlayerに対してreviveメソッドを呼び出した場合、メソッドはOption::Noneを返す必要があります。
let alive_player = Player { health: 1, mana: Option::Some(15), level: 11 };
alive_player.revive()
// Returns Option::None
cast_spellメソッドは、Playerへの可変参照と、呪文にかかるマナの量を示す仮引数mana_costを受け取ります。
戻り値は、唱えた呪文が与えるダメージ量です。
プレイヤーがマナプールを使えない場合、呪文を唱えようとすると、呪文のマナコストの分だけ体力が減ります。 返されるダメージは0でなければなりません。
let not_a_wizard_yet = Player { health: 79, mana: Option::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, Option::None);
プレイヤーがマナプールを持っていてもマナが足りない場合、メソッドはマナプールに影響を与えません。 返されるダメージは0でなければなりません。
let low_mana_wizard = Player { health: 93, mana: Option::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, Option::Some(3));
それ以外の場合、呪文は正常に唱えられます。
mana_costはPlayerのマナプールから差し引かれます。
返されるダメージは、呪文のマナコストの2倍でなければなりません。
let wizard = Player { health: 123, mana: Option::Some(30), level: 18 };
assert_eq!(wizard.cast_spell(10), 20);
assert_eq!(wizard.health, 123);
assert_eq!(wizard.mana, Option::Some(20));
楽しんでください!