为啥 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
注释添加到该类型,我们将收到编译时错误。
为什么设计决定不允许 Copy
和 Drop
出现在同一类型上?
【问题讨论】:
这有点像是 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
的自定义实现。它被称为Clone
。 Clone
始终是显式的,可以执行附加操作(例如增加引用计数),并且可以与 Drop
共存。【参考方案2】:
引用documentation。
[...] [A]实现
Drop
的任何类型都不能是Copy
,因为它除了自己的size_of::<T>
字节之外还管理着一些资源。
【讨论】:
【参考方案3】:这里的其他答案是关于为什么我们通常不想要为同一类型实现 Copy
和 Drop
,但这与解释它的原因不同 禁止。看起来像这样的玩具示例应该可以正常工作:
#[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 不允许“移动构造和仅复制分配”类型的移动分配?