Метаклассы Ruby: почему три, когда определены одноэлементные методы?
Давайте посчитаем классы в области МРТ:
def count_classes
ObjectSpace.count_objects[:T_CLASS]
end
k = count_classes
Определите класс с помощью метода класса:
class A
def self.foo
nil
end
end
И запустить:
puts count_classes - k
#=> 3
Пожалуйста, объясните мне, почему три?
3 ответа
Глядя на код МРТ, каждый раз, когда вы создаете Class
который в Ruby является объектом типа Class
автоматически, ruby создает класс метакласса для этого нового класса, который является другим Class
объект синглтонного типа.
Вызов функции C (class.c
) являются:
rb_define_class
rb_define_class_id
rb_class_new(super);
rb_make_metaclass(klass, RBASIC(super)->klass);
Таким образом, каждый раз, когда вы определяете новый класс, Ruby будет определять другой класс с метаинформацией.
Когда вы определяете метод класса, я имею в виду, def self.method
внутренне, рубиновые звонки rb_define_singleton_method
, Вы можете проверить это, выполнив следующий шаг:
Создать файл ruby test.rb
:
class A
def self.foo
end
end
И выполните следующую команду:
ruby --dump insns test.rb
У вас будет следующий вывод:
== 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
define_singleton_method
отображается на rb_obj_define_method
Функция C (object.c
), которые выполняют следующие вызовы:
rb_obj_define_method
rb_singleton_class(obj)
rb_mod_define_method
Функция rb_singleton_class
предоставляет метакласс, созданный при определении класса, но также создает новый метакласс для этого метакласса.
Согласно документации Ruby для этой функции: "если obj является классом, возвращаемый одноэлементный класс также имеет свой собственный одноэлементный класс, чтобы сохранить согласованность структуры наследования метаклассов".
По этой причине число классов увеличивается на 1 при определении метода класса.
Тот же эффект происходит, если вы измените свой код:
class A
end
A.singleton_class
singleton_class
сопоставлен с rb_obj_singleton_class
Функция C, которая вызывает rb_singleton_class
,
Даже если вы создаете метод класса и вызываете singleton_class
Метод, количество созданных классов не изменится, потому что все классы, необходимые для обработки метаинформации, уже созданы. Пример:
class A
def self.foo
nil
end
end
A.singleton_class
Код выше будет продолжать возвращаться 3.
Первый - это собственный класс. Второй относится и к собственному классу, как к обработчику методов:
>> 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
Я не достаточно знаком с внутренностями рубина, чтобы знать наверняка, но это было бы моим лучшим предположением.
На ObjectSpace
Страница документа, обратите внимание на предложение: "Содержимое возвращенного хэша зависит от конкретной реализации. Оно может быть изменено в будущем". Другими словами, с ObjectSpace.count_objects
вы никогда не узнаете, если только не углубитесь в конкретную реализацию Ruby. Позвольте мне продемонстрировать это для вас:
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}
И вы можете продолжать задаваться вопросом, пока коровы не вернутся домой, что случилось на небе ObjectSpace
пока ты ничего не сделал. Для :T_CLASS
Если вы заметили изменение поля на 3, то ответ Дениса применим: 1 вызван самим классом, 1 - его собственным классом, а 1 - тем, что мы не знаем, что (Обновление: как показал tlewin, это собственный класс собственного класса). Позвольте мне просто добавить, что у объектов Ruby при создании не было выделенных собственных классов (Обновление: как показал tlewin, классы являются исключением из этого правила).
Если вы не являетесь основным разработчиком, вам не нужно задумываться о содержании ObjectSpace.count_objects
хэш. Если вы заинтересованы в доступе к классам через ObjectSpace
использовать
ObjectSpace.each_object( Class )
В самом деле:
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