Preventing move semantics during pattern matching

2019-06-21 04:19发布

问题:

I have a silly example here, just to demonstrate an issue I'm running into with another library and pattern matching.

struct Person {
    name: String,
    age: i32,
    choice: Choices
}

#[derive(Debug)]
enum Choices {
    Good,
    Neutral,
    Evil
}

fn find(p: Person) {
    match (p.choice, p.age) {
        (Choices::Good, a) if a < 80 => {
            announce(p);
        }
        (_, a) if a >= 80 => {
            println!("You're too old to care.");
        }
        _ => {
            println!("You're not very nice!")
        }
    }
}

fn announce(p: Person) {
    println!("Your name is {}. You are {:?}.", p.name, p.choice);
}

fn main() {
    let p = Person {
                name: "Bob".to_string(),
                age: 20,
                choice: Choices::Good
            };
    find(p);
}

Now the issue seems to be that during pattern matching, move semantics will kick in and take ownership over the inner struct (Thing) in my Person.

When I go to move the person on to the next method, I can't because it's been partially moved.

Compiling match v0.1.0 (file:///home/jocull/Documents/Projects/Rust/learn/match)
src/main.rs:17:13: 17:14 error: use of partially moved value: `p`
src/main.rs:17          announce(p);
                                 ^
src/main.rs:15:9: 15:17 note: `p.choice` moved here because it has type `Choices`, which is non-copyable
src/main.rs:15  match (p.choice, p.age) {
                       ^~~~~~~~
error: aborting due to previous error
Could not compile `match`.

My gut says I need to get Rust to stop moving the value by using a reference or borrow of some kind. In this case I could change my method signature to borrow, but with some libraries you aren't always able to do that. (I am trying to deal with hyper in this case...)

Is there a way to get the match to use references during matching instead of moving the values? Thank you!

回答1:

Why?

When you make the tuple

(p.choice, p.age)

you memcpy both p.choice and p.age from your Person.

It's OK to do this for p.age because it's a Copy type - you can continue using the old value after memcpying from it.

p.choices is of type Choices which is not Copy. This means that the memcpy is treated as a "move", so the old value is not usable. This means p is in an invalid state, so you can't call announce on it.

Solution #1

Since Choices is a trivial enum, you can just #[derive(Copy, Clone)]. This means that you are allowed to continue using the old p.choices.

If you can only safely make Choices Clone, then you'd have to clone it in the match instead.

Solution #2

You can take p.choices by reference:

match (&p.choice, p.age) {
    (&Choices::Good, a) if a < 80 => {
        announce(p);
    }
    ...
}

This only works because &Choices::Good is an exact match so the borrow can be relinquished. If you had instead

match (&p.choice, p.age) {
    (&x, a) if a < 80 => {
        announce(p);
    }
    ...
}

the borrow would still be active and so the move when calling announce(p) would fail - the move would invalidate an active borrowed variable.

Notes

You're doing an awful lot of moving here - passing a few references is a lot more flexible! There's no reason for announce to consume a Person - it just needs to look at it for a bit. Taking by value when you could take a reference is only advisable for small Copy types.

Note that having announce take a reference means that the match is allowed to also be holding on to references inside p, which makes it more widely applicable.

to_string is mostly for use for non-string objects. into and to_owned are faster and into is also a lot shorter.

struct Person {
    name: String,
    age: i32,
    choice: Choices
}

#[derive(Copy, Clone, Debug)]
enum Choices {
    Good,
    Neutral,
    Evil
}

fn find(p: &Person) {
    match (p.choice, p.age) {
        (Choices::Good, a) if a < 80 => {
            announce(p);
        }
        (_, a) if a >= 80 => {
            println!("You're too old to care.");
        }
        _ => {
            println!("You're not very nice!")
        }
    }
}

fn announce(p: &Person) {
    println!("Your name is {}. You are {:?}.", p.name, p.choice);
}

fn main() {
    let p = Person {
        name: "Bob".into(),
        age: 20,
        choice: Choices::Good
    };

    find(&p);
}


回答2:

So I try again with the new code change :)

In your current code, if you use borrowing instead of moving in the match it works.

p.age doesn't need that only because it's a primitive type and primitive types implement the Copy trait

But Choices do not implement the copy trait and so they are moved in the match. Which causes them to not be available when you call announce()

match (&p.choice, p.age) {
        (&Choices::Good, a) if a < 80 => {
            announce(p);
        }
        ...
}

It get rids of the error about partial move. I guess it is because you moved choice in the match. But choice is a part of Person so it's partially moved.

I have not enough knowledge of Rust to truly explain why it works, so if you can add something useful, please do