Ruby元编程(一)
参考书籍《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都只会加入模块一次