RubyDev Ruby Tutorial > Копирование объектов в Ruby

Posted by Марк Мельник on September 3, 2012

В Ruby переменные — это просто ссылки на объект в памяти. Присваивая переменной значение, вы не самом деле ссылаетесь ей на объект. Присваивая переменной значение — другую переменную вы просто создаете две ссылки на один и тот же объект:

original = {
  :ruby => ["rails", "sinatra"],
  :erlang => ["chicago_boss", "nitrogen"]
}

another_var = original

Одинаковый object_id — значит и переменные ссылаются на один и тот же объект:

original.object_id #=> 72411200
another_var.object_id #=> 72411200

original[:ruby][0] = "Ruby on Rails"
another_var[:ruby][0] #=> "Ruby on Rails"

Для создания нового объекта — копии используют два метода — #dup и #clone. Ниже преведены примеры использования обоих.

duplicate = original.dup

original.object_id #=> 72411200
duplicate.object_id #=> 72298510

original[:erlang][0] = "Chicago Boss"
duplicate[:erlang][0] #=> "Chicago Boss"

original[:erlang][0].object_id #=> 71899560
duplicate[:erlang][0].object_id #=> 71899560

#dup создает shallow copy объекта. Что это узнаете ниже.

##А сейчас рассмотрим метод #clone:

clone = original.clone
original.object_id #=> 72411200
clone.object_id #=> 71139640

original[:ruby] << "Padrino"
clone[:ruby] #=> ["Ruby on Rails", "sinatra", "Padrino"]

#clone также создает shallow copy

##Что такое shallow copy?

Скажем так, существует 2 типа копий объекта: глубокие и мелкие копии. Shallow copy — в переводе означает «мелкая копия». Сейчас объясню почему.

#clone и #dup создают мелкие копии объектов, то есть копируют объект, но не составляющие его объекты. Если у нас есть массив строк, то делая его shallow copy мы получаем новый массив, но объекты — строки в нем будут те же. На практике это часто бывает бессмысленно, ведь действия выполненные над повторяющимися в обоих массивах элементами будут изменять элементы в обоих массивах, что небезопасно так как копирование объектов, чаще всего, нам необходимо в том случае, когда нам необходимо одновременно сохранить оригинальные данные и выполнить какие-то преобразования над ними. Используя shallow copy мы можем легко повредить оригинальные данные. Пример:

original = ["rails", "sinatra", "padrino"]
duplicate = original.dup

duplicate.each &:upcase!
duplicate #=> ["RAILS", "SINATRA", "PADRINO"]
original #=> ["RAILS", "SINATRA", "PADRINO"]

Как видите, оригинальные данные хранившиеся в переменной original были утеряны.

##Как быть, если необходима глубокая копия объекта?

В этом случае прибегают к трюку с использованием модуля Marshal. Marshal обладает 2 интересными методами: dump и load, первый (dump) позволяет закодировать объект в строку, чтобы потом, например, сохранить его в файл, а второй (load) позволяет восстановить объект из строки созданной методом dump. Ах да, эту строку называют дампом объекта, потому и название у метода такое — dump.

Выглядит эта техника следующим образом:

original = {
  :ruby => ["rails", "sinatra"],
  :erlang => ["chicago_boss", "nitrogen"]
}

deep_copy = Marshal.load(Marshal.dump(original))

original.object_id #=> 72729700
deep_copy.object_id #=> 72715820

deep_copy[:ruby][0] = "Ruby on Rails"
deep_copy[:ruby][0] #=> "Ruby on Rails"
original[:ruby][0] #=> "rails"

Пример попроще:

original = ["rails", "sinatra", "padrino"]
duplicate = Marshal.load(Marshal.dump(original))

duplicate.each &:upcase!
duplicate #=> ["RAILS", "SINATRA", "PADRINO"]
original #=> ["rails", "sinatra", "padrino"]

Как видите данная техника отлично работает и мы можем безопасно работать с данными.

При желании, можно добавить в класс Object метод #deep_copy, чтобы все было совсем «красиво»:

class Object
  def deep_copy
    Marshal.load(Marshal.dump(self))
  end
end

deep_copy = original.deep_copy
deep_copy.each &:upcase!

deep_copy #=> ["RAILS", "SINATRA", "PADRINO"]
original #=> ["rails", "sinatra", "padrino"]

Однако мне это кажется лишним потому, что необходимость в таких действиях возникает достаточно редко.

На этом можно было бы закончить статью, но мы кое-что упустили. Что? — Различие между методами #dup и #clone. Эти методы, хоть и оба выполняют создание shallow copy объекта, работают несколько отлично, то есть #clone не является синонимом для #dup — это действительно 2 разных метода!

##Так в чем же разница?

Разница в следующем — #clone выполняет несколько более сложную работу, чем #dup, эта более сложная работа заключается в следующих двух особенностях #clone:

  • #clone копирует singleton — класс объекта, то есть shallow-копие объекта доступны singleton методы скопированного объекта.
  • #clone сохраняет frozen-статус копируемого объекта. Пример:
original = {
  :ruby => ["rails", "sinatra"],
  :erlang => ["chicago_boss", "nitrogen"]
}

# создаем уникальный (singleton) метод объекта original
def original.framework_names
  self.values.flatten
end

original.singleton_methods #=> [:framework_names]
original.framework_names
#=> ["rails", "sinatra", "chicago_boss", "nitrogen"]

# "замораживаем" объект (объект нельзя изменять)
original.freeze
original.frozen? #=> true

# делаем копию с использованием #dup
duplicate = original.dup
duplicate.frozen? #=> false
duplicate.singleton_methods #=> []

# делаем копию с использованием #clone
clone = original.clone
clone.frozen? #=> true
clone.singleton_methods #=> [:framework_names]

Вот и все! Удачи!