Confusing @ Pattern matching

Why does the following code compile, but not after removing the comment.

fn main() {
    let msg = Message::World { id: 123 };
    match msg {
        x @ Message::World { id, .. } => println!("{x:?} {id}"),
        x => println!("{x:?}"),
    }
}

#[derive(Debug)]
enum Message {
    // Hello { a: i32 },
    World { id: i32 },
}
4 Likes

According to the Rust docs:

By default, match statements consume all they can, which can sometimes be a problem, when you don’t really need the value to be moved and owned.

i.e. in this piece of code

match msg {
    x @ Message::World { id, .. } => println!("{x:?} {id}"),
    x => println!("{x:?}"),
}

ownership of the enum msg: Message is moved into the first arm and msg will be dropped after you destructure it and extract your id. This means msg is no longer available in other match arms.

This doesn't cause a problem when your enum only has one variant, because the second match arm is unreachable, but when you add the Hello variant to the Message enum, the compiler will complain about a value being used after it's moved.

One possible solution is to match msg by reference rather than take ownership, i.e.

match msg {
    ref x @ Message::World { id, .. } => println!("{x:?} {id}"),
    ref x @ Message::Hello { a, .. } => println!("{x:?} {a}"),
}

Another would be derive Copy and Clone on your enum, which would allow Rust to make a copy of msg and take ownership of the copy.

4 Likes

Nop, borrow checks between different branches in pattern matching should not affect each other.

1 Like

It seems that with an irrefutable pattern, the compiler does a partial move, but with a refutable pattern it doesn't. Not sure why, maybe someone with more compiler knowledge can explain. With a partial move, only the id is copied out and there's no problem accessing the enum afterwards, but without a partial move it's a bit like both sides of the @ are trying to take ownership of the whole struct and that's not allowed.

fn main() {
    if let e @ E::A(s) = { E::A(String::new()) } {}
}

enum E {
    A(String),
    B,
}
error[E0382]: use of moved value
 --> src/main.rs:2:21
  |
2 |     if let e @ E::A(s) = { E::A(String::new()) } {}
  |            -        ^    ----------------------- move occurs because value has type `E`, which does not implement the `Copy` trait
  |            |        |
  |            |        value used here after move
  |            value moved here

vs

fn main() {
    if let e @ E::A(s) = { E::A(String::new()) } {}
}

enum E {
    A(String),
}
error[E0382]: use of partially moved value
 --> src/main.rs:2:12
  |
2 |     if let e @ E::A(s) = { E::A(String::new()) } {}
  |            ^        - value partially moved here
  |            |
  |            value used here after partial move
6 Likes

I seem that "the matching order" was different .

For irrefutable @ pattern x @ World { id: y, .. }, it will be matched as World { id, .. } first, copy it id to y, then bind(move) it to x.

But for normal pattern matching, it will firstly bind it to x, then match World { id: y, .. }(match after move).

1 Like

Looks to be the issue below. Note that the title is inaccurate: it's a Rust 1.56+ feature, not an edition specific feature. How the feature should work is underspecified IMO.

5 Likes

as a result, this feature (and the borrow checking rules around it) is very well tested.

For the linked comment[1], I would rather say the feature is not well tested (or very little specified).

And I raised a type inconsistency problem on @ binding which was closed as not a bug. Rust Playground


  1. and a bit annoying to see a dead link there :frowning: ↩︎

2 Likes