Ruby 元编程

Ch.1

打开类

1
2
3
4
5
6
7
8
9
class D
def x; 'x'; end
end
class D
def y; 'y'; end
end
obj = D.new
obj.x # => 'x'
obj.y # => 'y'

在上面的代码中,当第一次提及 class D 时,还没有一个叫做 D 的类存在。因此,Ruby 开始着手定义这个类,并定义 x() 方法。在第二次提及 D 类时,它已经存在,Ruby 就不用再定义它了。Ruby 只要重新打开这个已经存在的类,并为之定义 y() 方法。从某种意义上说,Rub y的 class 关键字更像是一个作用域操作符而不是类型声明语句。它的确可以创建一个还不存在的类,不过也可以把这看成是一种副作用。对于 class 关键字,其核心任务是把你带到类的上下文中,让你可以在其中定义方法。

对象中有什么

1
2
3
4
5
6
7
8
class MyClass
def my_method
@v = 1
end
end
obj = MyClass.new
obj.class # => MyClass

与 Java 这样的静态语言不一样,Ruby 中对象的类和它的实例变量没有关系,当给实例变量赋值时,它们就生成了。因此,对同一个类,你可以创建具有不同实例变量的对象。例如,如果 Bill 不曾调用 obj.my_method() 方法,那么 obj 对象根本不会有任何实例变量。你可以把 Ruby 中实例变量的名字和值理解为哈希表中的键/值对,每一个对象的键/值对都可能不同。
一个对象的实例变量存在于对象本身,而一个对象的方法存在于对象自身的类。这就是为什么同一个类的对象共享同样的方法,但不共享实例变量的原因。
既然类是对象,那么适用于对象的规则也适用于类。类和其他任何对象一样,也有自己的类,它的名字叫做 Class:

1
2
3
4
5
6
"hello".class # => String
String.class # => Class
String.superclass # => Object
Object.superclass # => BaseObject
BaseObject.suberclass # => nil

所有的类最终都继承于 Object,Object 本身又继承于 BasicObject,BasicObject 是 Ruby 对象体系中的根节点。

1
2
Class.superclass # => Module
Module.superclass # => Object

一个类只不过是一个增强的 Module,增强了三个方法——— new()、allocate()、superclass() 而已。
class.jpg-50.4kB

任何以大写字母开头的引用(包括类名和模块名),都是常量。常量的作用域不同于变量的作用域。
常量可以通过路径方式来唯一标识。常量路径使用双冒号进行分隔。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
module M
class C
X = 'a constant'
end
C::X # => 'a constant'
end
M::C::X # => 'a constant'
# 在常量前加一组双冒号表示根路径,从而得到一个绝对路径
module M
Y = 'another constant'
class C
::M::Y # => 'another constant'
end
end

什么是对象?对象无非是一组实例变量外加一个指向其类的引用。对象的方法并不存在于对象本身,而是存在于对象的类中。在类中,这些方法被称为类的实例方法。
什么是类?类无非就是一个对象(Class 类的一个实例)外加一组实例方法和一个对其超类的引用。Class 类是 Module 类的子类,因此一个类也是一个模块。

调用一个方法时发生了什么

当调用过一个方法时,Ruby 会做两件事

  1. 找到这个方法。这个过程称为方法查找。
  2. 执行这个方法。为了做到这点,Ruby 需要一个叫做 self 的东西。

接受者就是你调用方法所在的对象。例如,在 my_string.reverse() 语句中,my_string 就是接收者。
想象从一个类移动到它的超类,然后再移动到超类的超类,依此类推,直到到达 Object 类(所有类的默认超类),最后来到 BasicObject 类(Ruby 类体系结构的根节点)。在这个过程中,你所经历的类路径就是该类的祖先链。
方法查找:为了查找一个方法,Ruby 首先在接收者的类中查找,然后一层层地在祖先链中查找,直到找到这个方法为止。

当你在一个类(甚至可以是另外一个模块)中包含(include)一个模块时,Ruby 耍了些小花招。Ruby 创建了一个封装该模块的匿名类,并把这个匿名类插入到祖先链中,其在链中的位置正好包含在它的类上方。
包含模块的方法查找
Object 类包含了 Kernel 模块,因此 Kernel 就进入了每个对象的祖先链。这样在某个对象中可以随意调用 Kernel 模块的方法。这使得 print 看起来像是一个语言的关键字,其实它不过是一个方法而已。
每一行代码都会在一个对象中被执行———这个对象就是所谓的当前对象。当前对象也可以用 self 表示,因为可以用 self 关键字来访问它。
在给定时刻,只有一个对象能充当当前对象,但没有哪个对象能长期充当这一角色。特别地,当调用一个方法时,接收者就成为 self。从这一刻起,所有的实例变量都是self的实例变量,所有没有明确指明接收者的方法都在 self 上调用。当你的代码调用其他对象的方法时,这个对象就成为 self。
私有方法服从一个简单的规则:不能明确指定一个接收者来调用一个私有方法。换言之,每次调用一个私有方法时,只能调用于隐含的接收者—— self 上。

Ch.2

动态方法

1
2
3
4
5
6
7
8
class MyClass
def my_method(my_arg)
my_arg * 2
end
end
obj = MyClass.new
obj.my_method(3)
obj.send(:my_method, 3)

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

1
2
3
4
5
6
7
8
class MyClass
define_method :my_method do |my_arg|
my_args * 3
end
end
obj = MyClass.new
obj.my_method(2)

define_method() 方法在 MyClass 内部执行,因此 my_method() 定义为 MyClass 的实例方法。这种在运行时定义方法的技术称为动态方法。

method_missing

1
2
3
4
5
class Lawyer;end
nick = Lawyer.new
nick.talk_simple
=> NoMethodError: undefined method 'talk_simple' fro #<Lawyer:0x3c848>

当调用 talk_simple() 方法时,Ruby 回到 nick 对象的类中查询它的实例方法。如果在那里找不到 talk_simple() 方法,Ruby 会沿着祖先链向上搜寻进入 Object 类,并且最终来到 Kernal 模块。由于 Ruby 在哪里都没找到 talk_simple() 方法,它只好承认自己的失败,并在 nick 对象上调用一个名为 method_missing() 的方法。Kernel#method_missing() 方法会抛出一个 NoMethodError 进行响应,这是它全部的工作。
你可以通过覆写它来截获无主的消息。每一个来到 method_missiong 的消息都带着被调用方法的名字,以及所有调用时传递的参数和块。
覆写 method_missiong() 方法使得你可以调用实际上并不存在的方法。
被 method_missiong() 方法处理的消息,从调用者角度看,跟普通方法没什么区别,但是实际上接受者并没有相对应的方法。这被称为一个幽灵方法。

当一个幽灵方法和一个真实方法发生名字冲突时,后者会胜出。如果不需要那个继承来的真实方法,则可以通过删除它来解决这个问题。为了安全起见,你应该在代理类中删除绝大多数继承来的方法。这就是所谓的白板类,它所拥有的方法比 Object 类还要少。
你可以通过两种简单的途径来删除一个方法。可以使用 Module#undeft_method 方法,它会删除所有的(包括继承来的)方法;也可以使用 Module@remove_method() 方法,它删除接受者自己的方法,而保留继承来的方法。

Ch.3

作用域

块会在定义时获取周围的绑定。你可以在块的内部定义额外的绑定,但是这些绑定在块结束时就会消失。

1
2
3
4
5
6
7
8
9
10
11
12
13
def my_method
yield
end
top_level_variable = 1
my_method do
top_level_variable += 1
local_to_block = 1
end
top_level_variable # => 2
local_to_block # => Error

在一些语言中,比如 Java 和 C#,有“内部作用域”的概念。在内部作用于中可以看见“外部作用域”中的变量。但 Ruby 中没有这种嵌套的作用域,它的作用域之间是截然分开的:一旦进入一个新的作用域,原先的绑定就会被替换为一组新的绑定。

准确地说,程序会在三个地方关闭前一个作用域,同时打开一个新的作用域:

  • 类定义
  • 模块定义
  • 方法
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    v1 = 1
    class MyClass # 作用域门:进入 class
    v2 = 2
    local_variables # => [:v2]
    def my_method # 作用域门:进入 def
    v3 = 3
    local_variables
    end # 作用域门:离开 def
    local_variables # => [:v2]
    end # 作用域门:离开 class
    obj = MyClass.new
    obj.my_method # => [:v3]
    obj.my_method # => [:v3]
    local_variables # => [:v1, :obj]

每个Ruby作用域包含一组绑定,并且不同的作用域之间被作用域门分隔开来:class、module 和 def。
如果要让一两个绑定穿越作用域门,那么可以用方法调用来代替作用域门:用一个闭包获取当前的绑定,并把这个闭包传递给该方法。你可以使用 Class.new() 方法代替 class,使用 Module.new 代替 module,以及使用 Module#define_method() 代替 def。这就形成了一个扁平作用域,它是闭包中的一个基本概念。如果在一个扁平作用域中定义了多个方法,则这些方法可以用一个作用域门进行保护,并共享绑定,这种技术称为共享作用域。

1
2
3
4
5
6
7
8
9
10
def a_scope
$var = "some value"
end
def another_scope
$var
end
a_scope
another_scope => "some value"

全局变量(以$开头)可以在任何作用域中访问。如非必要,尽可能少使用全局变量。
有时可以用顶级实例变量来代替全局变量。他们是顶级 main 的实例变量。
只要 main 对象在扮演 self 的角色,就可以访问顶级实例变量。

1
2
3
4
5
6
@var = "The top-level @var"
def my_method
@var
end
my_method # => "The top-level @var"

instance_eval()

Object#instance_eval() 方法,它在一个对象的上下文中执行一个块。在运行时,该块的接收者会成为 self,因此它可以访问接收者的私有方法和实例变量。

1
2
3
4
5
6
7
8
9
10
class MyClass
def initialize
@v = 1
end
end
obj = MyClass.new
obj.instance_eval do
self # => <MyClass:0x3340dc @v=1>
@v # => 1
end

洁净室仅仅是一个用来执行块的环境,它通常还会暴露若干有用的方法供块调用。

可调用对象

可调用对象是可以执行的代码片段,而且它们有自己的作用域。
可调用对象可以有以下几种方式:

  • 块(虽然它们不是真正的“对象”,但是它们是“可调用的”):在定义它们的作用域中执行。
  • proc:Proc 类的对象,跟块一样,它们也在定义自身的作用域中执行。
  • lambda:也是 Proc 类的对象,但是它跟普通的 proc 有细微的区别。它跟块和 proc 一样都是闭包,因此也在定义自身的作用域中执行。
  • 方法:绑定于对象,在所绑定对象的作用域中执行。它们也可以与这个作用域解除绑定,再重新绑定到另一个对象的作用域上。

不同种类的可调用对象有细微的区别。在方法和 lambda 中,return 语句从可调用对象中返回。在块和 proc 中,return 语句从定义可调用对象的原始上下文中返回。
另外,不同的可调用对象对传入参数数目不符有不同的反应。其中方法处理方式最严格,lambda 同样严格(它与方法相比,在某些极端情况下略为宽松),而 proc 和块则要宽松一些。
尽管有这些区别,还是可以将一种可调用对象转换为另外一种可调用对象的,实现这样功能的方法包括 Proc.new() 方法、Method#to_proc() 方法和 & 操作符。

Ch.4

类定义

在类定义中,当前对象 self 就是正在定义的类。
Ruby 解释器总是追踪当前类(模块)的引用。所有使用 def 定义的方法都成为当前类的实例方法。
Module#class_eval 方法会在一个已存在类的上下文中执行一个块。

Ruby 解释器假定所有的实例变量都属于当前对象 self。在类定义时也是如此:

1
2
3
class MyClass
@my_var = 1
end

在类定义时,self 的角色由类本身担任,因此实例变量 @my_var 属于这个类。类的实例变量不同于类的对象的实例变量。

1
2
3
4
5
6
7
8
9
class MyClass
@my_var = 1
def self.read; @my_var; end
def write; @my_var = 2; end
def read; @my_var; end
end
obj = MyClass.new
obj.write obj.read # => 2
MyClass.read # => 1

类实例变量只不过属于 Class 类对象的普通实例变量而已。正因为如此,类实例变量仅仅可以被类本身所访问————而不能被类的实例或子类所访问。

类变量

类变量与类实例变量不同,它们可以被子类或类的实例所使用。

1
2
3
class C
@@v = 1
end

因为类变量并不真正属于类——它们属于类体系结构。由于 @@v 定义于 main 的上下文,它属于 main 的类 Object,所以也属于 Object 的所有后代。MyClass 继承自Object,因此它也共享了这个类变量。

1
2
3
4
5
@@v = 1
class MyClass
@@v = 2
end
@@v # => 2

eigenclass

Ruby 有一种特殊的基于 class 关键字的语法,它可以让你进入该 eigenclass 的作用域。

1
2
3
4
5
obj = Object.new
eigenclass = class << obj
self
end
eigenclass.class # => Class

每个 eigenclass 只有一个实例,并且不能被继承。更重要的是,eigenclass 是一个对象的单件方法的存活之所。

类方法定义的方式

1
2
3
4
5
6
7
class MyClass
def self.my_method1;end # 方法1
def MyClass.my_method2;end # 方法2
class << self
def my_method3;end # 方法3
end
end

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class C
def a_method
'C#a_method()'
end
end
cladd D < C; end
obj = D.new
obj.a_method # => 'C#a_method()'
# 辅助方法
class Object
def eigenclass
class << self; self; end
end
end
class << obj
def a_singleton_method
'obj#a_singleton_method()'
end
end

有 eigenclass 的方法查找
singleton_method.jpg-88.2kB

1
2
3
4
5
6
7
8
9
10
11
12
class C
class << self
def a_class_method
'C.a_class_method()'
end
end
end
C.eigenclass # => #<Class:C>
D.eigenclass # => #<Class:D>
D.eigenclass.superclass # => #<Class:C>
C.eigenclass.superclass # => #<Class:Object>

类的 eigenclass
class_singleton_method.jpg-113.5kB

如果把eigenclass、普通类和模块放到一起,Ruby 对象模型可以总结为 7 条规则:

  1. 只有一种对象——要么是普通对象,要么是模块。
  2. 只有一种模块——可以是普通模块、类、eigenclass 或代理类。
  3. 只有一个方法,它存在于一种模块中——通常是类中。
  4. 每个对象(包括类)都有自己的“真正的类”——要么是普通类,要么是 eigenclass。
  5. 除了 BasicObject 类无超类外,每个类有且只有一个超类。这意味着从任何类只有一条向上直到 BasicObject 的祖先链。
  6. 一个对象的 eigenclass 的超类是这个对象的类;一个类的 eigenclass 的超类是这个类的超类的 eigenclass。
  7. 当调用一个方法,Ruby 先向“右”迈一步进入接收者真正的类,然后向“上”进入祖先链。这就是 Ruby 查找方法的全部内容。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
module MyModule
def my_method; "hello";end
end
# 类扩展
class MyClass
class << self
include MyMoudle
end
end
MyClass.my_method # => "hello"
# 对象扩展
obj = Object.new
class << obj
include MyMoudl
end
obj.my_method # => "hello"

Object#entend() 在接收者 eigenclass 中包括模块的快捷方式。

1
2
3
4
5
6
class MyClass
extend MyMoule
end
MyClass.my_method # => "hello"
obj.extend MyMoule
obj.my_method # => "hello"

Ch.5

eval()

Kernel#eval() 方法会执行字符串中的代码,并返回执行结果。

1
2
3
array = [10, 20]
element = 30
eval("array << element") # => [10, 20, 30]

Binding 就是一个用对象表示的完整作用域。你可以通过创建 Binding 对象来捕获并带走当前的作用域。接下来,你还可以通过 eval() 方法、instance_eval() 方法或 class_eval() 方法,在 Binding 对象所携带的作用域中执行代码。可以使用 Kernel#bingding() 方法来创建 Bingding 对象。

污染对象和安全级别

Ruby 会自动把不安全的对象——尤其是从外部传入的对象——标记为被污染的。污染对象包括程序从Web表单、文件和命令行读入的字符串,甚至包括系统变量。每次从污染字符串运算而来的新字符串,也是被污染的。通过调用 tainted?() 方法来判断类是不是被污染了。
当设置一个安全级别(可以通过给$SAFE全局变量赋值来实现)时,就禁止了某些特定的潜在危险操作。有五个安全级别可供选择,从默认的 0(这里是一个“嬉皮士公社”,在这儿你可以不受约束,也可以格式化硬盘)到 4(这里是“军事管辖区”,在这儿你甚至不能自由地退出程序)。例如,在安全级别 2 上,会禁止绝大多数文件相关工作。值得注意的是,在任何大于 0 的安全级别上,Ruby 都会拒绝执行污染的字符串。

类扩展混入

1
2
3
4
5
6
7
8
module Merb::Cache::CacheMixin
def self.included(base)
base.extend(ClassMethods)
end
module ClassMethods
def cache(*actions)
# ...

Merb::Cache::CacheMixin 既充当了混入,也充当了内部模块的命名空间,这个内部模块被恰当地命名为 ClassMethods,并定义了像 cache() 这样的类宏。当包含 CacheMixin 时,就会触发一系列事件,如下:
• Ruby 调用一个钩子方法:included() 方法。
• 这个钩子方法接着会转而处理包含模块的类(有时会称其为包含者(inclusor),或者像在这里一样称为 base),并用 ClassMethods 模块扩展它。
• extend() 方法会把 ClassMethods 模块中的方法包含到包含者的 eigenclass 中。结果就是,cache() 等实例方法会混入到该类的 eigenclass 中,实际上成为包含者的类方法。