Calling Methods Dynamically

当调用一个方法时,通常会使用点(.)标记符,代码如下:

  class MyClass
    def my_method(my_arg)
      my_arg * 2
    end
  end

  obj = MyClass.new
  obj.my_method(3)         #=> 6

可以使用 Object#send() 取代点标记符来调用 MyClass#my_method() 方法:

  obj.send(:my_method, 3)   #=> 6

上面的代码还是调用了 my_method() 方法,但这次是通过 send() 方法来实现的。

  • 通过 send() 方法,你想调用的方法名可以成为一个参数,这样就可以在代码运行期间,直到最后一刻才决定调用哪个方法,这种技术被称为 动态派发

来自 Camping 的例子

动态派发的例子来自 Camping,这是一个极简主义的 Ruby web 框架。一个 Camping 应用程序将它的配置信息用键值对的方式存储在一个 YAML 格式的文件中。一个博客应用程序的配置文件可能像下面这样:

  admin : Bill
  title : Rubyland
  topic : Ruby and more

Camping 可以把这些键值对从文件中拷贝到自己的配置对象(这个对象的类型是 OpenStruct)中。假设你将文件的配置信息存入 conf 对象中,在理想状态下,存储配置信息的代码应该像下面这样:

  conf.admin = 'Bill'
  conf.title = 'Rubyland'
  conf.topic = 'Ruby and more'

但是 Camping 不可能事先知道特定应用中有哪些键值对,因此它无法知道应该去调用哪个方法。它只能在运行时才能根据 YAML 文件的内容发现给定的键值对。于是,Camping 求助于动态派发技术,为每个键值对都构造出一个赋值方法的名字,并且把这个方法发送给 conf 对象:

  if conf.rc and File.exists?(conf.rc)
    YAML.load_file(conf.rc).each do |k,v|
      conf.send("#{k}=", v)
    end
  end

来自 Test::Unit 的例子:

另外一个动态派发的例子来自 Test::Unit 标准库。Test::Unit 使用一个命名惯例来判定哪些方法是测试方法。一个 TestCase 对象会查找自己的公开方法,并选择其中名字以 test 开头的方法:

  method_names = public_instance_methods(true)
  tests = method_names.delete_if {|method_name| method_name !~ /^test./}

现在这个 TestCase 对象得到了测试方法数组。后面,它会使用 send() 方法来调用数组中的每个方法。动态派发的这种特殊用法有时被称为 模式派发,因为它基于方法名的某种模式来过滤方法。

TestCase实际使用的是 send() 方法的别名方法 __send()__。Object#send() 方法功能非常强大,可以用 send() 调用任何方法,甚至调用私有方法

Defining Methods Dynamically

可以利用 Module#define_method() 方法定义一个方法,只需要为其提供一个方法名和一个充当方法主体的块即可,代码如下:

  class MyClass
    define_method :my_method do |my_arg|
      my_arg * 3
    end
  end

  obj = MyClass.new
  obj.my_method(2)     #=> 6

define_method() 方法在 MyClass 内部执行,因此 my_method() 定义为 MyClass 的实例方法。

method_missing()

在 Ruby 中,编译器并不强制方法调用时的行为,这意味着你甚至可以调用一个并不存在的方法:

  class Lawyer; end

  nick = Lawyer.new
  nick.talk_simple   #=> NoMethodError: undefined method ......

当调用 talk_simple 方法时,Ruby 会到 nick 对象的类中查询它的实例方法。如果在那里找不到 talk_simple 方法,Ruby 会沿着祖先链向上搜寻进入 Object 类,并且最终来到 Kernel 模块。由于在哪里都没找到 talk_simple 方法,所以只好在 nick 上调用一个名为 method_missing 的方法,这个方法是 Kernel 的一个实例方法,而所有的对象都继承自 Kernel 模块。

可以直接调用 method_missing() 方法来进行实验,尽管这是一个私有方法,但是还是可以通过 send() 方法来做到:

  nick.send :method_missing, :my_method

  #=> NoMethodError: undefined method 'my_method' for ......

Kernel#method_missing() 方法会抛出一个 NoMethodError 进行响应,它就像对象的失物招领处,所有无法投递的消息最后都会来到这里。