2011-04-13 51 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,並且被複制,而不被克隆。
  • 要小心,因爲當您進行深度複製時,在原始文件中包含相同對象的哈希或數組的兩部分將在深克隆中包含不同的對象。
相關問題