Определение методов
Предпосылки
Компиляция — ISeq. Исполнение — фреймы,
self, EP. Объекты и классы — метакласс, singleton-класс, m_tbl. Модули — поиск методов по цепочке super, лексическая область (CREF).
← Диспетчеризация методов | Блоки →
В заметке о компиляции мы видели, что def создаёт отдельный ISeq для тела метода. В заметках о модулях и диспетчеризации — как Ruby находит метод по цепочке super и вызывает его. Но один шаг пропущен: как скомпилированный ISeq попадает в таблицу методов класса?
Куда попадает метод
Возьмём простой пример:
class Quote
def display
puts "Hello"
end
endМы уже знаем из заметки о компиляции, что при парсинге Ruby создаёт отдельный ISeq для display. Но куда он попадает при выполнении? В таблицу методов Quote. Вопрос — как Ruby узнаёт, что целевой класс именно Quote.
Ответ: из лексической области. Когда Ruby встречает class Quote, он создаёт структуру CREF (rb_cref_t в исходниках), которая содержит два ключевых поля — klass_or_self (указатель на класс Quote) и next (ссылка на предыдущую область). CREF-ы образуют цепочку: каждый вложенный class/module добавляет звено.
Эта цепочка хранится в специальном слоте фрейма: ep[-2] — зарезервированная ячейка ниже локальных переменных, где VM хранит CREF текущей области (или method entry для иных типов фреймов). Функция vm_env_cref() (vm_insnhelper.c:861) извлекает CREF, проходя по цепочке EP.
Когда Ruby выполняет def display, инструкция definemethod делает три шага:
- Получает CREF из текущего фрейма.
- Берёт из него
klass_or_self— этоQuote. - Вызывает
rb_add_method_iseq()(vm_method.c:1599), которая вставляет method entry вm_tblэтого класса.
Простое правило: def всегда добавляет метод в класс текущей лексической области. Но есть конструкции, которые меняют целевой класс — не через CREF, а через явное указание получателя.
def self.method — обход лексической области
class Quote
def self.display
puts "Hello"
end
endЗдесь self — это класс Quote (внутри области class Quote значение self равно самому классу). Но def self.display работает иначе, чем обычный def. Компилятор генерирует другую инструкцию — definesmethod (compile.c:11381). Она не использует CREF. Вместо этого:
- Вычисляет выражение перед точкой (
self→Quote). - Берёт
CLASS_OFэтого объекта. Для классаQuoteэто его метакласс (из заметки об объектах). - Добавляет метод в
m_tblметакласса.
Если бы вместо self стоял экземпляр объекта, Ruby создал бы singleton-класс для этого объекта и поместил метод туда:
obj = Quote.new
def obj.special
"only for this object"
endМетакласс и singleton-класс — один механизм (singleton-класс класса называется метаклассом). def prefix.method всегда идёт в CLASS_OF(prefix), минуя лексическую область полностью.
class << obj — новая лексическая область
class Quote
class << self
def display
puts "Hello"
end
end
endТретий способ получить тот же результат. class << self работает как обычный class, но вместо создания нового класса:
- Вычисляет выражение после
<<(self→Quote). - Получает singleton-класс этого объекта — через
rb_singleton_class(). - Создаёт новый CREF, в котором
klass_or_selfуказывает на этот singleton-класс.
После этого обычный def display внутри class << self идёт через стандартный механизм CREF — и попадает в singleton-класс. Это удобно, когда нужно определить несколько методов класса, не повторяя self. перед каждым.
Внутри это инструкция defineclass с типом VM_DEFINECLASS_TYPE_SINGLETON_CLASS (vm_insnhelper.c:5964).
self меняется вместе с лексической областью
Два правила, которые стоит запомнить:
- Внутри
class/module: Ruby устанавливаетselfравным этому классу/модулю. Лексическая область (CREF) указывает на тот же класс. - Внутри метода: Ruby устанавливает
selfравным receiver’у вызова. Лексическая область не меняется — метод наследует CREF из определения.
Именно поэтому Module.nesting (возвращает стек CREF) не меняется при вызове метода: CREF «замораживается» при компиляции, а self вычисляется при вызове.
Жизненный цикл метода
Определение — один шаг из четырёх. Полный путь от исходного кода до вызова (и, возможно, удаления) проходит через три заметки:
def display ... end
│
v
┌─ 1. Компиляция (01) ────────────────────────────────────┐
│ Ruby парсит тело метода и компилирует его │
│ в отдельный ISeq — набор инструкций YARV │
│ со своей таблицей локальных переменных. │
│ ISeq готов, но ещё не привязан ни к какому классу. │
└──────────────────────┬──────────────────────────────────┘
│
v
┌─ 2. Определение (08) ───────────────────────────────────┐
│ VM выполняет инструкцию definemethod: │
│ берёт текущую лексическую область (CREF), │
│ извлекает из неё целевой класс и вставляет │
│ method entry с типом ISEQ в таблицу методов (m_tbl). │
│ С этого момента метод доступен для вызова. │
└──────────────────────┬──────────────────────────────────┘
│
v
┌─ 3. Вызов (07) ─────────────────────────────────────────┐
│ VM встречает send :display — начинается поиск: │
│ от класса объекта по цепочке super до совпадения. │
│ Результат кешируется в callcache. По типу method entry │
│ выбирается путь: ISEQ → новый фрейм → выполнение. │
└──────────────────────┬──────────────────────────────────┘
│
v
┌─ 4. Удаление ───────────────────────────────────────────┐
│ remove_method — удаляет запись из m_tbl; │
│ метод суперкласса снова виден. │
│ undef_method — ставит заглушку UNDEF в m_tbl; │
│ блокирует поиск, даже по super chain. │
└─────────────────────────────────────────────────────────┘
Первые три стадии разобраны в заметке о компиляции, в этой заметке и в заметке о диспетчеризации. Четвёртая стадия — удаление — бывает двух видов, и разница между ними принципиальна.
remove_method удаляет method entry из m_tbl текущего класса. Запись исчезает, но поиск по цепочке super продолжает работать — если суперкласс имеет метод с тем же именем, он будет найден:
class Person
def greet; "hello from Person"; end
end
class Mathematician < Person
def greet; "hello from Mathematician"; end
end
class Mathematician
remove_method :greet
end
Mathematician.new.greet # => "hello from Person"undef_method действует иначе: вместо удаления записи Ruby заменяет её на заглушку типа VM_METHOD_TYPE_UNDEF (мы видели этот тип в заметке о диспетчеризации). Поиск находит запись в m_tbl, но dispatch вызывает method_missing. Это блокирует наследование — даже если суперкласс имеет метод, подкласс с undef его не увидит:
class Mathematician
undef_method :greet
end
Mathematician.new.greet # NoMethodErrorОба способа инвалидируют method cache: затронутые callcache помечаются невалидными, и при следующем вызове VM выполнит полный поиск заново.
Sources
- Pat Shaughnessy, 2013, Ruby Under a Microscope — глава 9: метапрограммирование и замыкания.
- Исходники Ruby (коммит
0d4538b57d, 2026-01-10):method.h(rb_cref_t — строка 45),vm_insnhelper.c(vm_env_cref — строка 863, vm_cref_push — строка 1037, vm_define_method — строка 6018, vm_find_or_create_class_by_id — строка 5966),vm_method.c(rb_method_entry_make — строка 1313, rb_add_method_iseq — строка 1601),vm.c(vm_cref_new0 — строка 315),compile.c(definemethod — строка 11364, definesmethod — строка 11381),vm_core.h(VM_ENV_DATA_INDEX_ME_CREF — строка 1420).
← Диспетчеризация методов | Блоки →