这是完整的故事,解释了必要的元编程概念,这些概念理解了为什么模块包含在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
,因此当我们调用foo
的实例时C
,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
,它的作用相同-即在对象的singleton类中包含一个模块:
class HostKlass
include M
extend M::ClassMethods
end
HostKlass.singleton_class.included_modules
#=> [M::ClassMethods, Kernel]
# ^ there it is!
将extend
呼叫移至模块中
前面的示例不是结构良好的代码,有两个原因:
- 我们现在必须调用都
include
和extend
在HostClass
定义中,以正确包括我们的模块。如果您必须包含许多类似的模块,则可能会非常麻烦。
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