2011-04-13 49 views
10

我知道序列化一个对象(据我所知)是有效深度复制对象的唯一方法(只要它不像IO那样有状态),但是是一种比另一种更有效的方法吗?在Ruby中深入复制对象的最有效方法是什么?

例如,因为我使用的是Rails,所以我总是可以使用ActiveSupport::JSONto_xml - 从我可以告诉编组该对象是最可接受的方法之一。我期望编组可能是最有效的,因为它是Ruby内部的,但是我错过了什么?

编辑:请注意,它的实现是我已经覆盖的 - 我不想取代现有的浅拷贝方法(如dupclone),所以我刚刚结束了有可能加入Object::deep_copy,结果其中无论上述哪种方法(或者您拥有的任何建议:)都具有最小的开销。

回答

21

我想知道同样的事情,所以我基准了几种不同的技术。我主要关心数组和哈希 - 我没有测试任何复杂的对象。也许毫不奇怪,定制的深度克隆实现被证明是最快的。如果你正在寻找快速和简单的实施,元帅似乎是要走的路。

我还使用Rails 3.0.7对XML解决方案进行了基准测试,未在下面显示。速度慢得多,只需要1000次迭代大约10秒钟(下面的解决方案都是基准测试的10000次)。

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

这是所有与红宝石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) 
+0

谢谢,埃文!好东西,我很欣赏基准。 :) – mway 2011-06-03 23:15:34

+1

嘿@Evan Pon,我在你的例子中添加了[MessagePack](http://msgpack.org/)。这是一个不错的选择。 – 2012-07-06 04:37:53

+0

MessagePack看起来非常快(比我的机器上的Custom快2倍)。你能否用建议更新答案而不是元帅? – 2013-02-03 09:31:03

1

我认为你需要为你正在复制的类添加一个initialize_copy方法。然后将深层副本的逻辑放在那里。然后当你调用克隆时,它会触发该方法。我没有这样做,但这是我的理解。

我觉得B计划将只是重写clone方法:

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}" 

输出

[email protected]:~/projects$ ruby ~/Desktop/clone.rb 
A: test 
B: 
C: test 

我相信你能有这样一个小的修修补补散热器但是好是坏这可能是我如何做到的。

+0

欣赏的反馈 - 这是一种方式来取代它,但但它最终被执行将非常不显眼,离开原来的方法,以浅拷贝完整(如我只是加上'对象:: deep_copy' )。你见过哪种方法提供最少的开销? – mway 2011-04-14 01:03:55

+0

已更新。我希望这会有所帮助。 – mikewilliamson 2011-04-14 01:57:30

+0

+1,看到这里的一些例子:http://blog.rubybestpractices.com/posts/rklemme/018-Complete_Class.html#Cloning – akostadinov 2014-07-29 13:20:03

0

可能的原因红宝石不包含深克隆具有与问题的复杂性有关。请参阅最后的注释。

要生成一个将“深度复制”,“哈希”,“数组”和元素值的克隆,即,使每个元素的副本,在原来使得副本都会有相同的价值观,但新的对象,你可以这样做:如果你想重新定义Ruby的clone方法的行为

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 

,你可以它只是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 

如果你想类deepclone得当,他们new方法(初始化)必须能够deepclone那个类的一个对象的标准方式,即如果第一个参数被给出,它被假定为要被深度克隆的对象。

假设我们想要一个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,并且被复制,而不被克隆。
  • 要小心,因为当您进行深度复制时,在原始文件中包含相同对象的哈希或数组的两部分将在深克隆中包含不同的对象。
相关问题