首页 文章

是否可以使类型只能移动而不能复制?

提问于
浏览
92

编者注:在Rust 1.0之前问过这个问题,问题中的一些断言在Rust 1.0中不一定正确 . 一些答案已更新,以解决这两个版本 .

我有这个结构

struct Triplet {
    one: i32,
    two: i32,
    three: i32,
}

如果我将它传递给函数,则会隐式复制它 . 现在,有时我读到某些值不可复制,因此必须移动 .

是否有可能使这个结构 Triplet 不可复制?例如,是否可以实现一个特性,使 Triplet 不可复制,因此"movable"?

我读到某个地方必须实现 Clone trait来复制那些不可隐式复制的东西,但我从来没有读过相反的东西,那就是有一些隐式可复制的东西,并使它不可复制,以便它移动 .

这甚至有意义吗?

2 回答

  • 6

    最简单的方法是在您的类型中嵌入不可复制的内容 .

    标准库为此用例提供了"marker type":NoCopy . 例如:

    struct Triplet {
        one: i32,
        two: i32,
        three: i32,
        nocopy: NoCopy,
    }
    
  • 161

    前言:这个答案是在opt-in built-in traits之前编写的 - 特别是the Copy aspects - 已经实现了 . 我已经使用块引号来指示仅应用于旧方案的部分(在询问问题时应用的部分) .


    旧:要回答基本问题,可以添加存储NoCopy值的标记字段 . 例如 . struct Triplet {
    一:int,
    二:int,
    三:int,
    _marker:NoCopy
    }
    您也可以通过使用析构函数(通过实现Drop特征)来实现,但如果析构函数不执行任何操作,则首选使用标记类型 .

    类型现在默认移动,也就是说,当您定义新类型时,它不会实现 Copy ,除非您为您的类型明确实现它:

    struct Triplet {
        one: i32,
        two: i32,
        three: i32
    }
    impl Copy for Triplet {} // add this for copy, leave it out for move
    

    只有当新 structenum 中包含的每个类型本身都是 Copy 时,才能存在实现 . 如果没有,编译器将打印错误消息 . 它也可以仅在类型没有 Drop 实现时存在 .


    要回答你没有问过的问题......“移动和复制有什么用?”:

    首先,我将定义两个不同的“副本”:

    • 一个字节拷贝,它只是逐字节地复制一个对象,而不是跟随指针,例如如果你有 (&usize, u64) ,它在64位计算机上是16字节,浅拷贝将占用这16个字节并在其他一些16字节的内存块中复制它们的值,而不触及另一端的 usize . & . 也就是说,它相当于调用 memcpy .

    • 一个语义副本,复制一个值以创建一个新的(有点)独立实例,可以安全地单独使用到旧实例 . 例如 . Rc<T> 的语义副本只涉及增加引用计数,而 Vec<T> 的语义副本涉及创建新分配,然后将每个存储元素从旧文件复制到新文件 . 这些可以是深拷贝(例如 Vec<T> )或浅(例如 Rc<T> 不触及存储的 T ), Clone 被宽松地定义为从 &T 内部语义复制 T 类型值到 T 所需的最小工作量 .

    Rust就像C一样,每个值的使用都是一个字节副本:

    let x: T = ...;
    let y: T = x; // byte copy
    
    fn foo(z: T) -> T {
        return z // byte copy
    }
    
    foo(y) // byte copy
    

    它们是字节副本,无论是 T 移动还是"implicitly copyable" . (要明确的是,它们不会被保留 . )

    然而,字节副本存在一个基本问题:你最终会在内存中出现重复值,如果它们具有析构函数,则可能非常糟糕,例如:

    {
        let v: Vec<u8> = vec![1, 2, 3];
        let w: Vec<u8> = v;
    } // destructors run here
    

    如果 w 只是 v 的一个普通字节副本,那么将会有两个向量指向相同的分配,两个都有析构函数释放它...导致a double free,这是一个问题 . NB . 如果我们将 v 的语义副本转换为 w ,那将完全没问题,因为那时 w 将是它自己的独立 Vec<u8> ,并且析构函数不会相互踩踏 .

    这里有一些可能的修复:

    • 让程序员像C一样处理它 . (那里's no destructors in C, so it'并没有那么糟糕......你只是留下了内存泄漏 . :P)

    • 隐式执行语义复制,以便 w 有自己的分配,就像C及其复制构造函数一样 .

    • 将按值使用视为所有权转移,以便不再使用 v 且不运行析构函数 .

    最后一个是Rust所做的:移动只是一个按值使用,其中源是静态无效的,因此编译器阻止进一步使用现在无效的内存 .

    let v: Vec<u8> = vec![1, 2, 3];
    let w: Vec<u8> = v;
    println!("{}", v); // error: use of moved value
    

    有类型析构函数必须在按值使用时移动(也就是在复制字节时),因为它们具有某些资源的管理/所有权(例如内存分配或文件句柄),并且字节副本不太可能正确复制此所有权 .

    “嗯......什么是隐含的副本?”

    想想像 u8 这样的原始类型:字节副本很简单,只需复制单个字节,语义副本就像复制单个字节一样简单 . 特别是,字节副本是一个语义副本...... Rust甚至有一个built-in trait Copy,它捕获哪些类型具有相同的语义和字节副本 .

    因此,对于这些 Copy 类型,按值使用也是自动语义副本,因此继续使用源是完全安全的 .

    let v: u8 = 1;
    let w: u8 = v;
    println!("{}", v); // perfectly fine
    

    旧:NoCopy标记覆盖编译器的自动行为,即假设可以复制的类型(即仅包含基元和&的聚合)是复制 . 但是,当实施选择加入的内置特征时,这将会改变 .

    如上所述,实现了opt-in内置特征,因此编译器不再具有自动行为 . 但是,过去用于自动行为的规则与检查实现 Copy 是否合法的规则相同 .

相关问题