#dup vs #clone in Ruby and Rails

I was recently fixing a failing test and discovered that Ruby and Rails implement #clone and #dup in confusing and occasionally opposite ways.

In Rails #clone is a less complete copy of an object than #dup

Rails versions have flip-flopped on how to implement #clone and #dup, and there is ambiguity in how Rails defines “shallow”. In Rails 4.0, #clone is a shallow copy of an ActiveRecord object. “Shallow” in this context means that the clone shares attributes with the original:

Identical to Ruby’s clone method. This is a “shallow” copy. Be warned that your attributes are not copied. That means that modifying attributes of the clone will modify the original, since they will both point to the same attributes hash. If you need a copy of your attributes hash, please use the #dup method.

However, #dup is also described as a shallow copy. “Shallow” in this context means that while the dup does not share attributes with the original, it does share associations.

Duped objects have no id assigned and are treated as new records. Note that this is a “shallow” copy as it copies the object’s attributes only, not its associations. The extent of a “deep” copy is application specific and is therefore left to the application to implement according to its need.

clone vs dup in Rails:

1
2
3
4
5
6
7
8
9
pry> original = User.find(3)
  User Load (0.7ms)  SELECT  "users".* FROM "users" WHERE "users"."id" = ? LIMIT 1  [["id", 3]]
=> #<User id: 3, first_name: "katie", last_name: "leonard", email: nil, created_at: "2015-01-10 17:37:00", updated_at: "2015-01-10 17:37:00">

pry> clone_copy = original.clone
=> #<User id: 3, first_name: "katie", last_name: "leonard", email: nil, created_at: "2015-01-10 17:37:00", updated_at: "2015-01-10 17:37:00">

pry> dup_copy = original.dup
=> #<User id: nil, first_name: "katie", last_name: "leonard", email: nil, created_at: nil, updated_at: nil>

Note that the clone_copy is an exact copy of the original (same user.id) and the dup_copy is a new record (user.id = nil). Any changes made to the clone_copy will be changed in the original, but any changes to the dup_copy attributes will remain isolated.

In Ruby #clone is a more complete copy of an object than #dup

With simple classes, clone() and dup() behave identically:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
irb> class User
irb>   attr_accessor :first_name, :last_name, :email
irb>   def initialize(options={})
irb>     @first_name = options[:first_name]
irb>     @last_name  = options[:last_name]
irb>     @email      = options[:email]
irb>   end
irb> end
=> :initialize

irb> original = User.new(first_name: "katie", last_name: "leonard")
=> #<User:0x007fd7e98e0aa8 @first_name="katie", @last_name="leonard", @email=nil>

irb> cloned_copy = original.clone
=> #<User:0x007fd7e98c87c8 @first_name="katie", @last_name="leonard", @email=nil>

irb> dup_copy = original.dup
=> #<User:0x007fd7e98b24a0 @first_name="katie", @last_name="leonard", @email=nil>

irb> cloned_copy.first_name = "foo"
=> "foo"

irb> original.first_name
=> "katie"

irb> dup_copy.first_name
=> "katie"

irb> dup_copy.first_name = "bar"
=> "bar"

irb> original.first_name
=> "katie"

clone() and dup() function the same way!

clone() from the Ruby docs:

Produces a shallow copy of obj — the instance variables of obj are copied, but not the objects they reference. Copies the frozen and tainted state of obj. See also the discussion under Object#dup.

dup() from the Ruby docs looks suspiciously like the docs for clone():

Produces a shallow copy of obj — the instance variables of obj are copied, but not the objects they reference. dup copies the tainted state of obj. This method may have class-specific behavior. If so, that behavior will be documented under the #initialize_copy method of the class.

This deserves further clarification:

In general, clone and dup may have different semantics in descendant classes. While clone is used to duplicate an object, including its internal state, dup typically uses the class of the descendant object to create the new instance. When using dup any modules that the object has been extended with will not be copied.

To paraphrase, #dup will act like #clone, but without the original’s singleton class (ergo a “shallower” copy).

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
irb> class User
irb> attr_accessor :first_name, :last_name, :email
irb>   def initialize(options={})
irb>     @first_name = options[:first_name]
irb>     @last_name  = options[:last_name]
irb>     @email      = options[:email]
irb>   end
irb> end
=> :initialize

irb> module Crunchy
irb>   def bacon
irb>     "bacon"
irb>   end
irb> end
=> :bacon

irb> a = User.new(first_name: "katie", last_name: "leonard")
=> #<User:0x007fd7e8882490 @first_name="katie", @last_name="leonard", @email=nil>

irb> a.extend(Crunchy)
=> #<User:0x007fd7e8882490 @first_name="katie", @last_name="leonard", @email=nil>

irb> a.bacon
=> "bacon"

irb> b = a.clone
=> #<User:0x007fd7e8843060 @first_name="katie", @last_name="leonard", @email=nil>

irb> b.bacon
=> "bacon"

irb> c = a.dup
=> #<User:0x007fd7e98f05c0 @first_name="katie", @last_name="leonard", @email=nil>

irb> c.bacon
NoMethodError: undefined method `bacon' for #<User:0x007fd7e98f05c0>
        from (irb):101
        from /usr/local/var/rbenv/versions/2.1.5/bin/irb:11:in `<main>'

There are subtle differences between #clone and #dup in Ruby, and less subtle differences in Rails (depending on your version). Take care that the object you want is the object you get.