how rails delegate method works?

2019-03-18 02:53发布

After reading the answer by jvans below and looking at the source code a few more time I get it now :). And in case anyone is still wondering how exactly rails delegates works. All rails is doing is creating a new method with (module_eval) in the file/class that you ran the delegate method from.

So for example:

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

  class B
    def hello
     p hello
    end
  end

At the point when delegate is called rails will create a hello method with (*args, &block) in class A (technically in the file that class A is written in) and in that method all rails do is uses the ":to" value(which should be an object or a Class that is already defined within the class A) and assign it to a local variable _, then just calls the method on that object or Class passing in the params.

So in order for delegate to work without raising an exception... with our previous example. An instance of A must already have a instance variable referencing to an instance of class 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

This is not a question on "how to use the delegate method in rails", which I already know. I'm wondering how exactly "delegate" delegates methods :D. In Rails 4 source code delegate is defined in the core Ruby Module class, which makes it available as a class method in all rails app.

Actually my first question would be how is Ruby's Module class included? I mean every Ruby class has ancestors of > Object > Kernel > BasicObject and any module in ruby has the same ancestors. So how exactly how does ruby add methods to all ruby class/modules when someone reopens the Module class?

My second question is.. I understand that the delegate method in rails uses module_eval do the actual delegation but I don't really understand how module_eval works.

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

end

2条回答
够拽才男人
2楼-- · 2019-03-18 03:20

Ruby isn't reopening the module class here. In ruby the class Module and the class Class are almost identical.

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

The main difference is that you can't 'new' a module. Module's are ruby's version of multiple inheritance so when you do:

 module A
 end
 module B
 end

 class C
   include A
   include B
 end

behind the scenes ruby is actually creating something called an anonymous class. so the above is actually equivalent to:

 class A
 end
 class B < A
 end
 class C < B
 end

module_eval here is a little deceptive. Nothing from the code you're looking at is dealing with modules. class_eval and module_eval are the same thing and they just reopen the class that they're called on so if you want to add methods to a class C you can do:

 C.class_eval do 
    def my_new_method
    end
  end

or

 C.module_eval do 
    def my_new_method
    end
  end

both of which are equivalent to manually reopening the class and defining the method

  class C
   end
  class C
     def my_new_method
     end 
  end

so when they're calling module_eval in the source above, they're just reopening the current class it's being called it and dynamically defining the methods that you're delegating

I think this will answer your question better:

 Class.ancestors #=> [Module, Object, PP::ObjectMixin, Kernel, BasicObject]

since everything in ruby is a class, the method lookup chain will go through all of these objects until it finds what it's looking for. By reoping module you add behavior to everything. The ancestor chain here is a little deceptive, since BasicObject.class #=> Class and Module is in Class's lookup hierarchy, even BasicObject inherits behavior from repening module. The advantage of reopening Module here over Class is that you can now call this method from within a module as well as within a class! Very cool, learned something here myself.

查看更多
我想做一个坏孩纸
3楼-- · 2019-03-18 03:26

After reading the answer by jvans below and looking at the source code a few more time I get it now :). And in case anyone is still wondering how exactly rails delegates works. All rails is doing is creating a new method with (module_eval) in the file/class that you ran the delegate method from.

So for example:

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

  class B
    def hello
     p hello
    end
  end

At the point when delegate is called rails will create a hello method with (*args, &block) in class A (technically in the file that class A is written in) and in that method all rails do is uses the ":to" value(which should be an object or a Class that is already defined within the class A) and assign it to a local variable _, then just calls the method on that object or Class passing in the params.

So in order for delegate to work without raising an exception... with our previous example. An instance of A must already have a instance variable referencing to an instance of class 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
查看更多
登录 后发表回答