美文网首页
Rust学习笔记10 Rust语法补充

Rust学习笔记10 Rust语法补充

作者: bradyjoestar | 来源:发表于2019-07-08 11:03 被阅读0次

    github地址:https://github.com/bradyjoestar/rustnotes(欢迎star!)
    pdf下载链接:https://github.com/bradyjoestar/rustnotes/blob/master/Rust%E8%AF%AD%E8%A8%80%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0.pdf
    参考:
    https://rustcc.gitbooks.io/rustprimer/content/ 《RustPrimer》
    https://kaisery.github.io/trpl-zh-cn/ 《Rust程序设计语言-简体中文版》

    第十章 Rust语法补充

    10.1 Result与错误处理

    大部分错误并没有严重到需要程序完全停止执行。有时,一个函数会因为一个容易理解并做出反应的原因失败。例如,如果尝试打开一个文件不过由于文件并不存在而失败,此时我们可能想要创建这个文件而不是终止进程。

    在Rust中,提供了 Result 枚举,它定义有如下两个成员,Ok 和 Err:

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

    T 和 E 是泛型类型参数;T代表成功时返回的 Ok 成员中的数据的类型,而 E 代表失败时返回的 Err 成员中的错误的类型。(T,E这也是Rust中的枚举不同于其它语言枚举的地方)。
    因为 Result 有这些泛型类型参数,我们可以将 Result 类型和标准库中为其定义的函数用于很多不同的场景,这些情况中需要返回的成功值和失败值可能会各不相同。
    在下面的例子中,我们增加根据 File::open 返回值进行不同处理的逻辑。

    use std::fs::File;
    
    fn main() {
        let f = File::open("hello.txt");
    
        let f = match f {
            Ok(file) => file,
            Err(error) => {
                panic!("There was a problem opening the file: {:?}", error)
            },
        };
    }
    

    注意与 Option 枚举一样,Result 枚举和其成员也被导入到了 prelude 中,所以就不需要在 match 分支中的 Ok 和 Err 之前指定 Result::。
    这里我们告诉 Rust 当结果是 Ok 时,返回 Ok 成员中的 file 值,然后将这个文件句柄赋值给变量 f。match 之后,我们可以利用这个文件句柄来进行读写。
    match 的另一个分支处理从 File::open 得到 Err 值的情况。在这种情况下,我们选择调用 panic!宏。如果当前目录没有一个叫做 hello.txt 的文件,当运行这段代码时会看到如下来自 panic! 宏的输出。

    10.1.1 匹配不同的错误

    上面的例子中代码不管 File::open 是因为什么原因失败都会 panic!。我们真正希望的是对不同的错误原因采取不同的行为:如果 File::open因为文件不存在而失败,我们希望创建这个文件并返回新文件的句柄。

    use std::fs::File;
    use std::io::ErrorKind;
    
    fn main() {
        let f = File::open("hello.txt");
    
        let f = match f {
            Ok(file) => file,
            Err(error) => match error.kind() {
                ErrorKind::NotFound => match File::create("hello.txt") {
                    Ok(fc) => fc,
                    Err(e) => panic!("Tried to create file but there was a problem: {:?}", e),
                },
                other_error => panic!("There was a problem opening the file: {:?}", other_error),
            },
        };
    }
    

    File::open 返回的 Err 成员中的值类型 io::Error,它是一个标准库中提供的结构体。这个结构体有一个返回 io::ErrorKind 值的 kind 方法可供调用。io::ErrorKind 是一个标准库提供的枚举,它的成员对应 io 操作可能导致的不同错误类型。我们感兴趣的成员是 ErrorKind::NotFound,它代表尝试打开的文件并不存在。所以 match 的 f 匹配,不过对于 error.kind() 还有一个内部 match。
    我们希望在匹配守卫中检查的条件是 error.kind() 的返回值是 ErrorKind的 NotFound 成员。如果是,则尝试通过 File::create 创建文件。然而因为 File::create 也可能会失败,还需要增加一个内部 match 语句。当文件不能被打开,会打印出一个不同的错误信息。外部 match 的最后一个分支保持不变这样对任何除了文件不存在的错误会使程序 panic。
    Result<T, E> 有很多接受闭包的方法,并采用 match 表达式实现。一个更老练的 Rustacean 可能会这么写:

    use std::fs::File;
    use std::io::ErrorKind;
    
    fn main() {
        let f = File::open("hello.txt").map_err(|error| {
            if error.kind() == ErrorKind::NotFound {
                File::create("hello.txt").unwrap_or_else(|error| {
                    panic!("Tried to create file but there was a problem: {:?}", error);
                })
            } else {
                panic!("There was a problem opening the file: {:?}", error);
            }
        });
    }
    

    10.1.2 unwrap与expect

    match 能够胜任它的工作,不过它可能有点冗长并且不总是能很好的表明其意图。Result<T, E> 类型定义了很多辅助方法来处理各种情况。其中之一叫做 unwrap,如果 Result 值是成员 Ok,unwrap 会返回 Ok 中的值。如果 Result 是成员 Err,unwrap 会为我们调用 panic!。这里是一个实践 unwrap 的例子:

    use std::fs::File;
    
    fn main() {
        let f = File::open("hello.txt").unwrap();
    }
    

    还有另一个类似于 unwrap 的方法它还允许我们选择 panic! 的错误信息:expect。使用 expect 而不是 unwrap 并提供一个好的错误信息可以表明你的意图并更易于追踪 panic 的根源。expect 的语法看起来像这样:

    use std::fs::File;
    
    fn main() {
        let f = File::open("hello.txt").expect("Failed to open hello.txt");
    }
    

    expect 与 unwrap 的使用方式一样:返回文件句柄或调用 panic! 宏。expect 用来调用 panic! 的错误信息将会作为参数传递给 expect ,而不像unwrap 那样使用默认的 panic! 信息。

    10.1.3 传播错误与传播错误的简写

    10.1.3.1 传播错误

    当编写一个其实现会调用一些可能会失败的操作的函数时,除了在这个函数中处理错误外,还可以选择让调用者知道这个错误并决定该如何处理。这被称为 传播(propagating)错误,这样能更好的控制代码调用,因为比起你代码所拥有的上下文,调用者可能拥有更多信息或逻辑来决定应该如何处理错误。

    例如,下面的例子中展示了一个从文件中读取用户名的函数。如果文件不存在或不能读取,这个函数会将这些错误返回给调用它的代码:

    use std::io;
    use std::io::Read;
    use std::fs::File;
    
    fn read_username_from_file() -> Result<String, io::Error> {
        let f = File::open("hello.txt");
    
        let mut f = match f {
            Ok(file) => file,
            Err(e) => return Err(e),
        };
    
        let mut s = String::new();
    
        match f.read_to_string(&mut s) {
            Ok(_) => Ok(s),
            Err(e) => Err(e),
        }
    }
    

    调用这个函数的代码最终会得到一个包含用户名的 Ok 值,或者一个包含 io::Error 的 Err 值。我们无从得知调用者会如何处理这些值。例如,如果他们得到了一个 Err 值,他们可能会选择 panic! 并使程序崩溃、使用一个默认的用户名或者从文件之外的地方寻找用户名。我们没有足够的信息知晓调用者具体会如何尝试,所以将所有的成功或失败信息向上传播,让他们选择合适的处理方法。
    这种传播错误的模式在 Rust 是如此的常见,以至于有一个更简便的专用语法:?。

    10.1.3.2 传播错误的简写

    下面例子展示了一个 read_username_from_file 的实现,它实现了与上面例子中的代码相同的功能,不过这个实现使用了问号运算符(运算符重载):

    
    use std::io;
    use std::io::Read;
    use std::fs::File;
    
    fn read_username_from_file() -> Result<String, io::Error> {
        let mut f = File::open("hello.txt")?;
        let mut s = String::new();
        f.read_to_string(&mut s)?;
        Ok(s)
    }
    

    File::open 调用结尾的 ? 将会把 Ok 中的值返回给变量 f。如果出现了错误,? 会提早返回整个函数并将一些 Err 值传播给调用者。同理也适用于 read_to_string 调用结尾的 ?。
    ? 消除了大量样板代码并使得函数的实现更简单。我们甚至可以在 ? 之后直接使用链式方法调用来进一步缩短代码,如下面的例子所示:

    use std::io;
    use std::io::Read;
    use std::fs::File;
    
    fn read_username_from_file() -> Result<String, io::Error> {
        let mut s = String::new();
    
        File::open("hello.txt")?.read_to_string(&mut s)?;
    
        Ok(s)
    }
    

    ? 只能被用于返回值类型为 Result 的函数。
    下面的例子中:

    use std::fs::File;
    
    fn main() {
        let f = File::open("hello.txt")?;
    }
    

    当编译这些代码,会得到如下错误信息:

    error[E0277]: the `?` operator can only be used in a function that returns `Result` or `Option` (or another type that implements `std::ops::Try`)
     --> src/main.rs:4:13
      |
    4 |     let f = File::open("hello.txt")?;
      |             ^^^^^^^^^^^^^^^^^^^^^^^^ cannot use the `?` operator in a function that returns `()`
      |
      = help: the trait `std::ops::Try` is not implemented for `()`
      = note: required by `std::ops::Try::from_error`
    

    错误指出只能在返回 Result 的函数中使用 ?。在不返回 Result 的函数中,当调用其他返回 Result的函数时,需要使用 match 或 Result 的方法之一来处理,而不能用 ? 将潜在的错误传播给代码调用方。

    10.2 Any和反射

    use std::any::Any;
    use std::fmt::Debug ;
    
    fn load_config<T:Any+Debug>(value: &T) -> Vec<String>{
        let mut cfgs: Vec<String>= vec![];
        let value = value as &Any;
        match value.downcast_ref::<String>() {
            Some(cfp) => cfgs.push(cfp.clone()),
            None => (),
        };
    
        match value.downcast_ref::<Vec<String>>() {
            Some(v) => cfgs.extend_from_slice(&v),
            None =>(),
        }
    
        if cfgs.len() == 0 {
            panic!("No Config File");
        }
        cfgs
    }
    
    fn main() {
        let cfp = "/etc/wayslog.conf".to_string();
        assert_eq!(load_config(&cfp), vec!["/etc/wayslog.conf".to_string()]);
        let cfps = vec!["/etc/wayslog.conf".to_string(),
                        "/etc/wayslog_sec.conf".to_string()];
        assert_eq!(load_config(&cfps),
                   vec!["/etc/wayslog.conf".to_string(),
                        "/etc/wayslog_sec.conf".to_string()]);
    }
    

    熟悉Java的同学肯定对Java的反射能力记忆犹新,同样的,Rust也提供了运行时反射的能力。但是,这里有点小小的不同,因为 Rust 不带 VM 不带 Runtime ,因此,其提供的反射更像是一种编译时反射。
    因为,Rust只能对 'static 生命周期的变量(常量)进行反射!
    我们来重点分析一下中间这个函数:

    fn load_config<T:Any+Debug>(value: &T) -> Vec<String>{..}
    

    首先,这个函数接收一个泛型T类型,T必须实现了Any和Debug。
    这里可能有同学疑问了,你不是说只能反射 'static 生命周期的变量么?我们来看一下Any限制:

    pub trait Any: 'static + Reflect {
        fn get_type_id(&self) -> TypeId;
    }
    

    看,Any在定义的时候就规定了其生命周期,而Reflect是一个Marker,默认所有的Rust类型都会实现他!注意,这里不是所有原生类型,而是所有类型。
    好的,继续,由于我们无法判断出传入的参数类型,因此,只能从运行时候反射类型。

    let value = value as &Any;
    

    首先,我们需要将传入的类型转化成一个 trait Object, 当然了,你高兴的话用 UFCS 也是可以做的,参照本章最后的附录。
    这样,value 就可以被堪称一个 Any 了。然后,我们通过 downcast_ref 来进行类型推断。如果类型推断成功,则 value 就会被转换成原来的类型。
    有的同学看到这里有点懵,为什么你都转换成 Any 了还要转回来?
    其实,转换成 Any 是为了有机会获取到他的类型信息,转换回来,则是为了去使用这个值本身。
    最后,我们对不同的类型处以不同的处理逻辑。最终,一个反射函数就完成了。

    相关文章

      网友评论

          本文标题:Rust学习笔记10 Rust语法补充

          本文链接:https://www.haomeiwen.com/subject/wqtohctx.html