How To Avoid The Ruby Class Variable Problem
Have you noticed the weirdeness yet? I know for a very long time, I didn’t see it.
You probably think class variables work like instance variables.
But they do not.
What is a class variable?
You’ve probably seen them here and there. They look a little different, just because they are preceded by the (
@@) sign. Here’s a quick example of a class variable in action.
# foo.rb class Foo @@class_var = 'Hello!' def self.read_it puts @@class_var end end Foo.read_it
And if you were to execute this little script, it would do exactly what you would expect. Nothing wrong here.
$ ruby ./foo.rb Hello!
The principle of least surprise
There’s a silent secret that we’ve all got used to, and to be completely honest; it kind of spoils us. That secret is called the principle of least surprise and it refers to the fact that Ruby most often does what you expect.
You can rely on your intuition most of the time and things just work.
The broken promise
There’s one issue thought that’s been bugging me for a while, and I think it breaks that principle. It’s about the way ruby looks up class variables before assignment.
To illustrate this, let’s continue with the little example from above and add two subclasses (I’m just appending the following code to the same file
# ... continuing foo.rb with the following code class Bar < Foo @@class_var = 'World' end class Baz < Foo @@class_var = 'Underworld' end Foo.read_it Bar.read_it Baz.read_it
Can you guess what the output of executing the script will be? If you do, you can probably skip the rest of the article.
$ ruby ./foo.rb Hello! Underworld Underworld Underworld
If you’re stuned by that output, don’t worry. I had the same exact reaction when I first saw it; it’s why I’m writing this article in the first place.
So what just happened here?
The problem lies in the way Ruby resolves class variables.
If the variable is not defined in the current class, Ruby will go up the inheritance tree looking for it. If it finds it in a superclass, it sets it right then and there. Otherwise, it creates one in the current class and sets that.
Obviously, that behavior is not obvious :) So how do you fix it?
Using class instance variables
Just like this title says, the solution is to use class instance variables instead.
Here’s how the code looks if you use class instance variables.
# foo.rb class Foo @ivar = 'Hello!' def self.read_it puts @ivar end end Foo.read_it class Bar < Foo @ivar = 'World' end class Baz < Foo @ivar = 'Underworld' end Foo.read_it Bar.read_it Baz.read_it
And if you run that, here’s what you will get.
$ ruby ./foo.rb Hello! Hello! World Underworld
That’s more like it. Or at least, that’s whay I would expect to see.