【问题标题】:Why is Rust's assert_eq! implemented using a match?为什么是 Rust 的 assert_eq!使用匹配实现?
【发布时间】:2018-02-11 13:57:05
【问题描述】:

这是 Rust 的 assert_eq! macro implementation。为简洁起见,我只复制了第一个分支:

macro_rules! assert_eq {
    ($left:expr, $right:expr) => ({
        match (&$left, &$right) {
            (left_val, right_val) => {
                if !(*left_val == *right_val) {
                    panic!(r#"assertion failed: `(left == right)`
  left: `{:?}`,
 right: `{:?}`"#, left_val, right_val)
                }
            }
        }
    });
}

match 的用途是什么?为什么检查不相等性还不够?

【问题讨论】:

  • 看起来它正在评估表达式。
  • 主要动机似乎是延长在 match 语句中创建的临时值的生命周期以生成 assert_eq!更有用的是,请参阅此提交:github.com/rust-lang/rust/commit/d3c831ba4a4 - 我没有回答,因为我无法解释为什么它有效(还)。请参阅此游乐场链接进行实验:play.rust-lang.org/…

标签: rust


【解决方案1】:

好的,让我们删除匹配项。

    macro_rules! assert_eq_2 {
        ($left:expr, $right:expr) => ({
            if !($left == $right) {
                panic!(r#"assertion failed: `(left == right)`
  left: `{:?}`,
 right: `{:?}`"#, $left, $right)
            }
        });
    }

现在,让我们选择一个完全随机的例子......

fn really_complex_fn() -> i32 {
    // Hit the disk, send some network requests,
    // and mine some bitcoin, then...
    return 1;
}

assert_eq_2!(really_complex_fn(), 1);

这将扩展为...

{
    if !(really_complex_fn() == 1) {
        panic!(r#"assertion failed: `(left == right)`
  left: `{:?}`,
 right: `{:?}`"#, really_complex_fn(), 1)
    }
}

如您所见,我们调用了函数两次。这不太理想,如果每次调用函数的结果都可能发生变化,则更是如此。

match 只是一种快速、简单的方法,可以对宏的两个“参数”只求值一次并将它们绑定到变量名。

【讨论】:

  • let left = $left; 之类的东西不可能吗?
  • @JeroenBollen 这与在边缘情况下出现的匹配略有不同,但我不记得细节了。我认为这与临时工的确切寿命有关。由于我不知道,所以我避免提及它。
  • 确实,match expr { x => {...} }let x = expr in ... 的 Rust 等价物,因为它在生命周期临时性方面是最轻松的,并且“应该始终有效”,例如match e { x => f(x) } 应始终等同于 f(e)。另一方面,let x = expr; 更加“势在必行”,不允许保留嵌套的临时对象,例如let x = f(&g()); 仅在 f 使用其引用参数而不返回它时编译,因为它指向的临时变量仅在调用期间存在。
  • 我之前找不到,但这是关于临时对象生命周期的博文之一(注意它可能不完全反映当前的 Rust):smallcultfollowing.com/babysteps/blog/2014/01/09/…
  • 旁白:代码的格式是这样的,因为它是带有嵌入前导空格的多行原始字符串文字的中间。缩进文本会改变它的打印方式。
【解决方案2】:

使用match 可确保表达式$left$right 仅被评估一次,并且在其评估期间创建的任何临时对象至少与结果绑定@987654326 一样长@和right

多次使用 $left$right 的扩展(一次在执行比较时,再次在插入错误消息时)如果任一表达式有副作用,则会出现意外行为。但是为什么扩展不能做let left = &$left; let right = &$right;之类的事情呢?

考虑:

let vals = vec![1, 2, 3, 4].into_iter();
assert_eq!(vals.collect::<Vec<_>>().as_slice(), [1, 2, 3, 4]);

假设这扩展为:

let left = &vals.collect::<Vec<_>>().as_slice();
let right = &[1,2,3,4];
if !(*left == *right) {
    panic!("...");
}

在 Rust 中,语句中生成的临时对象的生命周期通常仅限于语句本身。因此,本次扩容is an error

error[E0597]: borrowed value does not live long enough
  --> src/main.rs:5:21
   |
5  |         let left = &vals.collect::<Vec<_>>().as_slice();
   |                     ^^^^^^^^^^^^^^^^^^^^^^^^           - temporary value dropped here while still borrowed
   |                     |
   |                     temporary value does not live long enough

临时的vals.collect::&lt;Vec&lt;_&gt;&gt;() 至少需要和left 一样长,但实际上它在let 语句的末尾被删除了。

将此与扩展进行对比

match (&vals.collect::<Vec<_>>().as_slice(), &[1,2,3,4]) {
    (left, right) => {
        if !(*left == *right) {
            panic!("...");
        }
    }
}

这会产生相同的临时值,但它的生命周期会延伸到整个匹配表达式——足够我们比较 leftright,并在比较失败时将它们插入到错误消息中。

在这个意义上,match 是 Rust 的 let ... in 构造。

请注意,non-lexical lifetimes 不会改变这种情况。尽管它的名字,NLL 不会改变任何值的生命周期——即当它们被删除时。它只会使 borrows 的范围更精确。所以在这种情况下它对我们没有帮助。

【讨论】:

  • 这个答案对现有答案有什么补充,现有答案已经说明 如您所见,我们调用了两次函数。?
  • 恕我直言,另一个答案本质上是不正确的,或者至少忽略了使用match主要原因。可以使用let left = $left; let right = $right; 避免双重评估我相信,OP 问题的本质是:为什么这不起作用?
  • 我会支持这种观点。在阅读了另一个答案后,我对为什么使用 match 而不是 let 感到困惑;这个答案很好地完成了另一个答案的评论线程中提到的解释。谢谢你写下来,@John! (第一个评论有点指责,所以我想给你一些鼓励的话。两个答案真的很有用!)
  • 同样的感觉。接受的答案只解释了为什么match 有效,而不是为什么它是必要的,正如问题所问的那样。这应该是公认的答案。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2017-09-04
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2010-10-23
  • 2020-10-14
  • 2018-02-07
相关资源
最近更新 更多