學習軌道
/
Cairo
Cairo
/
練習
/
角色扮演遊戲
角色扮演遊戲

角色扮演遊戲

學習練習

簡介

空參照

如果你用過其他程式語言(C/C++、Python、Java、Ruby、Lisp 等),很可能已經遇過null或nil。 這些語言用null或nil來表示某個變數沒有值。 然而,這也讓不小心用到null變數變成一個很容易(而且很常發生)的錯誤。 可以想見,試圖呼叫一個不存在的函式,或存取一個不存在的值,都可能導致各種 bug 和程式崩潰。 null的創造者甚至稱它是他的「十億美元的錯誤」。

Option型別

為了避免這些問題,Cairo 的變數可以使用Option這個 enum,安全地表示沒有值的狀態。 這個 enum 有兩個變體:None(相當於 Cairo 的 null),以及Some(T),其中T是任意型別的值。

它長得像這樣:

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

你可以把Option想成一道安全防線,隔開你與空參照可能造成的問題,同時保留它在概念上的實用性。

使用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"

除了match之外,Cairo 還有其他工具可以檢查並存取Option裡的值,不過match現在對你來說應該已經很熟悉了。

就把這當作一個示範,說明為什麼 Cairo 用Option而不用空參照。 重點是,你_必須_檢查Option變數是Some(這種情況下你就可以放心取出並使用裡面包含的值),還是None。 否則你的程式無法編譯;編譯器會保護你不受null的影響。

說明

你正在實作一款角色扮演遊戲。 玩家的角色由以下內容表示:

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

在這款遊戲中,玩家必須達到等級 10 才能解鎖魔力池,並用魔力池來施展咒語。 在那之前,玩家的魔力是 Option::None。

你正在處理這款遊戲的兩項功能:復活機制和咒語施展機制。

revive 方法應該檢查玩家是否確實已經死亡(生命值已降到 0),若是如此,該方法應該回傳一個生命值為 100 的新玩家實例。 revive 方法應該保留玩家的等級。 如果玩家的等級為 10 或以上,復活後應該擁有 100 點魔力。 如果玩家的等級低於 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 或以上的玩家身上呼叫 revive 方法,該方法應該回傳 Option::None。

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

cast_spell 方法接受一個指向玩家的可變參考,以及一個 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 應該從玩家的魔力池中扣除。 回傳的傷害必須是咒語魔力消耗的兩倍。

    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));
    

玩得開心!

透過 GitHub 編輯 連結會在新視窗或分頁中開啟
Cairo Exercism

準備好開始 角色扮演遊戲 了嗎?

註冊 Exercism,透過 25 個概念68 個練習 和真人引導來學習並精通 Cairo,全部免費。