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.