这是完整的故事,解释了理解为什么模块包含在 Ruby 中的工作方式所需的元编程概念。
包含模块时会发生什么?
将模块包含到类中会将模块添加到类的祖先中。您可以通过调用其ancestors 方法查看任何类或模块的祖先:
module M
def foo; "foo"; end
end
class C
include M
def bar; "bar"; end
end
C.ancestors
#=> [C, M, Object, Kernel, BasicObject]
# ^ look, it's right here!
当您在C 的实例上调用方法时,Ruby 将查看此祖先列表的每一项,以便找到具有提供名称的实例方法。由于我们将M 包含在C 中,M 现在是C 的祖先,所以当我们在C 的实例上调用foo 时,Ruby 会在M 中找到该方法:
C.new.foo
#=> "foo"
请注意,包含不会将任何实例或类方法复制到类中——它只是向类添加一个“注释”,它还应该在包含的模块中查找实例方法。
我们模块中的“类”方法呢?
因为包含只会改变派发实例方法的方式,将模块包含到类中只会使其实例方法在该类上可用。模块中的“类”方法和其他声明不会自动复制到类中:
module M
def instance_method
"foo"
end
def self.class_method
"bar"
end
end
class C
include M
end
M.class_method
#=> "bar"
C.new.instance_method
#=> "foo"
C.class_method
#=> NoMethodError: undefined method `class_method' for C:Class
Ruby 是如何实现类方法的?
在 Ruby 中,类和模块是普通对象——它们是类 Class 和 Module 的实例。这意味着您可以动态创建新类,将它们分配给变量等:
klass = Class.new do
def foo
"foo"
end
end
#=> #<Class:0x2b613d0>
klass.new.foo
#=> "foo"
同样在 Ruby 中,您可以在对象上定义所谓的单例方法。这些方法作为新的实例方法添加到对象的特殊隐藏单例类中:
obj = Object.new
# define singleton method
def obj.foo
"foo"
end
# here is our singleton method, on the singleton class of `obj`:
obj.singleton_class.instance_methods(false)
#=> [:foo]
但是类和模块不也只是普通的对象吗?事实上他们是!这是否意味着他们也可以有单例方法?是的,它确实!这就是类方法的诞生方式:
class Abc
end
# define singleton method
def Abc.foo
"foo"
end
Abc.singleton_class.instance_methods(false)
#=> [:foo]
或者,更常见的定义类方法的方法是在类定义块中使用self,它指的是正在创建的类对象:
class Abc
def self.foo
"foo"
end
end
Abc.singleton_class.instance_methods(false)
#=> [:foo]
如何在模块中包含类方法?
正如我们刚刚建立的,类方法实际上只是类对象的单例类上的实例方法。这是否意味着我们可以在单例类中包含一个模块来添加一堆类方法?是的,确实如此!
module M
def new_instance_method; "hi"; end
module ClassMethods
def new_class_method; "hello"; end
end
end
class HostKlass
include M
self.singleton_class.include M::ClassMethods
end
HostKlass.new_class_method
#=> "hello"
self.singleton_class.include M::ClassMethods 这行看起来不太好,所以 Ruby 添加了 Object#extend,它的作用相同——即在对象的单例类中包含一个模块:
class HostKlass
include M
extend M::ClassMethods
end
HostKlass.singleton_class.included_modules
#=> [M::ClassMethods, Kernel]
# ^ there it is!
将extend 调用移动到模块中
前面的示例不是结构良好的代码,原因有两个:
- 我们现在必须在
HostClass 定义中调用 both include 和 extend 才能正确包含我们的模块。如果您必须包含许多类似的模块,这可能会变得非常麻烦。
-
HostClass 直接引用M::ClassMethods,这是M 模块的实现细节,HostClass 不需要知道或关心。
那么怎么样:当我们在第一行调用include 时,我们以某种方式通知模块它已被包含,并且还给它我们的类对象,以便它可以调用extend 本身。这样,模块的工作就是根据需要添加类方法。
这正是 特殊的self.included 方法 的用途。每当模块被包含到另一个类(或模块)中时,Ruby 会自动调用此方法,并将宿主类对象作为第一个参数传入:
module M
def new_instance_method; "hi"; end
def self.included(base) # `base` is `HostClass` in our case
base.extend ClassMethods
end
module ClassMethods
def new_class_method; "hello"; end
end
end
class HostKlass
include M
def self.existing_class_method; "cool"; end
end
HostKlass.singleton_class.included_modules
#=> [M::ClassMethods, Kernel]
# ^ still there!
当然,添加类方法并不是我们在self.included 中唯一可以做的事情。我们有类对象,所以我们可以在它上面调用任何其他(类)方法:
def self.included(base) # `base` is `HostClass` in our case
base.existing_class_method
#=> "cool"
end