《Metaprogramming Ruby》笔记
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.method
中 abc
就是显式的接收者) 。也就是说,私有方法只能只能在隐式的接收者(即 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 的对象模型:
- 对象只有一种,不管是标准类还是模块;
- 模块只有一种,不管是标准的模块,类,eigenclass,或是代理类;
- 方法只有一种,而且只存在于某一模块中——大多数情况下是在一个类中;
- 每一个对象,包括类,都有自己的实际的类,无论是标准类还是 eigenclass;
- 每一个类都只有一个父类(BasicObject 除外)。因此每个对象都有一条单一的直到 BasicObject 的继承链;
- 一个对象的 eigenclass 的父类是这个对象的类。一个类的 eigenclass 父类是这个类的父类的 eigenclass。听起来很绕口,但这句话是 Ruby 对象模型的核心之一,参考这篇文章的最后的一幅图可以帮助理解;
- 当方法被调用时,解释器首先在接收者的 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 元编程相关的特性:
- ActiveRecord:Validations
- alias_method_chain()
- AttributeMethods ActiveRecord 的属性第一次被访问时,在 method_missing 中调用 define_attribute_methods 添加相应的属性,类似的还有 respond_to?
- ActiveRecord::Base.method_missing() 这里定义了一些动态查找方法,例如 find_by_username 就是在这里被处理的
- ActiveRecord::Filters.append_before_fitler() before_filter 的实现就在这里