参考书籍《Ruby 元编程(第二版)》
Ruby 版本:书上使用的是 2.x,自己使用的 3.1.2

# 打开类

Ruby 中的 class 关键字只有在第一次使用时是作为类型声明语句使用,用于定义一个目前不存在的类。
对于已经存在的类,class 关键字的作用更像是一个作用域操作符,把你带到类的上下文中,让你可以在里面定义方法。
这种重新打开已有类并对之进行动态修改的方式就称为打开类
在 Ruby 中标准库中的类也是可以重新打开的

  1. 打开类的实例
打开类
# 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
  1. 打开类的问题

对标准库类来说,虽然使用打开类的情况是常见的,但是要注意在打开类时不要定义和类库中同名的方法

# 假如你打开了 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

# 方法查找

方法查找相关的两个概念:

  1. 接收者(receiver)

接收者就是调用方法所在的对象。比如,my_string.reverse () 语句中,my_string 就是接收者。

  1. 祖先链(ancestors chain)

当前类开始向上查找超类,直到找到 BasicObject 类为止,经过的类路径就称为该类的祖先链

方法查找的过程:Ruby 首先在接收者的类中查找,然后再顺着祖先链向上查找,直到找到这个方法为止,如果找到 BasicObject 还没有找到会报错(NoMethodError

祖先链中也包括模块。当一个模块包含在一个类(或一个模块)中时,Ruby 会把这个模块加入该类的祖先链中。可以使用 include prepend 将模块插入到一个类的祖先链中,include 在祖先链的插入位置是当前类的上方prepend 则是当前类的下方。可以通过 ancestors 方法获得类的祖先链

include和prepend
# 定义一个模块 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 都只会加入模块一次

更新于 阅读次数