Какая разница между методами Ruby dup и clone?

Ruby docs для dup говорят:

В общем случае clone и dup могут иметь разную семантику в классах потомков. Хотя clone используется для дублирования объекта, включая его внутреннее состояние, dup обычно использует класс объекта-потомка для создания нового экземпляра.

Но когда я делаю какой-то тест, я обнаружил, что они на самом деле одинаковы:

class Test
   attr_accessor :x
end

x = Test.new
x.x = 7
y = x.dup
z = x.clone
y.x => 7
z.x => 7

Итак, каковы различия между этими двумя методами?

Ответ 1

Подклассы могут переопределять эти методы для обеспечения различной семантики. В самом Object существует два ключевых отличия.

Во-первых, clone копирует одноэлементный класс, а dup - нет.

o = Object.new
def o.foo
  42
end

o.dup.foo   # raises NoMethodError
o.clone.foo # returns 42

Во-вторых, clone сохраняет замороженное состояние, а dup - нет.

class Foo
  attr_accessor :bar
end
o = Foo.new
o.freeze

o.dup.bar = 10   # succeeds
o.clone.bar = 10 # raises RuntimeError

Rubinius для этих методов часто является источником ответов на эти вопросы, поскольку это совершенно ясно и довольно совместимая реализация Ruby.

Ответ 2

При работе с ActiveRecord есть и значительная разница:

dup создает новый объект без установленного идентификатора, поэтому вы можете сохранить новый объект в базе данных, нажав .save

category2 = category.dup
#=> #<Category id: nil, name: "Favorites"> 

clone создает новый объект с тем же идентификатором, поэтому все изменения, внесенные в этот новый объект, будут перезаписывать исходную запись, если нажать .save

category2 = category.clone
#=> #<Category id: 1, name: "Favorites">

Ответ 3

Одно отличие от замороженных объектов. clone замороженного объекта также заморожен (тогда как dup замороженного объекта нет).

class Test
  attr_accessor :x
end
x = Test.new
x.x = 7
x.freeze
y = x.dup
z = x.clone
y.x = 5 => 5
z.x = 5 => TypeError: can't modify frozen object

Еще одно отличие заключается в методах singleton. Одинаковая история здесь, dup не копирует их, но clone делает.

def x.cool_method
  puts "Goodbye Space!"
end
y = x.dup
z = x.clone
y.cool_method => NoMethodError: undefined method `cool_method'
z.cool_method => Goodbye Space!

Ответ 4

более новый документ содержит хороший пример:

class Klass
  attr_accessor :str
end

module Foo
  def foo; 'foo'; end
end

s1 = Klass.new #=> #<Klass:0x401b3a38>
s1.extend(Foo) #=> #<Klass:0x401b3a38>
s1.foo #=> "foo"

s2 = s1.clone #=> #<Klass:0x401b3a38>
s2.foo #=> "foo"

s3 = s1.dup #=> #<Klass:0x401b3a38>
s3.foo #=> NoMethodError: undefined method `foo' for #<Klass:0x401b3a38>

Ответ 5

Оба почти идентичны, но клон делает еще одну вещь, чем dup. В клоне также скопировано замороженное состояние объекта. В dup он всегда будет разморожен.

 f = 'Frozen'.freeze
  => "Frozen"
 f.frozen?
  => true 
 f.clone.frozen?
  => true
 f.dup.frozen?
  => false 

Ответ 6

Вы можете использовать клон для программирования на прототипах в Ruby. Класс Ruby Object определяет как метод clone, так и метод dup. И clone, и dup создают мелкую копию объекта, который копируют; то есть переменные экземпляра объекта копируются, но не объекты, на которые они ссылаются. Я продемонстрирую пример:

class Apple
  attr_accessor :color
  def initialize
    @color = 'red'
  end
end

apple = Apple.new
apple.color
 => "red"
orange = apple.clone
orange.color 
 => "red"
orange.color << ' orange'
 => "red orange" 
apple.color
 => "red orange"

Обратите внимание, что в приведенном выше примере оранжевый клон копирует состояние (то есть переменные экземпляра) объекта apple, но там, где объект apple ссылается на другие объекты (например, цвет объекта String), эти ссылки не копируются. Вместо этого яблоко и апельсин оба ссылаются на один и тот же объект! В нашем примере ссылка является строковым объектом 'red'. Когда оранжевый использует метод добавления, <<, чтобы изменить существующий объект String, он изменяет строковый объект на "красный оранжевый". По сути, это также меняет apple.color, так как они оба указывают на один и тот же объект String.

Как примечание: оператор присваивания = назначит новый объект и, таким образом, уничтожит ссылку. Вот демонстрация:

class Apple
  attr_accessor :color
  def initialize
    @color = 'red'
  end
end

apple = Apple.new
apple.color
=> "red"
orange = apple.clone
orange.color
=> "red"
orange.color = 'orange'
orange.color
=> 'orange'
apple.color
=> 'red'

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

dup также создаст поверхностную копию объекта, который копирует, и если вы выполните ту же демонстрацию, показанную выше, для dup, вы увидите, что она работает точно так же. Но есть два основных различия между клоном и дупом. Во-первых, как уже упоминалось, клон копирует замороженное состояние, а dup - нет. Что это значит? Термин "замороженный" в Ruby - это эзотерический термин для неизменного, который сам по себе является номенклатурой в компьютерной науке, означая, что что-то нельзя изменить. Таким образом, замороженный объект в Ruby никак не может быть изменен; это, по сути, неизменным. Если вы попытаетесь изменить замороженный объект, Ruby вызовет исключение RuntimeError. Поскольку клон копирует замороженное состояние, если вы попытаетесь изменить клонированный объект, это вызовет исключение RuntimeError. И наоборот, поскольку dup не копирует замороженное состояние, такого исключения не будет, как мы покажем:

class Apple
  attr_accessor :color
  def initialize
    @color = 'red'
  end
end

apple = Apple.new
apple.frozen?
 => false 
apple.freeze
apple.frozen?
 => true 
apple.color = 'crimson'
RuntimeError: can't modify frozen Apple
apple.color << ' crimson' 
 => "red crimson" # we cannot modify the state of the object, but we can certainly modify objects it is referencing!
orange = apple.dup
orange.frozen?
 => false 
orange2 = apple.clone
orange2.frozen?
 => true 
orange.color = 'orange'
 => "orange" # we can modify the orange object since we used dup, which did not copy the frozen state
orange2.color = 'orange'
RuntimeError: can't modify frozen Apple # orange2 raises an exception since the frozen state was copied via clone

Во-вторых, и, что более интересно, клон копирует синглтон-класс (и, следовательно, его методы)! Это очень полезно, если вы хотите заняться программированием на основе прототипов в Ruby. Сначала давайте покажем, что действительно синглтон-методы копируются с помощью клона, а затем мы можем применить его на примере программирования на основе прототипов в Ruby.

class Fruit
  attr_accessor :origin
  def initialize
    @origin = :plant
  end
end

fruit = Fruit.new
 => #<Fruit:0x007fc9e2a49260 @origin=:plant> 
def fruit.seeded?
  true
end
2.4.1 :013 > fruit.singleton_methods
 => [:seeded?] 
apple = fruit.clone
 => #<Fruit:0x007fc9e2a19a10 @origin=:plant> 
apple.seeded?
 => true 

Как видите, синглтон-класс экземпляра объекта Fruit копируется в клон. И, следовательно, клонированный объект имеет доступ к одноэлементному методу: seeded?. Но в случае с dup это не так:

apple = fruit.dup
 => #<Fruit:0x007fdafe0c6558 @origin=:plant> 
apple.seeded?
=> NoMethodError: undefined method 'seeded?'

Теперь в программировании на основе прототипов у вас нет классов, которые расширяют другие классы, а затем создают экземпляры классов, чьи методы являются производными от родительского класса, который служит планом. Вместо этого у вас есть базовый объект, а затем вы создаете новый объект из объекта с копированием его методов и состояния (конечно, поскольку мы делаем мелкие копии с помощью клона, любые объекты, на которые ссылаются переменные экземпляра, будут совместно использоваться, как в JavaScript прототипы). Затем вы можете заполнить или изменить состояние объекта, заполнив детали клонированных методов. В приведенном ниже примере у нас есть базовый фруктовый объект. У всех фруктов есть семена, поэтому мы создаем метод number_of_seeds. Но у яблок есть одно семя, и поэтому мы создаем клон и заполняем детали. Теперь, когда мы клонируем яблоко, мы не только клонировали методы, но и клонировали государство! Помните, что клон делает поверхностную копию состояния (переменные экземпляра). И поэтому, когда мы клонируем яблоко, чтобы получить red_apple, у red_apple автоматически будет 1 семя! Вы можете думать о red_apple как об объекте, который наследуется от Apple, который, в свою очередь, наследуется от Fruit. Следовательно, именно поэтому я заглавная буква Fruit и Apple. Мы покончили с различием между классами и объектами благодаря клону.

Fruit = Object.new
def Fruit.number_of_seeds=(number_of_seeds)
  @number_of_seeds = number_of_seeds
end
def Fruit.number_of_seeds
  @number_of_seeds
end
 Apple = Fruit.clone
 => #<Object:0x007fb1d78165d8> 
Apple.number_of_seeds = 1
Apple.number_of_seeds
=> 1
red_apple = Apple.clone
 => #<Object:0x007fb1d892ac20 @number_of_seeds=1> 
red_apple.number_of_seeds
 => 1 

Конечно, у нас может быть метод конструктора в программировании на основе прототипов:

Fruit = Object.new
def Fruit.number_of_seeds=(number_of_seeds)
  @number_of_seeds = number_of_seeds
end
def Fruit.number_of_seeds
  @number_of_seeds
end
def Fruit.init(number_of_seeds)
  fruit_clone = clone
  fruit_clone.number_of_seeds = number_of_seeds
  fruit_clone
end
Apple = Fruit.init(1)
 => #<Object:0x007fcd2a137f78 @number_of_seeds=1> 
red_apple = Apple.clone
 => #<Object:0x007fcd2a1271c8 @number_of_seeds=1> 
red_apple.number_of_seeds
 => 1 

В конечном итоге, используя клон, вы можете получить нечто похожее на поведение прототипа JavaScript.