Какой самый эффективный способ глубокого копирования объекта в Ruby?
Я знаю, что сериализация объекта является (насколько мне известно) единственным способом эффективного глубокого копирования объекта (если только он не является IO
и еще много чего), но разве один способ особенно эффективнее другого?
Например, так как я использую Rails, я всегда мог использовать ActiveSupport::JSON
, to_xml
- и из того, что я могу сказать, маршалинг объекта - один из наиболее приемлемых способов сделать это. Я ожидаю, что сортировка, вероятно, самая эффективная из них, так как это внутренняя часть Ruby, но я что-то упустил?
Изменить: обратите внимание, что его реализация - это то, что я уже рассмотрел - я не хочу заменять существующие методы поверхностного копирования (например, dup
а также clone
), так что я просто закончу тем, что добавлю Object::deep_copy
В результате чего какой-либо из перечисленных выше методов (или любые ваши предложения:) имеет наименьшие накладные расходы.
3 ответа
Мне было интересно то же самое, поэтому я сравнил несколько различных методов друг с другом. В первую очередь меня интересовали массивы и хэши - я не тестировал сложные объекты. Возможно, неудивительно, что пользовательская реализация глубокого клона оказалась самой быстрой. Если вы ищете быструю и простую реализацию, маршал, похоже, является подходящим вариантом.
Я также сравнил решение XML с Rails 3.0.7, которое не показано ниже. Это было намного, намного медленнее, ~10 секунд всего за 1000 итераций (нижеприведенные решения работали 10000 раз для теста).
Два замечания относительно моего решения 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)
Я думаю, вам нужно добавить метод initialize_copy в класс, который вы копируете. Затем поместите логику для глубокой копии там. Затем, когда вы вызываете клон, он запускает этот метод. Я этого не делал, но это мое понимание.
Я думаю, что план Б будет просто отменять метод клонирования:
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
Я уверен, что вы могли бы сделать этот кулер, немного повозившись, но, к лучшему или к худшему, вероятно, я бы так и сделал.
Вероятно, причина, по которой 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's 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. Эти объекты всегда имеют один и тот же идентификатор объекта и копируются, а не клонируются. - Будьте осторожны, поскольку при глубоком копировании две части вашего хэша или массива, которые содержали один и тот же объект в оригинале, будут содержать разные объекты в глубоком клоне.