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

角色扮演游戏

学习练习

简介

空引用

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

Option类型

为了避免这些问题,Rust 没有使用空引用。 不过,它仍然需要一种安全的方式来表示某个变量没有值。 这正是Option的用武之地。 与其让变量缺少值,Rust 变量可以使用Option枚举。 这个枚举有两个变体:None,相当于 Rust 里的 null;以及Some(T),其中 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,Rust 还提供了其他工具来检查和访问Option中包含的值,不过你现在应该已经很熟悉match了。

另外,你也可以把这看作一个示范,说明 Rust 为什么用Option而不是空引用。 重点在于,你_必须_检查Option变量究竟是Some(如果是,你就可以继续取出并使用其中包含的值),还是None。 除此之外的任何情况,你的程序都无法通过编译;编译器会保护你免受null的影响。

说明

你正在实现一个角色扮演游戏。玩家的角色由下面的结构表示:

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

这个游戏里的玩家必须先升到 10 级,才能解锁法力池,进而开始施放法术。在那之前,玩家(Player)的法力值为 None。

你要实现这个游戏里的两项功能:复活机制和施法机制。

revive方法应当先检查 Player 是否确实已经死亡(生命值已经降到 0)。如果确实如此,就返回一个生命值为 100 的新 Player 实例。 如果 Player 的等级在 10 级或以上,复活时还应带 100 法力值。 如果 Player 的等级低于 10 级,其法力值应为 None。revive方法应当保留 Player 的等级。

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

如果对一个生命值为 1 或以上的 Player 调用revive方法,那么该方法应当返回 None。

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

cast_spell方法接受 Player 的可变引用,以及一个 mana_cost 形参,用来表示这个法术消耗多少法力。它返回所施放法术造成的伤害量。如果法术成功施放,伤害量始终是法术法力消耗的两倍。

  • 如果玩家无法力池可用,尝试施放法术会使其生命值减少该法术的法力消耗。返回的伤害必须是 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));
    
  • 否则,应从 Player 的法力池中扣除 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,全部免费。