我为什么能够使用内核单例方法如 `puts`?

7
在Ruby中,puts方法是Kernel模块的单例方法。
通常情况下,当一个模块被其他模块includeextend时,该模块(但不包括其单例类)会被添加到继承树中。这样实际上将该模块的实例方法可用于该模块或它的单例类(对于includeextend分别处理)......但混合模块的单例方法仍然无法访问,因为模块的单例类从未被添加到继承树中。
那么我为什么能使用puts(以及其他Kernel单例方法)?
Kernel.singleton_methods(false)

# => [:caller_locations, :local_variables, :require, :require_relative, :autoload, :sprintf, :format, :Integer, :Float, :String, :Array, :Hash, :test, :warn, :autoload?, :fork, :binding, :exit, :raise, :fail, :global_variables, :__method__, :__callee__, :__dir__, :URI, :eval, :iterator?, :block_given?, :catch, :throw, :loop, :gets, :sleep, :proc, :lambda, :trace_var, :untrace_var, :at_exit, :load, :Rational, :select, :Complex, :syscall, :open, :printf, :print, :putc, :puts, :readline, :readlines, :`, :p, :system, :spawn, :exec, :exit!, :abort, :set_trace_func, :rand, :srand, :trap, :caller]

请注意,puts似乎不是Kernel上的实例方法:

Kernel.instance_methods.grep(/puts/)

# []

虽然 Object 包括 Kernel

Object.included_modules

# [Kernel]

据我所知,Kernel 的单例类 (#<Class:Kernel>) 没有出现在任何对象的祖先链中。 is_a? 也没有显示包含它:
Object.is_a? Class.singleton_class # false
Object.is_a? Kernel.singleton_class # false

Object.singleton_class.is_a? Class.singleton_class # true
Object.singleton_class.is_a? Kernel.singleton_class # false

然而,由于某种原因,它们对于每个对象都显示为私有方法。

Object.puts "asdf"

# NoMethodError (private method `puts' called for Object:Class)

如果#<Class:Kernel>在祖先链中没有显示,那么方法查找如何找到这些方法呢?
相关内容:
3个回答

5
你正在错误的地方查找。

Kernel#Array, Kernel#Complex, Kernel#Float, Kernel#Hash, Kernel#Integer, Kernel#Rational, Kernel#String, Kernel#__callee__, Kernel#__dir__, Kernel#__method__, Kernel#`, Kernel#abort, Kernel#at_exit, Kernel#autoload, Kernel#autoload?, Kernel#binding, Kernel#block_given?, Kernel#callcc, Kernel#caller, Kernel#caller_locations, Kernel#catch, Kernel#eval, Kernel#exec, Kernel#exit, Kernel#exit!, Kernel#fail, Kernel#fork, Kernel#format, Kernel#gets, Kernel#global_variables, Kernel#initialize_clone, Kernel#initialize_copy, Kernel#initialize_dup, Kernel#iterator?, Kernel#lambda, Kernel#load, Kernel#local_variables, Kernel#loop, Kernel#open, Kernel#p, Kernel#pp, Kernel#print, Kernel#printf, Kernel#proc, Kernel#putc, Kernel#puts, Kernel#raise, Kernel#rand, Kernel#readline, Kernel#readlines, Kernel#require, Kernel#require_relative, Kernel#select, Kernel#set_trace_func, Kernel#sleep, Kernel#spawn, Kernel#sprintf, Kernel#srand, Kernel#syscall, Kernel#system, Kernel#test, Kernel#throw, Kernel#trace_var, Kernel#trap, Kernel#untrace_var, 和 Kernel#warn 这些方法与它们的接收者没有任何有用的关系。它们不调用私有方法,也不访问实例变量,它们实际上完全忽略了 self 是什么。

因此,如果您这样称呼它们,那将是误导的:
foo.puts 'Hello, World!'

因为读者可能会误解putsfoo有关,实际上它完全忽略了它。(这尤其适用于打印方法族,因为还存在IO#puts和其他方法,它们确实关心它们的接收者。)
因此,为了防止您使用接收者误导地调用这些方法,它们被设置为private,这意味着只能在没有显式接收者的情况下调用它们。(显然,它们仍然会在self上调用,但至少在视觉上不会那么明显。)
从技术上讲,这些并不是真正的方法,它们更像是过程,但Ruby没有过程,所以这是“伪造”它们的最佳方式。
它们也被定义为singleton methods的原因是,这样您仍然可以在没有Kernel在继承层次结构中的情况下调用它们,例如像这样的情况:
class Foo < BasicObject
  def works
    ::Kernel.puts 'Hello, World!'
  end

  def doesnt
    puts 'Hello, World!'
  end
end

f = Foo.new

f.works
# Hello, World!

f.doesnt
# NoMethodError: undefined method `puts' for #<Foo:0x00007f97cf918ed0>

而它们需要分别定义的原因是实例方法版本是“私有的”。如果它们不是私有的,那么你就可以直接调用“Kernel.puts”,因为“Object”包括“Kernel”,而“Kernel”是“Module”的实例,它是“Object”的子类,因此“Kernel”是自身的间接实例。然而,这些方法是“private”的,因此你将得到一个。
NoMethodError: private method `puts' called for Kernel:Module

因此,它们需要分别复制。实际上有一个辅助方法可以做到这一点: Module#module_function。(这也适用于Math,在这里您可以调用例如Math.sqrt(4)include Math; sqrt(4)。在这种情况下,您可以选择include或不包括 Math ,而Kernel始终预先includeObject中。)

因此,总结一下:这些方法被复制为Kernel私有实例方法以及public单例方法(实际上只是Kernel单例类实例方法)。它们被定义为private实例方法的原因是不能使用显式接收者调用它们,并且强制它们看起来更像过程。它们作为Kernel的单例方法被复制的原因是可以在明确接收者为Kernel的情况下调用它们,在继承层次结构中没有Kernel可用的情况下。
#ruby --disable-gems --disable-did_you_mean -e'puts Kernel.private_instance_methods(false).sort'
Array
Complex
Float
Hash
Integer
Rational
String
__callee__
__dir__
__method__
`
abort
at_exit
autoload
autoload?
binding
block_given?
caller
caller_locations
catch
eval
exec
exit
exit!
fail
fork
format
gets
global_variables
initialize_clone
initialize_copy
initialize_dup
iterator?
lambda
load
local_variables
loop
open
p
pp
print
printf
proc
putc
puts
raise
rand
readline
readlines
require
require_relative
respond_to_missing?
select
set_trace_func
sleep
spawn
sprintf
srand
syscall
system
test
throw
trace_var
trap
untrace_var
warn

3
"如果祖先链中没有#,方法查找如何找到这些方法呢?"
 1.class.included_modules #  => [Comparable, Kernel] 

引用OP的话:

Kernel.instance_methods.grep(/puts/) # []

正如你自己发现的那样,私有实例方法不会显示出来,可以使用Kernel.private_instance_methods


是的,Object 包括 Kernel 模块,但 puts 等方法不是 Kernel 上的方法 - 它们是 Kernel 的单例类上的方法,因此不应该对 Object 可用。 - rintaun
尝试这个:module M; def self.m; end end Object.include M Object.included_modules # => [M, Kernel] Object.m # NoMethodError (undefined method 'm' for Object:Class) - rintaun
1
我认为 Kernel 的工作方式如下:module M; private; def m; end; end; Object.include M - steenslag
啊哈……原来#instance_methods不包括私有方法。我之前没有意识到这一点。Kernel.private_instance_methods确实包括#puts和其他私有方法。如果你更新你的回答并解释清楚,我会接受它的。谢谢! - rintaun

2
事实证明,我问错了问题。为什么我能使用Kernel的单例方法,如puts?答案是:你不能。
Kernel的单例方法,就像模块上的所有其他单例方法一样,是不可继承的。诀窍在于它们不是单例方法,而是模块函数。
在Ruby中创建一个模块函数会创建两个方法副本:一个单例方法和一个私有实例方法。这就是为什么Kernel.singleton_method(:puts)和Kernel.instance_method(:puts)都有效的原因。
因此,由于Object包含Kernel,它可以访问其实例方法,包括puts。
我犯了一个错误,使用#instance_methods只显示公共实例方法。要查看私有方法,我需要使用#private_instance_methods,例如:
Kernel.private_instance_methods(false).grep(/puts/)

# [:puts]

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接