Ruby元编程(一)

参考书籍《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方法获得类的祖先链

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