本文翻译自 Yehuda Katz 的博客文章 Metaprogramming in Ruby: It’s All About the Self
在我写完上篇关于 Rails 插件的惯用语法的文章后,我意识到 Ruby 元编程,就核心而言,其实非常简单。
归根结底是因为所有的 Ruby 代码都是可执行代码 —— 不需要额外的编译或者运行时阶段。在 Ruby 中,每一行代码都是针对一个特定的 self
执行的。考虑下面五段代码:
class Person
def self.species
"Homo Sapien"
end
end
class Person
class << self
def species
"Homo Sapien"
end
end
end
class << Person
def species
"Homo Sapien"
end
end
Person.instance_eval do
def species
"Homo Sapien"
end
end
def Person.species
"Homo Sapien"
end
上面全部五段代码都定义了一个返回 “Homo Sapien” 字符串的 Person.species
方法。再看看另外一段代码:
class Person
def name
"Matz"
end
end
Person.class_eval do
def name
"Matz"
end
end
这段代码都是在 Person 类上定义了一个名为 name
的方法。所以 Person.new.name
将会返回 “Matz”。对于熟悉 Ruby 的人来讲,这并不是什么新鲜事情。在学习元编程的时候,这些代码都是分开独立展示的:另一种用于确定方法归属的机制。实际上,以上所有代码之所以如此工作,都可以归咎于统一的一个原因。
首先,理解 Ruby 的 metaclass 如何工作是非常重要的。当开始学习 Ruby 的时候,你就接触了类这个概念,Ruby 中的每个对象都归属于一个类:
class Person
end
Person.class #=> Class
class Class
def loud_name
"#{name.upcase}!"
end
end
Person.loud_name #=> "PERSON!"
Person
是 Class
类的一个实例,所以任何添加给 Class
类的方法在 Person
中都有效。但是有一点他们没有告诉你,其实,Ruby 中的每一个对象还拥有自己的 metaclass,也是一个可以拥有方法的 Class
,但是只关联于对象自己。
matz = Object.new
def matz.speak
"Place your burden to machine's shoulders"
end
当我们给 matz
的 metaclass 添加了方法 speak
的时候发生了什么呢?其实 matz
对象先继承自它的 metaclass,然后是 Object。这之所以多少有些不太好理解的原因是 metaclass 在 Ruby 中不可见:
matz = Object.new
def matz.speak
"Place your burden to machine's shoulders"
end
matz.class #=> Object
实际上 matz
的类应该是那个不可见的 metaclass。我们可以通过下面的方法进入 metaclass:
metaclass = class << matz; self; end
metaclass.instance_methods.grep(/speak/) #=> ["speak"]
这时如果是在其他相同话题的文章中,你可能已经在努力想要记住所有这些细节了;看起来有很多规则。还有,class << matz
到底是什么?
事实上,这些奇怪的规则都可以归纳为一个:掌控代码中的 self
。让我们回头看看前面的代码:
class Person
def name
"Matz"
end
self.name #=> "Person"
end
这里,我们给 Person
类添加了一个 name
方法。当我们说 class Person
时,self
直到 Person
类代码块的结尾都是其自身。
Person.class_eval do
def name
"Matz"
end
self.name #=> "Person"
end
在这,我们做了一件和上面完全相同的事:给 Person
类的实例添加了 name
方法,class_eval
将 self
设置为 Person
,直到代码块的结尾。类处理起来感觉非常的直观,其实 metaclass 也同样很直观:
def Person.species
"Homo Sapien"
end
Person.name #=> "Person"
在前面 matz
的例子中,我们在 Person
的 metaclass 上定义了 species
方法。我们没去操作 self
,但是你可以看到使用 def
方法并且关联到一个对象上的话,就会使方法定义到该对象的 metaclass 上。
class Person
def self.species
"Homo Sapien"
end
self.name #=> "Person"
end
这里,我们打开 Person
类,在代码块中将 self
设置为 Person
,正如上例所示。所以,当我们将方法定义到对象 self
上时,我们其实就是将方法定义到了 Person
类的 metaclass 上。还有,你可以看到,在 Person 类里面的 self.name
和外面的 Person.name
其实是一样的。
class << Person
def species
"Homo Sapien"
end
self.name #=> ""
end
Ruby 提供了一个可以直接进入对象 metaclass 的语法。通过 class << Person
,我们就将代码块中的 self
设置为了 Person
类的 metaclass。所以,species
方法就添加到了 Person
类的 metaclass 上,而不是 Person
类本身。
class Person
class << self
def species
"Homo Sapien"
end
self.name #=> ""
end
end
这里,我们将前面的几个技巧组合使用。首先,我们打开 Person
类,让 self
等价与 Person
类。其次,我们使用 class << self
让 self
等价于 Person
类的 metaclass。当我们定义 species
方法是,它就定义到了 Person
的 metaclass 上。
Person.instance_eval do
def species
"Homo Sapien"
end
self.name #=> "Person"
end
在最后这个 instance_eval
的示例中做了些有意思的事情。他将 self
分为用于执行代码的 self
和用于定义方法的 self
两部分。当使用 instance_eval
时,新方法定义在 metaclass,但是 self
还是对象本身。
上面的几个例子中,殊途同归自然地产生于 Ruby 的语义。通过上面的解释,应该明白了 def Person.species
,class << Person; def species
和 class Person; class << self; def species
并不是特地设计了这三种方法来做同一件事情,只是因为 Ruby 灵活地考虑到你程序中特定环境下的 self
而产生的。
另一方面,class_eval
稍有不同。因为它使用了代码块,不同于作为关键字,他可以捕捉到周围的局部变量。这可提供了强大的 DSL 能力,此外可以控制代码块中的 self
。但是除此之外,它和在这里的其它结构是完全相同的。
最后,instance_eval
将 self
分为两部分,同时也可以让你访问到定义之外的局部变量。
在下面的表格中,创建一个新作用域指的是代码块中的语句不能访问代码块之外的局部变量。
定义机制 | 方法调用 | 方法定义 | 新作用域 |
---|---|---|---|
class Person | Person | 相同 | 是 |
class « Person | Person’s metaclass | 相同 | 是 |
Person.class_eval | Person | 相同 | 非 |
Person.instance_eval | Person | Person’s metaclass | 非 |
需要注意的是 class_eval
仅仅在 Module(Class 继承自 Module)中有效,而且它是 module_eval
的别名。另外,Ruby 从 1.8.7 版本开始添加了 instance_exec
方法,和 instance_eval
类似,但是它允许你传入块参数。
更新:感谢 Ruby 核心团队的 Yugui 纠正原文中的错误,当时我忽略了在 instance_eval
中 self
是被分成两部分来对待的。