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

Категории