专栏
天翼云开发者社区

Rust入门(三) —— 错误处理(Option && Result)

2024-03-14 09:23:58 28阅读

rust 的枚举

在讲述Result或Option之前,我们有必要先了解一下rust的枚举;因为Result和Option都是枚举类型。

概念上rust的枚举与C语言的枚举是一致:定义一个类型,可以穷举所有可能的值。比如,定义一个IP地址类型:

enum IpAddrKind {
        IPV4,
        IPV6,
}

 

IP地址要么是IPV4, 要么是IPV6。 对于rust  ip :IpAddrKind , ip 的值也是IPV4 或IPV6之一。

枚举类型在使用时,通常也需要和数据关联。在C语言中,当我们需要将枚举类型和和数据关联时,通常是额外定义一个结构体,将类型标识和数据封装在一起:

struct IpAddr {
    enum IpAddrKind kind;
    char data[16];
}

如此,当我们获得一个IpAddr实例时,可以根据其kind值先确认其地址类型;然后再根据其地址类型,从data中解析出具体数地址数据。

注意:这里data使用了共享存储的方法;原因是这里将IPv6 和IPv4的数据都定义为了字符数组,并且IPv6的长度可以覆盖IPv4。假如,重新定义IP的数据类型为 IPv4为一个U32整形,IPv6为一个字符数组,那么IpAddr的结构体可能会设计为这样:

 

 

struct IpAddr {
    enum IpAddrKind kind;
    union {
         unsigned int ipv4;
         char ipv6[16];
    } data;
}

rust 相对于C的枚举,对枚举类型做了大幅优化,允许我们直接将关联数据类型直接嵌入到枚举的变体中。比如,rust定义的IpAddr 可能是这样:

enum IpAddr {
    IPV4 (String),
    IPV6 (String),
}

使用:
let loopback = IpAddr::IPV4("127.0.0.1".to_string()); // 定义了一个ipv4地址,其值“127.0.0.1”

简单起见,可以理解为rust 的枚举,融合了C枚举和联合体,实现了数据类型和关联数据的定义和绑定。

一个稍微复杂一点的枚举类型:

enum Message {
    Quit, // 无绑定数据
    Move {x: i32, y:i32}, // 绑定了一个匿名结构体 struct {x:i32, y:i32}
    Write(string), // 绑定了一个字符串数据
    ChangeColor(i32, i32, i32), // 绑定了一个元祖,由三个i32 组成
}

 

枚举方法

在rust 里面您还可以为枚举实现方法。这就像在面向对象编程时,为class (java)或结构体(rust, golang)绑定方法一样。和rust 的struct 实现方法一样,用impl关键字为指定的枚举类型添加方法:

impl Message {
    fn call(&self) {
          // do_something()
    }
}
// example
let msg = Message::Write(String::from("notice: processing going down"));
msg.call();

枚举与match(控制流运输符号)

rust中有一个强大的控制流运算符:match,它允许将一个值与一些列模式进行匹配,并根据匹配的模式执行相关代码(关于rust的模式匹配,本文不深入,读者自行补充);而其中枚举是模式匹配中最为常用的:

impl Message {
   fn call(&self) {
      // do_something()
   }

    fn to_string(&self) -> String {
         match self {
                 Message::Quit => String::from("quit"),
                 Message::Move { x, y } => format!("Move: <{},{}>", x, y),
                 Message::Write(s) => format!("String: {}", s),
                 Message::ChangeColor(x, y, z) => format!("ChangeColor: ({},{},{})", x, y, z),
          }
     }
}

上述示例中,为Message枚举实现的to_string方法返回某个具体示例的字符串值;其中就使用了match模式匹配。match 和C中的switch关键字比较类似,但比switch更为强大。

与其他语言不一样,rust在匹配枚举时,要求务必穷尽所有可能(当然,可以用通配的方法忽略不在意的变体)。

简单控制流 if let

前面已经提到,match 在遍历枚举时,要求务必穷尽所有可能。但有时候,我们确实只关注某一种匹配的情况,而忽略其他情况。当然,这种场景可以用match 的 '_' 通配的方式,来忽略其它不关心的变体,只是多写了了几行废代码而已。

幸运的是,rust 提供了一个if let 语法,可以简化这种场景的表达:

impl Message {
    fn on_quit(&self) {
             if let Message::Quit = self {
                    std::process::exit(0);
               }
     }
}

 Option

在其他语言中,大部分都支持空值(Null,nil):本身是一个值,却表示‘没有值’。在支持空值的语言中,一个值可能处于两种状态:空值或非空值。

比如 c语言中,定义一个变量 char* ptr ,那么默认情况下ptr 就是空值(null)。

空值的问题在于,当你尝试像使用非空值那样去使用空值的时候,就会触发某种程度的错误(通常可能导致程序崩溃,比如访问空指针)。另一方面,因为这种存在双状态的值被广泛应用于程序中时,你很难避免引发类似问题。

但是,不管怎样,空值本身所尝试表达的概念任然具有意义:它代表了因某种原因而无法获取、或者变为无效的值。

rust语言中没有空值,但却提供了一个拥有类似概念的枚举:`Option<T>`。我们可以用它来表示任意一个可能存在空值的值。

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

在对待空值上,rust和其他支持空值的语言上有所差异。一般支持空值的语言,对于数据是否为空值,由程序员自己保证,语言上并不限制。但在rust 中`Option<T>` 包裹的值,需要特别处理。例如:

let x = 5;
let y: Option<i32> = Some(8);
let sum = x+y;
println!(“{}”, sum);

上面代码看上去没有问题,但实际上却无法通过编译。编译器指出,i32 和`Option<T>`,不支持相加行为,因为他们是不同类型。

rust中,对于一个 给定类型的变量(基础类型或者结构体),例子中的x,编译器保证它是有效的;但相反,一个`Option<T>`的变量,rust要求我们必须确认它是具有值的情况下,才可以使用。

换句话说,`Option<T> `中可能存在T,也可能是空值;我们必须确认它有值,并且将其转换为T才能够使用它。经过这个过程,就帮助我们甄别了值是否真实存在,从而避免了“使用了一个值,但它却是空值”的陷阱!

let x = 5;
let y: Option<i32> = Some(8);
let sum: i32 = 0;

match y {
     Some(t) => {
              sum = x+t; // 确保有值,并使用该值
     },
     - => (),
}
println!("sum={}", sum)

使用模式匹配来处理返回值,调用者必须处理结果为None的情况。这往往是一个好的编程习惯,可以减少潜在的bug。Option 包含一些方法来简化模式匹配,毕竟过多的match会使代码变得臃肿,这也是滋生bug的原因之一。

unwrap

impl<T> Option<T> {
    fn unwrap(self) -> T {
             match self {
                    Option::Some(val) => val,
                    Option::None => {
                                panic!("called `Option::unwrap()` on a `None` value"),
                           }
            }
}

unwrap 是Option的一个工具函数。当遇到None值时会panic。

通常panic 并不是一个良好的工程实践,不过有些时候却非常有用:

  1. 在例子和简单快速的编码中 有的时候你只是需要一个小例子或者一个简单的小程序,输入输出已经确定,你根本没必要花太多时间考虑错误处理,使用unwrap变得非常合适。
  2. 当程序遇到了致命的bug,panic是最优选择

 

map

pub fn map<U, F>(self, f: F) -> Option<U>
where
F: FnOnce(T) -> U,
{
          match self {
             Some(x) => Some(f(x)),
              None => None,
         }
}

map 是Option的一个工具函数:对一个Option类型的值,如果其值非空,那么通过一个映射函数,映射为一个新类型;否则返回为None。

假如我们要在一个字符串中找到文件的扩展名,比如foo.rs中的rs, 我们可以这样:

fn extension_explicit(file_name: &str) -> Option<&str> {
    match find(file_name, '.') {
        None => None,
        Some(i) => Some(&file_name[i+1..]),
    }
}

fn main() {
    match extension_explicit("foo.rs") {
        None => println!("no extension"),
        Some(ext) =>  assert_eq!(ext, "rs"),
    }
}

 

// 使用map去掉match

fn extension(file_name: &str) -> Option<&str> {
    find(file_name, '.').map(|i| &file_name[i+1..])
}

注意上面 “|i| &file_name[i+1..]” 的写法是一个闭包函数。关于rust的闭包函数,请读者自行了解学习。

 

 unwrap_or

fn unwrap_or<T>(option: Option<T>, default: T) -> T {
    match option {
        None => default,
        Some(value) => value,
    }
}

unwrap_or 提供了一个默认值default,当值为None时返该默认值。

and_then

fn and_then<F, T, A>(option: Option<T>, f: F) -> Option<A>
        where F: FnOnce(T) -> Option<A> {
    match option {
        None => None,
        Some(value) => f(value),
    }
}

看起来and_then和map差不多, 当Option 非空时调用f函数,对传输数据进行处理,否则返回None。

与map的差异一方面是语义上的差异,map侧重于映射,而and_then表达丰富的后续处理;另一方面,在返回类型上and_then不限制,而map 保持输入和输出一致。可以认为,map 是and_then的一种特例。

 

Result

编程实践中,对于程序中的错误,通常分为两类: 不可恢复的错误 和可以恢复的错误。对于可恢复的错误,比如文件未找到,一般是报告给用户,让其重试;而不可恢复错误,比如数组访问越界了,则会引起程序进入异常状态。

在有异常处理的编程语言中,通常并不详细区分这两种错误,而是统一交由异常处理机制处理。rust没有异常处理机制,通常对于不可恢复错误,会采用panic结束程序;而对于可恢复错误,则更倾向通过显示的机制进行错误捕获和传递。对于可恢复错误,rust采用Result类型来描述。

Result 是一个枚举,有Ok 和 Err两个变体:

enum Result<T, E> {
     Ok(T),
     Err(E),
 }

其中,T和E均为泛型类型。

有了前两节的知识铺垫,理解这个枚举并不困难,可以描述为:

1. 一个可能处理失败的过程,其结果用Result来表示;

2. 如果处理成功,那么返回Result的Ok 变体,并且携带返回数据;

3. 如果处理失败,那么返回Result的Err变体,并且携带错误信息。

 

示例:

let ret = File::open("test.txt");
let f = match ret {
    Ok(file) => file,
    Err(err) = {
        panic!("fail to open test.txt, error: {:?}", err );
    }
}

// f.XXXX()

 

工具函数

Result 和Option 非常相似,甚至可以理解为,Result是Option更为通用的版本,在异常的时候,返回了更多的错误信息;而Option 只是Result Err 为空的特例。

type Option<T> = Result<T, ()>;

和Option一样,Result 也提供了 unwrap,unwrap_or, map,and_then 等系列工具方法。比如 unwarp实现:

impl<T, E: ::std::fmt::Debug> Result<T, E> {
    fn unwrap(self) -> T {
        match self {
            Result::Ok(val) => val,
            Result::Err(err) =>
              panic!("called `Result::unwrap()` on an `Err` value: {:?}", err),
        }
    }
}

没错和Option一样,不同的是,Result包括了错误的详细描述,这对于调试人员来说,这是友好的。

除此之外,相比于Option, Result也有一些特有的针对错误类型的方法map_err和or_else等。

其中:

map_err 处理一个Result,当前是某种错误类型时,通过传入的op方法,转换其错误类型; 如果是非错误类型,则不受影响。

 pub fn map_err<F, O: FnOnce(E) -> F>(self, op: O) -> Result<T, F> {
        match self {
            Ok(t) => Ok(t),
            Err(e) => Err(op(e)),
        }
    }

 

or_else 处理一个Result并返回一个Result,当前是某种错误时,通过传入的op方法,处理错误;如果是非错误类型,则不受影响。

 pub fn or_else<F, O: FnOnce(E) -> Result<T, F>>(self, op: O) -> Result<T, F> {
        match self {
            Ok(t) => Ok(t),
            Err(e) => op(e),
        }
    }

or_else 通常用于链式调用的流程控制。例如:

fn auto_fix(e: u32) -> Result<u32, u32> { Ok(e * e) }
fn keep(e: u32) -> Result<u32, u32> { Err(e) }

// 用例1和2,由于 原始Result值非 错误,所以不受or_else影响
assert_eq!(Ok(2).or_else(auto_fix).or_else(auto_fix), Ok(2));
assert_eq!(Ok(2).or_else(keep).or_else(auto_fix), Ok(2));

// 用例3, Err类型的Result 经过auto_fix 后已经转为Ok(9);经过第二个or_else 不受影响
assert_eq!(Err(3).or_else(auto_fix).or_else(keep), Ok(9));

// 用例4, Err类型的Result 连续调用or_else 的keep,由于keep实现保留err返回为Err(3); 注意实际上Result实例时变化了的
assert_eq!(Err(3).or_else(keep).or_else(keep), Err(3));

 

 

Result别名

在Rust的标准库中会经常出现Result的别名,用来默认确认其中Ok(T)或者Err(E)的类型,这能减少重复编码。比如io::Result

use std::num::ParseIntError;
use std::result;

type Result<T> = result::Result<T, ParseIntError>;

fn double_number(number_str: &str) -> Result<i32> {
    unimplemented!();
}

 

组合Option和Result

Option的方法ok_or:

fn ok_or<T, E>(option: Option<T>, err: E) -> Result<T, E> {
    match option {
        Some(val) => Ok(val),
        None => Err(err),
    }
}

可以在值为None的时候返回一个Result::Err(E),值为Some(T)的时候返回Ok(T),利用它我们可以组合Option和Result:

use std::env;

fn double_arg(mut argv: env::Args) -> Result<i32, String> {
    argv.nth(1)
        .ok_or("Please give at least one argument".to_owned())
        .and_then(|arg| arg.parse::<i32>().map_err(|err| err.to_string()))
        .map(|n| 2 * n)
}

fn main() {
    match double_arg(env::args()) {
        Ok(n) => println!("{}", n),
        Err(err) => println!("Error: {}", err),
    }
}

double_arg将传入的命令行参数转化为数字并翻倍,ok_or将Option类型转换成Result,map_err当值为Err(E)时调用作为参数的函数处理错误。

 

try! 宏

macro_rules! try {
    ($e:expr) => (match $e {
        Ok(val) => val,
        Err(err) => return Err(::std::convert::From::from(err)),
    });
}

 

try!事实上就是match Result的封装,当遇到Err(E)时会提早返回, ::std::convert::From::from(err)可以将不同的错误类型返回成最终需要的错误类型,因为所有的错误都能通过From转化成`Box<Error>`,所以下面的代码是正确的:

use std::error::Error;
use std::fs::File;
use std::io::Read;
use std::path::Path;

fn file_double<P: AsRef<Path>>(file_path: P) -> Result<i32, Box<Error>> {
    let mut file = try!(File::open(file_path));
    let mut contents = String::new();
    try!(file.read_to_string(&mut contents));
    let n = try!(contents.trim().parse::<i32>());
    Ok(2 * n)
}

 

在新版本中 try!宏被进一步简化为 一个?:

fn file_double<P: AsRef<Path>>(file_path: P) -> Result<i32, Error> {
    let mut file = File::open(file_path)?; // 注意这里的?, 和try功能一致,遇到错误,提前返回
    let mut contents = String::new();
    try!(file.read_to_string(&mut contents));
    let n = try!(contents.trim().parse::<i32>());
    Ok(2 * n)
}

 

总结

rust的Option 和Result 为返回、检测、处理错误,提供了系统支撑,这一点和golang的errors 设计比价类似。

熟练使用Option和Result是编写 Rust 代码的关键,Rust 优雅的错误处理离不开值返回的错误形式,编写代码时提供给使用者详细的错误信息是值得推崇的。

  • 2
  • 2
  • 2
2 评论
0/1000
王****际

写的真好

2024-03-18 08:29:38
0
回复
谭****勇 回复 王****际

感谢鼓励

2024-03-26 08:50:10
0
回复
评论(2) 发表评论
王****际

写的真好

谭****勇 回复 王****际

感谢鼓励

谭****勇

谭****勇

13 篇文章 1 粉丝
关注

Rust入门(三) —— 错误处理(Option && Result)

2024-03-14 09:23:58 28阅读

rust 的枚举

在讲述Result或Option之前,我们有必要先了解一下rust的枚举;因为Result和Option都是枚举类型。

概念上rust的枚举与C语言的枚举是一致:定义一个类型,可以穷举所有可能的值。比如,定义一个IP地址类型:

enum IpAddrKind {
        IPV4,
        IPV6,
}

 

IP地址要么是IPV4, 要么是IPV6。 对于rust  ip :IpAddrKind , ip 的值也是IPV4 或IPV6之一。

枚举类型在使用时,通常也需要和数据关联。在C语言中,当我们需要将枚举类型和和数据关联时,通常是额外定义一个结构体,将类型标识和数据封装在一起:

struct IpAddr {
    enum IpAddrKind kind;
    char data[16];
}

如此,当我们获得一个IpAddr实例时,可以根据其kind值先确认其地址类型;然后再根据其地址类型,从data中解析出具体数地址数据。

注意:这里data使用了共享存储的方法;原因是这里将IPv6 和IPv4的数据都定义为了字符数组,并且IPv6的长度可以覆盖IPv4。假如,重新定义IP的数据类型为 IPv4为一个U32整形,IPv6为一个字符数组,那么IpAddr的结构体可能会设计为这样:

 

 

struct IpAddr {
    enum IpAddrKind kind;
    union {
         unsigned int ipv4;
         char ipv6[16];
    } data;
}

rust 相对于C的枚举,对枚举类型做了大幅优化,允许我们直接将关联数据类型直接嵌入到枚举的变体中。比如,rust定义的IpAddr 可能是这样:

enum IpAddr {
    IPV4 (String),
    IPV6 (String),
}

使用:
let loopback = IpAddr::IPV4("127.0.0.1".to_string()); // 定义了一个ipv4地址,其值“127.0.0.1”

简单起见,可以理解为rust 的枚举,融合了C枚举和联合体,实现了数据类型和关联数据的定义和绑定。

一个稍微复杂一点的枚举类型:

enum Message {
    Quit, // 无绑定数据
    Move {x: i32, y:i32}, // 绑定了一个匿名结构体 struct {x:i32, y:i32}
    Write(string), // 绑定了一个字符串数据
    ChangeColor(i32, i32, i32), // 绑定了一个元祖,由三个i32 组成
}

 

枚举方法

在rust 里面您还可以为枚举实现方法。这就像在面向对象编程时,为class (java)或结构体(rust, golang)绑定方法一样。和rust 的struct 实现方法一样,用impl关键字为指定的枚举类型添加方法:

impl Message {
    fn call(&self) {
          // do_something()
    }
}
// example
let msg = Message::Write(String::from("notice: processing going down"));
msg.call();

枚举与match(控制流运输符号)

rust中有一个强大的控制流运算符:match,它允许将一个值与一些列模式进行匹配,并根据匹配的模式执行相关代码(关于rust的模式匹配,本文不深入,读者自行补充);而其中枚举是模式匹配中最为常用的:

impl Message {
   fn call(&self) {
      // do_something()
   }

    fn to_string(&self) -> String {
         match self {
                 Message::Quit => String::from("quit"),
                 Message::Move { x, y } => format!("Move: <{},{}>", x, y),
                 Message::Write(s) => format!("String: {}", s),
                 Message::ChangeColor(x, y, z) => format!("ChangeColor: ({},{},{})", x, y, z),
          }
     }
}

上述示例中,为Message枚举实现的to_string方法返回某个具体示例的字符串值;其中就使用了match模式匹配。match 和C中的switch关键字比较类似,但比switch更为强大。

与其他语言不一样,rust在匹配枚举时,要求务必穷尽所有可能(当然,可以用通配的方法忽略不在意的变体)。

简单控制流 if let

前面已经提到,match 在遍历枚举时,要求务必穷尽所有可能。但有时候,我们确实只关注某一种匹配的情况,而忽略其他情况。当然,这种场景可以用match 的 '_' 通配的方式,来忽略其它不关心的变体,只是多写了了几行废代码而已。

幸运的是,rust 提供了一个if let 语法,可以简化这种场景的表达:

impl Message {
    fn on_quit(&self) {
             if let Message::Quit = self {
                    std::process::exit(0);
               }
     }
}

 Option

在其他语言中,大部分都支持空值(Null,nil):本身是一个值,却表示‘没有值’。在支持空值的语言中,一个值可能处于两种状态:空值或非空值。

比如 c语言中,定义一个变量 char* ptr ,那么默认情况下ptr 就是空值(null)。

空值的问题在于,当你尝试像使用非空值那样去使用空值的时候,就会触发某种程度的错误(通常可能导致程序崩溃,比如访问空指针)。另一方面,因为这种存在双状态的值被广泛应用于程序中时,你很难避免引发类似问题。

但是,不管怎样,空值本身所尝试表达的概念任然具有意义:它代表了因某种原因而无法获取、或者变为无效的值。

rust语言中没有空值,但却提供了一个拥有类似概念的枚举:`Option<T>`。我们可以用它来表示任意一个可能存在空值的值。

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

在对待空值上,rust和其他支持空值的语言上有所差异。一般支持空值的语言,对于数据是否为空值,由程序员自己保证,语言上并不限制。但在rust 中`Option<T>` 包裹的值,需要特别处理。例如:

let x = 5;
let y: Option<i32> = Some(8);
let sum = x+y;
println!(“{}”, sum);

上面代码看上去没有问题,但实际上却无法通过编译。编译器指出,i32 和`Option<T>`,不支持相加行为,因为他们是不同类型。

rust中,对于一个 给定类型的变量(基础类型或者结构体),例子中的x,编译器保证它是有效的;但相反,一个`Option<T>`的变量,rust要求我们必须确认它是具有值的情况下,才可以使用。

换句话说,`Option<T> `中可能存在T,也可能是空值;我们必须确认它有值,并且将其转换为T才能够使用它。经过这个过程,就帮助我们甄别了值是否真实存在,从而避免了“使用了一个值,但它却是空值”的陷阱!

let x = 5;
let y: Option<i32> = Some(8);
let sum: i32 = 0;

match y {
     Some(t) => {
              sum = x+t; // 确保有值,并使用该值
     },
     - => (),
}
println!("sum={}", sum)

使用模式匹配来处理返回值,调用者必须处理结果为None的情况。这往往是一个好的编程习惯,可以减少潜在的bug。Option 包含一些方法来简化模式匹配,毕竟过多的match会使代码变得臃肿,这也是滋生bug的原因之一。

unwrap

impl<T> Option<T> {
    fn unwrap(self) -> T {
             match self {
                    Option::Some(val) => val,
                    Option::None => {
                                panic!("called `Option::unwrap()` on a `None` value"),
                           }
            }
}

unwrap 是Option的一个工具函数。当遇到None值时会panic。

通常panic 并不是一个良好的工程实践,不过有些时候却非常有用:

  1. 在例子和简单快速的编码中 有的时候你只是需要一个小例子或者一个简单的小程序,输入输出已经确定,你根本没必要花太多时间考虑错误处理,使用unwrap变得非常合适。
  2. 当程序遇到了致命的bug,panic是最优选择

 

map

pub fn map<U, F>(self, f: F) -> Option<U>
where
F: FnOnce(T) -> U,
{
          match self {
             Some(x) => Some(f(x)),
              None => None,
         }
}

map 是Option的一个工具函数:对一个Option类型的值,如果其值非空,那么通过一个映射函数,映射为一个新类型;否则返回为None。

假如我们要在一个字符串中找到文件的扩展名,比如foo.rs中的rs, 我们可以这样:

fn extension_explicit(file_name: &str) -> Option<&str> {
    match find(file_name, '.') {
        None => None,
        Some(i) => Some(&file_name[i+1..]),
    }
}

fn main() {
    match extension_explicit("foo.rs") {
        None => println!("no extension"),
        Some(ext) =>  assert_eq!(ext, "rs"),
    }
}

 

// 使用map去掉match

fn extension(file_name: &str) -> Option<&str> {
    find(file_name, '.').map(|i| &file_name[i+1..])
}

注意上面 “|i| &file_name[i+1..]” 的写法是一个闭包函数。关于rust的闭包函数,请读者自行了解学习。

 

 unwrap_or

fn unwrap_or<T>(option: Option<T>, default: T) -> T {
    match option {
        None => default,
        Some(value) => value,
    }
}

unwrap_or 提供了一个默认值default,当值为None时返该默认值。

and_then

fn and_then<F, T, A>(option: Option<T>, f: F) -> Option<A>
        where F: FnOnce(T) -> Option<A> {
    match option {
        None => None,
        Some(value) => f(value),
    }
}

看起来and_then和map差不多, 当Option 非空时调用f函数,对传输数据进行处理,否则返回None。

与map的差异一方面是语义上的差异,map侧重于映射,而and_then表达丰富的后续处理;另一方面,在返回类型上and_then不限制,而map 保持输入和输出一致。可以认为,map 是and_then的一种特例。

 

Result

编程实践中,对于程序中的错误,通常分为两类: 不可恢复的错误 和可以恢复的错误。对于可恢复的错误,比如文件未找到,一般是报告给用户,让其重试;而不可恢复错误,比如数组访问越界了,则会引起程序进入异常状态。

在有异常处理的编程语言中,通常并不详细区分这两种错误,而是统一交由异常处理机制处理。rust没有异常处理机制,通常对于不可恢复错误,会采用panic结束程序;而对于可恢复错误,则更倾向通过显示的机制进行错误捕获和传递。对于可恢复错误,rust采用Result类型来描述。

Result 是一个枚举,有Ok 和 Err两个变体:

enum Result<T, E> {
     Ok(T),
     Err(E),
 }

其中,T和E均为泛型类型。

有了前两节的知识铺垫,理解这个枚举并不困难,可以描述为:

1. 一个可能处理失败的过程,其结果用Result来表示;

2. 如果处理成功,那么返回Result的Ok 变体,并且携带返回数据;

3. 如果处理失败,那么返回Result的Err变体,并且携带错误信息。

 

示例:

let ret = File::open("test.txt");
let f = match ret {
    Ok(file) => file,
    Err(err) = {
        panic!("fail to open test.txt, error: {:?}", err );
    }
}

// f.XXXX()

 

工具函数

Result 和Option 非常相似,甚至可以理解为,Result是Option更为通用的版本,在异常的时候,返回了更多的错误信息;而Option 只是Result Err 为空的特例。

type Option<T> = Result<T, ()>;

和Option一样,Result 也提供了 unwrap,unwrap_or, map,and_then 等系列工具方法。比如 unwarp实现:

impl<T, E: ::std::fmt::Debug> Result<T, E> {
    fn unwrap(self) -> T {
        match self {
            Result::Ok(val) => val,
            Result::Err(err) =>
              panic!("called `Result::unwrap()` on an `Err` value: {:?}", err),
        }
    }
}

没错和Option一样,不同的是,Result包括了错误的详细描述,这对于调试人员来说,这是友好的。

除此之外,相比于Option, Result也有一些特有的针对错误类型的方法map_err和or_else等。

其中:

map_err 处理一个Result,当前是某种错误类型时,通过传入的op方法,转换其错误类型; 如果是非错误类型,则不受影响。

 pub fn map_err<F, O: FnOnce(E) -> F>(self, op: O) -> Result<T, F> {
        match self {
            Ok(t) => Ok(t),
            Err(e) => Err(op(e)),
        }
    }

 

or_else 处理一个Result并返回一个Result,当前是某种错误时,通过传入的op方法,处理错误;如果是非错误类型,则不受影响。

 pub fn or_else<F, O: FnOnce(E) -> Result<T, F>>(self, op: O) -> Result<T, F> {
        match self {
            Ok(t) => Ok(t),
            Err(e) => op(e),
        }
    }

or_else 通常用于链式调用的流程控制。例如:

fn auto_fix(e: u32) -> Result<u32, u32> { Ok(e * e) }
fn keep(e: u32) -> Result<u32, u32> { Err(e) }

// 用例1和2,由于 原始Result值非 错误,所以不受or_else影响
assert_eq!(Ok(2).or_else(auto_fix).or_else(auto_fix), Ok(2));
assert_eq!(Ok(2).or_else(keep).or_else(auto_fix), Ok(2));

// 用例3, Err类型的Result 经过auto_fix 后已经转为Ok(9);经过第二个or_else 不受影响
assert_eq!(Err(3).or_else(auto_fix).or_else(keep), Ok(9));

// 用例4, Err类型的Result 连续调用or_else 的keep,由于keep实现保留err返回为Err(3); 注意实际上Result实例时变化了的
assert_eq!(Err(3).or_else(keep).or_else(keep), Err(3));

 

 

Result别名

在Rust的标准库中会经常出现Result的别名,用来默认确认其中Ok(T)或者Err(E)的类型,这能减少重复编码。比如io::Result

use std::num::ParseIntError;
use std::result;

type Result<T> = result::Result<T, ParseIntError>;

fn double_number(number_str: &str) -> Result<i32> {
    unimplemented!();
}

 

组合Option和Result

Option的方法ok_or:

fn ok_or<T, E>(option: Option<T>, err: E) -> Result<T, E> {
    match option {
        Some(val) => Ok(val),
        None => Err(err),
    }
}

可以在值为None的时候返回一个Result::Err(E),值为Some(T)的时候返回Ok(T),利用它我们可以组合Option和Result:

use std::env;

fn double_arg(mut argv: env::Args) -> Result<i32, String> {
    argv.nth(1)
        .ok_or("Please give at least one argument".to_owned())
        .and_then(|arg| arg.parse::<i32>().map_err(|err| err.to_string()))
        .map(|n| 2 * n)
}

fn main() {
    match double_arg(env::args()) {
        Ok(n) => println!("{}", n),
        Err(err) => println!("Error: {}", err),
    }
}

double_arg将传入的命令行参数转化为数字并翻倍,ok_or将Option类型转换成Result,map_err当值为Err(E)时调用作为参数的函数处理错误。

 

try! 宏

macro_rules! try {
    ($e:expr) => (match $e {
        Ok(val) => val,
        Err(err) => return Err(::std::convert::From::from(err)),
    });
}

 

try!事实上就是match Result的封装,当遇到Err(E)时会提早返回, ::std::convert::From::from(err)可以将不同的错误类型返回成最终需要的错误类型,因为所有的错误都能通过From转化成`Box<Error>`,所以下面的代码是正确的:

use std::error::Error;
use std::fs::File;
use std::io::Read;
use std::path::Path;

fn file_double<P: AsRef<Path>>(file_path: P) -> Result<i32, Box<Error>> {
    let mut file = try!(File::open(file_path));
    let mut contents = String::new();
    try!(file.read_to_string(&mut contents));
    let n = try!(contents.trim().parse::<i32>());
    Ok(2 * n)
}

 

在新版本中 try!宏被进一步简化为 一个?:

fn file_double<P: AsRef<Path>>(file_path: P) -> Result<i32, Error> {
    let mut file = File::open(file_path)?; // 注意这里的?, 和try功能一致,遇到错误,提前返回
    let mut contents = String::new();
    try!(file.read_to_string(&mut contents));
    let n = try!(contents.trim().parse::<i32>());
    Ok(2 * n)
}

 

总结

rust的Option 和Result 为返回、检测、处理错误,提供了系统支撑,这一点和golang的errors 设计比价类似。

熟练使用Option和Result是编写 Rust 代码的关键,Rust 优雅的错误处理离不开值返回的错误形式,编写代码时提供给使用者详细的错误信息是值得推崇的。

文章来自专栏

后台开发技术分享

13 篇文章 3 订阅
2 评论
0/1000
王****际

写的真好

2024-03-18 08:29:38
0
回复
谭****勇 回复 王****际

感谢鼓励

2024-03-26 08:50:10
0
回复
评论(2) 发表评论
王****际

写的真好

谭****勇 回复 王****际

感谢鼓励

  • 2
    点赞
  • 2
    收藏
  • 2
    评论