Ruby的dup和clone方法有什么区别?


214

Ruby的文档的dup说:

在一般情况下,clonedup可能在派生类不同的语义。虽然clone用于复制对象(包括其内部状态),但dup通常使用后代对象的类创建新实例。

但是,当我进行一些测试时,我发现它们实际上是相同的:

class Test
   attr_accessor :x
end

x = Test.new
x.x = 7
y = x.dup
z = x.clone
y.x => 7
z.x => 7

那么这两种方法有什么区别?


29
我希望我知道不是单纯的区别在什么 dupclone做的,但为什么你会使用一个,而不是其他。
安德鲁·格林

1
这也是一个很好的链接-coderwall.com/p/1zflyg
Arup Rakshit

Answers:


298

子类可以覆盖这些方法以提供不同的语义。在Object本身,有两个关键区别。

首先,clone复制单例类,而dup不会。

o = Object.new
def o.foo
  42
end

o.dup.foo   # raises NoMethodError
o.clone.foo # returns 42

其次,clone保存冻结状态,而不保存dup

class Foo
  attr_accessor :bar
end
o = Foo.new
o.freeze

o.dup.bar = 10   # succeeds
o.clone.bar = 10 # raises RuntimeError

这些方法Rubinius实现 通常是我对这些问题的答案,因为它很清楚,并且是相当合规的Ruby实现。


15
万一有人试图再次改变它:“单身类”在Ruby中是一个定义明确的术语,不仅包括单身方法,还包括在单身类上定义的任何常量。考虑:o = Object.new; class << o; A=5; end; puts ( class << o.clone; A; end ); puts ( class << o.dup; A; end )
杰里米·罗马

3
很好的答案,后面有个很好的评论,但这使我陷入了疯狂的追赶中,以了解这种语法。这将帮助其他可能会感到困惑的人:devalot.com/articles/2008/09/ruby-singleton
davidpm4 '16

1
我认为值得一提的是,“单一类”还包括extend在原始对象上编辑过的所有模块。因此Object.new.extend(Enumerable).dup.is_a?(Enumerable)返回false。
丹尼尔(Daniel)

尽管此答案确实回答了问题并指出了区别。还值得注意的是,如Object#dup文档所述,这两种方法均适用于不同的情况。克隆的用例是克隆一个对象,目的是将该对象用作同一实例(同时具有不同的对象ID),而dup则打算复制一个对象作为新实例的基础。
3limin4t0r

189

在处理ActiveRecord时,也存在显着差异:

dup 创建一个没有设置其ID的新对象,因此您可以通过单击将一个新对象保存到数据库 .save

category2 = category.dup
#=> #<Category id: nil, name: "Favorites"> 

clone 创建一个具有相同ID的新对象,因此如果单击该对象,则对该新对象所做的所有更改将覆盖原始记录 .save

category2 = category.clone
#=> #<Category id: 1, name: "Favorites">

43
这个答案是向海事组织提供最重要的实用信息的方法...另一个答案是关于esoterica的,而这个答案指出了一个重要的实际差异。
2015年

37
上面是特定于ActiveRecord的;在标准Ruby中,区别要微妙得多。
ahmacleod

1
@Stefan和@jvalanen:当我在对象上应用dupclone方法时ActiveRecord,我得到的答案与您提到的结果相反。这意味着当我使用时dup,它将创建一个新对象并对其id进行设置,而在使用clone时会创建一个未id设置对象。您能否再次调查一下并进行澄清?。Thnx
huzefa biyawarwala'8

在Rails 5中也没有任何变化:api.rubyonrails.org/classes/ActiveRecord/…。因此,我认为您的情况有特殊之处……
jvalanen

但是,clone保存从未保存的新记录应该很安全吗?我可以以此方式构建“模板对象”并克隆它以保存特定实例吗?
西里尔·杜尚·多丽丝

30

区别在于冻结的对象。在clone一个冻结对象的也冻结(而dup冻结对象的不是)。

class Test
  attr_accessor :x
end
x = Test.new
x.x = 7
x.freeze
y = x.dup
z = x.clone
y.x = 5 => 5
z.x = 5 => TypeError: can't modify frozen object

另一个区别是单例方法。这里的故事相同,dup不会复制这些内容,但是clone会复制。

def x.cool_method
  puts "Goodbye Space!"
end
y = x.dup
z = x.clone
y.cool_method => NoMethodError: undefined method `cool_method'
z.cool_method => Goodbye Space!

这对我非常有用。如果您要创建一个冻结的常量值并将其传递给这样的东西:github.com/rack/rack/blob/master/lib/rack/utils.rb#L248(Rails cookie处理),那么您很容易得到错误当您不认识他们时,他们会克隆它,然后尝试修改该克隆。将冻结的值复制并传递给您,至少可以确保没有人意外修改您的常量,而不会在这里破坏Rack。
XP84 2016年

4

两者几乎相同,但克隆比dup做更多的事情。在克隆中,对象的冻结状态也会被复制。在dup中,它将始终被解冻。

 f = 'Frozen'.freeze
  => "Frozen"
 f.frozen?
  => true 
 f.clone.frozen?
  => true
 f.dup.frozen?
  => false 

4

新的文档包含一个很好的例子:

class Klass
  attr_accessor :str
end

module Foo
  def foo; 'foo'; end
end

s1 = Klass.new #=> #<Klass:0x401b3a38>
s1.extend(Foo) #=> #<Klass:0x401b3a38>
s1.foo #=> "foo"

s2 = s1.clone #=> #<Klass:0x401b3a38>
s2.foo #=> "foo"

s3 = s1.dup #=> #<Klass:0x401b3a38>
s3.foo #=> NoMethodError: undefined method `foo' for #<Klass:0x401b3a38>

0

您可以使用clone在Ruby中进行基于原型的编程。Ruby的Object类同时定义了clone方法和dup方法。克隆和dup都会生成它正在复制的对象的浅表副本。也就是说,将复制对象的实例变量,但不复制它们引用的对象。我将演示一个示例:

class Apple
  attr_accessor :color
  def initialize
    @color = 'red'
  end
end

apple = Apple.new
apple.color
 => "red"
orange = apple.clone
orange.color 
 => "red"
orange.color << ' orange'
 => "red orange" 
apple.color
 => "red orange"

请注意,在上面的示例中,橙色克隆复制了Apple对象的状态(即实例变量),但是在Apple对象引用其他对象(例如String对象颜色)的情况下,不会复制这些引用。相反,苹果和橙色都引用同一个对象!在我们的示例中,引用是字符串对象“ red”。当orange使用append方法<< <<修改现有的String对象时,它将字符串对象更改为'red orange'。实际上,这也会更改apple.color,因为它们都指向同一个String对象。

作为附带说明,赋值运算符=将分配一个新对象,从而破坏引用。这是一个示范:

class Apple
  attr_accessor :color
  def initialize
    @color = 'red'
  end
end

apple = Apple.new
apple.color
=> "red"
orange = apple.clone
orange.color
=> "red"
orange.color = 'orange'
orange.color
=> 'orange'
apple.color
=> 'red'

在上面的示例中,当我们将新对象分配给橙色克隆的颜色实例方法时,它不再引用与Apple相同的对象。因此,我们现在可以修改橙色的颜色方法而不会影响苹果的颜色方法,但是如果我们从苹果中克隆另一个对象,则该新对象将在复制的实例变量中引用与苹果相同的对象。

dup还将生成它正在复制的对象的浅表副本,并且如果您对dup做与上图所示相同的演示,则将看到它的工作方式完全相同。但是clone和dup之间有两个主要区别。首先,如其他人所述,克隆复制冻结状态,而dup不复制冻结状态。这是什么意思?Ruby中的“冻结”一词是不可变的深奥术语,它本身是计算机科学的术语,意味着无法更改某些内容。因此,不能以任何方式修改Ruby中的冻结对象。实际上,它是不可变的。如果您尝试修改冻结的对象,则Ruby将引发RuntimeError异常。由于克隆会复制冻结状态,因此,如果您尝试修改克隆的对象,它将引发RuntimeError异常。相反,由于dup不会复制冻结状态,

class Apple
  attr_accessor :color
  def initialize
    @color = 'red'
  end
end

apple = Apple.new
apple.frozen?
 => false 
apple.freeze
apple.frozen?
 => true 
apple.color = 'crimson'
RuntimeError: can't modify frozen Apple
apple.color << ' crimson' 
 => "red crimson" # we cannot modify the state of the object, but we can certainly modify objects it is referencing!
orange = apple.dup
orange.frozen?
 => false 
orange2 = apple.clone
orange2.frozen?
 => true 
orange.color = 'orange'
 => "orange" # we can modify the orange object since we used dup, which did not copy the frozen state
orange2.color = 'orange'
RuntimeError: can't modify frozen Apple # orange2 raises an exception since the frozen state was copied via clone

其次,更有趣的是,克隆复制单例类(及其方法)!如果您希望在Ruby中进行基于原型的编程,这将非常有用。首先,让我们证明确实使用克隆复制了单例方法,然后将其应用于Ruby中基于原型的编程示例。

class Fruit
  attr_accessor :origin
  def initialize
    @origin = :plant
  end
end

fruit = Fruit.new
 => #<Fruit:0x007fc9e2a49260 @origin=:plant> 
def fruit.seeded?
  true
end
2.4.1 :013 > fruit.singleton_methods
 => [:seeded?] 
apple = fruit.clone
 => #<Fruit:0x007fc9e2a19a10 @origin=:plant> 
apple.seeded?
 => true 

如您所见,水果对象实例的单例类已复制到克隆中。因此,克隆的对象可以访问单例方法:seeded?。但这不是dup的情况:

apple = fruit.dup
 => #<Fruit:0x007fdafe0c6558 @origin=:plant> 
apple.seeded?
=> NoMethodError: undefined method `seeded?'

现在,在基于原型的编程中,您没有扩展其他类然后再创建其类实例来自于作为蓝图的父类的类的实例的类。相反,您有一个基础对象,然后从该对象创建了一个新对象,并复制了其方法和状态(当然,由于我们正在通过克隆进行浅表复制,因此实例变量引用的所有对象都将像JavaScript一样被共享原型)。然后,您可以通过填充克隆方法的详细信息来填充或更改对象的状态。在下面的示例中,我们有一个基础水果对象。所有水果都有种子,因此我们创建了一个方法number_of_seeds。但是苹果只有一个种子,因此我们创建了一个克隆并填充了细节。现在,当我们克隆苹果时,我们不仅克隆了方法,还克隆了状态!请记住,clone会复制状态(实例变量)的浅表。因此,当我们克隆苹果以获得red_apple时,red_apple将自动具有1个种子!您可以将red_apple视为继承自Apple的对象,而Apple继而继承自Fruit。因此,这就是为什么我要大写Fruit和Apple。我们取消了克隆与类之间的区别。

Fruit = Object.new
def Fruit.number_of_seeds=(number_of_seeds)
  @number_of_seeds = number_of_seeds
end
def Fruit.number_of_seeds
  @number_of_seeds
end
 Apple = Fruit.clone
 => #<Object:0x007fb1d78165d8> 
Apple.number_of_seeds = 1
Apple.number_of_seeds
=> 1
red_apple = Apple.clone
 => #<Object:0x007fb1d892ac20 @number_of_seeds=1> 
red_apple.number_of_seeds
 => 1 

当然,我们可以在基于原型的编程中使用构造函数方法:

Fruit = Object.new
def Fruit.number_of_seeds=(number_of_seeds)
  @number_of_seeds = number_of_seeds
end
def Fruit.number_of_seeds
  @number_of_seeds
end
def Fruit.init(number_of_seeds)
  fruit_clone = clone
  fruit_clone.number_of_seeds = number_of_seeds
  fruit_clone
end
Apple = Fruit.init(1)
 => #<Object:0x007fcd2a137f78 @number_of_seeds=1> 
red_apple = Apple.clone
 => #<Object:0x007fcd2a1271c8 @number_of_seeds=1> 
red_apple.number_of_seeds
 => 1 

最终,使用克隆,您可以获得类似于JavaScript原型行为的信息。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.