可否在可变引用的枚举值之间切换变体?

14

你是否可以在不加额外约束和不使用不安全代码的情况下,切换可变引用(&mut E<T>)中变量的变体?

也就是说,给定一个枚举:

enum E<T> {
    VariantA(T),
    VariantB(T)
}

这应该怎么写才是正确的呢:


let x: E<???> = E::VariantA(??);
change_to_variant_b(&mut x);
assert_eq!(x, E::VariantB(??));

1
看起来这是由关闭的replace_with RFC(#1736)所解决的问题。 - kennytm
4
作为一种替代方案,将其更改为具有T和普通enumstruct在你的情况下是否可行? - Chris Emerson
@ChrisEmerson 很有可能会这样。我只是认为,变体应该有一种方式可以承担其兄弟姐妹的内容,因为这种方式从直觉上来看并不违反Rust的所有权模型。 - Doe
我不确定这个问题是否足够接近原问题以值得回答,但如果你愿意接受一个不包含 T 的变量,并容忍代码中的 "不可能" 路径,我已经在这里编写了一个安全、稳定的 change_to_variant_b 版本(链接在此:https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=f731602e09c7f8be91fd4a21d8c0e7ca),使用了 std::mem::replace - Ryan1729
3个回答

8

我会大胆猜测不行


只需要对签名进行小幅度的更改,就可以实现。

fn change_to_variant_b<T>(e: E<T>) -> E<T> {
    match e {
        E::VariantA(t) => E::VariantB(t),
        E::VariantB(t) => E::VariantB(t),
    }
}

可以使用unsafe来实现:

fn change_to_variant_b<T>(e: &mut E<T>) {
    use std::ptr;

    unsafe {
        match ptr::read(e as *const _) {
            E::VariantA(t) => ptr::write(e as *mut _, E::VariantB(t)),
            E::VariantB(t) => ptr::write(e as *mut _, E::VariantB(t)),
        }
    }
}

通过使用额外的范围(DefaultClone),这是可能的:

fn change_to_variant_b<T: Default>(e: &mut E<T>) {
    match std::mem::replace(e, E::VariantA(T::default())) {
        E::VariantA(t) => e = E::VariantB(t),
        E::VariantB(t) => e = E::VariantB(t),
    }
}

感谢您提供清晰的答案并详细列出了替代方案。我需要 &mut E<T>,因为我正在可变地迭代一个切片,所以我不能(?)暂时将元素移出它以使第一种解决方案起作用。 - Doe
另外,作为一种实用的解决方案,请参见https://dev59.com/S1gQ5IYBdhLWcg3wo1lE#frGhEYcBWogLw_1b8auG。 - Doe
@Doe:哦,如果Chris的解决方案适合你,那就一定采用它。在处理缩小的示例时,往往很难想出替代方案,因为不清楚可以更改和不能更改的边界在哪里。 - Matthieu M.
也许“解决方案”不是最好的词来描述Chris的评论(因为它完全回避了问题)。然而,对于大多数寻求解决方案的访客来说,Chris的评论可能已经足够了。"No"可能是唯一符合OP标准的答案。 - Doe
@Doe: 是的,Chris,就像我的一样,都是更多的解决方法。如果你 (1) 被困在 enum 中,(2) 被困在函数签名中,(3) 没有不安全或无边界......那么你就无法解决它。 - Matthieu M.
我从谷歌来到这个问题,为什么没有人建议在“self”上使用std::mem::replace - kalkronline

2

作为这种情况的替代方案,您可以考虑改为使用一个具有 T 和普通枚举的结构体:

struct Outer<T> {
    val: T,
    kind: Inner,
}

impl<T> Outer<T> {
    fn promote_to_b(&mut self) {
        self.kind.promote_to_b()
    }
}

enum Inner {
    VariantA,
    VariantB,
}

impl Inner {
    fn promote_to_b(&mut self) {
        if let Inner::VariantA = *self {
            *self = Inner::VariantB;
        }
    }
}

1

可否在可变引用的值中切换变体

正如Matthieu M.所说,一般来说是“不行”的。原因是这样做会让枚举处于不确定状态,从而允许访问未定义的内存,从而破坏Rust的安全保证。例如,假设此代码编译无误:

impl<T> E<T> {
    fn promote_to_b(&mut self)  {
        if let E::VariantA(val) = *self {
            // Things happen
            *self = E::VariantB(val);
        }
    }
}

问题在于,一旦将值从self移动到val中,应该怎样处理代表selfT的内存?
如果我们复制了位,然后在“发生事情”中发生了恐慌,那么valself内部的T的析构函数都会运行,但由于它们指向相同的数据,这将导致双重释放。
如果我们没有复制位,那么您无法安全地访问“发生事情”中的val,这将有一些用处。

按值传递的解决方案可行,因为编译器可以跟踪谁应该调用析构函数:即函数本身。一旦进入函数,编译器就知道哪些特定行可能需要释放该值,并在出现紧急情况时正确地调用它们。

CloneDefault解决方案可行,因为您从未将值移出原始枚举。相反,您可以用虚拟值替换原始枚举并获取原始枚举的所有权(使用Default),或复制整个原始值(使用Clone)。


replace_with RFC (#1736)提议添加一种方法,使其能够在确保正确的内存语义的同时工作,但是该RFC未被接受。


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