Метапрограммирование на языке Ruby
Манипулирование значениями переменных, удаление переменных
- Object#instance_variable_get
- Object#instance_variable_set
- Object#remove_instance_variable
- Module#class_variable_get
- Module#class_variable_set
- Module#remove_class_variable
Манипулирование значениями констант, удаление констант
- Module#const_get Module#const_set
- Module#remove_const
Добавление/удаление методов
- Module#define_method
- Module#remove_method
Запуск динамически генерируемого кода
- Object#send
- Object#instance_eval
- Module#module_eval
- Kernel#eval
- Kernel#method_missing
Reflection-методы (группа методов, позволяющих исследовать структуру кода в момент исполнения)
- Object#class
- Object#instance_variables
- Object#methods
- Object#private_methods
- Object#public_methods
- Object#singleton_methods
- Module#class_variables
- Module#constants
- Module#included_modules
- Module#instance_methods
- Module#name
- Module#private_instance_methods
- Module#protected_instance_methods
- Module#public_instance_methods
Вычисление строк и блоков Ruby предоставляет метод eval, позволяющий в runtime превратить в код строку или другой вычисляемый блок. Для того, чтобы сгенерировать некоторый код в контексте некоторого созданного объекта следует использовать методы instance_eval and module_eval (синоним class_eval). Метод instance_eval должен вызываться в пространстве имен созданного объекта:
[1,2,3,4].instance_eval('size') # returns 4
В предыдущем примере строка ‘size’ интерпретируется в контексте объекта-массива, что эквивалентно следующей записи:
[1,2,3,4].size
Методу instance_eval можно передать Ruby-блок кода:
# Get the average of an array of integers
[1,2,3,4].instance_eval { inject(:+) / size.to_f } # returns 2.5
Методы inject(:+) и size.to_f использованы без явного указания объекта, относительно которого они вызваны. Это возможно потому, что контекстом их выполнения является объект-массив, относительно которого вызван instance_eval (то есть self). В то время, как instance_eval вычисляет выражение в контексте созданного объекта, метод module_eval вычисляет код в контексте класса Module или Class:
Fixnum.module_eval do
def to_word
if (0..3).include? self
['none', 'one', 'a couple', 'a few'][self]
elsif self > 3
'many'
elsif self < 0
'negative'
end
end
end
1.to_word # returns 'one'
2.to_word # returns 'a couple'
В примере выше метод module_eval контекст класса Fixnum и добавляет новый метод. Этот код выполняет то же самое, что и код:
class Fixnum
def to_word
..
end
end
Наибольшую гибкость этот подход даёт при использовании строк, содержащих динамически генерируемые имя/тело метода. В следующем примере создаваемый статический метод create_multiplier добавляется в класс Fixnum:
class Fixnum
def self.create_multiplier(name, num)
module_eval "def #{name}; self * #{num}; end"
end
end
Fixnum.create_multiplier('multiply_by_pi', Math::PI)
4.multiply_by_pi # returns 12.5663706143592
В данном примере создаётся метод класса (или метод ‘singleton’)Ю при вызове которого, создаются методы инстанции, используемые любым объектом Fixnum.
Использование метода send
Использование метода send аналогично использованиию метода instance_eval с той разницей, что он передает объету имя вызываемого метода. Это особенно полезно когда имя метода формируется как строка или является символом (‘size’, :size):
method_name = 'size'
[1,2,3,4].send(method_name) # returns 4
Особенность метода состоит в том, что он вызывается в обход системы контроля доступа к методам и может вызывать private-методы, например, Module#define_method:
Array.define_method(:ducky) { puts 'ducky' }
# NoMethodError: private method `define_method' called for Array:Class
При использовании send:
Array.send(:define_method, :ducky) { puts 'ducky' }
Добавление методов
Для добавления методов в класс следует использовать метод define_method:
class Array
define_method(:multiply) do |arg|
collect{|i| i * arg}
end
end
[1,2,3,4].multiply(16) # returns [16, 32, 48, 64]
Использование метода method_missing
Если у объекта класса вызывается метод, ранее не определенный в теле класса, то вызывается метод method_missing, которому передается имя этого неизвестного метода. В method_missing появляется возможность обработать отсутствие метода в зависимости от имени вместо получения стандартной ошибки NoMethodError:
class Fixnum
def method_missing(meth)
method_name = meth.id2name
if method_name =~ /^multiply_by_(\d+)$/
self * $1.to_i
else
raise NoMethodError, "undefined method `#{method_name}' for #{self}:#{self.class}"
end
end
end
16.multiply_by_64 # returns 1024
16.multiply_by_x # NoMethodError
Как работает attr_accessor
Динамическая генерация кода методов используется во всем используемом используемый методе attr_accessor, которые генерирует переменную и метод-getter и метод-setter для неё. В качестве примера создадим собственные методы attr1 и attr2. Так как все классы наследуются от класса Module, то поместим методы в него:
class Module def attr1(symbol) instance_var = ('@' + symbol.to_s) define_method(symbol) { instance_variable_get(instance_var) } define_method(symbol.to_s + "=") { |val| instance_variable_set(instance_var, val) } end def attr2(symbol) module_eval "def #{symbol}; @#{symbol}; end" module_eval "def #{symbol}=(val); @#{symbol} = val; end" end end
Пример использования:
class Person attr1 :name attr2 :phone end person = Person.new person.name = 'John Smith' person.phone = '555-2344' person # результат <Person:.... @name="John Smith", @phone="555-2344">
Вызовы методов define_method и module_eval приводят к одинаковым результатам.
Подготовлено по материалам weare.buildingsky.net