In Ruby, the typical way to define a class is using the class keyword:

class Foo
  # ...
end

The class keyword, however, is effectively just syntax sugar for the Class constructor:

Foo = Class.new
  # ...
end

Using Class.new is occasionally preferable, e.g. when you want an anonymous class which isn’t assigned to a constant:

@myclass = Class.new
  # ...
end

I like to use this technique for certain metaprogramming tasks, and when testing/spec-ing modules:

describe MyModule
  before :each do
    @test_class = Class.new do
      include MyModule
    end
  end

  # ...

end

There are a few subtle semantic differences between the class keyword and Class.new. Because the Class constructor uses a block to define the contents of the class, it can reference the surrounding lexical scope:

>> method_name = "foo"
=> "foo"
>> class KeywordClass
>>   define_method(method_name) do
?>       puts "hello"
>>     end
>>   end
NameError: undefined local variable or method `method_name' for KeywordClass:Class
        from (irb):3
>> DynamicClass = Class.new do
?>     define_method(method_name) do
?>       puts "hello"
>>     end
>>   end
=> DynamicClass
>> DynamicClass.new.foo
hello
=> nil

I’d known about this difference for a long time. The other day I came across another difference between these two methods of class definition which was new to me, and can lead to potentially surprising behavior. It concerns the order of execution and the .inherited() callback.

When a parent class defines a class method called inherited:

class A
  def self.inherited(other)
    puts "in A.inherited"
  end
end

And then that class is subclassed:

class B < A
  puts "in class B body"
end

The order of execution is 1) run the A.inherited callback; then 2) execute the class definition body. You can see this if you run the above two blocks; the output will be:

in A.inherited
in class B body

If, however, we try to inherit from A dynamically, using the Class constructor:

C = Class.new(A) do
  puts "in class C body"
end

The output is reversed:

in class C body
in A.inherited

I’m not sure if either of these behaviors is right or wrong, per se; but it can catch you by surprise if you are expecting dynamic class definition to have the same order of operations as the keyword form. In particular, it can lead to some confusing side effects when using certain libraries that extend the behavior of Ruby’s classes. For instance, I discovered this difference while working on some code that used the class-inheritable attributes feature of ActiveSupport:

myclass = Class.new do
  class_inheritable_accessor :bar
  self.bar = 42
end
>> myclass.bar
=> nil
>> # hey!  where'd the value of bar go?! 

As it turns out, class_inheritable_accessor and its friends work by defining Object.inherited to initialize some variables that ActiveSupport uses for bookkeeping. In the dynamic form of the class definition, the class body would assign values to the inheritable attributes – and then Object.inherited would be called back, re-initializing the variables and wiping out my assigned values. The solution was to separate the class creation and definition into two steps:

myclass = Class.new
myclass.instance_eval do
  class_inheritable_accessor :bar
  self.bar = 42
end

So there you go, another, lesser-known semantic distinction between “static” and “dynamic” class definition in Ruby.

Published by Avdi Grimm

5 Comments

  1. Wow, I wrote a long comment… about as long as the original post.

    Reply
  2. Avdi,

    Thanks for sharing. I ran into this todo when refactoring some examples. It seems so odd that the order in which .inherited is called changes depending on how you construct the class.

    Reply
  3. Avdi,

    Thanks for sharing. I ran into this todo when refactoring some examples. It seems so odd that the order in which .inherited is called changes depending on how you construct the class.

    Reply
  4. Avdi, it looks like this behaviour was changed in 1.9. The output is no longer reversed when you do Class.new(A) { … }.

    Reply

Leave a Reply

Your email address will not be published. Required fields are marked *