Но внутри тела метода или в другом аналогичном месте нельзя заново открыть класс (если только это не синглетный класс). В таком случае в прежних версиях Ruby приходилось прибегать к помощи
eval
, теперь же у нас есть метод
define_method
. Он принимает символ (имя метода) и блок (тело метода).
Первая (ошибочная) попытка воспользоваться этим методом могла бы
выглядеть так:
# Не работает, так как метод define_method закрытый.
if today =~ /Saturday | Sunday/
define_method(:activity) { puts "Отдыхаем!" }
else
define_method(:activity) { puts "Работаем!" }
end
activity
Поскольку
define_method
— закрытый метод, приходится поступать так:
# Работает (Object - это контекст верхнего уровня).
То же самое можно сделать и на уровне экземпляра, а не класса:
class MyClass
def new_method(name, &block)
self.class.send(:define_method,name, &block)
end
end
x = MyClass.new
x.new_method(:mymeth) { puts "Это мой метод." }
x.mymeth # Печатается "Это мой метод."
Здесь метод экземпляра тоже определен динамически. Изменился только способ реализации метода
new_method
. Обратите внимание на трюк с
send
, позволивший нам обойти закрытость метода
define_method
. Он работает, потому что в текущей версии Ruby метод
send
позволяет вызывать закрытые методы. (Некоторые сочтут это «дыркой»; как бы то ни
было, пользоваться этим механизмом следует с осторожностью.)
По поводу метода
define_method
нужно сделать еще одно замечание. Он принимает блок, а в Ruby блок — замыкание. Это означает, что в отличие от обычного определения метода, мы запоминаем контекст, в котором метод был определен. Следующий пример практически бесполезен, но этот момент иллюстрирует:
class MyClass
def self.new_method(name, &block)
define_method(name, &block)
end
end
a,b = 3,79
MyClass.new_method(:compute) { a*b }
x = MyClass.new
puts x.compute # 237
a,b = 23,24
puts x.compute # 552
Смысл здесь в том, что новый метод может обращаться к переменным в исходной области видимости блока, хотя сама эта область более не существует и никаким другим способом не доступна. Иногда это бывает полезно, особенно в случае метапрограммирования или при разработке графических интерфейсов, когда нужно определить методы обратного вызова, реагирующие на события.
Отметим, что замыкание оказывается таковым только тогда, когда имя переменной то же самое. Изредка из-за этого могут возникать сложности. Ниже мы воспользовались методом
define_method
, чтобы предоставить доступ к переменной класса (вообще-то это следует делать не так, но для иллюстрации подойдет):
class SomeClass
@@var = 999
define_method(:peek) { @@var }
end
x = SomeClass.new p
x.peek # 999
А теперь попробуем проделать с переменной экземпляра класса такой трюк:
class SomeClass
@var = 999
define_method(:peek) { @var }
end
x = SomeClass.new
p x.peek # Печатается nil
Мы ожидали, что будет напечатано 999, а получили
nil
. Почему? Объясню чуть позже.
С другой стороны, такой код работает правильно:
class SomeClass
@var = 999
x = @var
define_method(:peek) { x }
end
x = SomeClass.new p
x.peek # 999
Так что же происходит? Да, замыкание действительно запоминает переменные в текущем контексте. Но ведь контекст нового метода - это контекст экземпляра объекта, а не самого класса.
Поскольку имя
@var
в этом контексте относится к переменной экземпляра объекта, а не класса, то переменная экземпляра класса оказывается скрыта переменной экземпляра объекта, хотя последняя никогда не использовалась и технически не существует.