ZhangHanDong/tao-of-rust-codes

[第五章]clone方法按位复制概念错误

kvinwang opened this issue · 29 comments

页码与行数

  • 第122页

编译器会默认自动调用x的clone方法
对于实现Copy的类型,其clone方法必须是按位复制的

实际上

  1. 实现Copy的类型,“赋值”的时候不会调用clone方法(clone也不能代表按位复制)
  2. 实现Copy的类型,clone并不是必须按位复制的方式实现(实际是随便怎么实现,根据业务需求)
  3. 即便必须,也不能推出y = x 会调用clone

--- edit ----
PS. Copy也不一定用于栈拷贝, 比如:

use std::cell::RefCell;

fn main() {
    let a = Box::new(RefCell::new(1));
    let b = Box::new(RefCell::new(2));
    *b.borrow_mut() = *a.borrow();
    println!("b = {}", b.borrow());
}

@kvinwang 感谢反馈。这个地方我再继续仔细斟酌一下,看看怎么把它描述的更加清晰精准。

P121页

「按位复制就是指栈复制,也叫浅复制,它只复制栈上的数据。相对而言,深复制就是对
栈上和堆上的数据一起复制。」

这个描述是对按位复制的误解,需要修正。以及修正章节中对「栈复制」和「按位复制」混乱使用的问题。

读者按照「按位复制」的理解是没有问题的。

P121页

「按位复制就是指栈复制,也叫浅复制,它只复制栈上的数据。相对而言,深复制就是对
栈上和堆上的数据一起复制。」

这个描述是对按位复制的误解,需要修正。以及修正章节中对「栈复制」和「按位复制」混乱使用的问题。

读者按照「按位复制」的理解是没有问题的。

这句话不只“按位复制就是指栈复制”一点问题哦,和浅复制、深复制对应起来也是不对的。

@kvinwang 是的,这句话是要修正的更加精准

mzji commented

根据官方文档, Copy 类型的 clone 实现应该是 trivial 的:

Types that are Copy should have a trivial implementation of Clone. More formally: if T: Copy, x: T, and y: &T, then let x = y.clone(); is equivalent to let x = *y;. Manual implementations should be careful to uphold this invariant; however, unsafe code must not rely on it to ensure memory safety.

同时, Copy 类型的语义就是按位拷贝:

Types whose values can be duplicated simply by copying bits.

也即 Copy 类型的 clone 实现也应当是按位拷贝的。 见下方回复

如果 clone 和 copy 行为不一致,即违反了标准库约定的语义。(但是我忘了是不是 UB) 确实不是 UB

@mzji 嗯,现在主要问题是通用概念里「按位拷贝」、「栈拷贝」、「浅复制」和「深复制」没有说清楚。

mzji commented

主要是说明

  1. 实现Copy的类型,“赋值”的时候不会调用clone方法(clone也不能代表按位复制)
  2. 实现Copy的类型,clone并不是必须按位复制的方式实现(实际是随便怎么实现,根据业务需求)
  3. 即便必须,也不能推出y = x 会调用clone

这里的第一点的括号内的部分和第二点是有问题的

之前我看过一些材料,提到过说对于 Copy 类型,调用它的 clone 方法会直接被编译器优化成按位拷贝

至于栈拷贝,不建议再提这个概念了,不是很重要

mzji commented

至于浅复制和深复制,更多的只和包含堆存储的类型相关,如果展开描述时也应注意这一点

根据官方文档, Copy 类型的 clone 实现应该是 trivial 的:

Types that are Copy should have a trivial implementation of Clone. More formally: if T: Copy, x: T, and y: &T, then let x = y.clone(); is equivalent to let x = *y;. Manual implementations should be careful to uphold this invariant; however, unsafe code must not rely on it to ensure memory safety.

首先“应该"并不代表"必须", 凡是必须的,都应该是编译器来保证。如果必须,Rust完全可以禁止Copy类型自定义实现clone,因为必须一致嘛,自定义就没有意义了。
文档同样说了unsafe code must not rely on it ,就是说clone是不能保证这个语义的,依赖它是不安全的。

同时, Copy 类型的语义就是按位拷贝:

Types whose values can be duplicated simply by copying bits.

也即 Copy 类型的 clone 实现也应当是按位拷贝的。
如果 clone 和 copy 行为不一致,即违反了标准库约定的语义。

即便我们遵从”应当“,文档的意思比较模糊,写文档的人应该没考虑到这里的表意有些不妥。
你可以考虑下面的方式实现clone是否属于"应当"描述的范畴:

struct A(i8);
impl Copy for A {}
impl Clone for A {
    fn clone(&self) -> A {
        A(self.0)
    }
}

我认为这样实现并没有什么问题,但是它已经不是按位复制了。

但是我忘了是不是 UB

当然不是UB,凡是safe rust出现UB,都属于bug。

mzji commented

这里引用的原文, should 应当理解为 must 。

Manual implementations should be careful to uphold this invariant.

至于那个例子是否算是“应当”,只看它满不满足判断标准即可

if T: Copy, x: T, and y: &T, then let x = y.clone(); is equivalent to let x = *y;

也即 clone 的结果应当与按位拷贝一致。
如仍有疑问,我建议在 rust repo 发 issue ,要求澄清此处语义。 见下方新回复

@kvingwang 追求严谨

这里引用的原文, should 应当理解为 must

unsafe code must not rely on it 已经说明了should不应该理解为must。

mzji commented

unsafe code 不允许依赖于此行为又没啥问题,因为总可以用按位拷贝语义。

mzji commented

已找到官方文档对这一点的描述: RFC 1521: Copy Clone semantics

It's generally been an unspoken rule of Rust that a clone of a Copy type is equivalent to a memcpy of that type; however, that fact is not documented anywhere. This fact should be in the documentation for the Clone trait, just like the fact that T: Eq should implement a == b == c == a rules.

因此修正我的观点,应当是“clone 的结果”与按位拷贝一致,没有约束具体实现。这样可以方便 rustc 将 Copy 类型的 clone 优化为按位拷贝(因为结果一致)。

Due to RFC 1521, we're allowed to assume that a Clone impl for Copy types is trivial and can be optimized out. Having the compiler automatically add trivial memcpy Clone implementation for all Copy types should be safe. As well there shouldn't be a backwards compatibility hazard as it would only allow more code to compile.

Rust issue #33507

其他相关资料:
Rust issue #31086
Rust issue #31085
Clippy PR #580 为 Copy 类型手写 clone impl 会被 lint

@kvinwang @mzji 这两周内我会针对这个问题出一份勘误稿,到时候两位可以帮忙审校下。

@mzji 谢谢你的资料。这些资料里大量使用了should。
关于should怎么理解,资料里也有提到:
rust-lang/rust#33420

3. SHOULD   This word, or the adjective "RECOMMENDED", mean that there
   may exist valid reasons in particular circumstances to ignore a
   particular item, but the full implications must be understood and
   carefully weighed before choosing a different course.

也就是说允许存在一些特殊场景不遵守这个约定。

mzji commented

按照定义来说确实如此,不过考虑到实现的语义我还是建议做一个 trivial 的实现,这样便于优化。
而且对于 Copy 类型来说 clone 实现不一致在语义角度上来说可能会比较令人困扰。
不如这样说:非极端特殊情况,不要手写 Copy 类型的 clone 实现,应使用 derive 让编译器自行生成 clone 实现,以利于 1) 优化代码 2) 维持语义一致性 。

@kvinwang @mzji 帮忙审校

第122页修改为:

代码清单5-3中的变量x为整数类型,当它作为右值赋值给变量y时,编译器会自动默认以按位复制的方式进行浅复制。x是值语义类型,被复制以后,x和y就是两个不同的值,互不影响。

这是因为整数类型实现了Copy trait,第4章介绍过,对于实现Copy的类型,其clone方法只需要简单地实现按位复制即可。对于拥有值语义的整数类型,整个数据存储于栈中,按位复制以后,不会对原有数据造成破坏,不存在内存安全的问题。

@kvinwang @mzji

其实主要的问题在于,书里我想表达「浅复制」的地方,错误地用了「按位复制」。主要是因为我最初把「按位复制」当成了「浅复制」。

Copy具体的实现方式,不一定必须是「按位复制」。但是实现Copy的类型,在Rust中,表示「浅复制」是安全的。

感觉越描越黑了,Copy类型"赋值"时就是按位复制,"浅复制"等其它概念我明天在另一个issue里面举例说明一下吧

mzji commented

Rust 本身没怎么提到深复制和浅复制,大多是时候只是强调 Copy 类型和 Move 类型之间的区别,最多说一句 clone 行为可自定,多了都没提

@mzji Rust是没提。这里主要是想和通用概念挂钩,让大家都能方便理解Rust。

先讲通用概念,然后再具体到Rust里是怎样的行为。

关键是,我对浅复制的理解有问题吗? @kvinwang 来点示例我看看。

@ZhangHanDong 我在 #77 里面给了些我的理解,还是觉得没必要强行关联理解。
Rust中只有实现了Copy的类型才有资格讲值语义/引用语义。未实现Copy的类型,"赋值"时永远只会move,值语义对它们没有意义。

@kvinwang 辛苦了。
也不是强行关联。主要是想和旧知识挂钩,这样方便理解新概念。 我再继续修正一下,力求清晰准确。

mzji commented

我认为是 Rust 有意淡化之前的浅拷贝和深拷贝的概念
为什么呢?因为浅拷贝和深拷贝是相对概念,而且在不同的类型系统中行为不同,有时候很难被正确理解。因此,不如只讨论 Move type / Copy type 、 clone 及其语义,这样简单又实用。
假如有一天这几个概念不够用了,那么再构建新语义模型不迟。

我认为是 Rust 有意淡化之前的浅拷贝和深拷贝的概念
为什么呢?因为浅拷贝和深拷贝是相对概念,而且在不同的类型系统中行为不同,有时候很难被正确理解。因此,不如只讨论 Move type / Copy type 、 clone 及其语义,这样简单又实用。
假如有一天这几个概念不够用了,那么再构建新语义模型不迟。

同意
深浅拷贝概念一般在一些复合数据结构的时候比较有用,推广到一般数据类型意义不大。