如果你曾经编写过任何规模大小的程序,你可能会遇到各种错误。你在编码时产生的微小的错误会导致你的程序执行失败。程序越复杂,发生错误的概率越高!
为了修复和防止错误,有很多方法可供程序员使用。其中一个是在运行程序之前确定程序的正确性:静态类型。这种技术是设计编程语言的一部分,并且可以防止简单的错误,例如尝试使用字符串作为整数,或者比较类型不同的对象,例如 Car
和 Book
。
我的个人观点是,编程语言及其实现应该尽可能地捕获程序员所犯的错误,从而使得他们能构建更好更安全的软件。虽然静态类型使得语言更加复杂和难以学习,但它为程序员提供了一个安全的机制,我相信这是非常值得的。
Rust 语言实现了这样的静态类型系统,并提供了捕获错误的新方法,这在其他语言中运行时会导致崩溃。在这篇文章中,我将会讲解其中一些方法。
子程序由于遇到某种边界情况而无法返回结果并不罕见。想想一个例子:在数组中查找并不包含某项元素的索引,从空堆栈中弹出元素或在很小的数组上的通过索引访问元素。这些处理方式在不同语言之间差别很大。最常见的处理方式似乎是抛出异常或返回 null(或-1)。
如果你忘记检查 -1 或 null 或捕获异常,你的程序将会崩溃。然而,Rust 有一个确保在编译时捕获和处理错误的策略。
使用Rust的标准库Option来处理函数边界情况。这是一个枚举类型(如果你熟悉C语言的话,有点像一个联合体)包含有两个可能的值。这是它的声明:
enum Option<T> { None, Some(T), }
例如,Vec::pop 方法,从堆栈向量中弹出最后一个元素,当堆栈向量中至少有一个元素时返回 Some 和元素,如果堆栈向量为空,则返回 None 。
现在,获取一个 Option 的值需要一个match结构。我们不能只是声明一个值已经被返回,并且像返回一个指针的语言一样使用它。那很好!程序员被迫考虑如何处理返回 None 的情况。如果在另一种语言中类似的代码会导致运行时错误,而在 Rust 中,将会阻止程序被编译:
let mut numbers = vec![21]; let maybe_number = numbers.pop(); // Option<i32> println!("{}", maybe_number * 2); // 编译错误!
这个出现这个错误报告: error[E0369]: binary operation * cannot be applied to type std::option::Option<{integer}>
必须使用 match 来判断是否有返回结果:
let mut numbers = vec![21]; let maybe_number = numbers.pop(); if let Some(my_number) = maybe_number { println!("{}", my_number * 2); // 现在正常工作了! } // 我们还可以添加一个else代码块来处理None情况
与 Option
类型相似,还有 Result
类型。Result
就像 Option
一样,但不仅仅是 Some
和 None
,它可以是包含函数返回结果的 Ok
,或者如果出现错误,它会有包含一个错误的 Err
。这种错误也作为值返回的错误处理方式最好与 Go语言的方式进行比较。和 Rust 不一样,关键的区别在于 Go 语言中结果和 null-able 错误都会返回。这意味着忘记检查是否已经返回错误并使用结果,将导致运行时错误。
Rust 没有这个陷阱,因为像 Option
一样,必须先检查 Result
枚举的内容。因此,在发生错误时,不会误用结果。
大多数其他语言允许程序员将变量的声明和初始化分开,这样的后果是,程序员有时往往会忘记初始化这样的变量,例如在分支中。虽然也有一些语言会在编译器中会终止编译(Java)或发出警告(C, C++),但这些并不会阻止程序员通过将变量初始化为 null 或零值而使得编译器不报错、在运行时引起崩溃或更糟糕的结果以及让程序做错误的处理。
对于未初始化的变量绑定,Rust 将拒绝编译,从而防止运行时错误:
let a: &str; println!("{}", a.len()); // Compilation error! a = "Hello";
这将报错:error[E0381]: use of possibly uninitialized variable: *a
。
当然,null 初始化技巧仍然是可用的,现在需要使用 Option
类型,如前所述,需要对 None
类型进行显式处理。
如果可以,一旦声明变量就初始化你的绑定:
let a = "hello"; println!("{}", a.len());
Traits(类似于其他语言中的接口)可以描述为可执行某些操作的类型的抽象定义。例如,Rust 标准库定义了一个 fmt::Display
trait 表示它们自己为字符串的类型。Traits 让 Rust 看起来像静态类型语言,可以用作通用函数和类型的约束。
思考下面的函数:使用字符串表示将一段整数写入文件:
fn write_list(out: &mut fs::File, numbers: &[i32]) -> io::Result<()> { for num in numbers { writeln!(out, "{}", num)?; } Ok(()) }
简单吧? 但是如果是无符号整数呢?字符串呢?浮点数? 自定义类型? 我们需要为所有类型的类型创建同样一个函数!
但是 Rust 可以使用一个类型参数为我们做到这一点:
fn write_list<W, T>(mut out: W, things: &[T]) -> io::Result<()> where W: io::Write, T: fmt::Display { for thing in things { writeln!(out, "{}", thing)?; } Ok(()) }
现在,此函数接受实现Display trait的任何类型。
我也可以使用W替换了一个类型参数&mut fs::File,并且必须实现 io::Write trait。 这使得更容易编写单元测试,因为不用使用fs api和临时文件,我们只是使用一个vector,因为它实现了io::Write。 请注意,W参数是所有而不是引用(&mut W),因为W的io::Write的实现是所有可变引用的类型的io::Write实现!
如果你以前用过C/C++,那你或许理解指针。引用(References),是一种可以通过其读写一块内存而不用关心其如何分配的东西。然而Rust中的引用和C中的指针不太一样,因为引用不能为null。
在我们了解引用存在的必要性以及工作模式之前,我想先解释一下所属(ownership)的概念。
其它翻译版本 (1) 加载中Rust和C相同的是,其程序中的数据被存在heap或者stack中,而且没有垃圾回收机制。其不同点是,Rust规定了特定的内存管理方式:其子过程(subroutine)的所属者(owner)负责分配和释放子过程的内存。
这种内存管理模式防止了忘记释放内存、释放两次以及释放后再次使用内存的情况,进而消除了烦人的安全bug。这是Rust语言内置的机制,并且用这个替换了垃圾回收器,这是一种在编译期间安全管理内存的方式。
其它翻译版本 (1) 加载中 本文中的所有译文仅用于学习和交流目的,转载请务必注明文章译者、出处、和本文链接。 2KB翻译工作遵照 CC 协议,如果我们的工作有侵犯到您的权益,请及时联系我们。2KB项目(www.2kb.com,源码交易平台),提供担保交易、源码交易、虚拟商品、在家创业、在线创业、任务交易、网站设计、软件设计、网络兼职、站长交易、域名交易、链接买卖、网站交易、广告买卖、站长培训、建站美工等服务