处理/解开嵌套的Result类型的惯用方法是什么?

13
我读到在Rust中使用unwrap来处理Result不是一个好的做法,最好使用模式匹配来适当处理发生的任何错误。
我明白这个观点,但考虑一下这个代码片段,它读取一个目录并打印每个条目的访问时间:
use std::fs;
use std::path::Path;

fn main() {
    let path = Path::new(".");
    match fs::read_dir(&path) {
        Ok(entries) => {
            for entry in entries {
                match entry {
                    Ok(ent) => {
                        match ent.metadata() {
                            Ok(meta) => {
                                match meta.accessed() {
                                    Ok(time) => {
                                        println!("{:?}", time);
                                    },
                                    Err(_) => panic!("will be handled")
                                }
                            },
                            Err(_) => panic!("will be handled")
                        }
                    },
                    Err(_) => panic!("will be handled")
                }
            }
        },
        Err(_) => panic!("will be handled")
    }
}

我想在上面的代码中处理所有可能的错误(panic宏只是一个占位符)。虽然上面的代码可以工作,但我认为它很丑陋。在这种情况下,处理的惯用方式是什么?
2个回答

21
我读到在Rust中对Result使用unwrap不是一个好的实践。
这并不容易。例如,阅读我的答案以了解更多信息。现在回到你的主要问题:

通过传递Ok值来减少右移操作

您的代码中存在一个大问题,即右移操作:例如,meta.accessed()调用缩进了很多。我们可以通过将想要处理的值从match中传递出去来避免这种情况:

let entries = match fs::read_dir(&path) {
    Ok(entries) => entries, // "return" from match
    Err(_) => panic!("will be handled"),
};

for entry in entries {  // no indentation! :)
    // ...
}

这已经是一种非常好的方法,可以使代码更易读。

使用?运算符将错误传递给调用函数

您的函数可以返回一个Result<_, _>类型,以便将错误传递给调用函数(是的,即使main()也可以返回Result)。在这种情况下,您可以使用?运算符:

use std::{fs, io};

fn main() -> io::Result<()> {
    for entry in fs::read_dir(".")? {
        println!("{:?}", entry?.metadata()?.accessed()?);
    }
    Ok(())
}

使用Result的辅助方法

Result类型还有许多辅助方法,例如map()and_then()。如果您想要在结果为Ok时执行某些操作,并且此操作将返回相同类型的结果,则and_then非常有用。以下是使用and_then()和手动处理错误的代码示例:

fn main() {
    let path = Path::new(".");
    let result = fs::read_dir(&path).and_then(|entries| {
        for entry in entries {
            let time = entry?.metadata()?.accessed()?;
            println!("{:?}", time);
        }
        Ok(())
    });

    if let Err(e) = result {
        panic!("will be handled");
    }
}

这种错误处理方式并不只有一种。你需要了解所有可用的工具,然后选择最适合你情况的工具。但在大多数情况下,?操作符是正确的工具。


2
另一个很好的资源是BurntSushi的“Rust错误处理”文章(http://blog.burntsushi.net/rust-error-handling/)。 - DK.
1
and_then(|_| { foo; Ok(()) }); 可以简写为 map(|_| { foo; }); - mcarton
@mcarton 谢谢!我简化了代码以避免那个问题。 - Lukas Kalbertodt

4

Result类有许多便利的方法可用于这些类型的问题:

use std::fs;
use std::path::Path;

fn main() {
    let path = Path::new(".");
    match fs::read_dir(&path) {
        Ok(entries) => {
            for entry in entries {
                match entry.and_then(|e| e.metadata()).map(|m| m.accessed()) {
                    Ok(time) => {
                        println!("{:?}", time);
                    },
                    Err(_) => panic!("will be handled")
                }
            }
        },
        Err(_) => panic!("will be handled")
    }
}

通常情况下,您不会在main函数中有太多的逻辑,只需要在另一个函数中使用?try!即可:

use std::fs;
use std::path::Path;

fn print_filetimes(path: &Path) -> Result<(), std::io::Error> {
    for entry in fs::read_dir(&path)? {
        let time = entry.and_then(|e| e.metadata()).map(|m| m.accessed())?;
        println!("{:?}", time);
    }

    Ok(())
}

fn main() {
    let path = Path::new(".");
    match print_filetimes(path) {
        Ok(()) => (),
        Err(_) => panic!("will be handled"),
    }
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接