Какой самый эффективный способ глубокого копирования объекта в Ruby? - PullRequest
10 голосов
/ 13 апреля 2011

Я знаю, что сериализация объекта является (насколько мне известно) единственным способом эффективного глубокого копирования объекта (при условии, что он не является состоящим из состояний, например IO и еще много чего), но является одним из способов, особенно более эффективным, чем другой

Например, поскольку я использую Rails, я всегда могу использовать ActiveSupport::JSON, to_xml - и из того, что я могу сказать, маршалинг объекта является одним из наиболее приемлемых способов сделать это. Я ожидаю, что сортировка, вероятно, самая эффективная из них, поскольку это внутренняя среда Ruby, но я что-то упустил?

Редактировать : обратите внимание, что его реализация - это то, что я уже рассмотрел - я не хочу заменять существующие методы поверхностного копирования (например, dup и clone), поэтому я просто закончу вероятнее всего, добавив Object::deep_copy, результатом которого будет какой-либо из вышеперечисленных методов (или любые ваши предложения :) с наименьшими издержками.

Ответы [ 3 ]

21 голосов
/ 03 июня 2011

Мне было интересно то же самое, поэтому я сравнил несколько различных методов друг с другом.В первую очередь меня интересовали массивы и хэши - я не тестировал сложные объекты.Возможно, неудивительно, что пользовательская реализация глубокого клона оказалась самой быстрой.Если вы ищете быструю и простую реализацию, маршал, похоже, вам поможет.

Я также сравнил решение 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)
1 голос
/ 13 апреля 2011

Я думаю, вам нужно добавить метод 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

I 'Я уверен, что вы могли бы сделать этот кулер, немного повозившись, но, к лучшему или к худшему, вероятно, я бы так и сделал.

0 голосов
/ 09 ноября 2015

Вероятно, причина, по которой 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

Если вы хотите переопределить поведение метода clone в Ruby, вы можете назвать его просто clone вместо deepclone (в 3 местах), но я понятия не имею, как переопределение поведения клонов в Ruby повлияет на библиотеки Ruby. или Ruby on Rails, так что будьте бдительны. Лично я не могу рекомендовать это делать.

Например:

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