RubyMiddleTechnical
В чём разница между freeze и иммутабельными объектами в Ruby?
freeze делает объект неизменяемым в runtime — любая мутация бросает FrozenError. Заморозка поверхностная: вложенные объекты не замораживаются автоматически.
Как работает freeze
Object#freeze устанавливает флаг frozen на объекте. После этого любая попытка изменить объект бросает FrozenError (Ruby >= 2.5; ранее — RuntimeError).
str = 'hello'
str.freeze
str << ' world' # FrozenError: can't modify frozen String: "hello"
str.upcase! # FrozenError
str.replace('x') # FrozenError
str.frozen? # => true
# Но: создание НОВОГО объекта — OK:
upcased = str.upcase # => "HELLO" (новый объект, оригинал не изменён)
Заморозка поверхностная (shallow freeze)
config = { host: 'localhost', ports: [8080, 9090] }.freeze
config[:host] = 'example.com' # FrozenError — нельзя менять hash
config[:ports] << 443 # OK! — массив внутри НЕ заморожен
puts config[:ports].inspect # => [8080, 9090, 443]
Для глубокой заморозки нужно явно замораживать вложенные объекты или использовать gems типа deep_freeze:
def deep_freeze(obj)
case obj
when Hash then obj.each_value { |v| deep_freeze(v) }.freeze
when Array then obj.each { |v| deep_freeze(v) }.freeze
else obj.freeze
end
end
config = deep_freeze({ host: 'localhost', ports: [8080, 9090] })
config[:ports] << 443 # FrozenError
Frozen string literals
# frozen_string_literal: true
STATUS = 'active' # автоматически заморожена
STATUS << '!' # FrozenError
# Если нужна изменяемая строка:
buffer = +'' # + создаёт незамороженную строку
buffer << 'hello'
Magic comment # frozen_string_literal: true замораживает все строковые литералы в файле. Это улучшает производительность (меньше аллокаций) и предотвращает случайную мутацию констант.
Зачем замораживать
- Константы —
ALLOWED_ROLES = %w[admin user guest].freeze. Без freeze массив-константу можно мутировать из любого места кода. - Hash-ключи — Ruby автоматически замораживает строки, используемые как ключи Hash, для экономии памяти.
- Thread safety — замороженные объекты безопасно разделять между потоками без Mutex.
- Выявление мутаций — в тестах можно заморозить входные данные, чтобы поймать неожиданные мутации в методе.
dup vs clone после freeze
frozen = 'immutable'.freeze
cloned = frozen.clone
duped = frozen.dup
cloned.frozen? # => true (clone сохраняет frozen-статус)
duped.frozen? # => false (dup создаёт изменяемую копию)
duped << '!' # => "immutable!"
Подводные камни
- Поверхностная заморозка вводит в заблуждение —
hash.freezeне защищает вложенные массивы и объекты. Всегда замораживайте рекурсивно для конфигурационных объектов. - FrozenError в старых Ruby — в Ruby < 2.5 это
RuntimeErrorс сообщением «can't modify frozen». Если поддерживаете старые версии, не ловите FrozenError по имени. - frozen_string_literal и мутирующий код — включение magic comment сломает код, который изменяет строковые литералы (
'foo' << 'bar'). Исправьте мутации или используйте+''/String.new. - Символы и Integer уже заморожены —
:foo.frozen?и1.frozen?всегда true. Вызывать на них freeze безопасно, но бессмысленно. - Сериализация замороженных объектов — Marshal корректно сериализует frozen-объекты; после десериализации объект не будет заморожен, если это не предусмотрено явно.
- Производительность — заморозка не ускоряет сам объект; выигрыш в том, что frozen strings могут дедуплицироваться в памяти и не требуют защиты Mutex при многопоточном доступе.
Common mistakes
- Сводить freeze immutability к названию метода без lifecycle и failure path.
- Игнорировать модель runtime: объектная динамическая модель Ruby, где почти всё является объектом, а методы ищутся через ancestor chain.
- Не отделять validation, authorization, transaction boundary и business logic.
- Менять похожие API местами без учёта семантики ошибок и ownership.
What the interviewer is testing
- Объясняет freeze immutability через конкретную точку lifecycle в Ruby.
- Приводит корректный минимальный пример без вымышленных методов или callbacks.
- Называет edge cases: пустые значения, ошибки, транзакции, безопасность или concurrency.