Ruby kernel 中的 puts 方法是模块实例方法还是模块方法

runup · 2015年12月17日 · 最后由 Evilcrow 回复于 2018年02月09日 · 4074 次阅读

问题:kernel 中的 puts 方法是模块实例方法还是模块方法(是否存在模块实例方法和模块方法这种区别)?Kernel中有下面的一句话:

The Kernel instance methods are documented in class Object while the module methods are documented here.

是否说明本篇文档罗列的方法都是模块方法,比如说该文档中的 puts 方法,可以通过 kernel.puts 实现的,但是在文档左侧的列表中出现的标示符号都是"#"符号,这应该是模块实例方法的标示符,如果是模块方法,应该使用“::”,见文档所示。官方文档都不会出错,能不能帮忙指出我的理解错误。

Ruby 中有一些方法(如 print)可以随时随地进行调用。看起来就像所有的对象都有 print 方法一样。这是因为这些方法实际上都是 Kernel 模块的私有实例方法:

Kernel.private_instance_methods.grep(/^pr/) # => [:printf, :print, :proc]

这里的秘密在于 Object 类包含了 Kernel 模块,因此 Kernel 就进入了每个对象的祖先链。于是,无论哪个对象都可以随意调用 Kernel 模块的方法。这使得 print 看起来就像一个关键字,其实它只是一个方法而已。

-----摘自《Ruby 元编程第 2 版》

我自己有点钻牛角尖了,不多还是想把细节搞清楚,看了原书的内容。下面有几个小问题。 问题 1:任何对象都可以调用 Kernel 模块的方法,但是下面的 obj 对象调用会报错。

class KernelDemo
end

obj = KernelDemo.new
obj.puts("this is the Kernel method")  #报错private method puts

问题 2:我构造了如下的代码,在模块中定义 private method, 并且 include 到类中,并且通过子类继承该类(类似于 Kernel、Object 和普通类的关系),但是在子类中调用 private method 会报错。

module KernelMdoule
  def cc
    puts "this is the private method"
  end
  private : cc 
end

class FatherClass
  include KernelModule
end

class SonClass < FatherClass
  cc
end

问题 3:官方文档 中所有的方法都是标为 public instance method,但是通过在 irb 中输入如下 Kernel.private_instance_methods 可以得知本页中的所有方法都是 private instance method.而

Kernel.public_instance_methods.grep(/send/) #=>[:send, :public_send]

可以得知 send 方法是 Kernel 的公有实例方法,因为 Kernel 被引入到类 object 中,因此可以像普通公开实例方法一样被对象调用,而 puts 是私有实例方法,可以像关键词一样调用。如果是这样,那么文档中的表述是否有问题:

The Kernel instance methods are documented in class Object while the module methods are documented here. These methods are called without a receiver and thus can be called in functional form:

#1 楼 @qinfanpeng 谢谢您的解答,能不能帮我看看我的理解和疑问,非常感谢。

#3 楼 @runup 问题 1: 私有方法不可以显式调用。 问题 2: 子类这种写法相当于执行父类的 self.cc 也就是 Father.cc. 问题 3 还没看。

#2 楼 @runup 第一,你的代码里到处都是 TYPO。 第二, 私有方法你只能在这个类或者其子类的实例方法里调用,而且不能指定调用对象。

@runup

我试着解答一下:

问题 1:

obj.puts("this is the Kernel method") #报错 private method puts

puts 是 private instance method。private 的意思就是不能显示地指定 receiver object。你在这里显式指定 obj 为 receiver,当然会报错啦。

问题 2: 你这里笔误了吧。class SonClass < KernelClass 应该是 class SonClass < FatherClass 才对。 而且,你调用 cc 方法的时候,receiver 是 SonClass,而不是 SonClass 的实例,当然获取不到实例方法了。

问题 3: Ruby 文档的确有问题

#3 楼 @runup 其他两个问题,其他小伙伴解释得很清楚了;第三个问题,你给的链接里都没内容,这得基于上下文来说。

#6 楼 @fighterleslie 非常感谢。问题二中代码的错误已经进行了修改。给出问题二的代码是为了和 Kernel 中的 puts 方法做比较,比如如下的代码:

puts "method one"  #当前对象是main

class KernelMethod
  puts "method two"  #当前对象是class KernelMethod

  def method_three
    puts "method three"  #当前对象是obj
  end
end

obj = KernelMethod.new
obj.method_threee

结合上面的小伙伴的解释,补充了私有方法不能显式调用的知识,但是在类中进行调用 puts 方法(puts "method two"),与实例方法(puts 是 private instance method)只能被实例化对象调用相违背。在该例中,puts "method two"方法可以实现,但是在我给出的问题 2 中类似的方法是不能实现的。 我应该存在理解上的误区或者知识漏洞,感谢指正。

#7 楼 @qinfanpeng 链接已经可以访问了

#8 楼 @runup

但是在类中进行调用 puts 方法(puts "method two"),与实例方法(puts 是 private instance method)只能被实例化对象调用相违背。

Good question. 你的疑惑是为什么在 KernelMethod 这个类上可以调用 puts,因为 KernelMethod 这个类是 Class 类的实例对象(Ruby 中,一切值都是对象。)

KernelMethod.class  # => Class
KernelMethod.instance_of?(Class)  # => true

The Kernel module is included by class Object, so its methods are available in every Ruby object. The Kernel instance methods are documented in class Object while the module methods are documented here. These methods are called without a receiver and thus can be called in functional form:

你是感觉其中的the module methods较为迷惑对吧?我猜测它说的是module_function, 若这样的话,它干了两件事情:首先,把紧跟其后的方法都标注成了私有方法; 其次,让紧跟其后的方法可以单例方式访问。

例如:

print 'Hello world'  # 毫无疑问,这样可以
Kernel.print 'Hello world'  # 这也可以。但如果 print 只是Kernel里一个普通的 private 方法的话,是不能这样玩的。

#10 楼 @fighterleslie 感谢解答,您的解答我也考虑过,同理参考问题 2 中的例子:

module KernelMdoule
  def cc
    puts "this is the private method"
  end
  private : cc 
end

class FatherClass
  include KernelMdoule
end

class SonClass < FatherClass
  cc
end

这样的写法会报错,原因为 undefined local variable or method,您的解释为:

你调用 cc 方法的时候receiver  SonClass而不是 SonClass 的实例当然获取不到实例方法了

但是其实 SonClass 也是一个对象,是 Class 的实例化对象:

puts SonClass.class  #Class
puts SonClass.instance_of?(Class) #true

在我的理解里面,问题二中的代码

class SonClass < FatherClass
  cc
end

class SonClass < FatherClass
  puts this is an private instance method 
end

的实现形式是一致的,我在问题二的代码中也实现在 module 中构造私有实例方法,并且 include 到父类中,并且在子类中直接进行调用,这和 Object、Kernel 以及其他 Class 的实例化对象(即普通类)实现方式类似,但是实现的结果却是有错的,这是我的困惑所在,也是我的理解上的误区,能不能帮忙指正一下,感谢。

#13 楼 @runup

简单的说,虽然 putscc 都是 private method,但是 puts 是定义在 Kernel 模块中,cc 方法是定义在 KernelModule 模块中。Kernel 是一个特殊的模块,它和你自己定义的 KernelModule 模块有一些行为上的不同。

详细地说,这涉及两方面的知识:

  1. Ruby 的对象体系
  2. 方法的查询机制
module KernelModule
  def cc
    puts "this is the private method"
  end
  private : cc 
end

class FatherClass
  include KernelMdoule
end

class SonClass < FatherClass
  cc
end

SonClass.ancestors  # => [SonClass, FatherClass, KernelModule, Object, Kernel, BasicObject]
SonClass.class.ancestors # => [Class, Module, Object, Kernel, BasicObject]

Kernel 模块已经在 Object 模块中 include,而几乎所有的 Ruby 类都是直接或间接地继承自 Object,所以 Kernel 模块会出现在所有类的 ancestors 列表中。当在 receiver 上调用 cc 方法时,Ruby 解释器会沿 receiver.class.ancestors 逐级查找是否有 cc 的定义,如果找到,会调用 cc 方法,同时不再向上查找;如果遍历 ancestors 而没有找到,则会在 receiver 上调用 method_missing 方法,一般情况下会抛出 NoMethodError 错误。

#14 楼 @fighterleslie 终于把这个知识点搞的有点明白了,感激,作为周星驰的粉丝,很喜欢你的头像。

#12 楼 @qinfanpeng 认真看了 module_function 的方法解释,实现了您提到的两个功能,一是作为 private instance method 方法被调用,二是可以直接用模块名进行调用。但是 send 方法为何可以被 Kernel 模块名调用,也可以直接通过对象对用,如下代码:

Kernel.public_instance_methods.grep(/send/) # [:send, :public_send]

可以知道 send 方法是 Kernel 的 public instance method 方法,那么这里又是什么原理或者方法,使得其可以通过两种方式调用,即可以通过对象调用,也可以被 Kernel 调用。

另外请教前辈,这些知识点您是怎么积累的?

#16 楼 @runup 由你的疑问可以看出sendKernel上的public_instance_methods,那么send只能通过对象调用。然而又确实可以这样调用:Kernel.send(:object_id),那只能说明Kernel也是对象,和 module_methods 那种可单例方式调用形式是不一样的。

Kernel.is_a?(Object)          # => true
Kernel.object_id                 # => 70359638759860 可能不同

确实不敢当前辈,也是新手,以你的钻研劲头,何愁大事不成。《Ruby 元编程第 2 版》还是可以看下的。

#17 楼 @qinfanpeng 感激,在看第一版中文的,理解好之后再啃第二版的。

runup 回复

我想说说一点我的看法,我也是新手。

cc 方法是在 KernelModule 模块中的实例方法,include 进入 FatherClass 后

因为 Mix-in 的关系,会变成 FatherClass 的实例方法,SonClass 继承之后,成为子类的实例方法

而类中是不能直接调用实例方法的

所以,直接在类中 cc 会报错的

class SonClass < FatherClass
  self.new.cc
end

是可以调用的,但是现在还有一个问题,就是在 module 中将 cc 声明为私有方法

所以,即使这样,调用 (访问) 还是失败的,去掉 private,就可以使用了

有一点要纠正的是,

但是其实 SonClass 也是一个对象,是 Class 的实例化对象:

这句话没问题,但是,cc 并不是 Class 中的实例方法,即使 SonClass 理解为 Class 类的实例对象,也不能调用此方法

而且,SonClass 并未继承 Class 类,其继承的是 Object 类,所以如果这样搞

class << Object
  include KernelModule
end

就可以直接在类中 cc 了,当然 private 也是不行的

就像@fighterleslie 说的,从继承机制上可以看出来

最后,看了一眼帖子的时间,挖了一个坟帖,抱歉了

只是进行问题的交流,并无其他恶意

需要 登录 后方可回复, 如果你还没有账号请 注册新账号