在Ruby中深度复制对象的最有效方法是什么?

时间:2011-04-13 01:20:39

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

我知道序列化一个对象是(据我所知)有效深度复制一个对象的唯一方法(只要它不像IO那样有状态),但是一种方式特别多效率高于另一个?

例如,由于我正在使用Rails,我总是可以使用ActiveSupport::JSONto_xml - 从我可以告诉编组的对象是最常用的方法之一。我希望编组可能是最有效的,因为它是一个Ruby内部,但我错过了什么?

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

3 个答案:

答案 0 :(得分:21)

我想知道同样的事情,所以我对几种不同的技术进行了基准测试。我主要关注数组和哈希 - 我没有测试任何复杂的对象。也许不出所料,自定义深度克隆实现被证明是最快的。如果您正在寻找快速简便的实施方案,那么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)

答案 1 :(得分:1)

我认为您需要在要复制的类中添加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

我确信你可以通过一点点的修补来制造更酷的但是无论好坏,这可能就是我的意思。

答案 2 :(得分:0)

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,因此使用Caveat 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的两个部分将包含深层克隆中的不同对象。