为什么 Rust 中的 assert_eq! 使用 match 实现?

55

这是 Rust 的 assert_eq! 宏实现。 由于篇幅太长,我仅复制了第一个分支:

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有什么作用?为什么不检查非等性就不够了?


2
看起来它正在评估表达式。 - erip
2个回答

64

好的,让我们删除这场比赛。

    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只是一种快速、简便的方法,用于对宏的“参数”进行精确的一次性评估,并将它们绑定到变量名上。


12
用类似 let left = $left; 这样的语句不就可以实现这个吗? - Jeroen
4
@JeroenBollen,这与在边缘情况下出现的匹配有些微小的区别,但我不记得详细情况了。我认为这与暂时对象的确切生存期有关。由于我不确定,所以避免提及它。 - DK.
45
在Rust中,match expr { x => {...} }是等同于let x = expr in ...的语法,它与生命周期临时变量相对较为宽松,并且应该始终有效,例如match e { x => f(x) }应该始终等同于f(e)。另一方面,let x = expr;则更加"命令式",不允许保留嵌套的临时变量,例如let x = f(&g());只有在f使用其引用参数而不返回它时才会编译通过,因为它所指向的临时变量只在函数调用期间存在。 - eddyb
4
以前我找不到,但这是一篇关于临时变量寿命的博客文章之一(请注意,它可能并不完全反映当前 Rust 的情况):http://smallcultfollowing.com/babysteps/blog/2014/01/09/rvalue-lifetimes-in-rust/ - eddyb
附注:代码的格式是这样的,因为它是一个带有嵌入式前导空格的多行原始字符串文字的中间部分。缩进文本会改变其打印方式。 - DK.

14

使用match可以确保表达式$left$right只被评估一次,并且在它们的评估过程中创建的任何临时对象至少存在与绑定结果leftright相同的时间。

如果展开在比较时和插入到错误消息中再次使用$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中,语句内产生的临时值的生命周期通常仅限于该语句本身。因此,该扩展是一个错误

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::<Vec<_>>() 的生命周期至少应与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,但它不会改变任何值的生命周期 - 即它们何时被丢弃。它只使得借用的作用域更加精确。因此,它在这种情况下不能帮助我们。


这个回答对已有的回答有什么补充呢?已有的回答已经说明了“正如你所看到的,我们调用了两次函数”。 - Shepmaster
10
在我的看法中,另一个答案基本上是错误的,或者至少忽略了使用“match”的主要原因。可以通过“let left = $left; let right = $right;”来避免双重计算。我认为,这篇文章的关键是:为什么那样做不起作用? - John
1
我完全同意这种看法。在阅读了其他答案后,我也对为什么要使用match而不是let感到困惑。这个答案很好地解释了在其他答案的评论线程中提到的内容。感谢您写下这篇文章,@John!(第一条评论有点责备,所以我想留下一些鼓励的话给您。拥有两个答案真的非常有用!) - benesch
1
同感。被采纳的答案只是解释了match为什么有效,但并没有解释为什么它是必要的,正如问题所问。这可能应该成为被采纳的答案。 - Federico

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