轨道
/
Cairo
Cairo
/
练习
/
角色扮演游戏
角色扮演游戏

角色扮演游戏

学习练习

简介

空引用

如果你用过其他编程语言(C/C++、Python、Java、Ruby、Lisp 等),很可能见过 null 或 nil。 这些语言用 null 或 nil 来表示某个变量没有值。 但这也很容易(而且经常)让人不小心用了 null 变量。 可以想象,试图调用一个不存在的函数,或者访问一个不存在的值,会引发各种各样的 bug 和崩溃。 null 的创造者甚至把它称为自己的“十亿美元错误”。

Option 类型

为了避免这些问题,Cairo 的变量可以用 Option 枚举安全地表示没有值的情况。 这个枚举有两个变体: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,全部免费。