5

Let's count classes in MRI scope:

def count_classes
  ObjectSpace.count_objects[:T_CLASS]
end
k = count_classes

Define class with class method:

class A
  def self.foo
    nil
  end
end

And run:

puts count_classes - k
#=> 3

Please, explain me, why three?

isqad
  • 290
  • 2
  • 10
  • 1
    Interesting question. In my (very, very limited) comprehension of MRI ruby's C object model, maybe: class A, class A's eigenclass ; class Class' eigenclass ? As i understand it, the "superclass" of A's eigenclass should be Class' eigenclass; and as i understand it, eigenclasses are not created until they are needed, so maybe Class' eigenclass is only created when you create your first class. Try your test twice, see if it still outputs three ? – m_x Nov 08 '13 at 17:38
  • 2
    Eigenclasses of classes are created immediately, since almost every class has at least one "class method" anyway. It doesn't make sense to create them lazily if you know that you have to create them anyway. – Jörg W Mittag Nov 08 '13 at 17:56
  • I just posted an answer (which I just deleted) that effectively replicated this exchange. FWIW, in the above example, if you don't define a class method when you define the class, the increment in number of classes is only 2 instead of 3. – Peter Alfvin Nov 08 '13 at 18:47
  • @m_x Thing is, if you create another class, APrime, the number of classes gets incremented again by 3, not by 2. – Peter Alfvin Nov 08 '13 at 19:11
  • @PeterAlfvin: it seems related to the class method handler (see the irb dump I posted). – Denis de Bernardy Nov 08 '13 at 20:10

3 Answers3

4

Looking at MRI code, every time you create a Class which in Ruby is object of type Class, automatically, ruby creates "metaclass" class for that new class, which is another Class object of singleton type.

The C function calls (class.c) are:

rb_define_class
  rb_define_class_id
    rb_class_new(super);
    rb_make_metaclass(klass, RBASIC(super)->klass);

So, every time that you define a new class, Ruby will define another class with meta information.

When you define a class method, I mean, def self.method, internally, ruby calls rb_define_singleton_method. You can check it doing the follow step:

Create a ruby file test.rb:

class A
  def self.foo
  end
end

And run the following command:

ruby --dump insns test.rb

You will have the following output:

== disasm: <RubyVM::InstructionSequence:<main>@kcount.rb>===============
0000 trace            1                                               (  70)
0002 putspecialobject 3
0004 putnil
0005 defineclass      :A, <class:A>, 0
0009 leave
== disasm: <RubyVM::InstructionSequence:<class:A>@kcount.rb>============
0000 trace            2                                               (  70)
0002 trace            1                                               (  71)
0004 putspecialobject 1
0006 putself
0007 putobject        :foo
0009 putiseq          foo
0011 opt_send_simple  <callinfo!mid:core#define_singleton_method, argc:3, ARGS_SKIP>
0013 trace            4                                               (  73)
0015 leave                                                            (  71)
== disasm: <RubyVM::InstructionSequence:foo@kcount.rb>==================
0000 trace            8                                               (  71)
0002 putnil
0003 trace            16                                              (  72)
0005 leave

The define_singleton_method is mapped to the rb_obj_define_method C function (object.c), which do following calls:

 rb_obj_define_method
   rb_singleton_class(obj)
   rb_mod_define_method

The function rb_singleton_class exposes the metaclass created when the class was defined, but it also creates a new metaclass for this metaclass.

According the Ruby documentation for this function: "if a obj is a class, the returned singleton class also has its own singleton class in order to keep consistency of the inheritance structure of metaclasses".

That is the reason why the number of class increases by 1 when you define a class method.

The same effect happens if you change your code by:

class A
end
A.singleton_class

The singleton_class is mapped to rb_obj_singleton_class C function, which calls the rb_singleton_class.

Even if you create a class method and call the singleton_class method, the number of created classes will not change, because all classes necessary to handle meta information is already created. Example:

class A
  def self.foo
    nil
  end
end

A.singleton_class

The code above will keep returning 3.

Thiago Lewin
  • 2,810
  • 14
  • 18
  • wow. All I thought about ruby's object model is wrong. What is the difference between the metaclass and the singleton class ? I always thought it was the same thing. – m_x Nov 08 '13 at 22:55
  • Thank you, Tlewin! But in the literature, always write that metaclass and sinlgeton class (and eigen class) is the same. It will be necessary to open source rb_define_singleton_method C function. – isqad Nov 09 '13 at 04:02
  • 1
    @m_x I updated the post content to elucidate this question of metaclass and singleton class. – Thiago Lewin Nov 09 '13 at 05:05
  • I think that it is true answer. – isqad Nov 09 '13 at 06:00
2

The first one is the class' eigenclass. The second one is related to the eigenclass as well, as a method handler:

>> def count_classes
>>   ObjectSpace.count_objects[:T_CLASS]
>> end
=> nil
>> k = count_classes
=> 890
>> class A; end
=> nil
>> puts count_classes - k
2                                         # eigenclass created here
=> nil
>> k = count_classes
=> 892
>> class A; def self.foo; nil; end; end
=> nil
>> puts count_classes - k
1                                         # A/class eigenclass method handler?
=> nil
>> k = count_classes
=> 893
>> class A; def bar; nil; end; end
=> nil
>> puts count_classes - k
0                                         # instance method don't count
=> nil
>> class A; def self.baz; nil; end; end
=> nil
>> puts count_classes - k
0                                         # A/eigenclass already has a handler
=> nil
>> class B < A; end
=> nil
>> puts count_classes - k
2                                         # the class and its eigenclass, again
=> nil
>> class C; end
=> nil
>> k = count_classes
=> 897
>> class C; def foo; end; end
=> nil
>> puts count_classes - k
0                                         # so... definitely class method related
>> class B; def self.xyz; end; end
=> nil
>> puts count_classes - k
1                                         # B/eigenclass handler
=> nil
>> k = count_classes
=> 898
>> a = A.new
=> #<A:0x007f810c112350>
>> puts count_classes - k
0
=> nil
>> def a.zyx; end
=> nil
>> puts count_classes - k
1                                         # a/eigenclass handler
=> nil

I'm not familiar enough with ruby internals to know for sure, but that would be my best guess.

Denis de Bernardy
  • 75,850
  • 13
  • 131
  • 154
2

At the ObjectSpace doc page, note the sentence: "The contents of the returned hash are implementation specific. It may be changed in future." In other words, with ObjectSpace.count_objects you never know, unless you dig deep in the particular Ruby implementation. Let me demonstrate this for you:

def sense_changes prev
  ObjectSpace.count_objects.merge( prev ) { |_, a, b| a - b }.delete_if { |_, v| v == 0 }
end

prev = ObjectSpace.count_objects
# we do absolutely nothing
sense_changes( prev )
#=> { :FREE=>-364,
      :T_OBJECT=>8,
      :T_STRING=>270,
      :T_HASH=>11,
      :T_DATA=>4,
      :T_MATCH=>11,
      :T_NODE=>14}

And you can keep wondering until the cows come home what the heaven has happened in the ObjectSpace while you have done nothing. As for the :T_CLASS field change by 3, that you observed, Denis's answer applies: 1 is caused by the class itself, 1 by its eigenclass, and 1 by we don't know what (Update: As tlewin has shown, it's the eigenclass's eigenclass). Let me just add that Ruby objects do not have allocated eigenclasses upon creation (Update: As tlewin has shown, classes are an exception to this rule.).

Unless you are a core developer, you barely need to wonder about the contents of ObjectSpace.count_objects hash. If you are interested in accessing classes via ObjectSpace, use

ObjectSpace.each_object( Class )

Indeed:

k = ObjectSpace.each_object( Class ).to_a
a = Class.new
ObjectSpace.each_object( Class ).to_a.size - k.size
#=> 1
ObjectSpace.each_object( Class ).to_a - k == [ a ]
#=> true
Boris Stitnicky
  • 12,444
  • 5
  • 57
  • 74