Why is Rust's assert_eq! implemented using a match?

ぃ、小莉子 提交于 2021-01-20 15:25:26

问题


Here's Rust's assert_eq! macro implementation. I've copied only the first branch for brevity:

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)
                }
            }
        }
    });
}

What's the purpose of the match here? Why isn't checking for non-equality enough?


回答1:


Alright, let's remove the match.

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

Now, let's pick a completely random example...

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);

This would expand to...

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

As you can see, we're calling the function twice. That's less than ideal, even more so if the result of the function could change each time it's called.

The match is just a quick, easy way to evaluate both "arguments" to the macro exactly once and bind them to variable names.




回答2:


Using match ensures that the expressions $left and $right are each evaluated only once, and that any temporaries created during their evaluation live at least as long as the result bindings left and right.

An expansion which used $left and $right multiple times -- once while performing the comparison, and again when interpolating into an error message -- would behave unexpectedly if either expression had side effects. But why can't the expansion do something like let left = &$left; let right = &$right;?

Consider:

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

Suppose this expanded to:

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

In Rust, the lifetime of temporaries produced within a statement is generally limited to the statement itself. Therefore, this expansion 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

The temporary vals.collect::<Vec<_>>() needs to live at least as long as left, but in fact it is dropped at the end of the let statement.

Contrast this with the expansion

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

This produces the same temporary, but its lifetime extends over the entire match expression -- long enough for us to compare left and right, and interpolate them into the error message if the comparison fails.

In this sense, match is Rust's let ... in construct.

Note that this situation is unchanged with non-lexical lifetimes. Despite its name, NLL does not change the lifetime of any values -- i.e. when they are dropped. It only makes the scopes of borrows more precise. So it does not help us in this situation.



来源:https://stackoverflow.com/questions/48732263/why-is-rusts-assert-eq-implemented-using-a-match

标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!