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