Ruby 元编程

一、Ruby 对象模型

class MyClass; end
obj1 = MyClass.new

这段代码中 obj1 和 MyClass 都是引用,唯一的区别在于前者是一个变量,后者是一个常量。

Ruby 对象由两部分组成:实例变量和指向类的引用。

在类中包含一个模块时,Ruby 会在继承链中插入一个匿名类,这个匿名类包含了该模块所有方法:

class A; include Math; end

A.ancestors # => [A, Math, Object, Kernel, BasicObject]

正因为这种机制,puts 方法才可以在一个普通类中使用。因为 Object 包含了 Kernel 模块,所以 Kernel 模块的方法会包含在每一个普通类的继承链中,其中包括了 puts 方法,当你在其他类中调用 puts 方法输出时,其实是调用了 Kernel 的 puts 方法。

Ruby 中私有方法的限制很简单,即不能为私有方法指定一个显式的接收者(例如 abc.methodabc 就是显式的接收者) 。也就是说,私有方法只能只能在隐式的接收者(即 self)上调用。

二、方法

动态定义方法

Blank Slate

某些情况下需要覆盖类中已经存在的方法(如 jbuilder )。使用 undef_method 可以取消类中已经存在的函数,但仍需保留 instance_eval 以及 __ 开头的方法。这种特殊的状态被称为 Blank Slate。

blankslate 提供了一个现成的 Blank Slate 基类。此外 Ruby 1.9 中 BasicObject 也有类似的作用。

三、Block

Scope Gate

Scope Gate 指程序中创建新的作用域的地方,Ruby 中 class module 和 def 三个关键字有 Scope Gate 的作用。

如果要在定义类/方法的同时避免产生新的作用域,可以用 Class.new() 和 Module#define_method() 代替 class 和 def。这种技巧被称为 nested lexical scope,也有人称它为 flat scope。

& 符号

& 符号的作用是使 Proc 和 block 相互转换,例如:

def my_method(&the_proc)
  the_proc
end

p = my_method { |name| "Hello, #{name}!" }
puts p.class # => Proc
puts p.call('Bill') # => Hello, Bill!

Block、Proc、Lambda 和 Method

  • Block 并不是真正的对象,虽然它们也可以被调用。Block 在定义的上下文中执行。

  • Proc 是实际的对象,也在定义的上下文中执行。

  • Lambda 对象和 Proc 对象有一些细微的差别(return 的行为和参数的检查上,Lambda 更像 Method),在定义的上下文中执行。

  • Method 被绑定在某个对象上,在该对象的作用域中执行。

四、类定义

self

类定义中的 self 指向当前类本身:

result = class MyClass
  self
end

result # => MyClass

类变量

在类定义中以 @@ 前缀开始的变量就是类变量,与类的实例变量不同的是,他们可以被子类和实例方法访问到。类变量同时属于所有的子类,任何一个子类对它的修改都会对父类起作用,这会导致一些难以发现的问题。因此现在更推荐用类的实例变量代替类变量。

eigenclass

Ruby 中每一个对象都有一个属于自己的隐藏的类,这个类就被称为 eigenclass。使用 class << obj 语法可以访问该对象的 eigenclass。

obj = Object.new
eigenclass = class << obj
  self
end

eigenclass => #<Class:#<Object:0x000001022097b8>>

事实上 instance_eval 也会修改类对象,只是它修改的是接收者的 eigenclass。

extend 方法可以把一个模块中的方法添加到接收者的 eigenclass 中。

Ruby 对象模型

以下七条规则描述了 Ruby 的对象模型:

  1. 对象只有一种,不管是标准类还是模块;
  2. 模块只有一种,不管是标准的模块,类,eigenclass,或是代理类;
  3. 方法只有一种,而且只存在于某一模块中——大多数情况下是在一个类中;
  4. 每一个对象,包括类,都有自己的实际的类,无论是标准类还是 eigenclass;
  5. 每一个类都只有一个父类(BasicObject 除外)。因此每个对象都有一条单一的直到 BasicObject 的继承链;
  6. 一个对象的 eigenclass 的父类是这个对象的类。一个类的 eigenclass 父类是这个类的父类的 eigenclass。听起来很绕口,但这句话是 Ruby 对象模型的核心之一,参考这篇文章的最后的一幅图可以帮助理解;
  7. 当方法被调用时,解释器首先在接收者的 eigenclass 中查找该方法,如果找不到,则沿着继承链一路网上找。

方法重命名

当一个方法被重定义时,它本身没有被修改,只是原来的名字绑定了一个新方法。因此只要有指向原来的方法的引用,该方法仍然能被调用到。

RubyGems 库就是这么处理 require 的,新定义的 require 方法先调用原来的 require,如果发生异常,则在 gems 库中再查找一次需要的包,找到了就调用原来的 require 把这个包加载进来。具体可以看 custom_require.rb

五、编写代码的代码

Binding 对象

在 Ruby 中可以把整个作用域打包成一个 Binding 对象,配合 eval() 相关的一系列方法可以在这些作用域中执行代码。

Taint Tracking

Ruby 里也有类似 taint tracking 的机制,来自外部的输入会被加上 tainted 标记,用 tainted? 方法可以查看某个实例是否来自不可信的内容。

为了保证 eval() 方法安全性,Ruby 中还有一个名为 $SAFE 的全局变量控制 eval 的安全级别。当它为 0 时不做任何安全检查;当它为 4 时安全级别最高,程序甚至无法随意地退出。

Hooks

Class#inhertied()Module#included()Module#method_added() 等都是 Ruby 提供的 hook 方法。它们会分别在类被继承、模块被引用和方法被添加时执行。

代码分析

下半部分主要结合 Rails 2.3 的源代码,分析了 Rails 中用到的和 Ruby 元编程相关的特性: