最近阅读《Ruby 元编程》收益良多,对 Ruby 代码的执行,在理论上得到了更深的认识,这里,粗略地总结一下 Ruby 代码的执行环境。
这个总结只是初步的一些结论,许多特殊情况或者例外,可能被我忽略了,而造成了结论其实不正确,如果您发现了,不妨指出;另外,一些术语是我自己总结的,不一定符合惯例。
以下是正文。
什么是 Ruby 代码的执行环境?Ruby 是一种比较严格的单步解释型语言,每一行代码的执行,在不同的环境下结果显然不同,例如puts a
,这个 a 存在么?是什么?导致的结果显然不一样,一般把这种环境称之为 binding。我认为,完整的执行环境,包括两大类五个方面:
第一类:scope,它包括 1)当前对象,也就是 self 所指代的对象,决定了在不指明 receiver 的情况下,默认的接受者,也决定了实例变量(就是@打头的变量)存在于那个对象之中 2)局部变量池(我自己发明的),就是已经存在的局部变量及其值
第二类:level,它包括 1)当前类,也就是用 def 定义方法时,这个方法作为哪个类的实例方法? 2)常量所属的目录(要把常量理解为目录和文件一样的东西,类和模块是目录同时也是文件,其他普通常量就是文件) 3)类变量(也就是@@打头的变量)所属的类,也就是使用类变量时,理解为哪个类?
下面的问题是,如何分析一段代码的上述 5 个方面?这个需要分为两个步骤:一个是初始状态,另外一个是这种状态如何改变。
初始状态,也就是 Ruby 代码最开始执行时,没有切换 scope 和 level 时,5 个方面的初始状况,分别是: 1)当前对象是一个名为“main”的 Object 实例对象,这个 main 只是一个普通的名称而已,其实就是实例化 Object 之后,覆盖了 to_s 方法返回“main” 2)局部变量池为空 3)当前类是 Object 4)当前常量目录是:: 5)当前类变量所属的类是 Object
了解了初始状态后,我们就知道,在初始状态执行代码的环境了。那么,scope 和 level 是如何变化的呢? 1.在使用 module,class,def 关键字定义模块,类,方法时(以及用 end 退出时),切换 scope,具体的规则是: 在 module/clsss 定义处,切换 scope,当前对象是定义的模块或类本身,当前变量池为空 在 def 定义处,切换 scope,调用该方法的 receiver 为当前对象,当前变量池为空
当遇到 end 时,对应的切换结束,切换回切换之前的 scope。
2.在使用 module,class 关键字定义模块和类时,切换 level,规则很简单,目录是递进式切换,从根目录进入 A 的 module,目录就是::A,再进入里面的类 B,则目录是::A::B,当前类以及当前类变量所属的类,均是定义的类。
了解了初始状态,也了解了状态的变化,那么我们就可以完整地分析代码的执行环境了。但是,我们为什么要分为 5 个方面呢?比如 scope 的两个方面,切换时都切换了,作为一个方面分析不是更简单吗?这是因为,ruby 提供了一些手段,使得我们只切换 scope 中的一些内容(比如只更改 self,也有人称之为 flat scope,认为没有切换 scope)。也就是说,有一些特殊手法,可以按照需要,只切换其中一些方面,而保留另外一些方面,这就比我们上面所说的切换方法灵活多了。具体来说:
一、任何对象均可以调用一个名为 instance_eval 的方法,提供一个 block,有两方面发生变化: 1)这个 block 里面的当前对象,也就是 self 所指的对象,临时地脱离当前 scope 所指的对象,而变为调用此方法的对象 2)这个 block 里面定义的方法,临时脱离当前类,而是这个对象的 eigenclass
二、任何模块对象(也就是 Module 及其子类(主要是 Class)的实例)均可以调用一个一个名为 class_eval 的方法,提供一个 block,有以下变化: 1)这个 block 里面的当前对象为该模块 2)当前类也是该模块。(但是并不影响其他内容,尤其是,在这个 block 定义的类变量不属于该类,定义的常量,也不会置于这个类代表的目录之下,这就说明了要把 level 分为 3 个方面分析的重要性) 值得注意的是:通过 Class.new 执行的 block,其实在内部是使用了 class_eval 来执行,所以,规则同 class_eval 相同。
三、class << object 定义,会有以下变化: 1)切换 scope,当前对象为 eigenclass,当前作用域为新作用域 2)当前类为 eigenclass(但是当前目录和当前类对象均维持不变)
除此之外,block 并非 gate(既不是 scope gate,也不是 level gate),所以不会切换任何 scope 和 level,维持原先的 5 个方面状态不变。
最后出一个小小的谜题,便于你理解 ruby 执行代码的方式:
puts = 5
puts puts
上述代码是合法的吗?如果合法,结果是什么?(反之,错误在哪?)