Quick little confusion or even foot-gun I ran into (while working on the challenge I posed earlier).
TLDR
My understanding of what I ran into here:
- Matching on multiple variables simultaneously requires assigning them to a tuple (?),
- which happens more or less implicitly (?),
- and which takes ownership of said variables.
- This ownership doesn’t occur when matching against a single variable (?)
- Depending on the variables and what’s happening in the match arms this difference can be the difference between compiling and not.
Anyone got insights they’re willing to share??
Intro
I had some logic that entailed matching on two variables. Instead of having two match statements, one nested in the other, I figured it’d be more elegant to match on both simultaneously.
An inline tuple seemed the obvious way to do so and it works, but it seems that the tuple creates some ownership problems I didn’t anticipate, mostly because I was thinking of it as essentially syntax and not an actual tuple variable.
As you’ll see below, the same logic with nested match statements each on a single variable doesn’t suffer from the same issues.
Demo Code
fn main() {
// # Data structures
enum Kind {
A,
B
}
struct Data {
kind: Kind
}
// # Implementation
let data = vec![Data{kind: Kind::A}];
// ## Basic idea: process two adjacent data points
let prev_data = data.last().unwrap();
let new_data = Data{kind: Kind::B};
// --- MATCH STATEMENTS ---
// ## This works: match on one then the other
let next_data = match prev_data.kind {
Kind::A => match new_data.kind {
Kind::A => 1,
Kind::B => 2,
},
Kind::B => match new_data.kind {
Kind::A => 3,
Kind::B => 4,
},
};
// ## This does NOT work: match on both
let next_data2 = match (prev_data.kind, new_data.kind) {
(Kind::A, Kind::A) => 1,
(Kind::A, Kind::B) => 2,
(Kind::B, Kind::A) => 3,
(Kind::B, Kind::B) => 4,
};
}
The Error
The error is on the line let next_data = match (prev_data.kind, new_data.kind)
, specifically the tuple and its first element prev_data.kind
, with the error:
error[E0507]: cannot move out of `prev_data.kind` which is behind a shared reference
move occurs because `prev_data.kind` has type `Kind`, which does not implement the `Copy` trait
The Confusion
So prev_data.kind
needs to be moved. That’s ok. Borrowing it with (&prev_data.kind, ...)
fixes the problem just fine, though that can cause issues if I then want to move the variable within the match statement, which was generally the idea of the logic I was trying to write.
What got me was that the same logic but with nested match statements works just fine.
I’m still not clear on this, but it seems that the inline tuple in the second tuple-based approach is a variable that takes ownership of the variables assigned to it. Which makes perfect sense … my simple mind just thought of it as syntax for interleaving multiple match statements I suppose. In the case of nested match statements however, I’m guessing that each match statement is its own scope.
The main thing I haven’t been able to clarify is what are the ownership dynamics/behaviours of match statements?? It seems that there’s maybe a bit happening implicitly here?? I haven’t looked super hard but it does seem like something that’s readily glossed over in the materials I’ve seen thus far??
General Implications
AFAICT:
- if you want to match on two or more variables simultaneously, you’ll probably need borrow them in the match statement if they’re anything but directly owned variables.
- If you want to then use or move them in the match arms you may have to wrangle with ownership or just use nested match statements instead (or refactor your logic/implementation).
- So probably don’t do multi-variable matching unless the tuple of variables is a variable native to the logic?
If I understood correctly, the first
match
expression doesn’t take the ownership of theprev_data.kind
because theprev_data.kind
is a place expression:https://doc.rust-lang.org/stable/reference/expressions.html#place-expressions-and-value-expressions
https://doc.rust-lang.org/stable/reference/expressions/match-expr.html#match-expressions
I’m not sure what “a by-value binding may copy or move from the memory location” does mean, but I beleive no allocation means no move.
For the second
match
, move happens. The tuple(prev_data.kind, new_data.kind)
tries to take an ownership of theprev_data.kind
, but theprev_data
is&Data
(borrowed from the vecdata
), so the tuple can’t take the ownership.Thanks!!!
Yea this matches (heh) where I got up to, including that there’s some “smart” stuff happening under the hood regarding how exactly the “scrutinee” (variable being matched on) is passed into the March statement.
But I was not aware of the place/value expression distinction, terminologically at least, and those links are very helpful (I’d looked into the reference a bit but it felt like a rabbit hole).
Additionally, I think this line from your second link is relevant:
So whenever you’re using a tuple as I showed above, I’d guess that it’s the tuple that enters the arm’s block and not any bound variables directly, which disturbs the ownership flow.