Каков наиболее эффективный способ глубокого копирования объекта в 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. Эти объекты всегда имеют один и тот же идентификатор объекта и копируются, а не клонируются.
  • Будьте осторожны, потому что, когда вы глубоко копируете, две части вашего хэша или массива, содержащие один и тот же объект в оригинале, будут содержать разные объекты в глубине.