在 Ruby 中深度复制对象的最有效方法是啥?
Posted
技术标签:
【中文标题】在 Ruby 中深度复制对象的最有效方法是啥?【英文标题】:What's the most efficient way to deep copy an object in Ruby?在 Ruby 中深度复制对象的最有效方法是什么? 【发布时间】:2011-08-04 08:18:31 【问题描述】:我知道序列化对象是(据我所知)有效地深度复制对象的唯一方法(只要它不像IO
和诸如此类的那样有状态),但它是一种特别有效的方法比另一个?
例如,由于我使用的是 Rails,我总是可以使用 ActiveSupport::JSON
、to_xml
- 据我所知,编组对象是最被接受的方法之一。我希望编组可能是其中最有效的,因为它是 Ruby 内部的,但我错过了什么吗?
编辑:请注意,它的实现是我已经介绍过的 - 我不想替换现有的浅拷贝方法(如 dup
和 clone
),所以我只最终可能会添加Object::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/…以上是关于在 Ruby 中深度复制对象的最有效方法是啥?的主要内容,如果未能解决你的问题,请参考以下文章