Объекты и классы
Предпосылки: Исполнение — 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].class — Array: 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 # => NilClassImmediate 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).
← Управление потоком | Модули →