【问题标题】:What's the difference between Ruby's dup and clone methods?Ruby 的 dup 和 clone 方法有什么区别?
【发布时间】:2012-04-28 07:53:15
【问题描述】:

Ruby docs for 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

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

【问题讨论】:

  • 我希望我不仅知道 what dupclone 的区别,而且 为什么 你会使用一个而不是另一个。
  • 这里也有一个很好的链接 - coderwall.com/p/1zflyg

标签: ruby clone dup


【解决方案1】:

您可以使用 clone 在 Ruby 中进行基于原型的编程。 Ruby 的 Object 类同时定义了 clone 方法和 dup 方法。 clone 和 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 和 orange 都引用了同一个对象!在我们的示例中,引用是字符串对象“red”。当 orange 使用 append 方法

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

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'

在上面的例子中,当我们为橙色克隆的 color 实例方法分配一个新的对象时,它不再引用与 apple 相同的对象。因此,我们现在可以在不影响 apple 的 color 方法的情况下修改 orange 的 color 方法,但是如果我们从 apple 克隆另一个对象,则该新对象将在复制的实例变量中引用与 apple 相同的对象。

dup 还会生成它正在复制的对象的浅表副本,如果您要对上面显示的 dup 进行相同的演示,您将看到它的工作方式完全相同。但是克隆和复制之间有两个主要区别。首先,正如其他人提到的, clone 复制冻结状态而 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 中进行基于原型的编程,这将非常有用。首先,让我们证明单例方法确实是用 clone 复制的,然后我们可以将它应用到 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 原型的行为。

【讨论】:

    【解决方案2】:

    子类可以覆盖这些方法以提供不同的语义。 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 implementation for these methods 通常是我回答这些问题的来源,因为它非常清晰,并且是相当兼容的 Ruby 实现。

    【讨论】:

    • 以防万一有人再次尝试更改:“单例类”,这是 Ruby 中定义明确的术语,不仅包括单例方法,还包括任何在单例类上定义的常量。考虑:o = Object.new; class &lt;&lt; o; A=5; end; puts ( class &lt;&lt; o.clone; A; end ); puts ( class &lt;&lt; o.dup; A; end ).
    • 很好的答案,然后是很好的评论,但它让我大吃一惊以理解该语法。这将帮助其他可能也感到困惑的人:devalot.com/articles/2008/09/ruby-singleton
    • 我认为值得一提的是,“单例类”还包括在原始对象上 extended 的任何模块。所以Object.new.extend(Enumerable).dup.is_a?(Enumerable) 返回 false。
    • 虽然这个答案确实回答了问题并说明了差异。还值得注意的是,这两种方法都适用于 Object#dup 文档中所述的不同情况。 clone 的用例是克隆一个对象以将其用作同一个实例(同时具有不同的对象 ID),而 dup 旨在复制一个对象作为新实例的基础。
    【解决方案3】:

    newer doc 包含一个很好的例子:

    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>
    

    【讨论】:

      【解决方案4】:

      在处理 ActiveRecord 时也有很大的不同:

      dup 创建一个新对象而不设置其 id,因此您可以通过点击.save 将新对象保存到数据库中

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

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

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

      【讨论】:

      • 这个答案是 IMO 最重要的实用信息的答案......其他答案都在深奥上,而这个答案指出了一个关键的实际差异。
      • 以上是特定于 ActiveRecord 的;这种区别在标准 Ruby 中要微妙得多。
      • @Stefan 和 @jvalanen :当我在我的 ActiveRecord 对象上应用 dupclone 方法时,我得到的结果与您在答案中提到的相反。这意味着当我使用dup 时,它会创建一个新对象,其中设置了id,而使用clone 时,它会创建一个没有设置id 的对象。你能再看看它并清除吗? .谢谢
      • Rails 5 中也没有任何变化:api.rubyonrails.org/classes/ActiveRecord/…。所以我相信你的情况有些特别......
      • 但是,cloneing 一个从未保存过的新记录应该是相当安全的吧?我可以用这种方式构建一个“模板对象”,然后克隆它以保存特定的实例吗?
      【解决方案5】:

      两者几乎相同,但 clone 比 dup 多做一件事。在克隆中,对象的冻结状态也被复制。在 dup 中,它总是会被解冻。

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

      【讨论】:

        【解决方案6】:

        一个区别是冻结对象。冻结对象的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。
        猜你喜欢
        • 1970-01-01
        • 2015-07-17
        • 2018-08-16
        • 2022-07-22
        • 2012-02-01
        • 2011-11-10
        • 2010-10-11
        • 2013-06-18
        • 2010-11-14
        相关资源
        最近更新 更多