Объекты и классы

Предпосылки: Исполнение — VALUE, стек значений, фреймы.

Управление потоком | Модули

В заметке об исполнении мы видели, как VM манипулирует значениями на стеке: putobject 2 кладёт число, putself — текущий получатель. Все значения — тип VALUE в C-коде. Но программа работает не с голыми числами: она создаёт сущности с атрибутами и поведением — математика с именем и методом full_name. Ruby должен где-то хранить атрибуты и откуда-то брать методы при вызове. Для этого нужны объекты и классы.

Объект = указатель на класс + инстанс-переменные

Создадим класс и посмотрим, что Ruby хранит для каждого экземпляра:

class Mathematician
  attr_accessor :first_name, :last_name
end
 
euler = Mathematician.new
euler.first_name = "Leonhard"
euler.last_name = "Euler"
p euler
# => #<Mathematician:0x00007f... @first_name="Leonhard", @last_name="Euler">
 
euclid = Mathematician.new
euclid.first_name = "Euclid"
p euclid
# => #<Mathematician:0x00007f... @first_name="Euclid">

Ruby показывает для каждого объекта две вещи: имя класса (Mathematician) и набор инстанс-переменных (@first_name, @last_name). Это и есть содержимое объекта. Внутри C-кода Ruby каждый пользовательский объект хранится в структуре RObject, которая содержит:

  • klass — указатель на класс, экземпляром которого является объект.
  • Массив значений инстанс-переменных. Именно значений — не имён. Имена хранятся отдельно (об этом дальше).

Структура RObject начинается с заголовка RBasic — набор флагов (flags) и указатель на класс (klass). За заголовком следует массив значений инстанс-переменных. RBasic — общая часть для любого объекта Ruby, не только пользовательских.

Для встроенных типов — String, Array, Hash, Regexp — Ruby использует другие C-структуры (RString, RArray и т.д.), оптимизированные под конкретный тип данных. Но у всех есть RBasic с klass. Поэтому "hello".class возвращает String, а [1,2].classArray: Ruby читает klass из заголовка, и механизм одинаков для любого объекта.

Каждый Ruby-объект — это указатель на класс и набор инстанс-переменных. Всё остальное — методы, константы, имена переменных — хранится не в самом объекте, а в его классе.

Объекты без структуры: immediate values

Не каждое значение на стеке нуждается в структуре RObject. Когда VM кладёт число 2 инструкцией putobject, нет смысла выделять память под заголовок и массив ivar — у числа нет инстанс-переменных. Целые числа, символы, true, false, nil — особый случай.

В Ruby тип VALUE — это целое число размером с указатель. Обычно VALUE является указателем на структуру объекта в памяти. Но для простых значений Ruby кодирует данные прямо в VALUE, без выделения памяти:

  • Integer — младший бит VALUE равен 1. Остальные биты хранят само число. VALUE для числа 5 — это 11 (5 сдвинутое на один бит влево + бит-флаг).
  • Symbol — специальная комбинация битов, верхние биты хранят числовой идентификатор символа.
  • false, nil, true — фиксированные константы (0x00, 0x02, 0x04).

Кодирование числа прямо в VALUE напоминает принцип tagged pointers на уровне ABI — младшие биты отличают тип данных от указателя.

Такие значения называются immediate values. У них нет структуры в памяти, они не занимают место в куче, сборщику мусора нечего собирать. Ruby определяет их тип по битовому паттерну и «подставляет» нужный класс: видит бит-флаг целого числа — значит класс Integer, видит паттерн символа — класс Symbol.

1.class      # => Integer
:sym.class   # => Symbol
true.class   # => TrueClass
nil.class    # => NilClass

Immediate values — полноценные объекты: у них есть класс, они отвечают на сообщения. Но у них нет структуры в памяти, и поэтому у них нет собственных инстанс-переменных — попытка приведёт к ошибке:

1.instance_variable_set(:@x, 42)
# => FrozenError: can't modify frozen Integer: 1

Как Ruby находит инстанс-переменные

Вернёмся к обычным объектам. При вызове euler.first_name Ruby должен прочитать @first_name из RObject. Но RObject хранит массив значений инстанс-переменных — без имён. Если у euler есть @first_name и @last_name, в массиве лежат "Leonhard" и "Euler". Как Ruby знает, что @first_name — это элемент с индексом 0, а @last_name — с индексом 1?

Имена инстанс-переменных не хранятся в самом объекте. Ruby сопоставляет имена с индексами через отдельный механизм — object shapes (появились в Ruby 3.2, заменили прежние хеш-таблицы на уровне класса). Каждый объект хранит идентификатор своей «формы» (shape_id) в заголовке. Shape описывает, какие переменные в каком порядке присутствуют у объекта. Все объекты с одинаковым набором переменных (в одинаковом порядке) разделяют один shape — даже если они экземпляры разных классов. Подробнее о shapes, их мотивации и механизме — в отдельной заметке.

Сами значения хранятся в массиве внутри объекта. Если переменных немного — массив встроен прямо в структуру RObject (embedded storage). Когда переменных становится больше, чем помещается — Ruby выносит массив в отдельный участок памяти (heap storage).

Класс = объект + таблица методов + суперкласс + константы

Допустим, Ruby выполняет euler.full_name. Значения @first_name и @last_name он найдёт в массиве ivar объекта. Но где лежит сам метод full_name? Объект хранит только klass — указатель на класс. Значит, метод нужно искать в классе. Определим Mathematician чуть подробнее:

class Mathematician
  FIELD = "Mathematics"
 
  def full_name
    "#{@first_name} #{@last_name}"
  end
end

Класс содержит метод full_name, константу FIELD и знает о переменных @first_name, @last_name. Класс также может наследоваться от другого класса. Всю эту информацию Ruby хранит в структуре RClass и связанной с ней rb_classext_t (вынесена отдельно, чтобы скрыть внутренние поля от C-расширений — они видят только RClass):

  • Таблица методов (m_tbl) — хеш-таблица: имя метода → определение (включая скомпилированный ISeq для Ruby-методов, о котором мы говорили в заметке о компиляции).
  • Таблица констант (const_tbl) — хеш: имя константы → значение.
  • Указатель на суперкласс (super) — один родитель. Если суперкласс не указан, Ruby назначает Object. Когда Ruby ищет метод, он идёт по цепочке super вверх — подробности в заметке о модулях.

Классы — тоже объекты. Mathematician.class возвращает Class. У класса есть собственный klass, собственные флаги, собственные инстанс-переменные. Это приводит к определению:

Ruby-класс — это Ruby-объект, который дополнительно содержит таблицу методов, таблицу констант и указатель на суперкласс.

Инстанс-переменные класса vs. переменные класса

У класса как у объекта могут быть собственные инстанс-переменные — @var в теле класса. Они принадлежат конкретному объекту-классу и не разделяются с подклассами:

class Mathematician
  @field = "General"
  def self.field; @field; end
end
 
class Statistician < Mathematician
  @field = "Statistics"
end
 
Mathematician.field  # => "General"
Statistician.field   # => "Statistics"

Переменные класса (@@var) работают иначе. При чтении или записи @@var Ruby идёт вверх по цепочке суперклассов и использует значение из самого верхнего:

class Mathematician
  @@field = "General"
  def self.field; @@field; end
end
 
class Statistician < Mathematician
  @@field = "Statistics"
end
 
Mathematician.field  # => "Statistics"  ← перезаписано!
Statistician.field   # => "Statistics"

Одна @@field на всю иерархию. @field — у каждого класса своя.

Метакласс: где живут методы класса

Вызовем Mathematician.field — метод, определённый через def self.field. Методы экземпляра хранятся в m_tbl класса, но где хранятся методы самого класса? Куда Ruby кладёт def self.field?

Не в таблицу методов Mathematician — иначе метод был бы доступен экземплярам. Не в таблицу методов Class — иначе он появился бы у всех классов:

class AnotherClass; end
AnotherClass.field
# => NoMethodError: undefined method 'field' for class AnotherClass

Подсказку даёт ObjectSpace:

before = ObjectSpace.count_objects[:T_CLASS]
class Mathematician; end
after = ObjectSpace.count_objects[:T_CLASS]
after - before  # => 2

Мы объявили один класс, но Ruby создал два. Второй — скрытый класс, который называется метакласс (metaclass) или singleton class (класс для одного конкретного объекта). Ruby автоматически создаёт его при объявлении любого класса.

Указатель klass объекта-класса Mathematician ведёт не на Class, а на метакласс. Именно в таблице методов метакласса хранятся методы класса:

Mathematician (RClass)
  klass → #<Class:Mathematician>  (метакласс)
            m_tbl: { field: ... }  ← метод класса здесь
  m_tbl: { full_name: ... }       ← методы экземпляров здесь
  super → Object

Метакласс доступен через singleton_class:

Mathematician.singleton_class
# => #<Class:Mathematician>

Метакласс — не уникальная особенность классов. Любой объект может получить singleton class: когда вы определяете метод на конкретном экземпляре (def euler.unique_method), Ruby создаёт для него singleton class и кладёт метод туда. Разница в том, что для классов метакласс создаётся сразу при объявлении, а для обычных объектов — лениво, при первом обращении.

Sources

  • Pat Shaughnessy, 2013, Ruby Under a Microscope — глава 5: объекты и классы.
  • Исходники Ruby (коммит 0d4538b57d, 2026-01-10): include/ruby/internal/core/rbasic.h (RBasic: flags, klass), include/ruby/internal/core/robject.h (RObject: embedded/heap storage), internal/class.h (RClass, rb_classext_t: m_tbl, const_tbl, super), shape.h (object shapes: rb_shape_t, shape_id, transitions).

Управление потоком | Модули