Как работает метод делегирования рельсов?
Прочитав ответ jvans ниже и посмотрев на исходный код еще раз, я получаю его сейчас:). И в случае, если кому-то все еще интересно, как работают делегаты рельсов. Все рельсы делают это, создавая новый метод с (module_eval) в файле/классе, из которого вы запускали метод делегата.
Итак, например:
class A
delegate :hello, :to => :b
end
class B
def hello
p hello
end
end
В момент, когда делегат называется rails, будет создан метод hello с (* args, & block) в классе A (технически в файле, в котором написан класс A), и в этом методе все рельсы используют ": to" (который должен быть объектом или классом, который уже определен в классе A), и назначить его локальной переменной _, а затем просто вызывает метод для этого объекта или класса, проходящий в параметрах.
Итак, чтобы делегат работал без создания исключения... с нашим предыдущим примером. Экземпляр A должен иметь переменную экземпляра, ссылающуюся на экземпляр класса B.
class A
attr_accessor :b
def b
@b ||= B.new
end
delegate :hello, :to => :b
end
class B
def hello
p hello
end
end
Это не вопрос о том, как использовать метод делегата в rails, о котором я уже знаю. Мне интересно, как именно "делегировать" делегаты методы: D. В Rails 4 делегат исходного кода определен в основном классе Ruby Module, что делает его доступным как метод класса во всех приложениях rails.
На самом деле мой первый вопрос: каким будет класс Ruby Module? Я имею в виду, что у каждого класса Ruby есть предки > Object > Kernel > BasicObject, и любой модуль в ruby имеет одинаковые предки. Итак, как именно Ruby добавляет методы ко всем классам/модулям ruby, когда кто-то снова открывает класс модуля?
Мой второй вопрос: я понимаю, что метод делегата в rails использует module_eval для фактической делегирования, но я действительно не понимаю, как работает module_eval.
def delegate(*methods)
options = methods.pop
unless options.is_a?(Hash) && to = options[:to]
raise ArgumentError, 'Delegation needs a target. Supply an options hash with a :to key as the last argument (e.g. delegate :hello, to: :greeter).'
end
prefix, allow_nil = options.values_at(:prefix, :allow_nil)
if prefix == true && to =~ /^[^a-z_]/
raise ArgumentError, 'Can only automatically set the delegation prefix when delegating to a method.'
end
method_prefix = \
if prefix
"#{prefix == true ? to : prefix}_"
else
''
end
file, line = caller.first.split(':', 2)
line = line.to_i
to = to.to_s
to = 'self.class' if to == 'class'
methods.each do |method|
# Attribute writer methods only accept one argument. Makes sure []=
# methods still accept two arguments.
definition = (method =~ /[^\]]=$/) ? 'arg' : '*args, &block'
# The following generated methods call the target exactly once, storing
# the returned value in a dummy variable.
#
# Reason is twofold: On one hand doing less calls is in general better.
# On the other hand it could be that the target has side-effects,
# whereas conceptually, from the user point of view, the delegator should
# be doing one call.
if allow_nil
module_eval(<<-EOS, file, line - 3)
def #{method_prefix}#{method}(#{definition}) # def customer_name(*args, &block)
_ = #{to} # _ = client
if !_.nil? || nil.respond_to?(:#{method}) # if !_.nil? || nil.respond_to?(:name)
_.#{method}(#{definition}) # _.name(*args, &block)
end # end
end # end
EOS
else
exception = %(raise DelegationError, "#{self}##{method_prefix}#{method} delegated to #{to}.#{method}, but #{to} is nil: \#{self.inspect}")
module_eval(<<-EOS, file, line - 2)
def #{method_prefix}#{method}(#{definition}) # def customer_name(*args, &block)
_ = #{to} # _ = client
_.#{method}(#{definition}) # _.name(*args, &block)
rescue NoMethodError => e # rescue NoMethodError => e
if _.nil? && e.name == :#{method} # if _.nil? && e.name == :name
#{exception} # # add helpful message to the exception
else # else
raise # raise
end # end
end # end
EOS
end
end
конец
Ответы
Ответ 1
Ruby не открывает класс модуля здесь. В рубине класс Module и класс Class почти идентичны.
Class.instance_methods - Module.instance_methods #=> [:allocate, :new, :superclass]
Основное отличие состоит в том, что вы не можете "новый" модуль.
Модуль - это рубиновая версия множественного наследования, поэтому когда вы делаете:
module A
end
module B
end
class C
include A
include B
end
за кулисами ruby фактически создает нечто, называемое анонимным классом. поэтому приведенное выше фактически эквивалентно:
class A
end
class B < A
end
class C < B
end
module_eval здесь немного обманчив. Ничто из кода, на которое вы смотрите, не имеет дело с модулями. class_eval и module_eval - это одно и то же, и они просто снова открывают класс, на который они вызваны, поэтому, если вы хотите добавить методы в класс C, который вы можете сделать:
C.class_eval do
def my_new_method
end
end
или
C.module_eval do
def my_new_method
end
end
оба из них эквивалентны ручному открытию класса и определению метода
class C
end
class C
def my_new_method
end
end
поэтому, когда они вызывают module_eval в источнике выше, они просто открывают текущий класс, который он называется, и динамически определяют методы, которые вы делегируете
Я думаю, что это лучше ответит на ваш вопрос:
Class.ancestors #=> [Module, Object, PP::ObjectMixin, Kernel, BasicObject]
поскольку все в ruby - это класс, цепочка поиска метода будет проходить через все эти объекты, пока не найдет то, что ищет. По модулю reoping вы добавляете поведение ко всему. Цепочка предков здесь немного обманчива, так как BasicObject.class # = > Класс и модуль находятся в иерархии поиска класса, даже BasicObject наследует поведение от модуля repening. Преимущество повторного открытия модуля здесь по классу состоит в том, что вы можете теперь вызывать этот метод как внутри модуля, так и внутри класса! Очень круто, узнал что-то здесь сам.
Ответ 2
Прочитав ответ jvans ниже и посмотрев на исходный код еще раз, я получаю его сейчас:). И в случае, если кому-то все еще интересно, как работают делегаты рельсов. Все рельсы делают это, создавая новый метод с (module_eval) в файле/классе, из которого вы запускали метод делегата.
Итак, например:
class A
delegate :hello, :to => :b
end
class B
def hello
p hello
end
end
В момент, когда делегат называется rails, будет создан метод hello с (* args, & block) в классе A (технически в файле, в котором написан класс A), и в этом методе все рельсы используют ": to" (который должен быть объектом или классом, который уже определен в классе A), и назначить его локальной переменной _, а затем просто вызывает метод для этого объекта или класса, проходящий в параметрах.
Итак, чтобы делегат работал без создания исключения... с нашим предыдущим примером. Экземпляр A должен иметь переменную экземпляра, ссылающуюся на экземпляр класса B.
class A
attr_accessor :b
def b
@b ||= B.new
end
delegate :hello, :to => :b
end
class B
def hello
p hello
end
end