Ruby元编程读书笔记四
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 进行响应,它就像对象的失物招领处,所有无法投递的消息最后都会来到这里。