rails委托方法如何工作?

时间:2014-01-01 21:43:03

标签: ruby delegates ruby-on-rails-4

在阅读了以下jvans的答案后,再看一下源代码,我现在得到它:)。如果有人仍然想知道rails代表是如何工作的。所有rails正在使用(module_eval)在您运行委托方法的文件/类中创建一个新方法。

例如:

  class A
    delegate :hello, :to => :b
  end

  class B
    def hello
     p hello
    end
  end

当委托被调用时,rails将在A类中创建一个hello方法(技术上在写入类A的文件中),并且在该方法中所有rails都使用“:to”value(应该是已在A类中定义的对象或类)并将其分配给局部变量_,然后只调用该对象上的方法或传入params的类。

因此,为了让代表在不引发异常的情况下工作......使用前面的示例。 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类的祖先都是>对象>内核> BasicObject和ruby中的任何模块都具有相同的祖先。那么当有人重新打开Module类时,ruby如何为所有ruby类/模块添加方法呢?

我的第二个问题是..我理解rails中的delegate方法使用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

2 个答案:

答案 0 :(得分:7)

Ruby没有在这里重新打开模块类。在ruby中,类Module和类Class几乎完全相同。

    Class.instance_methods - Module.instance_methods #=> [:allocate, :new, :superclass]

主要区别在于你不能'新'一个模块。 模块是ruby的多重继承版本所以当你这样做时:

 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中的所有内容都是一个类,所以方法查找链将遍历所有这些对象,直到找到它要查找的内容为止。通过重新定义模块,您可以为所有内容添加行这里的祖先链有点欺骗,因为BasicObject.class#=&gt; Class和Module在Class的查找层次结构中,甚至BasicObject也从repening模块继承行为。在Class上重新打开Module的优点是,您现在可以在模块内以及类内调用此方法!非常酷,我自己在这里学到了一些东西。

答案 1 :(得分:2)

在阅读了以下jvans的答案后,再看一下源代码,我现在得到它:)。如果有人仍然想知道rails代表是如何工作的。所有rails正在使用(module_eval)在您运行委托方法的文件/类中创建一个新方法。

例如:

  class A
    delegate :hello, :to => :b
  end

  class B
    def hello
     p hello
    end
  end

当委托被调用时,rails将在A类中创建一个hello方法(技术上在写入类A的文件中),并且在该方法中所有rails都使用“:to”value(应该是已在A类中定义的对象或类)并将其分配给局部变量_,然后只调用该对象上的方法或传入params的类。

因此,为了让代表在不引发异常的情况下工作......使用前面的示例。 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