Ruby has class variables, which are somewhat similar to static members of classes in Java. They can be used to conveniently share references between class methods and instance methods on classes. Ruby’s class variables have some very surprising behavior at times, however. I’ll illustrate just two cases here.

Order of Initialization

Suppose we have a class and a subclass that use a class variable of the same name? Is there one variable or two? Well, that all depends on the order in which they are initialized. Suppose the classes are defined as such.


  class Superclass
    def self.superclass_value=(v)
      @@shared_class_var = v
    end

    def self.superclass_value
      @@shared_class_var
    end
  end

  class Subclass < Superclass
    def self.subclass_value=(v)
      @@shared_class_var = v
    end

    def self.subclass_value
      @@shared_class_var
    end
  end

If you set the value through the superclass first, all behaves as you might expect:


    def test_setting_in_superclass_first
      Superclass.superclass_value = 1
      Subclass.subclass_value = 2

      assert_equal 2, Superclass.superclass_value
      assert_equal 2, Subclass.subclass_value
    end

However, if you set the value through the subclass first, you end up with two separate variables!


    def test_setting_in_subclass_first
      Subclass.subclass_value = 2
      Superclass.superclass_value = 1

      assert_equal 1, Superclass.superclass_value
      assert_equal 2, Subclass.subclass_value
    end

Note that if you want to run these two tests in the same Ruby instance, you’ll need to do some setup since both tests use the same variable name.


    def setup
      Superclass.send(:remove_class_variable,"@@shared_class_var") if Superclass.class_variables.member?("@@shared_class_var")
      Subclass.send(:remove_class_variable,"@@shared_class_var") if Subclass.class_variables.member?("@@shared_class_var")
    end

When I first learned that class variables depended on the order initialization in this way, it was enough to make me never want to use them. I did, however, want to know more about how they worked. In that exploration, I found out another oddity.

Lookup Context

Class variables also behave somewhat strangley with respect to how they are looked up with some eval contexts. For instance, if you access a class variable from within a class_eval block, you would naturally expect class variable references to be looked up within the target class. Instead they’re looked up in the context of the caller.


    def test_variable_lookup_in_class_eval_block_uses_calling_context
      @@shared_class_var, Superclass.superclass_value = 9,3
      assert_equal 9, Superclass.class_eval {@@shared_class_var}
    end

On the other hand, if you pass class_eval a string instead of a block, you get the opposite behavior!


    def test_variable_lookup_in_class_eval_string_uses_target_context
      @@shared_class_var, Superclass.superclass_value = 9,3
      assert_equal 3, Superclass.class_eval("@@shared_class_var")
    end

I haven’t verified this, but my belief is that this difference is because by the time Ruby parses the code passed to class_eval in the second example the context has already changed to Superclass, while Ruby parses the block in the first example in the context of the test class. Ok, so we’ll cut Ruby a break since it’s all dynamic and doesn’t have all the information when it parses. Then what about instance_eval? Let’s try it with a block first.


    def test_variable_lookup_in_instance_eval_block_uses_calling_context
      @@shared_class_var, Superclass.superclass_value = 9,3
      assert_equal 9, Superclass.new.instance_eval {@@shared_class_var}
    end

So far so good—the rules are consistent. Ruby parsed the block and is resolving the reference to the test class’s @@shared_class_var. Unfortunately, passing a string to instance_eval is not so nice…


    def test_variable_lookup_in_instance_eval_string_uses_calling_context
      @@shared_class_var, Superclass.superclass_value = 9,3
      assert_equal 9, Superclass.new.instance_eval("@@shared_class_var")
    end

I can’t come up with an explanation for this case. How the variable referenced in the String is resolved to find the class variable from the test rather than the class of the instance where the eval is done is a mystery to me. Perhaps one day I’ll dive into the C code and figure it out, but for now I’m content to simply avoid using class variables if at all possible. They are certainly convenient at times, but not worth the cost to me.

Sorry, comments are closed for this article.