【问题标题】:Why is the Copy trait needed for default (struct valued) array initialization?为什么默认(结构值)数组初始化需要 Copy 特征?
【发布时间】:2015-01-10 13:12:18
【问题描述】:

当我定义这样的结构时,我可以通过值将其传递给函数,而无需添加任何特定内容:

#[derive(Debug)]
struct MyType {
    member: u16,
}

fn my_function(param: MyType) {
    println!("param.member: {}", param.member);
}

当我想创建一个具有默认值的 MyType 实例数组时

fn main() {
    let array = [MyType { member: 1234 }; 100];
    println!("array[42].member: ", array[42].member);
}

Rust 编译器告诉我:

error[E0277]: the trait bound `MyType: std::marker::Copy` is not satisfied
  --> src/main.rs:11:17
   |
11 |     let array = [MyType { member: 1234 }; 100];
   |                 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ the trait `std::marker::Copy` is not implemented for `MyType`
   |
   = note: the `Copy` trait is required because the repeated element will be copied

当我实现 CopyClone 时,一切正常:

impl Copy for MyType {}
impl Clone for MyType {
    fn clone(&self) -> Self {
        MyType {
            member: self.member.clone(),
        }
    }
}
  1. 为什么我需要指定一个空的Copy trait 实现?

  2. 有没有更简单的方法可以做到这一点,还是我必须重新考虑一些事情?

  3. 为什么通过值将MyType 的实例传递给函数时会起作用?我的猜测是它正在被移动,所以一开始就没有副本。

【问题讨论】:

    标签: struct rust traits


    【解决方案1】:

    与 C/C++ 不同,Rust 对复制的类型和移动的类型有非常明确的区别。请注意,这只是语义上的区别;在实现级别上 move 一个浅字节复制,然而,编译器对你可以对你从中移动的变量做什么施加了一些限制。

    默认情况下每个类型只能移动(不可复制)。这意味着这些类型的值会被移动:

    let x = SomeNonCopyableType::new();
    let y = x;
    x.do_something();      // error!
    do_something_else(x);  // error!
    

    您看,存储在x 中的值已移至y,因此您无法对x 执行任何操作。

    移动语义是 Rust 中所有权概念的一个非常重要的部分。你可以在in the official guide阅读更多内容。

    然而,有些类型足够简单,所以它们的字节拷贝也是它们的语义拷贝:如果你逐字节拷贝一个值,你会得到一个新的完全独立的值。例如,原始数字就是这样的类型。这样的属性在 Rust 中由Copy trait 指定,即如果一个类型实现了Copy,那么这个类型的值是隐式可复制的。 Copy 不包含方法;它的存在只是为了标记实现类型具有某些属性,因此它通常被称为标记特征(以及一些其他做类似事情的特征)。

    但是,它不适用于所有类型。例如,动态分配的向量之类的结构不能自动复制:如果是,则其中包含的分配地址也将被字节复制,然后此类向量的析构函数将在同一分配上运行两次,从而导致指针被释放两次,这是内存错误。

    所以默认情况下,Rust 中的自定义类型是不可复制的。但是您可以使用#[derive(Copy, Clone)] 选择加入(或者,如您所见,直接使用impl;它们是等效的,但derive 通常读起来更好):

    #[derive(Copy, Clone)]
    struct MyType {
        member: u16
    }
    

    (派生Clone是必要的,因为Copy继承Clone,所以Copy的所有内容也必须是Clone

    如果您的类型原则上可以自动复制,即它没有关联的析构函数并且其所有成员都是Copy,那么使用derive,您的类型也将是Copy

    您可以在数组初始化程序中使用Copy 类型,因为数组将使用此初始化程序中使用的值的字节副本进行初始化,因此您的类型必须实现Copy 以指定它确实可以 被自动复制。

    以上是1和2的答案。至于3,是的,您完全正确。它确实有效,因为值被移动到函数中。如果您在将 MyType 类型的变量传递给函数后尝试使用它,您会很快注意到使用移动值时出现错误。

    【讨论】:

    • "与 C/C++ 相反,Rust 对复制的类型和移动的类型有非常明确的区别。" 你是什么意思? C一开始就没有这些概念。 C++ 有它们,但它们是显式的并且是类型系统的一部分。
    • 问题在于语义。在 C 中,一切都被复制了,即使是那些在逻辑上应该被移动的类型,并且语言不支持强制或简化它(即你必须显式地编写移动逻辑)。 C++ 类似,只是其中有语言中与移动相关的工具(移动构造函数、std::move 等)。但是,这些工具非常明确且容易出错,即您需要不要忘记调用std::move,并且您需要正确实现移动构造函数。另外,我不太确定移动在 C++ 的类型系统中是如何可见的?
    • "C++ 类似......但是,这些工具非常明确且容易出错" 这是错误的。 C++ 类型是静态可复制的、可移动的、两者兼有或无。如果类型需要,您不能“忘记调用std::move”。 您需要正确实现移动构造函数编译器会为您完成 Rust 中的等效语义。 Rust 在这里的实际优势是仿射类型。
    • "move 在 C++ 的类型系统中是如何可见的?" 它被嵌入到类型系统中。您甚至可以在编译时向编译器查询这些特征(如静态反射的子集)并相应地调整生成的代码(包括静态分支等)。 Rust 也可以通过 const eval 来实现这一点,但它是一个很难指定和实现的特性,因此目前在这种情况下它不如 C++ 强大。
    • 好吧,那可能我的C++知识还不够。谢谢你的解释!
    【解决方案2】:

    为什么我需要指定一个空的 Copy trait 实现?

    Copy 是一个特殊的内置特征,因此 T 实现 Copy 表示使用浅字节副本复制 T 类型的值是安全的。

    这个简单的定义意味着只需要告诉编译器这些语义是正确的,因为运行时行为没有根本的变化:移动(非Copy 类型)和“复制”都是浅的字节副本,这只是源是否以后可用的问题。见an older answer for more details

    (如果MyType 的内容不是Copy 本身,编译器会报错;之前它会自动实现,但都改变了with opt-in built-in traits。)

    创建一个数组是通过浅拷贝复制值,如果TCopy,这保证是安全的。在更一般的情况下它是安全的,#5244 涵盖了其中一些,但在核心,非Copy 结构将无法用于自动创建固定长度数组,因为编译器不能告诉复制是安全/正确的。

    有没有更简单的方法可以做到这一点,还是我必须重新考虑一些事情(我来自 C)?

    #[derive(Copy)]
    struct MyType {
        member: u16
    }
    

    将插入适当的空实现(#[derive] 与其他几个特征一起使用,例如,经常看到#[derive(Copy, Clone, PartialEq, Eq)]。)

    为什么将MyType 的实例按值传递给函数时会起作用?我的猜测是它正在被移动,所以一开始就没有副本。

    好吧,如果不调用该函数,就不会看到移动与复制行为(如果您要调用它两次相同的非Copy 值,编译器会发出关于移动值的错误)。但是,机器上的“移动”和“复制”本质上是相同的。在 Rust 中,值的所有按值使用在语义上都是浅拷贝,就像在 C 中一样。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2021-10-10
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2020-03-24
      • 1970-01-01
      相关资源
      最近更新 更多