Rust编程学习 - 如何利用代数类型系统做错误处理的另外一大好处是可组合性(composability)

组合错误类型

利用代数类型系统做错误处理的另外一大好处是可组合性(composability)。比如 Result 类型有这样的一系列成员方法:

js 复制代码
fn map < U,
F > (self, op: F) - >Result < U,
E > where F: FnOnce(T) - >U fn map_err < F,
0 > (self, op: 0) - >Result < T,
F > where O: FnOnce(E) - >F fn and < U > (self, res: Result < U, E > ) - >Result < U,
E > fn and_then < U,
F > (self, op: F) - >Result < U,
E > where F: FnOnce(T) - >Result < U,
E > fn or < F > (self, res: Result < T, F > ) - >Result < T,
F > fn or_else < F,
0 > (self, op: 0) - >Result < T,
F > where O: FnOnce(E) - >Result < T,
F > 

这些方法的签名稍微有点复杂,涉及许多泛型参数。它们之间的区别也就表现在方法签 名中。我们可以用下面的方式去掉语法干扰之后,来阅读函数签名,从而理解这些方法之间 的区别:

通过这个表格的对比,我们可以很容易看出它们之间的区别。比如map和and_ then 的主要区别是闭包参数:map的参数是做的T->U的转换,而and_then的参数是 T->Result 的转换。 Option 类型也有类似的对应的方法,读者可以自己建一个表格,对 比一下这些方法签名之间的区别。

下面用一个示例演示一下这些组合函数的用法:

js 复制代码
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(lerr | err.to_string())).map( | n | 2 * n)
}
fn main() {
    match double_arg(env: :args()) {
        Ok(n) = >println ! ("{}", n),
        Err(err) = >println ! ("Error:{}", err),
    }
}

问号运算符

Result类型的组合调用功能很强大,但是它有一个缺点,就是经常会发生嵌套层次太 多的情况,不利于可读性。比如下面这个示例:

js 复制代码
use std: :fs: :File;
use std: :io: :Read;
use std: :path: :Path;
fn file_double < P: AsRef < Path >> (file_path: P) - >Result < i32,
String > {
    File: :open(file_path)

    .map_err( | err | err.to_string()).and_then( | mut file | {
        let mut contents = String: :new();
        file.read_to_string( & mut contents).map_err( | err | err.to_string()).map(I_I contents)
    }).and_then( | contents | {
        contents.trim()·parse: :<i32 > ().map_err( | err | err.to_string())
    }).map( | n | 2 * n)

    fn main() {
        match file_double("foobar") {
            Ok(n) = >println ! ("{}", n),
            Err(err) = >println ! ("Error:{}", err),
        }
    }

这说明我们还有继续改进的空间。为了方便用户,Rust 设计组在前面这套系统的基础上, 又加入了一个问号运算符,用来简化源代码。这个问号运算符完全是建立在前面这套错误处 理机制上的语法糖。

问号运算符意思是,如果结果是Err,则提前返回,否则继续执行。使用问号运算符,我们可以把 file_double 函数简化成这样:

js 复制代码
fn file_double < P: AsRef < Path >> (file_path: P) - >Result < i32,
String > {
    let mut file = File: :open(file_path).map_err( | e | e.to_string()) ? ;
    let mut contents = String: :new();
    file.read_to_string( & mut contents).map_err( | err | err.to_string()) ? ;
    let n = contents.trim()·parse: :<i32 > ().map_err( | err | err.to_string()) ? ;
    Ok(2 * n)
}

这里依然有不少的map_err 调用,主要原因是返回类型限制成了Result<i32, String> 。 如果改一下返回类型,代码还能继续精简。

因为这段代码总共有两种错误:

  • 一种是io 错误,用std::io::Error 表示
  • 另外一种是字符串转整数错误,用std::num::ParseIntError表示。

我们要把这两种类型统一起 来,所以使用了一个自定义的enum 类型,这样map_err 方法调用就可以省略了。我们再补 充这两种错误类型到自定义错误类型之间的类型转换,问题就解决了。完整源码如下所示:

js 复制代码
use std: :fs: :File;
use std: :io: :Read;
use std: :path: :Path;

# [derive(Debug)] enum MyError {
    Io(std: :io: :Error),
    Parse(std: :num: :ParseIntError),
}
impl From < std: :io: :Error >
for MyError {
    fn from(error: std: :io: :Error) - >Self {
        MyError: :Io(error)
    }
}
impl From < std: :num: :ParseIntError >
for MyError {
    fn from(error: std: :num: :ParseIntError) - >Self {
        MyError: :Parse(error)
    }
}
fn file_double < P: AsRef < Path >> (file_path: P) - >Result < i32,
MyError > {
    let mut file = File: :open(file_path) ? ;
    let mut contents = String: :new();
    file.read_to_string( & mut contents) ? ;
    let n = contents.trim().parse: :<i32 > () ? ;
    Ok(2 * n)
}
fn main() {
    match file_double("foobar") {
        Ok(n) = >println ! ("{}", n),
        Err(err) = >println ! ("Error:{:?}", err),
    }
}

这样一来,这个file_double 函数就精简太多了。它只需管理正常逻辑,对于可能出 错的分支,直接一个问号操作符提前返回,错误处理和正常逻辑互不干扰,清晰易读。

下面继续讲解一下问号运算符背后做了什么事情。跟其他很多运算符一样,问号运算符 也对应着标准库中的一个trait std::ops::Try。它的定义如下:

js 复制代码
trait Try {
    type Ok;
    type Error;
    fn into_result(self) - >Result < Self: :Ok,
    Self: :Error > ;
    fn from_error(v: Self: :Error) - >Self;
    fn from_ok(v: Self: :Ok) - >Self;
}
   match Try: :into_result(expr) {
    Ok(V) = >V,
    //here,the    'return    presumes    that    there    is

    Err(e) = >
    return Try: :from_error(From: :from(e)),
}

哪些类型支持这个问号表达式呢?标准库中已经为Option、Result两个类型impl了 这个trait:

js 复制代码
impl < T > ops: :Try
for Option < T > {
    type Ok = T;
    type Error = NoneError;
    fn into_result(self) - >Result < T,
    NoneError > {
        self.ok_or(NoneError)
    }
    fn from_ok(V: T) - >Self {
        Some(V)
    }
    fn from_error(_: NoneError) - >Self {
        None
    }
}
impl < T,
E > ops: :Try
for Result < T,
E > {
    type Ok = T;
    type Error = E;
    fn into_result(self) - >Self {
        self
    }
    fn from_ok(v: T) - >Self {
        Ok(V)
    }
    fn from_error(v: E) - >Self {
        Err(V)

    }

把这些综合起来,我们就能理解对于Result类型,执行问号运算符做了什么了。其实 就是碰到Err的话,调用From trait 做个类型转换,然后中断当前逻辑提前返回。

相关推荐
清水2 小时前
Spring Boot企业级开发入门
java·spring boot·后端
一个不称职的程序猿2 小时前
高并发场景下的缓存利器
java·缓存
星释2 小时前
Rust 练习册 :Proverb与字符串处理
开发语言·后端·rust
Source.Liu2 小时前
【ISO8601库】Serde 集成模块详解(serde.rs文件)
rust·time·iso8601
2301_801252222 小时前
Tomcat的基本使用作用
java·tomcat
lkbhua莱克瓦242 小时前
Java基础——常用算法3
java·数据结构·笔记·算法·github·排序算法·学习方法
麦麦鸡腿堡2 小时前
Java_TreeSet与TreeMap源码解读
java·开发语言
snakecy2 小时前
系统架构设计师学习大纲目录
学习·系统架构
教练、我想打篮球3 小时前
05 kafka 如何存储较大数据记录
java·kafka·record