写给前端看的Rust教程(13)Results & Options

1,914 阅读7分钟

原文:24 days from node.js to Rust

前言

教程8 我们介绍HashMap的时候提到过Option,当时我们讲到查询HashMapkey值对应数据时,不能百分百保证对应的数据一定存在,所以返回结果必然会存在“空”的情况。Rust中没有类似JavaScriptundefinednull的概念,为了满足安全表示“空”,就需要Option来解决这个问题了

“空”就像是一个预期错误,这种情况一般要么是结果正确,要么结果为“空”意味着失败,为应对预期中的错误处理,Result应运而生。ResultOption 常常是一块出现,处理方式也相似,而且必要时可以相互转换

正文

相关阅读

Option

如果你还没仔细学习过enums,那么好好读完上面列出的相关文章

Rustenums与其它很多语言的实现不同,其中Option enums是这么定义的:

pub enum Option<T> {
    /// No value
    None,
    /// Some value `T`
    Some(T),
}

取值要么是Option::None表示“空”,要么是Option::Some(T)表示某个值,我们之前接触过泛型,你在 教程10 的阅读列表 里可以看到更多有关文章

Rust中创建和使用Option十分简单:

fn main() {
    let some = returns_some();
    println!("{:?}", some);
    let none = returns_none();
    println!("{:?}", none);
}

fn returns_some() -> Option<String> {
    Some("my string".to_owned())
}

fn returns_none() -> Option<String> {
    None
}

在返回None的时候,我们也需要指定Option<T>中的T

注意:我们可以用SomeNone替代Option::Some()Option::None是因为它们都被Rust预先引入了,相关信息可以阅读 std::prelude.

Result

ResultOption除了失败也会有个数值以外,其它地方都很相似,Result::Err变量中的值通常遵循一些约定,不过查看其实现的话会发现实际上也没有什么约束

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

Ok()Err()的创建没有什么特殊之处

fn main() {
    let value = returns_ok();
    println!("{:?}", value);

    let value = returns_err();
    println!("{:?}", value);
}

fn returns_ok() -> Result<String, MyError> {
    Ok("This turned out great!".to_owned())
}

fn returns_err() -> Result<String, MyError> {
    Err(MyError("This failed horribly.".to_owned()))
}

#[derive(Debug)]
struct MyError(String);

这里之所以用到struct是为了表示Err可以包含任何值,比如structStringHashMap或其它

.unwrap()

OptionResult看上去简单易懂,但实际上真的是这样么?

容易困惑之处来自于如何获取其值,在教程中你已经遇到过.unwrap(),如果你对NoneErr使用.unwrap()会报错。在Rust中,只要对警告足够重视,你会减少99%的异常,实现更快、更稳定的目标

如何获取值

.unwrap()

只在你确保Some()Ok()上使用.unwrap(),已之前IP地址的示例为例:

let ip_address = std::net::Ipv4Addr::from_str("127.0.0.1").unwrap();

能放心的使用.unwrap()是基于编码者对代码的了解,在使用前需要能够确认其值。例如我们可以使用 .contains_key() 来确认HashMaps是否包含指定的key,如果是的话我们就可以放心的使用.unwrap()

.unwrap_or()

.unwrap_or()会给失败提供一个默认值,默认值的类型需要和Ok(T)Some(T)T保持一致:

fn main() {
    let value = returns_ok();
    println!("{:?}", value.unwrap());

    let value = returns_err();
    // 这里必须是String类型
    println!("{:?}", value.unwrap_or("真的失败了".to_owned()));
}

fn returns_ok() -> Result<String, String> {
    Ok("成功!".to_owned())
}

fn returns_err() -> Result<String, String> {
    Err("失败!".to_owned())
}

.unwrap_or_else(|| {})

.unwrap_or_else() 接受一个函数,当是NoneErr的情况时将会采纳函数的返回值,如果默认值的计算代价很高且提前计算它没有任何价值,那么就可以使用这种方法

.unwrap_or()一样,函数的返回值类型需要和T保持一致

let unwrap_or_else = returns_none()
  .unwrap_or_else(|| format!("Default value from a function at time {:?}", Instant::now()));

println!(
  "returns_none().unwrap_or_else(|| {{...}}): {:?}",
  unwrap_or_else
);

|| ...Rust中的闭包语法(closure syntax),后面会深入介绍

unwrap_or_default()

unwrap_or_default() 会遵循指定类型的Default值如果不存在的话,Default是一个类似DebugDisplay之类的trait

TypeScript中你可能会写这样的代码:

et my_string = maybe_undefined || "";

Rust中则是这样的:

let my_string = maybe_none.unwrap_or_default(); // Assuming `T` is `String`.

你可以像这样来实现Default

enum Kind {
    A,
    B,
    C,
}

impl Default for Kind {
    fn default() -> Self { Kind::A }
}
match表达式

你可以使用match表达式来获取到数值:

let match_value = match returns_some() {
  Some(val) => val,
  None => "My default value".to_owned(),
};

println!("match {{...}}: {:?}", match_value);
if let表达式

你可以对枚举的指定变体采用一个条件判断:

if let Some(val) = returns_some() {
  println!("if let : {:?}", val);
}

如果returns_some()返回的OptionSome(),那么它的内部值将被绑定到标识符val,这是一个比较奇怪的语法,但很有用处

? 运算符

下面的代码展示了?运算符的使用技巧:

use std::fs::read_to_string;

fn main() -> Result<(), std::io::Error> {
    let html = render_markdown("../README.md")?;
    println!("{}", html);
    Ok(())
}

fn render_markdown(file: &str) -> Result<String, std::io::Error> {
    let source = read_to_string(file)?;
    Ok(markdown::to_html(&source))
}
  • 首先我们我们在第3用-> Result<(), std::io::Error>main()返回值类型改为Result,注意()unit type,这是另一种表达“空”的方式。-> Result<(), ...>意味着返回“空”或失败
  • 其次,在第10行我们用std::fs::read_to_string(),它会接受一个路径并返回Result<String, std::io::Error>,这意味着它会返回String类型的文件内容或std::io::Error类型的错误
  • 接着在第10行用?运算符自动进行unwrap,如果结果是失败,那么?运算符会直接将结果返回给调用者main()函数
  • 在第4行我们继续使用?运算符来做自动unwrap,由于main()函数没有调用者,所以如果存在失败,则会导致程序终止
  • 由于main()的返回类型是Result<(), ...>,所以我们在最后需要再写一个Ok(())
? 与 try!

在一些老的文章中,你会读到关于try!宏的内容,try!实际上就是?运算符的先驱,无论谁好谁坏,多了解下总归是好的,我们可以看下 try!的实现

宏不是本教程的重点,不过看下try!的实现源码,相信也不难理解:

macro_rules! r#try {
    ($expr:expr $(,)?) => {
        match $expr {
            $crate::result::Result::Ok(val) => val,
            $crate::result::Result::Err(err) => {
                return $crate::result::Result::Err($crate::convert::From::from(err));
            }
        }
    };
}

try!宏接收一个表达式,并且在match语句中使用了该表达式,如果表达式是Ok则返回其值,如果是失败,则会提前返回且将返回值转化为Result Error类型

错误处理

最后让我们看下,如果文件路径是从环境变量中获取有什么区别:

use std::fs::read_to_string;

fn main() -> Result<(), std::io::Error> {
    let html = render_markdown()?;
    println!("{}", html);
    Ok(())
}

fn render_markdown() -> Result<String, std::io::Error> {
    let file = std::env::var("MARKDOWN")?;
    let source = read_to_string(file)?;
    Ok(markdown::to_html(&source))
}

我们增加了一行代码,从名为"MARKDOWN"的环境变量获取文件路径,如果没有这个环境变量则程序会报错,我们用一个?表达式来做短路处理,现在我们会遇到一个编译错误:? could not convert the error to std::io::Error

image.png

你现在已经了解了OptionResult,这还不是最麻烦的,最麻烦的是如何处理不同的错误,这我们会在下一篇教程中讲到

总结

OptionResultRust中无处不在,你需要对它们有正确的理解。同样Enums也是无处不在,你经常会用到Enums而不是具体的某个字符串、数字或布尔值

在下一篇文章中,我们会介绍关于Err的知识

更多