Каков наиболее эффективный способ глубокого копирования объекта в Ruby?
Я знаю, что сериализация объекта (насколько мне известно) является единственным способом эффективного глубокого копирования объекта (до тех пор, пока он не выглядит так, как IO
и whatnot), но является одним из способов, особенно более эффективным, чем другой?
Например, поскольку я использую Rails, я всегда мог использовать ActiveSupport::JSON
, to_xml
- и из того, что я могу сказать, маршаллинг объекта является одним из наиболее приемлемых способов сделать это. Я бы ожидал, что маршаллинг, вероятно, самый эффективный из них, поскольку он является внутренним Ruby, но я что-то пропустил?
Изменить: обратите внимание, что его реализация - это то, что я уже рассмотрел - я не хочу заменять существующие методы мелкой копии (например, dup
и clone
), поэтому я просто вероятно, добавит Object::deep_copy
, результат которого зависит от того, какой из вышеперечисленных методов (или любых предложений у вас есть), которые имеют наименьшие издержки.
Ответы
Ответ 1
Мне было интересно то же самое, поэтому я сравнивал несколько разных методов друг против друга. Я был в основном связан с массивами и хэшами - я не тестировал сложные объекты. Возможно, неудивительно, что обычная реализация с глубоким клоном оказалась самой быстрой. Если вы ищете быструю и легкую реализацию, Маршал, похоже, подходит для этого.
Я также сравнивал XML-решение с Rails 3.0.7, не показано ниже. Это было намного, намного медленнее, ~ 10 секунд всего за 1000 итераций (решения ниже всего выполнялись в 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)
Ответ 2
Я думаю, вам нужно добавить метод initialize_copy в класс, который вы копируете. Затем поставьте логику для глубокой копии там. Затем, когда вы вызываете клон, он будет запускать этот метод. Я этого не сделал, но я понял.
Я думаю, что план 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}"
Выход
[email protected]:~/projects$ ruby ~/Desktop/clone.rb
A: test
B:
C: test
Я уверен, что вы могли бы сделать этот кулер небольшим мастером, но лучше или хуже, вероятно, так, как я это сделаю.
Ответ 3
Вероятно, причина, по которой 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
(initialize) должен быть способен глубже обтекать объект этого класса стандартным способом, то есть, если задан первый параметр, он считается объектом для глубокого склеивания.
Предположим, что мы хотим, например, класс 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. Эти объекты всегда имеют один и тот же идентификатор объекта и копируются, а не клонируются.
- Будьте осторожны, потому что, когда вы глубоко копируете, две части вашего хэша или массива, содержащие один и тот же объект в оригинале, будут содержать разные объекты в глубине.