参考书籍《Ruby 元编程(第二版)》
Ruby 版本:书上使用的是 2.x,自己使用的 3.1.2
# 类定义的本质
在 ruby 中类不过是增强的模块,因此对类的相关内容也适用于模块
# 当前类
不管处在 ruby 程序的那个位置,总存在一个当前对象:self。同样,也总是存在一个当前类(或模块)存在。
- 在程序的顶层,当前类是 Object,也就是 main 对象所属的类(也是为什么在顶层定义的方法会成为 Object 的实例方法的原因)
- 在一个方法中,当前类就是当前对象的类。(试着在一个方法中用 def 关键字定义另外一个方法,会发现新方法定义在 self 所属的类中。)
class C | |
def m1 | |
def m2; end | |
end | |
end | |
class D < C; end | |
obj = D.new | |
obj.m1 | |
C.instance_methods(false) #=> [:m1, :m2] |
用 class 关键字打开类时(或用 module 关键字打开模块时),这个类成为当前类
# class_eval 方法
如何在不知道类名字的情况下打开一个类?
比如:想要设计一个以类为参数的方法,给这个类添加一个新的实例方法
def add_method_to(a_class) | |
# TODO:在 a_class 上定义方法 m () | |
end |
可以使用 class_eval 方法:它会在一个已存在类的上下文中执行一个块
def add_method_to(a_class) | |
a_class.class_eval do | |
def m | |
'Hello class_eval method' | |
end | |
end | |
end | |
add_method_to(String) | |
'abc'.m #=> "Hello class_eval method" |
# 类实例变量
ruby 解释器假定所有实例变量都属于当前对象 self,在类定义时也是如此:
class MyClass | |
@my_var = 1 | |
end |
在类定义的时候,self 角色由类本身担任,因此实例变量 @my_var 属于这个类。注意:类的实例变量 和 类的对象的实例变量是不同的。
注意区分:类变量 和 类的实例变量
@@开头的是类变量,可以被子类或者类的实例使用;@开头的是类的实例变量,只能被类本身使用(类本身也是一个对象 ——Class 类的实例对象)
class MyClass | |
@my_var = 1 # MyClass 类的实例变量 | |
def self.read | |
@my_var | |
end | |
def write | |
@my_var = 2 | |
end | |
def read | |
@my_var | |
end | |
end | |
obj = MyClass.new | |
obj.read #=> nil 因为 obj 对象上此时还没有 @my_var 实例变量 | |
obj.write | |
obj.read #=> 2 | |
MyClass.read #=> 1 这里返回的是 MyClass 类本身的实例变量 |
# 单件方法和单件类
# 单件方法
ruby 允许给单个对象增加一个方法:例如:
str = 'just a regular string' | |
# 只在 str 这个变量上添加了 title? 方法 | |
def str.title? | |
upcase == self | |
end | |
str.title? #=> false | |
str.methods.grep(/title?/) #=> [:title?] | |
str.singleton_methods #=> [:title?] |
# 类方法的真相
类本身也是对象(Class 类的实例),而类名只是一个常量。因此,类方法的实质就是:它是类的一个单件方法。
用 def 定义单件方法多的语法总是如下:
def object.method | |
# 方法主体 | |
end |
上面定义中,object 可以是对象的引用、常量类名或者 self。在这三种形式下,定义的语法看起来有些不同,但实际上,底层机制是一样的。
# 单件类
单件类也成为元类、本征类
# 如何获取单件类
- 方式一:通过
class << object
语法
class << an_object | |
# 自定义代码 | |
end | |
# 如果像获得这个单件类的引用,可以在离开作用域的时候返回 self: | |
obj = Object.new | |
singleton_class = class << obj | |
self | |
end | |
singleton_class.class #=> Class |
- 方式二:直接用
obj.singleton_class
方法
obj.singleton_class #=> #<Class:#<Object:0x0000000001d0d760>> | |
## 上述的例子说明单件类也是类(一种特殊的类)。同时每个单件类只有一个实例,而且不能被继承。最重要的是单件类是对象的单件方法存活之所(类方法实际就是一个单件方法): | |
def obj.my_singleton_method; end | |
singleton_class.instance_methods.grep(/my_singleton_method/) #=> [:my_singleton_method] |
# 方法查找补充
class C | |
def a_method | |
'C#a_method' | |
end | |
end | |
class D < C; end | |
obj = D.new | |
obj.a_method #=> "C#a_method" | |
## 在 obj 上定义一个单件方法 | |
class << obj | |
def a_singleton_method | |
'obj#a_singleton_method' | |
end | |
end | |
# obj 单件类的父类是 D | |
obj.singleton_class.superclass #=> D |
因此如果对象有单件类,ruby 不是从对象所在的类开始查找,而是从对象的单件类开始查找方法。如果在单件类中找不到这个方法,那么它会沿着祖先链向上查找。
# 类扩展和对象扩展
# 类扩展
module MyModule | |
def my_method; 'hello' end | |
end | |
class MyClass | |
class << self | |
include MyModule | |
end | |
end | |
MyClass.my_method #=> "hello" |
# 对象扩展
module MyModule | |
def my_method | |
'MyModule#my_method' | |
end | |
end | |
obj = Object.new | |
class << obj | |
include MyModule | |
end | |
obj.my_method #=> "MyModule#my_method" |
# 扩展的简化形式
Object#extend 方法
module MyModule | |
def my_method; 'hello' end | |
end | |
obj = Object.new | |
obj.extend MyModule | |
obj.my_method #=> "hello" | |
class MyClass | |
extend MyModule | |
end | |
MyClass.my_method #=> "hello" |
# 方法包装器
# 方法别名
ruby 中可以 alias_method 给一个方法取一个别名,一个参数是新名称,第二个参数是原始名称
class MyClass | |
def my_method | |
'my_method' | |
end | |
alias m my_method | |
end | |
obj = MyClass.new | |
obj.my_method # => "my_method" | |
obj.m # => "my_method" |
除此之外 ruby 还提供了 alias 关键字,可以代替 Module#alias_method 方法(当你需要在顶级作用域进行修改时使用,因为此时 Module#alias_method 不可用)
如果给一个方法起个别名,然后又重新定义它,会怎么样?
class String | |
alias real_length length | |
def length | |
real_length > 5 ? 'long' : 'short' | |
end | |
end | |
'War and Peace'.length # => "long" | |
'War and Peace'.real_length # => 13 |
上述的代码重新定义了 String#length 方法,但是别名方法引用的还是原始方法。这说明重定义方法的工作方式:重定义方法时,并不真正修改这个方法。相反,你定义了一个新方法并吧当前存在的这个方法名字跟它绑定。只要老方法还存在一个绑定的名字,仍旧可以调用它(通过老方法的别名调用,就比如上面的 real_length 方法)
# 环绕别名
通过环绕别名可以给已用方法包装新的功能
module Kernel | |
alias old_puts puts | |
def puts(*args) | |
# 自定义的一些代码 | |
old_puts(*args) # 调用原始的 puts 方法 | |
# 自定义的一些代码 | |
end | |
end |
环境别名定义的三个步骤:
- 给原始方法起一个别名
- 重定义原始方法
- 在新的方法中调用老的方法(通过之前定义的别名来进行)
# 细化包装器
细化除了可以把一段代码直接加入一个类中,还可以用来替换环绕别名。
如果在细化的方法中,调用了 super 方法,则会调用没有细化的原始方法。
module StringRefinement | |
refine String do | |
def length | |
super > 5 ? 'long' : 'short' | |
end | |
end | |
end | |
using StringRefinement | |
'War and Peace'.length #=> "long" |
上述代码技术成为细化封装器,其作用范围和之前的细化一样,作用范围只到文件末尾,在 ruby2.1 中是在模块的定义范围之内(这样要比环绕别名方法更加安全,因为环绕别名是全局性的)
# Module#prepend 方法
Module#prepend 方法和 include 类似,但是它会把包含的模块插入到祖先链中该类下方。这意味着被 prepend 方法包含的模块可以覆写该类的同名方法,同时可以通过 super 调用该类中的原始方法:
module ExplicitString | |
def length | |
super > 5 ? "long" : "short" | |
end | |
end | |
String.class_eval do | |
prepend ExplicitString | |
end | |
"War and Peace".length #=> "long" |
这种技术也被称为下包含包装器。和细化包装器相比,它不是一种局部化方法,但是一般认为其比细化包装器和环绕别名都更清晰。