赞 | 2 |
VIP | 143 |
好人卡 | 1 |
积分 | 1 |
经验 | 216792 |
最后登录 | 2019-10-10 |
在线时间 | 24 小时 |
Lv1.梦旅人
- 梦石
- 0
- 星屑
- 61
- 在线时间
- 24 小时
- 注册时间
- 2008-8-5
- 帖子
- 1924
|
本帖最后由 紫苏 于 2009-11-2 15:15 编辑
来晚了,都已经解决了?我看未必呢……
Ruby 的顶层环境确实比较特殊,还有 Kernel 和 Object 的关系,都是有点绕脑子的,值得探讨——
首先给楼主介绍一个在网页中嵌入了 IRB 的站点:
http://tryruby.sophrinix.com/
可以在这里把相同的 Ruby 代码做个对比,看看 RMXP 嵌入的 Ruby 和纯粹的 Ruby 有些什么不同,这个稍后再细说(不过由于还是 Beta,所以不能保证它是 BUG-free 的,最好还是自己下一个 Ruby 来测试)
考虑以下在 Ruby 顶层环境中运行的代码:这个看似纯命令的语句,在完全面向对象的 Ruby 语言中,实际上也是一个面向对象的方法调用。因为在 Ruby 中,任何没有接受者的方法调用,解释器都会隐式地让 self 成为接受者。即是说,上面的代码等同于但是如果直接用第二种方式调用,会有一个访问权限的问题,这个稍后再说。我们知道 self 在一个类上下文中可以引用到类本身,在一个类的实例上下文中引用实例/对象本身(当然在单例环境中,self 引用的就是这个单一实例本身),那么在顶层环境中它引用的是什么呢?这一行代码告诉了你 self 是什么,那就是一个 Object 类型的直接实例,而它的 to_s 返回 "main"(既一个描述对象的字符串),所以我们可以把它看作是一个名字叫 main 的 Object 类型对象。这是你的程序开始运行后就生成的一个默认对象,纯粹是为了满足 Ruby 完全面向对象编程“在任何上下文中都有且仅有一个 self 指向当前的对象”的概念。
这里就有第一个 RMXP 的 Ruby 和纯 Ruby 的区别,在 RMXP 中,顶层的 self 很神奇地指向 nil 这个 NilClass 的单一实例,即是说在顶层中调用的方法实际上其接受者都是 nil ……
那么顶层函数在 Ruby 中到底属于谁呢?顶层函数实际上是属于 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 没有显示声明父类,只有在包含 Kernel 时隐式添加的匿名父类)和包含的模块中的实例方法,也就是只获取这个类自己定义的方法。输出发现只有私有的实例方法中有一个 initialize 是 Object 定义的。
那么平时我们用的那些 Object 的实例方法到底是从哪儿来的?答案是:全都是在 Kernel 中定义的。有兴趣的可以去翻一翻 Ruby 的手册,你会发现 Object 类的帮助文档开头就写着“虽然 Object 的实例方法是由 Kernel 模块定义的,但为了泾渭分明,我们还是选择在这里(即 Object 类的说明中)记载这些方法。”什么 __id__ 啊,clone 啊,equal? 啊,都是在 Kernel 中定义的私有实例方法。证明:- p Kernel.public_instance_methods(false)
复制代码 输出了所有 Kernel 本身定义的公有实例方法,而这些方法都是我们平时耳熟能详的所谓的“Object 实例方法”!Object 类建立时通过 include 方法混入了 Kernel 模块的所有实例方法,所以每当我们让某个对象调用 Object 的实例方法时,解释器实际上在 Object 中根本找不到该方法,而是通过 Object 的继承图表路径搜索父类,发现 Object 包含了 Kernel 模块,于是搜索到 Kernel 模块,终于在 Kernel 模块中发现了相应的方法。
那么我们用得更多的那些来自 Kernel 的 p、print、exit 等方法呢?
- p Kernel.private_instance_methods(false) # Kernel 定义的私有实例方法
- p Kernel.singleton_methods(false) # Kernel 定义的模块方法
复制代码 输出发现,原来这些函数同时是 Kernel 的模块方法和私有实例方法。按照常理来想,这些函数都是有关 Ruby 程序核心的一些重要方法,它们应该有全局和静态的特性,所以把他们写为 Kernel 的模块方法再合适不过。而这里 Kernel 把所有这些静态的方法都复制了一份,弄成了私有的实例模块方法,想来纯粹是为了让 Object 类包含 Kernel 时能够一并包含 Kernel 的这些特殊方法(因为包含一个模块并不能包含它的模块方法,即属于该模块本身这个对象的单例方法),这样它们同时就变成了 Object 的私有实例方法,那么只要不指定接受者,程序员就可以在 Ruby 的任何上下文中访问这些函数了,因为不显示指定接受者就会隐式指定 self 为接受者,而 self 永远都是 Object 类型,这样就能够访问 Object 的私有实例方法了。
由于 p 是 Object 通过包含 Kernel 而得到的私有实例方法,当你像 self.p "hello" 这样直接指定了 self 为接受者时,不管 self 是什么,Ruby 都会认为调用者对 p 的调用是来自 Object 外部,于是发生了越权访问的错误。
以上都是回答楼主问题而必须的一些理论,现在开始才是真正的解答——
首先楼主的第一句话完全正确。
但是,“以前有人说直接def exit(a)想重定义右上角的"×",但是最好把重定义的内容前加上module Kernel,因为如果有人比较严谨的话,可能会用Kernel.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。
至于顶层的常量和局部变量,实际上就相当于在 Object 这个类的上下文中的常量和局部变量,局部变量在类定义结束后生命结束,所以不能够再访问;常量则会一直保存,所以像——这样的常量,就相当于全局的常量了,因为它们是在 Object 类的上下文中定义的,而这个上下文是所有 Ruby 上下文的老大,所以在任何地方都能够访问~
最后再给一个例子:- 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 本身重定义,覆盖掉了…… |
|