Определение методов

Диспетчеризация методов | Блоки

В заметке о компиляции мы видели, что 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 делает три шага:

  1. Получает CREF из текущего фрейма.
  2. Берёт из него klass_or_self — это Quote.
  3. Вызывает 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. Вместо этого:

  1. Вычисляет выражение перед точкой (selfQuote).
  2. Берёт CLASS_OF этого объекта. Для класса Quote это его метакласс (из заметки об объектах).
  3. Добавляет метод в 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, но вместо создания нового класса:

  1. Вычисляет выражение после << (selfQuote).
  2. Получает singleton-класс этого объекта — через rb_singleton_class().
  3. Создаёт новый 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).

Диспетчеризация методов | Блоки