Ruby元编程(四)
参考书籍《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"
这种技术也被称为下包含包装器。和细化包装器相比,它不是一种局部化方法,但是一般认为其比细化包装器和环绕别名都更清晰。