简介
面向对象语言(OOP)的枚举相当无聊。它们只是给某些常量命名的一种方式。Rust枚举更强大。它们比OOP更优雅地解决了一些设计问题。继续读下去,看看Rust枚举能做哪些OOP枚举不能做的事情。
OOP枚举
让我们看一下C#中的枚举:
enum Colour {
Red,
Green,
Blue
}
你能用它做什么?你可以将它与其他相同类型的值进行比较:
var colour1 = Colour.Red;
var colour2 = Colour.Green;
//Compare colour1 and colour2 and do something if equal
if (colour1 == colour2) {
//do something
}
或者你可以把它们转换成整数:
var colourInt = (int)colour1;
现在,我并没有完全放弃OOP语言中的枚举。它们确实提高了类型安全性,并限制了变量可能具有的值。更不用说对普通整数的可读性了。这样可以使代码更干净,并减少出现bug的机会。但是OOP枚举还没有充分发挥其潜力。
Rust枚举
当编程语言允许枚举随身携带数据时,枚举的真正威力就开始显现了。
Option
Rust的Option是一个值的容器。容器可以是空的,也可以保存一些值。它是这样定义的:
enum Option<T> {
None,
Some(T),
}
在Option中,None变量表示空状态,Some变量可以携带类型T的数据。为什么这很有用?想想如何在c#中实现类似的东西,需要是一个带有标志的类,指示值是否存在。事实上,c#中的Nullable是类似于这样定义的:
class Nullable<T> {
public bool hasValue;
public T value;
}
现在考虑一下这个类的可用性。如何从Nullable实例中获取值:
var mightBeNull = new Nullable<string>();
if (mightBeNull.hasValue) {
//do something with mightBeNull.value
}
上面代码中的 if 非常关键。如果你省略了它,那么mightBeNull.value.Length表达式将抛出一个NullReferenceException:
var mightBeNull = new Nullable<string>();
//no compiler error but still a NullReferenceException
var length = mightBeNull.value.Length;
与之形成鲜明对比的是,你不能直接访问Rust中的值:
let might_be_null: Option<String> = Option::None;
//error[E0609]: no field `value` on type `Option<String>`
let some_other_var = might_be_null.value;
相反,Rust编译器会强迫你在得到Option在里面的值之前检查mightBeNull变量是否为Some变量:
let might_be_null: Option<String> = //get an Option<String> from somewhere
if let Some(value) = might_be_null {
//do something with value
}
很酷,不是吗?在使用c#时搬起石头砸自己的脚,但是Rust阻止了你犯下这些愚蠢的错误。让我们看另一个枚举。
Result
Result是Rust中错误处理的核心。任何函数即可以返回成功,也可以返回错误。下面是Result的定义:
enum Result<T, E> {
Ok(T),
Err(E),
}
这里Result携带两类数据,如果函数成功执行,Ok(T)将携带值返回。如果失败,Err(E)将携带错误返回。
如果你用过C语言,那么一定看到过这种错误处理模式,返回值可以同时表示成功和错误。例如,atof函数在C语言中是这样声明的:
double atof(const char* str);
这个函数将尝试从str中解析一个double。如果函数成功,它将返回解析后的值。但是如果它失败,则会返回0。那如果输入的字符串是"0",这也将返回0。它意味着如果atof返回0,就无法判断这是因为输入字符串是“0”,还是解析错误返回的0。
在Rust中,同样的函数会有一个更清晰的返回类型:
fn atof(str: &str) -> Result<f64, u8> {
//...
}
这个atof成功解析一个数字时,将返回Ok(f64)。如果不能解析,将返回Err(u8)。不可能使用某个有效值作为错误代码,因为Ok和Err变量携带单独的值。
与Option一样,你不能直接获得Result的值, 唯一安全的获取值的方法是匹配atof的返回值:
match atof("123.56") {
Ok(val) => { println!("Parsed value is: {}", val)}
Err(e) => { println!("Error is {}", e)}
}
最后,Result还支持一个安全特性。在C语言中,很容易忘记检查错误代码。在Rust中,如果你没有使用Result,编译器会警告你。
何时使用枚举
内置的Option和Result类型很棒,但是如何设计自己的枚举呢?通常,如果变量表示几种状态中的任何一种时,枚举可能是一个很好的选择。考虑下面来自serde-yaml crate 的例子:
pub enum Value {
/// Represents a YAML null value.
Null,
/// Represents a YAML boolean.
Bool(bool),
/// Represents a YAML numerical value, whether integer or floating point.
Number(Number),
/// Represents a YAML string.
String(String),
/// Represents a YAML sequence in which the elements are
/// `serde_yaml::Value`.
Sequence(Sequence),
/// Represents a YAML mapping in which the keys and values are both
/// `serde_yaml::Value`.
Mapping(Mapping),
}
这里的Value 枚举表示一个yaml文件中的值,值可以是null,也可以是bool或数字等等。因此,这是enum最佳的选择。
本文翻译自:
https://hashrust.com/blog/why-rust-enums-are-so-cool/