Ruby — очень интересный язык. Одной из его особенностей является возможность выполнения заданных функций при добавлении модуля в класс. Стандартный пример выглядит следующим образом:
Здесь создаются два под-модуля в рамках текущего модуля для разделения методов инстанса и методов класса. При «примешивании» модуля MyModule в класс выполняется функция included, которая добавляет необходимые методы класса и методы объектов класса.
Не так давно я открыл для себя еще одну подобную функцию, которая выполняется при наследовании
На мой взгляд, в некоторых ситуациях эта конструкция может быть очень удобна, к примеру в Ruby on Rails версии 3.0 и старше существует модуль InheritableAttributes, который позволяет копировать аттрибуты класса при наследовании. Вот простой пример использования данного модуля:
Удобно? Вполне. Правда в последних версиях rails модуль inheritable_attributes был признан устаревшим и был заменен на class_attribute.
Как только вводится понятие callback-а, сразу же возникает вопрос о том, когда этот callback будет выполняться: до eval-а тела класса или после. Проверим:
Т.е. callback выполняется до eval-а тела класса. И все бы хорошо, если не одно «но»: в ruby вместе с именованными классами существуют еще и анонимные, которые объявляются так:
Портируя известный gem active_attr я столкнулся с такой особенностью: все spec-и при запуске в ruby ветки 1.9 проходят нормально, но стоит лишь попросить rvm использовать старый добрый ruby 1.8.7 (или ree) — так половина тестов начинает отваливаться без видимых на то причин (отмечу, что до моих коммитов, поддержки rails 3.0 не было и в связи с этим все spec-и отрабатывали корректно как на ruby ветки 1.9, так и на ruby ветки 1.8)
Как оказалось, причина в следующей особенности работы ruby 1.8.7:
В ruby 1.8.7 для анонимного класса сначала eval-ится тело, а уже потом выполняется callback inherited. Таким образом, модуль InheritableAttributes, ожидающий пустого класса сталкивается с наличием некоторых методов и отрабатывает неправильно.
Как можно решить данную проблему?
1. перейти на ruby 1.9
2. не использовать функционал, опирающийся на callback inherited (если данный функционал не используется явно, а только в рамках rails — то достаточно перейти на rails 3.1)
3. не использовать анонимные классы
4. сначала создать пустой анонимный класс, а потом добавить в него нужное содержимое с помощью class_eval:
В заключение, хочу отметить, что описываемый баг — достаточно специфичен и встретится с ним далеко не каждый, но с другой стороны, зная эту особенность ruby 1.8.7 и все же нарвавшись на описываемую ситуацию можно сэкономить пару часов на дебаге.
module MyModule module InstanceMethods end module ClassMethods end def self.included(base) base.include(InstanceMethods) base.extend(ClassMethods) end end
Здесь создаются два под-модуля в рамках текущего модуля для разделения методов инстанса и методов класса. При «примешивании» модуля MyModule в класс выполняется функция included, которая добавляет необходимые методы класса и методы объектов класса.
Не так давно я открыл для себя еще одну подобную функцию, которая выполняется при наследовании
class Ancestor def self.inherited(successor) end end class Successor < Ancestor end
На мой взгляд, в некоторых ситуациях эта конструкция может быть очень удобна, к примеру в Ruby on Rails версии 3.0 и старше существует модуль InheritableAttributes, который позволяет копировать аттрибуты класса при наследовании. Вот простой пример использования данного модуля:
require "active_support/core_ext/class/inheritable_attributes" class Base class_inheritable_accessor :color end Base.color = "red" class Ancestor < Base end Ancestor.color # => "red" Ancestor.color = "green" Base.color # => "red"
Удобно? Вполне. Правда в последних версиях rails модуль inheritable_attributes был признан устаревшим и был заменен на class_attribute.
Как только вводится понятие callback-а, сразу же возникает вопрос о том, когда этот callback будет выполняться: до eval-а тела класса или после. Проверим:
class Base def self.inherited(m) puts "Hello from inherited callback" end end class NamedClass < Base puts "Hello from class body" end # Output: # Hello from inherited callback # Hello from class body
Т.е. callback выполняется до eval-а тела класса. И все бы хорошо, если не одно «но»: в ruby вместе с именованными классами существуют еще и анонимные, которые объявляются так:
anonymous_class = Class.new(Base) do # body end
Портируя известный gem active_attr я столкнулся с такой особенностью: все spec-и при запуске в ruby ветки 1.9 проходят нормально, но стоит лишь попросить rvm использовать старый добрый ruby 1.8.7 (или ree) — так половина тестов начинает отваливаться без видимых на то причин (отмечу, что до моих коммитов, поддержки rails 3.0 не было и в связи с этим все spec-и отрабатывали корректно как на ruby ветки 1.9, так и на ruby ветки 1.8)
Как оказалось, причина в следующей особенности работы ruby 1.8.7:
class Base def self.inherited(m) puts "--> Hello from inherited callback" end end puts "declare named class" class NamedClass < Base puts "--> Hello from named class" end puts puts "declare anonymous class" Class.new(Base) do puts "--> Hello from anonymous class" end # Output for ruby 1.9.3 # declare named class # --> Hello from inherited callback # --> Hello from named class # # declare anonymous class # --> Hello from inherited callback # --> Hello from anonymous class # Output for ruby 1.8.7 # declare named class # --> Hello from inherited callback # --> Hello from named class # # declare anonymous class # --> Hello from anonymous class # --> Hello from inherited callback
В ruby 1.8.7 для анонимного класса сначала eval-ится тело, а уже потом выполняется callback inherited. Таким образом, модуль InheritableAttributes, ожидающий пустого класса сталкивается с наличием некоторых методов и отрабатывает неправильно.
Как можно решить данную проблему?
1. перейти на ruby 1.9
2. не использовать функционал, опирающийся на callback inherited (если данный функционал не используется явно, а только в рамках rails — то достаточно перейти на rails 3.1)
3. не использовать анонимные классы
4. сначала создать пустой анонимный класс, а потом добавить в него нужное содержимое с помощью class_eval:
c = class.new(Base) c.class_eval do # body end
В заключение, хочу отметить, что описываемый баг — достаточно специфичен и встретится с ним далеко не каждый, но с другой стороны, зная эту особенность ruby 1.8.7 и все же нарвавшись на описываемую ситуацию можно сэкономить пару часов на дебаге.
