delphi memo 不允许复制_《Rust编程之道》读者答疑精选:对书中值语义、引用语义、栈拷贝、按位复制等概念的澄清...

ded5ab6e85a5e3fa23d7d07de2b848b0.png
最近,由读者朋友 @kvinwang 对本书第五章所有权系统中出现的「按位复制」、「栈复制」、「值类型」、「值语义」和「引用语义」等概念提出了质疑,并且指出了这些概念混乱使用的问题。
经过连续多天的讨论,今天整理出结果来一致澄清一下这些概念。同时,也公开感谢一下这位对严谨性和准确性有高度追求的朋友。
[第五章]clone方法按位复制概念错误 · Issue #75 · ZhangHanDong/tao-of-rust-codes
[第五章] 诸多名词概念不应强行关联 · Issue #77 · ZhangHanDong/tao-of-rust-codes

编译器默认自动调用x的clone方法

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

修改为:

代码清单5-3中的变量x为整数类型,当它作为右值赋值给变量y时,编译器会默认自动按位复制。x是值语义类型,被复制以后,x和y就是两个不同的值,互不影响。
这是因为整数类型实现了Copy trait,第4章介绍过,对于实现Copy的类型,其clone方法只需要简单地实现按位复制即可。对于拥有值语义的整数类型,整个数据存储于栈中,按位复制以后,不会对原有数据造成破坏,不存在内存安全的问题。

说明:

其实这里说「自动调用x的clone方法」,是为了方便读者理解这种默认行为。对于Rust中Copy的语义,开发者是无法修改的。也就是说,对于赋值、或者传参等行为发生的时候,实现Copy的类型默认是按位复制。开发者自己实现Copy trait,必须也实现clone方法。至于clone方法是如何实现的不重要,重要的是,它们必须有按位复制的能力。但是标准库文档里建议你只需要实现按位复制即可。注意,这里指的是隐式调用clone的行为,而非显式调用clone方法。

按位复制和栈复制

其实书里问题的根源在于,我当时错误地将「按位复制」理解为「栈复制」。虽然按「栈复制」来理解Rust中的Copy行为、所有权机制也没有什么影响,但确实不太严谨。读者朋友kvinwang说,最好还是澄清一下,以免误导了新人。

所以,首先需要明确「按位复制」,等同于C语言里的memcpy。 所以,我将书里出现的相关批注做了修改:

C语言中的memcpy会从源所指的内存地址的起始位置开始拷贝n个字节,直到目标所指的内存地址的结束位置。但如果要拷贝的数据中包含指针,该函数并不会连同指针指向的数据一起拷贝。

按位复制,只是复制「值」,而不会复制「值」中包含指针指向的数据。也可以说,它是浅复制的一种特定形式。它不会进行深复制。拿Rust中的String字符串来说,其本质是一个智能指针,在栈上存储着元信息,但是在堆里存储的具体的数据。如果对其进行按位复制,只会复制其栈上的元信息,而不会复制其堆里的数据。如果想深复制,只能显式地调用其clone函数。

所以,这是我书里没有说明清楚的一个地方。 因为Rust默认是在栈上存储的,所以,按位复制通常都是发生在栈上复制。但是按位复制,并不一定只能复制栈上的数据。

对于值类型和引用类型的修改如下:

值类型一般是指可以将数据都保存到同一位置的类型,一些原生类型,比如数值、布尔值、结构体等都是值类型。因此对值类型的操作效率一般比较高,使用完立即会被回收。值类型作为右值(在值表达式中)执行赋值或传入函数等操作时,会自动复制一个新的值副本,并且该副本和原始的值没有直接关系,互不影响。
引用类型则会存在一个指向实际存储区域的指针。比如通常一些引用类型会将数据存储在堆中,而栈中只存放指向堆中数据的地址(指针)。因此对引用类型的操作效率一般比较低,使用完交给GC回收,这样更安全一些。但是没有GC的语言则需要靠手工来回收,就多了很多风险。

对于值语义和引用语义的修改如下:

为了更加精准地对这种复合类型或对象进行描述,值语义(Value Semantic)和引用语义(Reference Semantic)被引入,定义如下。
- 值语义:复制(赋值操作)以后,两个数据对象拥有的存储空间是独立的,相互之间互不影响。
- 引用语义:复制(赋值操作)以后,两个数据对象,相互之间互为别名。操作其中任意一个数据对象,则会影响到另一个。
值语义可以保证变量值的独立性(Independence)。独立性的意思是,如果想修改某个变量,只能通过它本身来修改;而如果修改了它本身,并不影响其复制品。也就是说,如果只能通过变量本身来修改值,那么它就是具有值语义的变量。
对于引用语义的数据对象,赋值操作时按位复制,可能存在内存不安全风险。比如只复制了栈上的指针,堆上的数据就多了一个管理者,多了一层内存安全的隐患。

「Copy语义和Move语义」 vs 「值语义、引用语义」

在Rust中,可以通过能否实现Copy trait来区分数据类型的值语义和引用语义。但为了描述的更加精准,Rust也引入了新的语义:复制(Copy)语义和移动(Move)语义。复制语义对应值语义,也就是说,实现了Copy的类型,在进行按位复制的时候,是安全的。移动语义对应引用语义,也就是说,在传统语言(比如C++)中本来是引用语义的类型,在Rust中不允许按位复制,只允许移动所有权,只有这样才能保证安全。这样划分是因为引入了所有权机制,在所有权机制下同时保证内存安全和性能。 Rust的数据默认存储在栈上。
对于默认可以安全地在栈上进行按位复制的类型,就只需要按位复制,也方便管理内存。对于默认只可在堆上存储的数据,因为无法安全地进行按位复制,如果要保证内存安全,就必须进行深度复制。当然,你也可以把实现Copy的类型,通过Rust提供的特定API(比如Box语法)将其放到堆上,但它既然是实现了Copy,就是一个可以安全进行按位复制的类型。深度复制需要在堆内存中重新开辟空间,这会带来更多的性能开销。如果堆上的数据不变,只需要在栈上移动指向堆内存的指针地址,不仅保证了内存安全,还可以拥有与在栈上进行复制的等同性能。
也许有的人会说,即便只移动存储在栈上的指针,那其实在Rust编译器内部也很可能是一个按位复制行为,因为单论指针而言,它也可以看作是一个值。但我们这里说的是上层的语义。对于Move语义而言,代表的是按位复制不安全,所以Rust编译器不允许它实现Copy。
所以,对于Rust而言,可以实现Copy trait的类型,则表示它拥有复制语义,在赋值或传入函数等行为时,默认会进行按位复制。它和传统概念中的值语义类型相对应,因为两个独立不关联的值,操作其中一个,不影响另外一个,是安全的。对于不能实现Copy trait的类型,它实际上和传统的引用语义类型相对应,只不过在Rust中,如果只是简单的按位复制,则会出现图5-1那样的不安全问题。所以,为了安全,它必须是移动语义。移动语义实际上在告诉编译器,该类型不要简单的按位复制,那样不安全。所以,其他语言中的引用语义到了Rust中,就成了移动语义。但是被移动的值,相当于已经废弃了,无法使用。如果从这个角度来看,你如果认为Rust语言中并不存在引用语义类型,只有值语义类型,也是可以的。 另外,需要注意,Rust中默认的引用和指针也都实现了Copy。

说明:

这几段,主要是澄清Rust中的Copy语义。Copy的重点在于,是否可以安全地进行按位复制。实际上,要不要把它看成值语义或引用语义,都是看你自己。书里,只是给你提供一个视角,也方便你把Rust中的新概念「Copy语义」和「Move语义」与旧知识「值语义」和「引用语义」挂上钩。这样,即方便你理解所有权机制,又重点体现了,Rust以「内存安全」为设计原则对这门语言的精巧设计。

以上。 该文讨论地址: 对书中值语义、引用语义、栈拷贝、按位复制等概念的澄清 · Issue #104 · ZhangHanDong/tao-of-rust-codes

后续还有两个根据勘误要着重说明的主题,容我抽时间整理出来再发。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值