欢迎光临Software MyZone,有问题可留言或到站点论坛发帖,争取第一时间帮忙解决 || 站点论坛:火龙论坛 || 淘宝小店:应小心的易淘屋 【欢迎大家提建设性意见】

论 Ruby 顶层及 Object、Kernel 的关系

Ruby 的顶层环境、Kernel 模块和 Object 类之间的关系是一个让很多人都觉得费解的问题,这篇文章便是要对这个话题进行深入的探讨。考虑以下在 Ruby 顶层环境中运行的代码:

p "hello"

这个看似纯命令的语句,在完全面向对象的 Ruby 语言中,实际上也是一个面向对象的方法调用。因为在 Ruby 中,任何没有接收者的方法调用,解释器都会隐式地让 self 成为接收者。即是说,上面的代码等同于:

self.p "hello"

但是如果直接用第二种方式调用,会有一个访问权限的问题,这个稍后再说。我们知道 self 在一个类上下文中可以引用到类本身,在一个类的实例上下文中可以引用实例/对象本身(当然在单例环境中,self 引用的就是这个单一实例本身),那么在顶层环境中它引用的是什么呢?

p self.class

这一行代码告诉了你 self 是什么,那就是一个 Object 类型的直接实例,而它的 to_s 返回 “main”(既一个描述对象的字符串),所以我们可以把它看作是一个名字叫 main 的 Object 类型对象。这是你的程序开始运行后就生成的一个默认对象,纯粹是为了满足 Ruby 完全面向对象编程“在任何上下文中都有且仅有一个 self 指向当前的对象”的概念。

摘自:http://firedragonpzy.iteye.com/admin/blogs/1527068

这里有一个 RMXP 的 Ruby 和标准 Ruby 的区别,在 RMXP 中,顶层的 self 很神奇地指向 nil 这个 NilClass 的单一实例,即是说在顶层中调用的方法实际上其接收者都是 nil。

那么顶层函数在 Ruby 中到底属于谁呢?由于底层获取的 self 是 Object 的一个实例,所以顶层实际上就是 Object 的实例上下文,顶层函数自然也是属于 Object 类的。

class Object
def self.method_added(id)
print "添加方法 ", id.id2name
end
end
def foo
p "foo"
end

这段代码中的 method_added 方法是 Module 的一个回调函数,每当一个 Module(当然,包括其子类 Class 类)被(动态地)添加了实例方法时,这个函数都会被调用。

当我们在顶层中定义了一个 foo 时,输出了 “添加方法 foo”,可见在顶层中定义的方法确实是被作为一个实例方法加入到了 Object 类中。

这里就有了第二个 RMXP 的 Ruby 的特点——用户在顶层中定义的方法加入到 Object 中后,其访问权限被设为了公有(public);然而在标准 Ruby 中,加入到 Object 后其访问权限应该是私有的(private)。这个的区别就在于,上面定义的方法 foo 在 RMXP 的 Ruby 中可以任意使用任何接收者,因为 Object 是所有对象的泛型,所以 foo 也同时属于任何类(包括各种 Module 和 Class 类型,所以下面的 Module.foo 也是合法的,这是 Ruby 完全面向对象概念中一个很容易产生混淆的地方)。

3.1415926.foo
true.foo
nil.foo
Module.foo

而这样的调用在纯 Ruby 中就会因为无权限访问而报错,因为添加了任何接收者后,不管这个接收者是不是这个方法所属类的实例,Ruby 都会认为方法的调用者是来自 Object 外部的,那么接收者那个类自然就不能访问 Object 类私有的成员方法。换句话说就是私有的方法永远不能有显式指定的接收者。以下代码证明了这一点:

class A
def fn
p "hello"
end
private :fn
def func
fn        # OK
self.fn   # Error
end
end
A.new.func

所以在标准 Ruby 中,永远不能显式地去指定顶层方法的接收者(包括显式指定 self)。

然而在 RMXP 中,类似 self.p “hello” 的语句就会和标准 Ruby 一样发生越权访问的错误了。要解释清楚这个,还得先解释清楚 Kernel 和 Object 的关系。

首先,Object 类本身只定义了一个方法,那就是 initialize,除此之外,可以说 Object 就是个空的框架。证明:

p Object.singleton_methods                 # Object 定义的类方法
p Object.public_instance_methods(false)    # Object 定义的公有实例方法
p Object.protected_instance_methods(false) # Object 定义的受保护实例方法
p Object.private_instance_methods(false)   # Object 定义的私有实例方法

这几行代码都是获取 Object 类定义的各种方法,传递一个 false 表示除去父类(当然,Object 在 Ruby 1.8 中没有可见的父类,只有在 mixin 了 Kernel 后隐式添加的匿名父类)和 mixin 的模块中的实例方法,也就是只获取这个类自己定义的方法。输出发现只有一个私有实例方法 initialize 是 Object 定义的。

那么平时我们用的那些 Object 的实例方法到底是从哪儿来的?答案是:全都是在 Kernel 中定义的。有兴趣的可以去翻一翻 Ruby 的手册,你会发现 Object 类的帮助文档开头就写着“虽然 Object 的实例方法是由 Kernel 模块定义的,但为了泾渭分明,我们还是选择在这里(即 Object 类的说明中)记载这些方法。”像 __id__ 、clone、,equal? 之类的方法,都是在 Kernel 中定义的私有实例方法。证明:

p Kernel.public_instance_methods(false)

输出了所有 Kernel 本身定义的公有实例方法,而这些方法都是我们平时耳熟能详的所谓的“Object 实例方法”!Object 类建立时 mixin 了 Kernel 模块的所有实例方法,所以每当我们调用某个对象继承自 Object 的方法时,解释器实际上并没有在 Object 中找到该方法,而是在 Kernel 被 mixin 后产生的 Object 的匿名父类中找到的。

那么我们用得更多的那些来自 Kernel 的 p、print、exit 等方法呢?

p Kernel.private_instance_methods(false) # Kernel 定义的私有实例方法
p Kernel.singleton_methods(false)        # Kernel 定义的模块方法

输出发现,原来这些函数同时是 Kernel 的模块方法和私有实例方法。按照常理来想,这些函数都是有关 Ruby 程序核心的一些重要方法,它们应该有全局和静态的特性,所以把他们写为 Kernel 的模块方法再合适不过。而这里 Kernel 把所有这些静态的方法都复制了一份,弄成了私有的实例模块方法,想来纯粹是为了让 Object 类进行 Kernel 的 mixin 时能够一并 mixin Kernel 的这些特殊方法(因为 mixin 一个模块并不能 mixin 它的模块方法,即属于该模块本身这个对象的单例方法),这样它们同时就变成了 Object 的私有实例方法,那么只要不指定接收者,程序员就可以在 Ruby 的任何上下文中访问这些函数,因为不显式指定接收者就会默认接收者为 self,而 self 永远都是 Object 类型,自然也就永远能够访问 Object 的私有实例方法。

由于 p 是 Object 通过 Kernel mixin 而得到的私有实例方法,当你像 self.p “hello” 这样直接指定了 self 为接收者时,不管 self 是什么,Ruby 都会认为调用者对 p 的调用是来自 Object 外部,于是发生了越权访问的错误。

以下两种方式,调用的是完全不同、相互独立的两个方法:

Kernel.exit
exit

前者是在 Kernel 中定义的模块方法,定义方式类似 def Kernel.exit … end;后者则是 Object 中的私有实例方法,只不过它不是在 Object 中定义的,所以会路由到 Kernel 中的同名私有实例方法。这两者的区别差不多就是下面两种 foo 方法的区别:

module Kernel

def Kernel.foo
p "类方法 foo 调用"
end

private
def foo
p "Object/Kernel 私有实例方法 foo 调用"
end

end

foo        # => "Object/Kernel 私有实例方法 foo 调用"
Kernel.foo # => "类方法 foo 调用"

这就是为什么你直接在顶层重定义 print 能够覆盖不加接受者时的 print,而不能覆盖 Kernel.print,因为这样的重定义是相当于给 Object 类定义了一个公有的(在 RMXP 中是公有的)实例方法,而不是覆盖了 Kernel 的模块方法(实际上连 Kernel 的同名私有实例方法都没有覆盖)。由于这样的覆盖是在 Object 中定义了 print,当你直接调用 print 的时候就不会再次路由到 Kernel 中的那个私有实例方法 print 了。你可以很容易地发现覆盖 print 前后的区别——覆盖前 self.print 报错,而覆盖之后 self.print 就能正常运行,这便是因为覆盖之后你定义的就是公有的方法了。

如果直接采用这样的写法:

module Kernel
def print(x)
# ...
end
end

那就是覆盖的 Kernel 的私有实例方法;如果:

module Kernel
def Kernel.print(x)
# ...
end
end

那就是覆盖的 Kernel 的模块方法。Kernel.print 这样的调用方法就是在调用 Kernel 的模块方法 print。

至于在顶层定义的局部变量,是真正仅属于顶层的局部变量,在其它上下文中,只能通过顶层的 Binding 进行 eval 来访问这些局部变量。而在顶层的常量,又涉及到另一个概念——常量定义点——一个被 Ruby 核心开发团队的成员称为 cref 的东西。这不属于本文讨论的范畴,我们目前只需要知道,顶层的常量会被定义到 Object 这个命名空间中。所以像——

ORZ = 1
p Object::ORZ

这样的常量,就相当于全局的常量了,因为它们属于 Object 这个命名空间,所以在任何其它上下文中都能访问。

最后再给一个例子:

module Mod
def self.foo
p "模块方法"
end
def foo
p "模块实例方法"
end
end
class A
include Mod
def foo
p "类实例方法"
end
end
class B
include Mod
end
A.new.foo               # "类实例方法"
B.new.foo               # "模块实例方法"
Mod.foo                 # "模块方法"
module Mod
def foo
p "lmao"
end
end

A.new.foo               # "类实例方法"
B.new.foo               # "lmao"
Mod.foo                 # "模块方法"

模块方法 foo 自始至终没有被改变,B 类的实例方法 foo 自始至终指向 Mod 模块的实例方法 foo,而 A 类的实例方法 foo 一开始就被 A 本身重定义、覆盖了。

Tags:
  1. Pingback: celine boston tote bag price

    • You can call your friends and classmates together to learn, very welcome.At present, this is not my top-level domain, shortly after I put on my top-level domain:www.firedragonpzy.com.cn