在模式匹配过程中防止移动语义

17
我这里有一个愚蠢的例子,只是为了演示我在使用另一个库和模式匹配时遇到的问题。
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);
}

现在的问题似乎是在模式匹配过程中,移动语义会介入并接管我Person中的内部结构(Thing)。
当我试图将这个人传递给下一个方法时,由于已经部分移动,我无法这样做。
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`.

我的直觉告诉我,我需要通过使用引用或借用来阻止Rust移动该值。在这种情况下,我可以改变我的方法签名为借用,但是对于某些库,你并不总是能够这样做。(在这种情况下,我正在尝试处理hyper...)
有没有办法让match在匹配过程中使用引用而不是移动值?谢谢!
2个回答

27

为什么?

当您创建元组时:

(p.choice, p.age)

你需要从你的 Person 中使用 memcpy 复制 p.choicep.age。对于 p.age 来说这样做没有问题,因为它是一个 Copy 类型 - 在从中复制后,您可以继续使用旧值。

p.choices 是类型为 Choices 的枚举类型,它不是 Copy 类型。这意味着memcpy被视为“移动”,因此旧值不可用。这意味着 p 处于无效状态,因此您不能在其上调用 announce

解决方案#1

由于 Choices 是一个平凡的 enum,所以你可以简单地使用 #[derive(Copy, Clone)]。这意味着您可以继续使用旧的 p.choices

如果只能安全地将 Choices 设为Clone,那么您必须在match中进行 clone 操作。

解决方案#2

您可以通过引用获取 p.choices

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

之所以这只能起作用,是因为&Choices::Good是一个精确匹配,因此可以放弃borrow。如果你使用的是

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

如果调用announce(p),那么借用将仍然是活动状态,因此操作将失败 - 移动将使活动借用的变量无效。

注释

这里做了很多移动 - 传递一些引用会更加灵活!没有理由让 announce 消耗一个 Person - 它只需要查看它一段时间。当你可以使用引用时,按值获取是不可取的,这只适用于小的Copy类型。

请注意,让announce接受引用意味着match也允许在p内部保留引用,这使得它更广泛适用。

to_string主要用于非字符串对象。 intoto_owned速度更快,into也更短。

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

非常感谢您详细的解释 :) - Mathieu David
在 hyper(提到的库)中,枚举类型不可复制是一个问题。当底层库遗漏某些内容时,借用技巧是一个很好的解决方案。谢谢! - jocull

1

所以我尝试使用新的代码更改 :)

在您当前的代码中,如果您在匹配中使用借用而不是移动,它可以工作。

p.age 不需要这样做,因为它是一个原始类型,原始类型实现了 Copy trait

但是,选项不实现复制特性,因此它们在匹配中被移动。这导致当您调用 announce() 时它们不可用。

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

它解决了关于部分移动的错误。我猜这是因为你在匹配中移动了choice。但是,choice是Person的一部分,所以它被部分移动了。
我对Rust的知识不足以真正解释它为什么有效,如果您能添加有用的内容,请添加。

我对原始示例进行了一些修改,以使非复制项更清晰(在新示例中的枚举)。我试图更好地理解模式匹配如何获取所有权而不是修改基础代码。其中一些是库代码,我无法更改访问权限。 - jocull
1
@jocull,我针对你的更新修改了我的答案,希望有所帮助。 - Mathieu David

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接