【问题标题】:What's the most efficient way to deep copy an object in Ruby?在 Ruby 中深度复制对象的最有效方法是什么?
【发布时间】:2011-08-04 08:18:31
【问题描述】:

我知道序列化对象是(据我所知)有效地深度复制对象的唯一方法(只要它不像IO 和诸如此类的那样有状态),但它是一种特别有效的方法比另一个?

例如,由于我使用的是 Rails,我总是可以使用 ActiveSupport::JSONto_xml - 据我所知,编组对象是最被接受的方法之一。我希望编组可能是其中最有效的,因为它是 Ruby 内部的,但我错过了什么吗?

编辑:请注意,它的实现是我已经介绍过的 - 我不想替换现有的浅拷贝方法(如 dupclone),所以我只最终可能会添加Object::deep_copy,其结果是上述任何一种方法(或您有的任何建议:) 开销最小。

【问题讨论】:

    标签: ruby-on-rails ruby serialization marshalling deep-copy


    【解决方案1】:

    Ruby 不包含深度克隆的原因可能与问题的复杂性有关。见文末注释。

    要进行“深度复制”、哈希、数组和元素值的克隆,即复制原始元素中的每个元素,以便副本具有相同的值,但新对象,您可以使用这个:

    class Object
      def deepclone
        case
        when self.class==Hash
          hash = {}
          self.each { |k,v| hash[k] = v.deepclone }
          hash
        when self.class==Array
          array = []
          self.each { |v| array << v.deepclone }
          array
        else
          if defined?(self.class.new)
            self.class.new(self)
          else
            self
          end
        end
      end
    end
    

    如果您想重新定义 Ruby 的 clone 方法的行为,您可以将其命名为 clone 而不是 deepclone(在 3 个位置),但我不知道重新定义 Ruby 的克隆行为将如何影响 Ruby 库, 或 Ruby on Rails,所以警告 Emptor。就个人而言,我不建议这样做。

    例如:

    a = {'a'=>'x','b'=>'y'}                          => {"a"=>"x", "b"=>"y"}
    b = a.deepclone                                  => {"a"=>"x", "b"=>"y"}
    puts "#{a['a'].object_id} / #{b['a'].object_id}" => 15227640 / 15209520
    

    如果您希望您的 类正确地进行深度克隆,它们的new 方法(初始化)必须能够以标准方式深度克隆该类的对象,即,如果给出第一个参数, 假定它是要深度克隆的对象。

    假设我们想要一个类 M,例如。第一个参数必须是 M 类的可选对象。这里我们有第二个可选参数z 来预先设置新对象中 z 的值。

    class M
      attr_accessor :z
      def initialize(m=nil, z=nil)
        if m
          # deepclone all the variables in m to the new object
          @z = m.z.deepclone
        else
          # default all the variables in M
          @z = z # default is nil if not specified
        end
      end
    end
    

    z 预设在此处克隆期间将被忽略,但您的方法可能具有不同的行为。这个类的对象会这样创建:

    # a new 'plain vanilla' object of M
    m=M.new                                        => #<M:0x0000000213fd88 @z=nil>
    # a new object of M with m.z pre-set to 'g'
    m=M.new(nil,'g')                               => #<M:0x00000002134ca8 @z="g">
    # a deepclone of m in which the strings are the same value, but different objects
    n=m.deepclone                                  => #<M:0x00000002131d00 @z="g">
    puts "#{m.z.object_id} / #{n.z.object_id}" => 17409660 / 17403500
    

    M 类的对象是数组的一部分:

    a = {'a'=>M.new(nil,'g'),'b'=>'y'}               => {"a"=>#<M:0x00000001f8bf78 @z="g">, "b"=>"y"}
    b = a.deepclone                                  => {"a"=>#<M:0x00000001766f28 @z="g">, "b"=>"y"}
    puts "#{a['a'].object_id} / #{b['a'].object_id}" => 12303600 / 12269460
    puts "#{a['b'].object_id} / #{b['b'].object_id}" => 16811400 / 17802280
    

    注意事项:

    • 如果deepclone 尝试克隆一个没有以标准方式克隆自身的对象,它可能会失败。
    • 如果deepclone 尝试克隆一个可以以标准方式克隆自身的对象,并且如果它是一个复杂的结构,它可能(并且可能会)对其自身进行浅层克隆。
    • deepclone 不会深度复制哈希中的键。原因是它们通常不被视为数据,但如果您将hash[k] 更改为hash[k.deepclone],它们也会被深度复制。
    • 某些元素值没有new 方法,例如Fixnum。这些对象始终具有相同的对象 ID,并且是复制的,而不是克隆的。
    • 请小心,因为当您进行深度复制时,原始中包含相同对象的 Hash 或 Array 的两个部分将在深度克隆中包含不同的对象。

    【讨论】:

      【解决方案2】:

      我也想知道同样的事情,所以我比较了一些不同的技术。我主要关心数组和哈希——我没有测试任何复杂的对象。不出所料,定制的深度克隆实现被证明是最快的。如果您正在寻找快速简便的实施方式,Marshal 似乎是您的最佳选择。

      我还使用 Rails 3.0.7 对 XML 解决方案进行了基准测试,如下所示。仅 1000 次迭代就慢得多,大约 10 秒(下面的解决方案都运行了 10,000 次基准测试)。

      关于我的 JSON 解决方案的两个注意事项。首先,我使用了 C 变体,版本 1.4.3。其次,它实际上并不能 100% 工作,因为符号将被转换为字符串。

      这都是使用 ruby​​ 1.9.2p180 运行的。

      #!/usr/bin/env ruby
      require 'benchmark'
      require 'yaml'
      require 'json/ext'
      require 'msgpack'
      
      def dc1(value)
        Marshal.load(Marshal.dump(value))
      end
      
      def dc2(value)
        YAML.load(YAML.dump(value))
      end
      
      def dc3(value)
        JSON.load(JSON.dump(value))
      end
      
      def dc4(value)
        if value.is_a?(Hash)
          result = value.clone
          value.each{|k, v| result[k] = dc4(v)}
          result
        elsif value.is_a?(Array)
          result = value.clone
          result.clear
          value.each{|v| result << dc4(v)}
          result
        else
          value
        end
      end
      
      def dc5(value)
        MessagePack.unpack(value.to_msgpack)
      end
      
      value = {'a' => {:x => [1, [nil, 'b'], {'a' => 1}]}, 'b' => ['z']}
      
      Benchmark.bm do |x|
        iterations = 10000
        x.report {iterations.times {dc1(value)}}
        x.report {iterations.times {dc2(value)}}
        x.report {iterations.times {dc3(value)}}
        x.report {iterations.times {dc4(value)}}
        x.report {iterations.times {dc5(value)}}
      end
      

      结果:

      user       system     total       real
      0.230000   0.000000   0.230000 (  0.239257)  (Marshal)
      3.240000   0.030000   3.270000 (  3.262255)  (YAML) 
      0.590000   0.010000   0.600000 (  0.601693)  (JSON)
      0.060000   0.000000   0.060000 (  0.067661)  (Custom)
      0.090000   0.010000   0.100000 (  0.097705)  (MessagePack)
      

      【讨论】:

      • 嘿@Evan Pon,我在您的示例中添加了MessagePack。这是一个不错的选择。
      • MessagePack 看起来非常快(在我的机器上比 Custom 快 2 倍)。您能否更新答案并推荐使用它而不是 Marshal?
      • @AndreyBotalov,MessagePack 只处理几个类——主要是数字、字符串、数组和散列。如果您有任何其他类型的对象,例如 Date 对象,它将不适合您。
      • 我发现如果你深度克隆的值更大(更多的键、更多的嵌套、更大的值),自定义的 ruby​​ 代码比 MessagePack 慢。
      【解决方案3】:

      我认为您需要在要复制的类中添加一个 initialize_copy 方法。然后把深拷贝的逻辑放在那里。然后,当您调用 clone 时,它​​将触发该方法。我没做过,但这是我的理解。

      我认为 B 计划只是覆盖克隆方法:

      class CopyMe
          attr_accessor :var
          def initialize var=''
            @var = var
          end    
          def clone deep= false
            deep ? CopyMe.new(@var.clone) : CopyMe.new()
          end
      end
      
      a = CopyMe.new("test")  
      puts "A: #{a.var}"
      b = a.clone
      puts "B: #{b.var}"
      c = a.clone(true)
      puts "C: #{c.var}"
      

      输出

      mike@sleepycat:~/projects$ ruby ~/Desktop/clone.rb 
      A: test
      B: 
      C: test
      

      我敢肯定,只要稍加修改,你就可以让它变得更酷,但无论好坏,我都会这样做。

      【讨论】:

      • 感谢反馈 - 这是一种替换它的方法,但它最终被实施最好是不引人注目的,并且将原始方法保留为浅拷贝(例如,我只需添加 Object::deep_copy )。您是否了解哪种方法的开销最小?
      • 已更新。我希望这会有所帮助。
      • +1,在此处查看一些示例:blog.rubybestpractices.com/posts/rklemme/…
      猜你喜欢
      • 2021-03-20
      相关资源
      最近更新 更多