参考书籍《Ruby 元编程(第二版)》
Ruby 版本:书上使用的是 2.x,自己使用的 3.1.2
# 打开类
Ruby 中的 class 关键字只有在第一次使用时是作为类型声明语句使用,用于定义一个目前不存在的类。
对于已经存在的类,class 关键字的作用更像是一个作用域操作符,把你带到类的上下文中,让你可以在里面定义方法。
这种重新打开已有类并对之进行动态修改的方式就称为打开类。
在 Ruby 中标准库中的类也是可以重新打开的。
- 打开类的实例
# monetize 是 Ruby 中一个管理资金和现金的工具类 | |
# 没有安装过这个包的话可以使用 gem install 命名安装 | |
require "monetize" | |
# 创建 Money 对象 | |
bargain_price = Monetize.from_numeric(99, "USD") | |
bargain_price.format | |
# 这个类库还提供了一种便捷形式: | |
# 可以通过 Numeric#to_money 方法,把任意数值转为一个 Money 对象 | |
standard_price = 100.to_money("USD") | |
standard_price.format |
monetize 包就是通过打开类的方式,对 Numeric 这个 Ruby 标准类进行扩展的。
在 gems/monetize-1.12.0/lib/monetize/core_extensions/numberic.rb 中可以找到如下代码
class Numeric | |
def to_money(currency = nil) | |
Monetize.from_numeric(self, currency || Money.default_currency) | |
end | |
end |
- 打开类的问题
对标准库类来说,虽然使用打开类的情况是常见的,但是要注意在打开类时不要定义和类库中同名的方法。
# 假如你打开了 Array 类并定义 replace 方法,用于对数组中的元素进行替换 | |
def Array | |
def replace(original, replacement) | |
self.map { |e| e == original ? replcement : e } | |
end | |
end |
因为 Array 类中原来就有 replace 方法,在打开类中定义 replace 方法时,就会覆盖原有的 replace 方法。
可能导致依赖原有 replace 方法的一些库、代码块出现异常或错误。
# 实例变量、方法
和 Java 这样的静态语言不同,Ruby 中对象的类和它的实例变量没有关系。
当对象调用方法并给实例变量赋值时,才会产生在对象中。因此,同一个类创建的不同对象可能会有不同的实例变量。
# Ruby 中实例变量以 "@" 符号开头 | |
# initialize 方法作用类似于 Java 类中的构造方法,只能显示定义一次,不能像 Java 中进行方法重载 | |
class MyClass | |
@filed1 = 1 | |
def initialize | |
@filed2 | |
end | |
end | |
obj = MyClass.new | |
p (obj.instance_variables) #=> [] | |
class MyClass2 | |
@filed1 = 1 | |
def initialize | |
@field2 = 1 # 产生实例变量 @filed2 | |
end | |
end | |
obj2 = MyClass2.new | |
p (obj2.instance_variables) #=> [:@field2] |
除了实例变量外,对象还有方法。通过 Oject#methods 方法,可以获得一个对象的方法列表。
但实际上,一个对象仅包含它的实例变量和其所属类的引用。而方法是存在对象的类中,这也是同一个类的对象共享相同方法的原因。
+------------+ class +-------------+ | |
| obj +-------->| SomeClass | | |
+------------+ +-------------+ | |
| | | | | |
| @filed = 1 | | method1 | | |
| | | | | |
| .... | | ... | | |
| | | | | |
+------------+ | | | |
+-------------+ |
# 类的本质
# 类是 Class 的对象
类本身也是对象,类像其他对象一样,也有自己的类,叫做 Class。
p "hello".class #=> String | |
p String.class #=> Class |
把类看作 Class 的对象的话,那么一个类中的方法就是 Class 的实例方法。
# false 表示忽略继承的方法 | |
p Class.instance_methods(false) #=> [:allocate, :superclass, :subclasses, :new] |
Ruby 中除 BasicObject 这个最顶层类没有父类以外,其他类都有自己的父类,像 Array 类的父类是 Object 类,Object 类的父类是 BasicObject 类。
而 Class 的父类是 Module。换而言之,每个类都是一个模块。
如果希望代码包含(include)到别的代码中,就使用模块(可以用来充当命名空间);如果希望代码被实例化或者被继承,就使用类。
类就是一个对象(Class 类的一个实例)外加一组实例方法和一个对其父类的引用
注意:Class 类的类是它本身
# 类名和常量
和普通对象一样,类也可以通过引用来访问。变量可以像引用普通对象一样引用类。
class MyClass | |
end | |
# my_class 也是 Class 实例的一个引用 | |
my_class = MyClass | |
obj = my_class.new |
MyClass 和 my_class 都是对同一个 Class 类的实例的引用,唯一的区别在于,my_class 是变量,而 MyClass 是一个常量。
Ruby 中任何以大写字母开头的引用(包括模块名和类名)都是常量。
常量可以通过路径来标识。类似于 C++ 中的域操作符,Ruby 中常量的路径使用双冒号进行分隔。
module M | |
class C | |
X = 'a content' | |
end | |
C::X #=> 'a content' | |
end | |
M::C::X #=> 'a content' |
双冒号开头可以表示路径的根位置
Outer = 'a root-level constant' | |
module Y | |
Inner = 'a constant in Y' | |
p Inner #=> 'a constant in Y' | |
p ::Outer #=> 'a root-level constant' | |
end |
对象模型图 | |
+-------------+ | |
| | | |
| BasicObject | | |
+-------------+ class | |
| +------------------------------------+ | |
| ... | | | |
| | | | |
+-------------+ | | |
^ | | |
| | | |
| superclass | | |
| | | |
+------+------+ +-------------+ | | |
| | | | | | |
| Object +------+ | Module | | | |
+-------------+ | +-------------+ class | | |
| | | | +-------+ | | |
| ... | | | ... | | | | |
| | | | | | | | |
+-------------+ | +-------------+ | | | |
^ | ^ | | | |
| | | | | | |
| superclass | | superclass | | | |
+--------+ | | | | | | |
| obj1 | +------+------+ | +------+-------+ | | | |
| +--+ | | +--->| | | | | |
+--------+ | class | | | |<-----+ | | |
+--------->| MyClass | class | Class | | | |
+-------------+---------->+--------------+ | | |
+--------+ +--------->| ... | | ... | | | |
| obj2 | | class | | | |<--------+ | |
| +--+ +-------------+ +-----------+--+ | |
+--------+ ^ | | |
| | | |
+------+ | |
class |
# 方法查找
方法查找相关的两个概念:
- 接收者(receiver)
接收者就是调用方法所在的对象。比如,my_string.reverse () 语句中,my_string 就是接收者。
- 祖先链(ancestors chain)
从当前类开始向上查找超类,直到找到 BasicObject 类为止,所经过的类路径就称为该类的祖先链
方法查找的过程:Ruby 首先在接收者的类中查找,然后再顺着祖先链向上查找,直到找到这个方法为止,如果找到 BasicObject 还没有找到会报错(NoMethodError)
祖先链中也包括模块。当一个模块包含在一个类(或一个模块)中时,Ruby 会把这个模块加入该类的祖先链中。可以使用 include 和 prepend 将模块插入到一个类的祖先链中,include 在祖先链的插入位置是当前类的上方,prepend 则是当前类的下方。可以通过 ancestors 方法获得类的祖先链
# 定义一个模块 M1 | |
module M1 | |
end | |
# 定义一个类 C1,它包含 M1 | |
class C1 | |
include M1 | |
end | |
# 定一个类 C2,它也包含 M1 | |
class C2 | |
prepend M1 | |
end | |
# 打印 C1、C2 的祖先链 | |
p C1.ancestors #=> [C1, M1, Object, PP::ObjectMixin, Kernel, BasicObject] | |
p C2.ancestors #=> [M1, C2, Object, PP::ObjectMixin, Kernel, BasicObject] |
在 Ruby3 中 prepend 一个模块时,它总会加入到祖先链中,即使它在祖先链中已经出现过;而 include 一个模块时,它只会加入到祖先链中一次,如果已有,就不会再加入了。
在 Ruby2 中 prepend 和 include 都只会加入模块一次