如何避免“if let”的嵌套链?

发布于 2025-01-10 00:54:32 字数 2158 浏览 2 评论 0原文

我正在浏览充满这样代码的代码库:

if let Some(i) = func1() {
    if let Some(j) = func2(i) {
        if let Some(k) = func3(j) {
            if let Some(result) = func4(k) {
                // Do something with result
            } else {
                println!("func 4 returned None");
            }
        } else {
            println!("func 3 returned None");
        }
    } else {
        println!("func 2 returned None");
    }
} else {
    println!("func 1 returned None");
}

这是一个愚蠢的简化示例,但一般模式是:

  • 有一堆不同的函数返回 Option
  • 所有函数必须按顺序调用,并将前一个函数的返回值(如果不是 None)传递给下一个函数。
  • 如果所有函数都返回Some,则返回的最终值将用于某些用途。
  • 如果any返回None,则执行停止并记录某种错误 - 通常是一条错误消息,准确告知用户哪个函数返回None

当然,问题是上面的代码丑陋且不可读。当您用在真实代码中实际含义的变量/函数名称替换 ifunc1 等时,它会变得更加丑陋,并且我的真实代码库中的许多示例都具有更多含义多于四个嵌套的 if let。这是 箭头反模式 的示例,它完全失败了 squint test,令人困惑的是错误消息如何以与可能导致错误的函数相反的顺序出现。

难道真的没有更好的方法吗?我想将上面的内容重构为具有更清晰、更扁平的结构,其中所有内容都以合理的顺序出现。 if let 链接 可能有帮助,但看起来没有就像 Rust 中已经提供了这个功能一样。我想也许我可以通过使用 ? 和/或提取一些辅助函数来清理东西,但我无法让它工作,而且我不想在整个系统中提取大量新函数如果我能避免的话。

这是我能想到的最好的:

let i : u64;
let j : u64;
let k : u64;
let result : u64;

if let Some(_i) = func1() {
    i = _i;
} else {
   println!("func 1 returned None");
   return;
}
if let Some(_j) = func2(i) {
    j = _j;
} else {
   println!("func 2 returned None");
   return;
}
if let Some(_k) = func3(j) {
    k = _k;
} else {
   println!("func 3 returned None");
   return;
}
if let Some(_result) = func3(k) {
    result = _result;
} else {
   println!("func 4 returned None");
   return;
}


// Do something with result

但这仍然感觉很长很冗长,而且我不喜欢引入这些额外变量 _i_j 等。

有什么我没看到的吗?写我想写的东西最简单、最干净的方法是什么?

I'm wading through a codebase full of code like this:

if let Some(i) = func1() {
    if let Some(j) = func2(i) {
        if let Some(k) = func3(j) {
            if let Some(result) = func4(k) {
                // Do something with result
            } else {
                println!("func 4 returned None");
            }
        } else {
            println!("func 3 returned None");
        }
    } else {
        println!("func 2 returned None");
    }
} else {
    println!("func 1 returned None");
}

That's a stupid, simplified example, but the general pattern is that:

  • There are a bunch of different functions which return an Option.
  • All the functions must be called in sequence, with the return value of the previous function (if it's not None) passed to the next function.
  • If all functions return a Some, then the final returned is used for something.
  • If any returns None, then execution stops and some kind of error is logged - usually an error message that informs the user exactly which function returned None.

The problem, of course, is that the above code is an ugly and unreadable. It gets even uglier when you substitute i, func1 etc. with variable/function names that actually mean something in the real code, and many examples in my real codebase have far more than four nested if lets. It's an example of the arrow anti-pattern, it completely fails the squint test, and it's confusing how the error messages appear in reverse order to the functions which can cause them.

Is there really not a better way to do this? I want to refactor the above into something that has a cleaner, flatter structure where everything appears in a sensible order. if let chaining might help but it doesn't look like that feature is available in Rust yet. I thought maybe I could clean things up by using ? and/or extracting some helper functions, but I couldn't get it to work and I'd rather not extract a ton of new functions all over the place if I can avoid it.

Here's the best I could come up with:

let i : u64;
let j : u64;
let k : u64;
let result : u64;

if let Some(_i) = func1() {
    i = _i;
} else {
   println!("func 1 returned None");
   return;
}
if let Some(_j) = func2(i) {
    j = _j;
} else {
   println!("func 2 returned None");
   return;
}
if let Some(_k) = func3(j) {
    k = _k;
} else {
   println!("func 3 returned None");
   return;
}
if let Some(_result) = func3(k) {
    result = _result;
} else {
   println!("func 4 returned None");
   return;
}


// Do something with result

But this still feels very long and verbose, and I don't like how I'm introducing these extra variables _i, _j etc.

Is there something I'm not seeing here? What's the simplest and cleanest way to write what I want to write?

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(6

千年*琉璃梦 2025-01-17 00:54:33

您可以使用 let-else 语句 ,一个功能,已添加到 版本 1.65

RFC 3137

引入一个新的 let PATTERN: TYPE = EXPRESSION else DIVERGING_BLOCK; 构造(非正式地称为 let-else 语句),它是 if-let 表达式的对应部分。 构造(非正式地称为 let-else 语句)。 p>

如果指定表达式的模式匹配成功,则其绑定将被引入到周围的范围中。如果不成功,则必须发散(返回!,例如return 或break)。

使用此功能,您可以编写:

let Some(i) = func1() else {
    println!("func 1 returned None");
    return;
};
let Some(j) = func2(i) else {
    println!("func 2 returned None");
    return;
};
let Some(k) = func3(j) else {
    println!("func 3 returned None");
    return;
};
let Some(result) = func3(k) else {
    println!("func 4 returned None");
    return;
};

如果您想在旧版本的 rust 上尝试它,则必须启用不稳定功能:

#![feature(let_else)]

You can use let-else statements, a feature which was added to stable rust in version 1.65.

RFC 3137

Introduce a new let PATTERN: TYPE = EXPRESSION else DIVERGING_BLOCK; construct (informally called a let-else statement), the counterpart of if-let expressions.

If the pattern match from the assigned expression succeeds, its bindings are introduced into the surrounding scope. If it does not succeed, it must diverge (return !, e.g. return or break).

With this feature you can write:

let Some(i) = func1() else {
    println!("func 1 returned None");
    return;
};
let Some(j) = func2(i) else {
    println!("func 2 returned None");
    return;
};
let Some(k) = func3(j) else {
    println!("func 3 returned None");
    return;
};
let Some(result) = func3(k) else {
    println!("func 4 returned None");
    return;
};

If you wanted to try it on an older version of rust, you would have to enable the unstable feature:

#![feature(let_else)]
如痴如狂 2025-01-17 00:54:33

If-let 链接将使这变得更好,但现在(假设您不想每晚使用),轻微的重构可能会有所帮助。例如,将除最后一个调用之外的所有调用拉入其自己的函数中,您可以使用 ? 运算符:

fn get_result() -> Option<u64> {
  let i = func1()?;
  let j = func2(i)?;
  let k = func3(j)?;
  func3(k)
}

fn main() {
  if let Some(result) = get_result() {
    // do something
  }
}

如果您需要对错误情况进行更细粒度的控制,您可以返回一个 结果改为:

enum Error {
  Func1,
  Func2,
  Func3,
  Func4,
}

fn get_result() -> Result<i64, Error> {
  let i = func1().ok_or(Error::Func1)?;
  let j = func2(i).ok_or(Error::Func2)?;
  let k = func3(j).ok_or(Error::Func3)?;
  func4(k).ok_or(Error::Func4)
}

fn main() {
  use Error::*;
  match get_result() {
    Ok(result) => {},
    Err(Func1) => {},
    // ...
  }
}

If-let chaining will make this a lot nicer, but for now (assuming you don't want to use nightly), it's possible a slight refactor could help. For example, pulling all but the last call of the chain into its own function allows you to use the ? operator:

fn get_result() -> Option<u64> {
  let i = func1()?;
  let j = func2(i)?;
  let k = func3(j)?;
  func3(k)
}

fn main() {
  if let Some(result) = get_result() {
    // do something
  }
}

If you need more fine-grained control over the error cases, you could return a Result instead:

enum Error {
  Func1,
  Func2,
  Func3,
  Func4,
}

fn get_result() -> Result<i64, Error> {
  let i = func1().ok_or(Error::Func1)?;
  let j = func2(i).ok_or(Error::Func2)?;
  let k = func3(j).ok_or(Error::Func3)?;
  func4(k).ok_or(Error::Func4)
}

fn main() {
  use Error::*;
  match get_result() {
    Ok(result) => {},
    Err(Func1) => {},
    // ...
  }
}
痴梦一场 2025-01-17 00:54:33

我想在您的“最终研究”列表中再添加两件事:简单的 Option::and_then

let result = func1().and_then(func2).and_then(func3).and_then(func4);
match result {
  Some(result) => …,
  None => …,
}

以及稍微棘手但非常方便的 anyhow::Context :

use anyhow::Context;
let j = func2(i).context("Func2 failed")?;

I'd like to put two more things on your list of "eventually look into" things: The simple Option::and_then:

let result = func1().and_then(func2).and_then(func3).and_then(func4);
match result {
  Some(result) => …,
  None => …,
}

And the slightly more tricky, but incredibly convenient anyhow::Context:

use anyhow::Context;
let j = func2(i).context("Func2 failed")?;
漆黑的白昼 2025-01-17 00:54:33

可以使用 if let ... else { return } 的稍微好一点的版本:

let i = if let Some(i) = func1() { i } else {
   println!("func 1 returned None");
   return;
};
let j = if let Some(j) = func2(i) { j } else {
   println!("func 2 returned None");
   return;
};
let k = if let Some(k) = func3(j) { k } else {
   println!("func 3 returned None");
   return;
};
let result = if let Some(result) = func3(k) { result } else {
   println!("func 4 returned None");
   return;
};

A slightly better version of the if let ... else { return } can be used:

let i = if let Some(i) = func1() { i } else {
   println!("func 1 returned None");
   return;
};
let j = if let Some(j) = func2(i) { j } else {
   println!("func 2 returned None");
   return;
};
let k = if let Some(k) = func3(j) { k } else {
   println!("func 3 returned None");
   return;
};
let result = if let Some(result) = func3(k) { result } else {
   println!("func 4 returned None");
   return;
};
我ぃ本無心為│何有愛 2025-01-17 00:54:33

我有一个非常相似的问题,@Caesar 的解决方案对我帮助很大。

我想补充一下,在我的例子中,我需要所有涉及的函数的结果,并且我已经修改了该代码以通过这种方式实现这一点:

let Some((first, Some(second)) = a.func().and_then(|first| Some((first, b.func(second))) {
    result += c.func(first, second);
}

缺点是它最终可能会产生大量嵌套的 Some 对于长链,但就我而言,我只有两个级别,并且这个解决方案似乎非常有效。

PS:cargo Clippy建议使用map而不是and_then,在这种情况下确实更短更干净:)

I had a very similar issue and the solution of @Caesar helped me a lot.

I would add in my case I need the results of all the functions involved, and I've modified that code to achieve that in this way:

let Some((first, Some(second)) = a.func().and_then(|first| Some((first, b.func(second))) {
    result += c.func(first, second);
}

The down side is it could end up by having a lot of nested Some for long chains, but in my case I only have two levels and this solution seems to be quite efficient.

PS: cargo clippy suggested to use map instead of and_then, which is indeed shorter and cleaner in this case :)

◇流星雨 2025-01-17 00:54:33

当我们等待 if-let-chains 稳定时,还有一个替代方案:

if let (Some(foo), Some(bar)) = (try_get_foo(), try_get_bar()) {
    foo(bar);
}

还有 if_chain板条箱:

fn get_argument_fmtstr_parts(expr: &Expr) -> Option<(InternedString, usize)> {
    if_chain! {
        if let ExprAddrOf(_, ref expr) = expr.node; // &["…", "…", …]
        if let ExprArray(ref exprs) = expr.node;
        if let Some(expr) = exprs.last();
        if let ExprLit(ref lit) = expr.node;
        if let LitKind::Str(ref lit, _) = lit.node;
        then {
            return Some((lit.as_str(), exprs.len()));
        }
    }
    None
}

An alternative while we wait for if-let-chains to be stabilized:

if let (Some(foo), Some(bar)) = (try_get_foo(), try_get_bar()) {
    foo(bar);
}

There's also the if_chain crate:

fn get_argument_fmtstr_parts(expr: &Expr) -> Option<(InternedString, usize)> {
    if_chain! {
        if let ExprAddrOf(_, ref expr) = expr.node; // &["…", "…", …]
        if let ExprArray(ref exprs) = expr.node;
        if let Some(expr) = exprs.last();
        if let ExprLit(ref lit) = expr.node;
        if let LitKind::Str(ref lit, _) = lit.node;
        then {
            return Some((lit.as_str(), exprs.len()));
        }
    }
    None
}
~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文