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.

Sources

Related topics