deep
ly convinced that I’m a lion..The Complete Guide to Create a Copy of an Object in Ruby: Part II
In this article, we’re going to explore the following topics:
- the
#initialize_copy
method - the
Marshal
class - the
#deep_dup
method in Rails
Have a quick look to the Part I (3 minutes read)
Introduction
Ruby doesn’t handle deep copy by default.
Indeed, it only handles shallow copy through the Object#clone
and Object#dup
methods.
But what if we still need to make a deep copy of an object?
In this article, we’ll detail 3 ways to make a deep copy of an object in Ruby.
The #initialize_copy
method
The #initialize_copy
hook method can be used to interact with the freshly created copy of a given object.
We can tweak this hook to achieve a deep copy.
First, let’s recap how to achieve a shallow copy in Ruby
Here the problem is that khabib
and tony_fergusson
share the same belt — UFC Lightweight Champion
.
This means that if khabib
changes the title.name
attribute then the change will be propagated to the tony_fergusson.title.name
one — as they share the same Title
instance.
So, let’s implement the Fighter#initialize_copy
to make a deep copy of khabib
In the above example, we define Fighter#initialize_copy
that assigns a shallow copy of the original_fighter.title
(khabib
) to self.title
(tony_fergusson
).
So, if we compare the object_id
of the 2 titles we can see that they’re now 2 different instances of Title
.
That works just fine!
But the problem here is that we “tweak” a method that is called in the context of a shallow copy (via #dup
and #clone
) in order to process a deep copy.
Of course, it’s not a serious issue!
But let’s have a look at the other alternatives.
The Marshal
class
Object Marshalling
is the concept of formatting the in-memory representation of an object to make it suitable for storage & deserialization.
Basically, for a given object:
- we serialize all its attribute values
- we store the serialization (for example, in a file or a variable)
- we deserialize it at any time and get back a new instance of the serialized object with the same values.
In Ruby, the Object Marshalling
logic is mainly implemented in the Marshal
class.
Let’s have a look at the following example to detail how this class works
In the above example, we can see that the jd
variable contains an instance of User
.
Then we serialize this instance by using the Marshal.dump
method.
This method returns a string that is the serialization of the memory representation of the jd
variable.
Finally, we deserialize this string by using the Marshal.load
method.
This method returns a new instance of User
that contains the exact same values as jd
.
As we can see, jd
and other_jd
are two distinct instances of User
.
Now, let’s see how to process a deep copy using Object Marshalling
.
To do so, let’s refactor the Fighter
and Title
classes example
Here, we serialize the khabib
instance of Fighter
by using the Marshal.dump
method.
Then we deserialize it by using the Marshal.load
method and we store the result in the tony_ferguson
variable.
As we can see, we achieve the same result as with the #initialize_copy
method.
In effect, the title
attribute of khabib
and tony_fergusson
are two distinct instances of Title
.
There can be 2 main concerns to processing a deep copy by using object marshaling:
- it can become a very slow operation at scale
- it’s not fully working with complex objects.
Otherwise, it’s quite an efficient way to achieve deep copy in Ruby.
Now let’s have a look at what Ruby on Rails proposes to handle deep copy.
Note that I will dig into
Object Marshalling
in Ruby in another article.
The #deep_dup
method in Rails
The Ruby on Rails framework provides a Object#deep_dup
method that allows you to create a deep copy of a given object.
This solution is implemented in ~30 LOC
1- The Object#duplicable?
method returns true
.
This means that an instance that contains the Object
class in its ancestor chain is eligible to deep copy.
There are only a few classes that are not “duplicable”:
NilClass FalseClass TrueClass Symbol Numeric BigDecimal Method Complex Rational
2- Object#deep_dup
returns the return value of the dup
method call or self
if the object is not eligible to deep copy.
3- The Array#deep_dup
method map
through the calling array and calls #deep_dup
on each element of the array.
4- The Hash#deep_dup
method dup
the calling hash and iterates through the calling hash.
Then, for each pair, it checks if the key is a frozen object.
If so, then only the value
is #deep_dup
.
Otherwise, the key
and the value
are #deep_dup
.
There is also a set of #initialize_copy
methods that are defined to handle complex objects (in ActiveRecord::Relation
, etc..).
Ruby Mastery
We’re currently finalizing our first online course: Ruby Mastery.
Join the list for an exclusive release alert! 🔔
Also, you can follow us on x.com as we’re very active on this platform. Indeed, we post elaborate code examples every day.
💚