- Item 1: Understand What Ruby Considers to Be True
- Item 2: Treat All Objects as If They Could Be nil
- Item 3: Avoid Ruby's Cryptic Perlisms
- Item 4: Be Aware That Constants Are Mutable
- Item 5: Pay Attention to Run-Time Warnings
Item 4: Be Aware That Constants Are Mutable
If you’re coming to Ruby from another programming language, there’s a good chance that constants don’t behave the way you expect them to. But before we dig into that let’s review what Ruby considers to be a constant.
When you first learned Ruby you were probably taught that constants are identifiers that are made up of uppercase alphanumeric characters and underscores. Some examples include STDIN, ARGV, and RUBY_VERSION. But that’s not the entire story. In reality, a constant is any identifier that begins with an uppercase letter. This means that identifiers like String and Array are also constants. That’s right...the names of classes and modules are actually constants in Ruby. With that in mind, let’s take a closer look at how constants differ from other variable-like things in Ruby.
As their name suggests, constants are meant to remain unchanged during the lifetime of a program. You might assume, therefore, that Ruby would prevent you from altering the value stored in a constant. Well, that assumption would be wrong. Consider this:
module
Defaults
NETWORKS
= ["192.168.1", "192.168.2"
]end
def
purge_unreachable (networks=Defaults
::NETWORKS
) networks.delete_ifdo
|net| !ping(net +".1"
)end
end
If you invoke the purge_unreachable method without an argument, it will accidentally mutate a constant. It will do this without so much as a warning from Ruby. Essentially, constants are more like global variables than unchanging values. If you think about it, since class and module names are constants, and you can change a class at anytime (e.g., add methods), then the objects referenced by constants need to be mutable in Ruby. That’s fine for classes and modules, but not so great for the values we actually want to be constant and immutable. Thankfully, there’s a solution to this problem—the freeze method:
module
Defaults
NETWORKS
= ["192.168.1", "192.168.2"
].freezeend
With this change in place, the purge_unreachable method will raise a RuntimeError exception if it tries to alter the array referenced by the NETWORKS constant. As a general rule of thumb, always freeze constants to prevent them from being mutated. Unfortunately, freezing the NETWORKS array isn’t quite enough. Consider this:
def
host_addresses (host, networks=Defaults
::NETWORKS
) networks.map {|net| net <<"
.#{
host}
"
}end
The host_addresses method will modify the elements of the NETWORKS array if it isn’t given a second argument. While the NETWORKS array itself is frozen, its elements are still mutable. You might not be able to add or remove elements from the array, but you can surely make changes to the existing elements. So, if a constant references a collection object such as an array or hash, freeze the collection and its elements:
module
Defaults
NETWORKS
= ["192.168.1"
,"192.168.2"
, ].map!(&:freeze
).freezeend
(If you happen to be using Ruby 2.1 or later you can make use of a trick from Item 47 and freeze the string literals directly. This can save you a bit of memory while keeping the elements from accidentally being mutated.)
Freezing a constant will change an obscure, hard-to-track-down bug into an exception. That’s an obvious win. Unfortunately, it’s still not enough. Even if you freeze the object a constant refers to, you can still cause problems by assigning a new value to an existing constant. See for yourself:
irb> TIMEOUT = 5 ---> 5 irb> TIMEOUT += 5 (irb):2: warning: already initialized constant TIMEOUT (irb):1: warning: previous definition of TIMEOUT was here ---> 10
As you can see, assigning a new value to an existing constant is perfectly legal in Ruby. You can also see that Ruby produces a warning telling us that we’re redefining a constant. But that’s it, just a warning. Thankfully, if we take things into our own hands, we can make Ruby raise an exception if we accidentally redefine a constant. The solution is a bit clumsy, and may be too heavy-handed for some situations, but it’s simple. To prevent Ruby from assigning new values to existing constants, freeze the class or module they’re defined in. You may even want to structure your code so that all constants are defined in their own module, isolating the effects of the freeze method:
module
Defaults
TIMEOUT
=5
end
Defaults
.freeze
There are three levels of freezing you should consider when defining constants. The first two are easy: freeze the object that the constant references and the module the constant is defined in. Those two steps prevent the constant from being mutated or assigned to. The third is a bit more complicated. We saw that if a constant references an array of strings, we need to freeze the array and the elements. In other words, you need to deeply freeze the object the constant refers to. Each constant will be different, just make sure it’s completely frozen.
Things to Remember
- Always freeze constants to prevent them from being mutated.
- If a constant references a collection object such as an array or hash, freeze the collection and its elements.
- To prevent assignment of new values to existing constants, freeze the module they’re defined in.