Ruby 【译】Ruby 中的对象拷贝-dup vs clone

lanzhiheng · 2020年08月02日 · 最后由 Q1042883322 回复于 2020年08月03日 · 1520 次阅读

翻译一篇简单文章,并附上一些个人测试结果。总是记不住dupclone的区别?这篇文章或许可以帮到你,翻译自https://www.rubyguides.com/2018/11/dup-vs-clone/ 。原文发布于 https://www.lanzhiheng.com/posts/object-copy-in-ruby-dup-vs-clone

你是否知道你可以拷贝 Ruby 中的对象?不仅如此,你还能用两种不同的方法来做这件事情。

两种方法分别是:

  • dup
  • clone

我们立马会探讨它们之间的差别,不过首先...

为什么你想要去拷贝一个对象呢?

Ruby 中的许多对象都是可变的,你可以去修改它们。

如果你想改变一个对象,却还想以副本的方式保留它原有的样子,那你可以去拷贝它。

举个例子:

你可能会想得到一个数组,它包含某个数组中除了第一个元素之外的所有元素。

你可以用这种方式来实现

a = [1,2,3,4,5]
a[1..-1] # => [2,3,4,5]

或者还有另一种方式

b = a.clone
b.shift # => [1]
b # => [2,3,4,5]

上述两个例子都让你能够保留最原始的数组。

Dup vs Clone

被冻结的对象

dupclone两个方法并不是彼此的别名,这有别于 Ruby 中其他常用的方法map/collect,它们之间还是有些许不同的。

窥探两个事物之间的相同点和不同点,是加深对它们理解的好的方法

两个方法都是用来拷贝对象,不同之处在于dup并不拷贝对象的属性。

什么是对象的属性?

  • 冻结的状态
  • 受污染的状态 (Object#tainted?,这个属性Object#dup会拷贝,应该是作者忘了提醒了)
  • 单例类

下面有个例子:

a = Object.new.freeze
b = a.dup
b.frozen? => # false
b = a.clone
b.frozen? => # true

Ruby2.4 包含一个可选项,让clone在拷贝对象的时候能够忽略掉冻结状态,下面是例子:

a.clone(freeze: true)
a.clone(freeze: false)

译者附加研究结果

根据研究,新版本 Ruby 的Object#dup方法会拷贝对象的tainted状态(其实旧版本也会拷贝)

dup copies the tainted state of obj.

以下是我在 Rubu1.8 跟 Ruby2.6 环境的测试结果

# ruby 1.8.7

> a = Object.new.taint
=> #<Class:0x7fd987e11028>
> a.tainted?
=> true
> a.dup.tainted?
=> true

# ruby 2.6.5
> a = Object.new.taint
=> #<Object:0x00007fe3011afec8>
> a.tainted?
=> true
> a.dup.tainted?
=> true

所以Object#dup不会拷贝的属性应该要把它给划掉

  • 冻结的状态
  • 受污染的状态
  • 单例类

以下是代码示例:

a = Object.new.taint
a.tainted? => true
a.dup.tainted? => true

def a.hello
  "hello"
end
a.hello => "hello"
a.dup.hello => NoMethodError (undefined method `hello' for ....)
a.clone.hello => "hello"

为何不测试 2.7?因为 2.7 之后taint 的检测会被移除掉,后面也打算移除掉这个属性,所以可以不必太关注这个了。2.7 测出来的结果也是有点迷,tainted状态直接不可用:

# ruby 2.7.1

> a = Object.new.taint
> a.tainted?
=> false

对比深拷贝跟浅拷贝

还有比你所能见到的更多的拷贝行为。

不管是采用dup还是clone,当你进行拷贝的时候,你正在做的都是浅拷贝

这意味着该对象所包含的其他对象并不会被拷贝。

换句话说:

如果你有一个由字符串组成的数组,你对它进行拷贝,那么只有数组本身会被拷贝,它自身所包含的字符串却不会。

可以自己看看:

original = %w(apple orange banana)
copy     = original.clone
original.map(&:object_id)
# [23506500, 23506488, 23506476]
copy.map(&:object_id)
# [23506500, 23506488, 23506476]

即便是在数组拷贝了之后,它们所包含的数组对象的 id 都是一致的,因此它们是相同的字符串。

针对这个场景,你可以这样解决:

strings.clone.map(&:clone)

结果就是无论是数组本身还是它所包含的字符串都会被拷贝,不过请记住,这种方式只适用于一个层级深度的拷贝。作为替代品,你可以尝试使用 ActiveSupport 中的deep_dup

总结

你已经学会在 Ruby 中拷贝对象的方法了!这篇文章所讲述的东西包含了dupclone这两个方法的异同,以及深拷贝和浅拷贝之间的区别。感谢您的阅读。

需要 登录 后方可回复, 如果你还没有账号请点击这里 注册