https://inasa.dev/blog/rss.xml

Rust | Clone vs Copy

2023-09-13

Copy的含义

Copy的全名是std::marker::Copy

std::marker模块里面所有的trait都是特殊的trait

它们是跟编译器密切绑定的,impl这些trait对编译器的行为有重要影响。在编译器眼里,它们与其他的trait不一样。这几个trait内部都没有方法,它们的唯一任务是给类型打一个“标记”,表明它符合某种约定——这些约定会影响编译器的静态检查以及代码生成

那么,Copy这个trait在编译器的眼里代表的是什么意思呢?

简单点总结就是,如果一个类型implCopy trait,意味着任何时候,我们都可以通过简单的内存复制(在C语言里按字节复制memcpy)实现该类型的复制,并且不会产生任何内存安全问题。

一旦一个类型实现了Copy trait,那么它在变量绑定、函数参数传递、函数返回值传递等场景下,都是copy语义,而不再是默认的move语义。

最简单的赋值语句x = y来说明move语义和copy语义的根本区别

  • move语义是“剪切、粘贴”操作,变量y把所有权递交给了x之后,y就彻底失效了,后面继续使用y就会出编译错误
  • copy语义是“复制、粘贴”操作,变量y把所有权递交给了x之后,它自己还留了一个副本,在这句赋值语句之后,xy依然都可以继续使用

Rust里,move语义和copy语义具体执行的操作,是不允许由程序员自定义的。

move语义或者copy语义都是执行的memcpy,无法更改,这个过程中绝对不存在其他副作用。

如果考虑后端优化,在许多情况下,不必要的内存复制实际上已经彻底优化掉了,不必担心执行效率的问题。没有必要每次都把move或者copy操作与具体的汇编代码联系起来,因为场景不同,优化结果不同,生成的代码也是不同的。只需记住的是语义。

Copy的实现条件

并不是所有的类型都可以实现Copy traitRust规定,对于自定义类型,只有所有成员都实现了Copy trait,这个类型才有资格实现Copy trait

  • 常见的数字类型、bool类型、共享借用指针&,都是具有Copy属性的类型

  • BoxVec、可写借用指针&mut等类型都是不具备Copy属性的类型

  • 对于数组类型,如果内部的元素类型是Copy,那么这个数组也是Copy类型

  • 对于元组tuple类型,如果每一个元素都是Copy类型,那么这个tuple也是Copy类型

  • structenum类型不会自动实现Copy trait。只有当structenum内部的每个元素都是Copy类型时,编译器才允许我们针对此类型实现Copy trait

        struct T(i32);
        fn main() {
            let t1 = T(1);
            let t2 = t1;
            println! ("{} {}", t1.0, t2.0);
        }
    

    编译错误。原因是在let t2 = t1;这条语句中执行的是move语义。但是可以手动为它impl Copy trait,这样它就具备了copy语义

Rust中只有PODC++语言中的Plain Old Data)类型才有资格实现Copy trait

Rust中,如果一个类型只包含POD数据类型的成员,并且没有自定义析构函数,那它就是POD类型。

比如:整数、浮点数、只包含POD类型的数组等,都属于POD类型;而Box String Vec等不能按字节复制的类型,都不属于POD类型。

并不是所有满足POD的类型都应该实现Copy trait,是否实现Copy取决于业务需求

Clone的含义

Clone的全名是std::clone::Clone

    pub trait Clone : Sized {
        fn clone(&self) -> Self;
        fn clone_from(&mut self, source: &Self) {
self = source.clone()
        }
    }

两个关联方法,分别是clone_fromclone

  • clone_from是有默认实现的,依赖于clone方法的实现
  • clone方法没有默认实现,需要手动实现

clone方法一般用于“基于语义的复制”操作。所以,跟具体类型的作用息息相关。比如,对于Box类型,clone执行的是“深复制”;而对于Rc类型,clone做的事情就是把引用计数值加1。

虽然Rust中的clone方法一般是用来执行复制操作的,但是如果在自定义的clone函数中做点别的什么工作,编译器也没办法禁止。可以根据需要在clone函数中编写任意的逻辑。

对于实现了copy的类型,它的clone方法应该跟copy语义相容,等同于按字节复制

自动derive

绝大多数情况下,实现Copy Clone这样的trait都是一个重复而无聊的工作。因此,Rust提供了一个attribute,可以利用编译器自动生成这部分代码。示例如下:

    #[derive(Copy, Clone)]
    struct MyStruct(i32);

derive会让编译器自动生成impl Copyimpl Clone这样的代码。自动生成的clone方法,会依次调用每个成员的clone方法。

通过derive方式自动实现Copy和手工实现Copy有微小的区别。当类型具有泛型参数的时候,比如struct MyStruct<T>{},通过derive自动生成的代码会自动添加一个T:Copy的约束。

总结

  • Copy内部没有方法,Clone内部有两个方法
  • Copy trait是给编译器用的,告诉编译器这个类型默认采用copy语义,而不是move语义。Clone trait是给程序员用的,我们必须手动调用clone方法,它才能发挥作用
  • Copy trait不是想实现就能实现的,它对类型是有要求的,有些类型不可能impl Copy。而Clone trait则没有什么前提条件,任何类型都可以实现(unsized类型除外,因为无法使用unsized类型作为返回值)
  • Copy trait规定了这个类型在执行变量绑定、函数参数传递、函数返回等场景下的操作方式。即这个类型在这种场景下,必然执行的是“简单内存复制”操作,这是由编译器保证的,程序员无法控制。Clone trait里面的clone方法究竟会执行什么操作,则是取决于程序员自己写的逻辑。一般情况下,clone方法应该执行一个“深复制”操作,但这不是强制性的,如果你愿意,在里面启动一个人工智能程序都是有可能的
  • 如果确实不需要Clone trait执行其他自定义操作(绝大多数情况都是这样),编译器提供了一个工具,我们可以在一个类型上添加#[derive(Clone)],来让编译器自动生成那些重复的代码。编译器自动生成的clone方法非常机械,就是依次调用每个成员的clone方法
  • Rust语言规定了在T: Copy的情况下,Clone trait代表的含义。即:当某变量t: T符合T: Copy时,它调用t.clone() 方法的含义必须等同于“简单内存复制”。也就是说,clone的行为必须等同于let x = std::ptr::read(&t);,也等同于let x = t;。当T: Copy时,不要在Clone trait里面乱写逻辑。所以,当需要指定一个类型是Copy的时候,最好使用#[derive(Copy,Clone)]方式,避免手动实现Clone导致错误。

  • 深入浅出Rust