模块
是 Ruby的特色功能之一. 如果说类表现的是实物的实体(数据)及其行为(处理), 那么模块表现的就只是事物的行为部分. 模块与类有以下两点不同:
- 模块不能拥有实例
- 模块不能被继承
利用 Mix-in 扩展功能
Mix-in 就是将模块混合到类中. 在类的定义中, 使用 include, 模块中的方法, 常量就都能被类使用.
如下, 我们把 MyClass1 和 MyClass2共通的功能定义在 MyModule 中. 虽然有点类似于类的继承, 但 Mix-in 可以更加灵活地解决下面的问题.
- 虽然两个类拥有相似的功能, 但是不希望把他们作为相同的种类(Class)来考虑
- Ruby 不支持父类的多重继承, 无法继承多个类
module MyModule
# 共通的方法
end
class MyClass1
include MyModule
# MyModule 中独有的方法
end
class MyClass2
include MyModule
# MyModule 中独有的方法
end
提供命名空间
所谓命名空间( namespace ), 就是对方法, 常量, 类等名称进行区分及管理的单位. 由于模块提供各自独立的命名空间, 因此 A 模块中的 foo 方法与 B 模块中的 foo 方法就会被程序认为两个不同的方法. 同样, A模块中的 FOO 常量与B模块中的 FOO 常量也是两个不同的常量.
我们使用模块名.方法名
的形式调用模块中定义的方法, 这一的方法称为模块函数
.
A.foo
B.foo
A.FOO
B.FOO
另外, 如果没有定义与模块内的方法, 常量等同名的名称, 那么引用时就可以省略模块名.
创建模块
我们使用 module 语句来创建模块. 语法与创建类时几乎相同, 模块名的首字母必须大写.
module 模块名
模块定义
end
示例:
module HelloModule
Version = '1.0' # 定义常量
def hello(name) # 定义方法
puts "Hello, #{name}."
end
module_function :hello # 指定 hello 方法为模块函数, 为模块私有
end
p HelloModule::Version #=> "1.0"
HelloModule.hello("San") #=> Hello, San.
include HelloModule # 包含模块
p Version #=> "1.0"
hello("Jiang") #=> Hello, Jiang.
常量
和类一样, 在模块中定义的常量可以通过模块名访问.
p HelloModule::Version #=> "1.0"
方法的定义
和类一样, 我们也可以在 module 语句中定义方法.
然而, 如果只定义了方法, 虽然在模块内部与包含此模块的语句中都可以直接调用, 但却不能以"模块名.方法名"的形式调用. 如果希望把方法作为模块函数公开给外部使用, 就需要用到 module_function 方法. module_function 的参数是表示方法名的符号.
def hello(name)
puts "Hello, #{name}."
end
module_function :hello
以"模块名.方法名"的形式调用时, 如果在方法中调用self(接收者), 就会获得该模块的对象.
module FooModule
def foo
p self
end
module_function :foo
end
FooModule.foo #=> FooModule
此外, 如果类 Mix-in 了模块, 就相当于为该类添加了实例方法. 在这种情况下, self 代表的就是被 Mix-in 的类的对象.
即使是相同的方法, 在不同的上下文调用时, 其含义也会不一样, 因此对于 Mix-in 的模块, 我们要注意根据实际情况判断是否使用模块函数功能. 一般不建议在定义为模块函数的方法中使用self.
Mix-in
我们来讨论一下Mix-in, 先看示例:
module M
def meth
"meth"
end
end
class C
include M # 包含 M 模块
end
p C.new.meth #=> "meth"
类C 包含模块M 后, 模块M 中定义的方法就可以作为类C 的示例方法供程序调用.
另外, 如果想知道类是否包含某个模块, 可以使用 include? 方法.
p C.include?(M) #=> true
类C 的实例在调用方法时, Ruby会按类C -> 模块M -> 类C的父类Object
这个顺序查找该方法, 并执行第一个找到的方法. 被包含的模块的作用就类似于虚拟的父类.
我们用 ancestors 方法和 superclass 方法调查类的继承关系.
p C.ancestors #=> [C, M, Object, Kernel, BasicObject]
p C.superclass #=> Object
通过 ancestors 可以获得继承关系的列表, 进而也可以看出, 被包含的模块M 也被认为是类C 的一个"祖先". 而 superclass 方法则直接返回类C 的父类.
备注: ancestors方法的返回值中的 Kernel 是Ruby内部的一个核心模块, Ruby 程序运行时所需的共通函数都封装在此模块中. 例如 p方法, raise方法 等都是由 Kernel 模块提供的模块函数.
虽然 Ruby采用的是不允许具有多个父类的单一继承模型, 但是通过利用 Mix-in, 就即可以保持单一继承的关系, 又可以同时让多个类共享功能.
在 Ruby标准类库中, Enumerable 模块就是利用 Mix-in 扩展功能的一个典型例子. 使用 each方法的类中包含 Enumerable 模块后, 就可以 each_with_index, collect等对元素进行排序处理的方法. Array, Hash, IO 类等都包含了 Enumerable 模块. 这些类虽然没有继承这样的血缘关系, 但是从可以使用 each 方法遍历元素这一点来看, 可以说他们都拥有了某种相似甚至相同的属性.

单一继承的优点就是简单, 不会因为过多的继承而导致类之间的关系变得复杂. 但是另一个方面, 有时我们又会希望更加积极地重用已有的类, 或者把多个类的特性合并为更高级的类, 在那样的情况下, 灵活使用单一继承和 Mix-in, 既能使类结构简单易懂, 又能灵活地应对各种需求.
查找方法的规则
我们来了解一下使用 Mix-in 时方法的查找顺序.
同继承关系一样, 原类中已经定义了同名的方法时, 优先使用该方法.
module M
def meth
"M#meth"
end
end
class C
include M # 包含M
def meth
"C#meth"
end
end
p C.new.meth #=> "C#meth"
在同一个类中包含多个模块时, 优先使用最后一个包含的模块
module M1
def meth
"M1#meth"
end
end
module M2
def meth
"M2#meth"
end
end
class C
include M1 # 包含M1
include M2 # 包含M2
end
p C.new.meth #=> "M2#meth"
嵌套 include 时, 查找顺序也是线性的, 此时的关系如下所示:
module M1
def meth
"M1#meth"
end
end
module M2
def meth
"M2#meth"
end
end
module M3
include M2 # 包含M2
end
class C
include M1 # 包含M1
include M3 # 包含M3
end
p C.ancestors #=> [C, M3, M2, M1, Object, Kernel, BasicObject]

相同的模块被包含两次以上时, 第二次以后的会被省略.
module M1
def meth
"M1#meth"
end
end
module M2
def meth
"M2#meth"
end
end
class C
include M1 # 包含M1
include M2 # 包含M2
include M1 # 包含M1
end
p C.ancestors #=> [C, M2, M1, Object, Kernel, BasicObject]
extend 方法
include 可以帮助我们突破继承的限制, 通过模块扩展类的功能; 而 extend 则可以帮助我们跨过类, 直接通过模块扩展对象的功能.
module Edition
def edition(n)
"#{self} 第#{n}版"
end
end
class MyStr < String
end
str = "Ruby基础教程 "
str.extend(Edition) # 将模块 Mix-in 进对象
p str.edition(5) #=> "Ruby基础教程 第5版"
MyStr.extend(Edition) # 所有的类本身都是 Class 类的对象.
p MyStr.edition(44) #=> "Ruby基础教程 第44版", 请继续往下看.
类与 Mix-in
在Ruby中, 所有类本身都是Class类的对象. 之前我们介绍过接收者为类本身的方法就是类方法. 也就是说, 类方法就是类对象的实例方法. 这样的方法有以下两种:
- Class 类的实例方法
- 类对象的单例方法
继承类后, 这些方法就会作为类方法被子类继承. 对子类定义单例方法, 实际上也就是定义新的类方法.
除了之前介绍的定义类方法语法外, 使用 extend 方法也同样能为类对象追加类方法.
module ClassMethods
def cmethod
"class method"
end
end
module InstanceMethods
def imethod
"instance method"
end
end
class MyClass
# 使用 extend 方法定义类方法
extend ClassMethods
# 使用 include 定义实例方法
include InstanceMethods
end
p MyClass.cmethod #=> "class method"
p MyClass.new.imethod #=> "instance method"
备注: 在 Ruby 中, 所有方法的执行, 都需要通过作为接收者的某个对象的调用. 换句话说, Ruby 的方法(包括单例方法)都一定属于某个类, 并且作为接收者的对象的实例方法被程序调用. 从这个角度来说, 人们只是为了便于识别接收者的类型, 才分别使用了"实例方法"和"类方法"这样的说法.
网友评论