Freezing an Object to Prevent Changes
Problem
You want to prevent any further changes to the state of an object.
Solution
Freeze the object with Object#freeze:
frozen_string = 'Brrrr!' frozen_string.freeze frozen_string.gsub('r', 'a') # => "Baaaa!" frozen_string.gsub!('r', 'a') # TypeError: can't modify frozen string
Discussion
When an object is frozen, its instance variables are permanently bound to their current values. The values themselves are not frozen: their instance variables can still be modified, to the extent they were modifiable before:
sequences = [[1,2,3], [1,2,4], [1,4,9]].freeze sequences << [2,3,5] # TypeError: can't modify frozen array sequences[2] << 16 # => [1, 4, 9, 16]
A frozen object cannot be unfrozen, and if cloned, the clone will also be frozen. Calling Object#dup (as opposed to Object#clone) on a frozen object yields an unfrozen object with the same instance variables.
frozen_string.clone.frozen? # => true frozen_string.dup.frozen? # => false
Freezing an object does not prevent reassignment of any variables bound to that object.
frozen_string = 'A new string.' frozen_string.frozen? # => false
To prevent objects from changing in ways confusing to the user or to the Ruby interpreter, Ruby sometimes copies objects and freezes the copies. When you use a string as a hash key, Ruby actually copies the string, freezes the copy, and uses the copy as the hash key: that way, if the original string changes later on, the hash key isn't affected.
Constant objects are often frozen as a second line of defense against the object being modified in place. You can freeze an object whenever you need a permanent reference to an object; this is most commonly seen with strings:
API_KEY = "100f7vo4gg".freeze API_KEY[0] = 4 # TypeError: can't modify frozen string API_KEY = "400f7vo4gg" # warning: already initialized constant API_KEY
Frozen objects are also useful in multithreaded code. For instance, Ruby's internal file operations work from a frozen copy of a filename instead of using the filename directly. If another thread modifies the original filename in the middle of an operation that's supposed to be atomic, there's no problem: Ruby wasn't relying on the original filename anyway. You can adopt this copy-and-freeze pattern in multithreaded code to prevent a data structure you're working on from being changed by another thread.
Another common programmer-level use of this feature is to freeze a class in order to prevent future modifications to it (by yourself, other code running in the same environment, or other people who use your code as a library). This is not quite the same as the final construct in C# and Java, because you can still subclass a frozen class, and override methods in the subclass. Calling freeze only stops the in-place modification of a class. The simplest way to do it is to call freeze as the last statement in the class definition:
class MyClass def my_method puts "This is the only method allowed in MyClass." end freeze end class MyClass def my_method "I like this implementation of my_method better." end end # TypeError: can't modify frozen class class MyClass def my_other_method "Oops, I forgot to implement this method." end end # TypeError: can't modify frozen class class MySubclass < MyClass def my_method "This is only one of the methods available in MySubclass." end def my_other_method "This is the other one." end end MySubclass.new.my_method # => "This is only one of the methods available in MySubclass."
See Also
- Recipe 4.7, "Making Sure a Sorted Array Stays Sorted," defines a convenience method for making a frozen copy of an object
- Recipe 5.5, "Using an Array or Other Modifiable Object as a Hash Key"
- Recipe 8.16, "Making a Copy of an Object"
- Recipe 8.17, "Declaring Constants"