为啥 Rust 不允许在一种类型上复制和删除特征?

Posted

技术标签:

【中文标题】为啥 Rust 不允许在一种类型上复制和删除特征?【英文标题】:Why does Rust not allow the copy and drop traits on one type?为什么 Rust 不允许在一种类型上复制和删除特征? 【发布时间】:2019-01-13 04:52:09 【问题描述】:

来自the book:

如果类型或其任何部分实现了Drop trait,Rust 不会让我们用Copy trait 来注释类型。如果该类型需要在值超出范围时发生一些特殊情况,并且我们将 Copy 注释添加到该类型,我们将收到编译时错误。

为什么设计决定不允许 CopyDrop 出现在同一类型上?

【问题讨论】:

这有点像是 C++ 所称的 "Rule of Three" 的形式化。 【参考方案1】: Drop 特征在 RAII 上下文中使用,通常在对象被销毁时需要释放/关闭某些资源时使用。 另一方面,Copy 类型是一种普通类型,只能使用 memcpy 进行复制。

通过这两个描述,更清楚它们是排他性的:memcpy 非平凡数据毫无意义:如果我们复制数据,然后删除其中一个副本怎么办?另一个副本的内部资源将不再可靠。

事实上,Copy 甚至不是一个“真正的”特征,因为它没有定义任何功能。它是一个特殊的标记,它对编译器说:“你可以用简单的字节副本复制自己”。所以你不能提供Copy的自定义实现,因为根本没有实现。但是,您可以将类型标记为可复制:

impl Copy for Foo 

或更好,带有派生:

#[derive(Clone, Copy)]
struct Foo  /* ... */ 

仅当所有字段都实现 Copy 时才会构建。否则,编译器会拒绝编译,因为这是不安全的。


为了举例,我们假设File 结构实现了Copy。当然,这不是,而且这个例子是错误的,无法编译:

fn drop_copy_type<T>(T x)
where
    T: Copy + Drop,

    // The inner file descriptor is closed there:
    std::mem::drop(x);


fn main() 
    let mut file = File::open("foo.txt").unwrap();
    drop_copy_type(file);
    let mut contents = String::new();

    // Oops, this is unsafe!
    // We try to read an already closed file descriptor:
    file.read_to_string(&mut contents).unwrap();

【讨论】:

“通过这两个描述,更清楚它们是独占的”共享指针的实现怎么样? @sdgfsdh 当你复制一个共享指针时,你必须增加内部引用计数器。这不是一个简单的memcpy。例如,请参阅drop implementation of Rc 所以 Rust 不允许 Copy 的自定义实现? @sdgfsdh 不。让我在我的回答中添加有关此的更多信息。 @sdgfsdh 实际上 rust 允许 Copy 的自定义实现。它被称为CloneClone 始终是显式的,可以执行附加操作(例如增加引用计数),并且可以与 Drop 共存。【参考方案2】:

引用documentation。

[...] [A]实现Drop的任何类型都不能是Copy,因为它除了自己的size_of::&lt;T&gt;字节之外还管理着一些资源。

【讨论】:

【参考方案3】:

这里的其他答案是关于为什么我们通常不想要为同一类型实现 CopyDrop,但这与解释它的原因不同 禁止。看起来像这样的玩具示例应该可以正常工作:

#[derive(Copy, Clone)]
struct Foo 
    i: i32,


impl Drop for Foo 
    fn drop(&mut self) 
        // No problematic memory management here. Just print.
        println!("", self.i);
    


fn main() 
    let foo1 = Foo  i: 42 ;
    let foo2 = foo1;
    // Shouldn't this just print 42 twice?

但事实上,如果我们尝试编译它(使用 Rust 1.52),它会按预期失败:

error[E0184]: the trait `Copy` may not be implemented for this type; the type has a destructor
 --> src/main.rs:1:10
  |
1 | #[derive(Copy, Clone)]
  |          ^^^^ Copy not allowed on types with destructors
  |
  = note: this error originates in a derive macro (in Nightly builds, run with -Z macro-backtrace for more info)

error: aborting due to previous error

For more information about this error, try `rustc --explain E0184`.

查看底部的“了解更多信息”说明?这些通常很有帮助。让我们运行rustc --explain E0184

The `Copy` trait was implemented on a type with a `Drop` implementation.

Erroneous code example:

```
#[derive(Copy)]
struct Foo; // error!

impl Drop for Foo 
    fn drop(&mut self) 
    

```

Explicitly implementing both `Drop` and `Copy` trait on a type is currently
disallowed. This feature can make some sense in theory, but the current
implementation is incorrect and can lead to memory unsafety (see
[issue #20126][iss20126]), so it has been disabled for now.

[iss20126]: https://github.com/rust-lang/rust/issues/20126

在该问题链接之后,我们会讨论“droping-on-drop”。现在的 Rust 不再这样做了,而是 up until around 2016 Rust implemented "dynamic drop" by zeroing all the bits of a value when dropping it。但是,如果一个类型既可以是Copy 也可以是Drop,那么这当然不是一个有效的实现——Rust 不能将你被允许继续使用的值归零——所以实现这两个特征不允许使用相同的类型。讨论以this interesting comment结束:

无论如何,现在最容易禁止它。如果有人提出有说服力的用例,我们总是可以在以后使其合法化。幂等析构函数似乎有点奇怪。


以上是对 Rust 当前行为的解释,据我所知。但我认为还有另一个理由让事情保持原样,我还没有看到讨论过:Copy 目前暗示一个值既可以按位复制也可以按位覆盖。考虑这段代码:

#[derive(Copy, Clone)]
struct Foo 
    i: i32,


fn main() 
    let mut ten_foos = [Foo  i: 42 ; 10];
    let ten_more_foos = [Foo  i: 99 ; 10];
    // Overwrite all the bytes of the first array with those of the second.
    unsafe 
        std::ptr::copy_nonoverlapping(&ten_more_foos, &mut ten_foos, 1);
    

这个不安全的代码今天完全没问题。事实上,[T]::copy_from_slice 对任何T: Copy 都会做同样的事情。但是,如果允许Foo(或任何其他Copy 类型)成为Drop,它仍然可以吗?我们这里的代码以及copy_from_slice 中的标准库代码会在不丢弃对象的情况下销毁对象!

现在,技术上,允许调用对象的析构函数失败。那天有一个 very interesting discussion 导致 std::mem::forget 在 Rust 1.0 之前不久从 unsafe 变为安全。因此,尽管存在此问题,但 Rust 可能允许 Copy + Drop 而不导致任何未定义的行为。但是某些(标准!)函数无法调用您期望的析构函数,这将是相当令人惊讶的。 “Copy 对象可以按位复制和按位覆盖”的属性似乎是一个很好的保留。

【讨论】:

以上是关于为啥 Rust 不允许在一种类型上复制和删除特征?的主要内容,如果未能解决你的问题,请参考以下文章

为啥 Rust 的示例猜谜游戏允许具有不同返回类型的 match 语句?

为啥 PHP 在一种情况下允许将文字传递给按引用传递的参数,而在其他情况下不允许?

为啥 std::optional 不允许“移动构造和仅复制分配”类型的移动分配?

为啥 Rust 在 main 函数中没有返回值,以及如何返回值?

尝试使用多态时,“错误:结构定义中不允许使用特征边界”

为啥 Rust 不能推断出 Iterator::sum 的结果类型?