是否有一种快捷方式可以在循环中取消或继续执行?

22

考虑以下内容:

loop {
    let data = match something() {
        Err(err) => {
            warn!("An error: {}; skipped.", err);
            continue;
        },
        Ok(x) => x
    };

    let data2 = match something_else() {
        Err(err) => {
            warn!("An error: {}; skipped.", err);
            continue;
        },
        Ok(x) => x
    };

    // and so on
}

如果我不需要将ok-value分配给data,我会使用if let Err(err) = something(),但是是否有一种快捷方式可以避免在这种典型场景中复制/粘贴Err/Ok分支的代码?是否有类似if let的东西也会返回ok-value。


你可以去掉 let datacontinue,然后只需要将 "other stuff" 放在 Ok(x) => 分支里。 - sepp2k
@sepp2k 没错,但是当循环体内有多个resultish操作时,这将很快变成大量嵌套的代码。编写宏是一种解决方法吗? - Yuri Geinish
你没有理解Result这样的结构的意义,它强制代码检查错误。如果有什么可以绕过这个的东西,那还有什么意义呢?在Rust中,这种结构非常常见,所以使用?来避免重复代码。只有在测试或主函数中,你可以使用.unwrap().expect() - Stargateur
我不打算编辑问题,但我认为标题更适合为“如何简洁地组合结果?”-似乎你的问题并不特别涉及if let,因为它实际上是在寻求一种替代语言结构的方法。 - MutantOctopus
2
一种简洁地组合 Result 的方法是使用其中一个推导式库,例如 map_formdo。(注意:我是 map_for 的作者) - Jmb
显示剩余4条评论
6个回答

26

虽然我认为E_net4的答案可能是最好的,但为了纪念起见,如果出于某些原因不想创建单独的函数并使用?运算符进行早期返回,则可以添加一个宏。

这里是一个简单的skip_fail!宏,当传递了错误时,会continue包含的循环:

macro_rules! skip_fail {
    ($res:expr) => {
        match $res {
            Ok(val) => val,
            Err(e) => {
                warn!("An error: {}; skipped.", e);
                continue;
            }
        }
    };
}

这个宏可以这样使用:let ok_value = skip_fail!(do_something());

这个 Playground 链接演示了如何使用 skip_fail 来打印可被 1、2 和 3 整除的数字,当其中一个除法会截断时会打印错误。

再次强调,我认为在单独的函数中使用 ? 并且如果没有失败则返回Ok(end_result) 可能是最惯用的解决方案,所以如果可以使用它的答案,您可能应该这样做。


非常感谢,这有助于封装数组访问。 - AlexNe

22
Rust 1.65.0 添加了let-else语句。因此,您可以这样编写它:
    loop {
        let data = something()
        let Ok(ok_data) = data else {
            warn!("skipped.");
            continue;
        };
        // ok_data is available here
        let Ok(data2) = something_else(ok_data) else {
            continue;
        };
        // and so on
    }

但是您将无法访问err变量


12
如果您经常需要在结果上执行“unwrap or continue”操作,请考虑将该逻辑封装在一个单独的函数中。这样,您可以利用?语法从函数中抛出错误。循环的流程逻辑可以写在一个地方(尽管在这一点上,您可能不再需要continue)。
loop {
    if let Err(err) = do_event() {
        warn!("An error: {}; skipped.", err);
        // continue; // you also don't need this
    }
}

fn do_event() -> Result<(), YourErrorType> {
    let data = do_something()?; // 
    let x = something_more()?;  // error propagation!
    Ok(())
}

值得注意的是,我相信你也可以使用闭包来完成这个任务,如果你不想在源代码的另一个部分中创建完全独立的函数。虽然我不知道那样看起来会有多好... - MutantOctopus
@BHustus 的确如此,但我认为在这里使用单独的函数更易于阅读。 :) - E net4
如果 do_event 返回的东西很丑陋,你需要吗?并且在检查错误后必须解包结果? - Sean
不确定我是否理解了你的问题@Sean,但是,通过?从结果引发错误非常干净和惯用,如所示。在调用链的顶部,一个良好的最终用户应用程序将具有适当的_error reporter_,以人类可读的方式呈现错误。考虑查看eyremiette箱子,以获取有关错误报告库的示例,以及这个Rust中的错误处理概述视频 - E net4
@E_net4thecandidatesupporter 抱歉我的问题让您感到困惑,我的意思是,如果do_event()有一些返回值而不是(),也许可以通过某些代码来演示 https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=248ae38826cecc369cb07476753b8a60 - Sean
1
@Sean 如果期望在循环外获得最终结果,那么可以调整顶层逻辑以使用match而不是if let。另一个答案也提到了可以构建宏来实现这一点。但是,如果仅仅是为了继续处理该循环步骤内的数据,则此模式期望您在do_event内部扩展该逻辑。 - E net4

6
如果你需要将多个Ok链接在一起,并需要在下一个操作中使用其中一个Ok的值,而且不关心错误在链的哪个位置出现,请考虑使用and_then函数:
loop {
    let outcome = something()
                  .and_then(|a| something_else(a))
                  .and_then(|a| another_thing(a))
                  .and_then(|a| {
                      let b = a + salt;
                      one_more(b)
                  });
    if let Err(e) = outcome {
        warn!("An error: {}; skipped.", e);
    }
}

somethingsomething_elseanother_thingone_more都返回某种形式的Result。即使此示例删除了continue语句,and_then通过在ResultErr类型时短路,有效地模拟了它。所有后续调用将被跳过。

您可以通过对仅需要一个函数调用的语句使用非闭包来使其更加简洁:

loop {
    let outcome = something()
                  .and_then(something_else)
                  .and_then(another_thing)
                  .and_then(|a| one_more(a + salt));
    if let Err(e) = outcome {
        warn!("An error: {}; skipped.", e);
    }
}

(请注意函数中没有括号,这意味着它们被用作可调用对象而不是取回其返回值)

2
如果你愿意使用不稳定的功能,你可以使用try块来实现这个目标:
#![feature(try_blocks)]

pub fn something() -> Result<String, String> {
    Err(String::from("Badness"))
}

pub fn something_else() -> Result<String, String> {
    Ok(String::from("Ok"))
}

pub fn main() {
    loop {
        let result: Result<(), String> = try {
            let data = something()?;
            let data2 = something_else()?;
        };
        if let Err(e) = result {
            println!("An error: {}; skipped.", e)
        }
    }
}

正如shepmaster在评论中提到的那样,可以使用立即调用的闭包(Immediately Invoked Function Expression或者简称IIFE)来实现,而无需使用任何不稳定的特性。这是E_net4的解决方案所建议的修改,由MutantOctopus在该解决方案的评论中提出。

pub fn something() -> Result<String, String> {
    Err(String::from("Badness"))
}

pub fn something_else() -> Result<String, String> {
    Ok(String::from("Ok"))
}

pub fn main() {
    loop {
        let result: Result<(), String> = (|| {
            let data = something()?;
            let data2 = something_else()?;
            Ok(())
        })();
        if let Err(e) = result {
            println!("An error: {}; skipped.", e)
        }
    }
}

IIFE是稳定等效的,这与E_net4的回答中指出的实际上是相同的。 - Shepmaster
@Shepmaster,很好。我已经将该代码添加到问题中。但是出于兴趣,IIFE代表什么? - Michael Anderson
立即调用函数表达式 - Shepmaster

1
你可以使用我的 unwrap_or crate 来完成这个任务。
你可以使用它来编写简洁、清晰的代码:
unwrap_or_ok!(callable(&mut param), _, return);

loop {
    let data = unwrap_ok_or!(something(), err, {
        warn!("An error: {}; skipped.", err);
        continue;
    });

    let data2 = unwrap_ok_or!(somethingElse(), err, {
        warn!("An error: {}; skipped.", err);
        continue;
    });
}

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