Object#freeze turns an object into an immutable one. In this article we are going to uncover a counterintuitive behavior related to the difference between a reference and a variable.
From the documentation:
Prevents further modifications to obj. A RuntimeError will be raised if modification is attempted. There is no way to unfreeze a frozen object.
a = ["a", "b", "c"].freeze a.frozen? # => true a << "d" # => RuntimeError: can't modify frozen Array a.push("d") # => RuntimeError: can't modify frozen Array a += ["d"] # => ["a", "b", "c", "d"] a # => ["a", "b", "c", "d"] a.frozen? # => false
In fact, the explanation behind this behavior is quite obvious.
Object#freeze acts on an object reference, not on the variable itself.
+= is an assignment operation, not a method call. This is why using
+= cannot throw an exception.
a += x is equivalent to
a = a + x.
a + x produces a new object that will be assigned to
a. The object previously referenced by
a is still present in memory until the garbage collector starts. Now
a can no longer be used to access this (frozen) object because
a refers to the newly created object (not frozen).
Demonstration with code:
GC.disable a = [ "a", "b", "c" ].freeze a.object_id # => 70102913497680 a += ["d"] # => ["a", "b", "c", "d"] a.object_id # => 70102913523760 ObjectSpace._id2ref(70102913497680) # => ["a", "b", "c"] GC.enable GC.start ObjectSpace._id2ref(70102913497680) # => RangeError: 0x003fc21b41ba50 is recycled object
In this demonstration we clearly see that
+= produces a new object (cf.
Object#object_id) that is assigned to
a. We can still access the previous object by using
ObjectSpace::_id2ref. Once we start the garbage collector, this object is freed from memory (
The question you have to ask yourself is whether you want to modify an existing object (calling a method) or create a new one (assignment).