《Ruby元编程》之类定义笔记。
写在前面
这是《Ruby元编程》第五章类定义的学习笔记,这一章有很多的概念,类宏,单件方法,单件类,类扩展,环绕别名……还见到了我曾一度迷惑不解的 duck typing. 这应该算是全书最精彩的一个章节了。
好,来看看都有些啥。
当前类
像无所不在的当前对象一样,也有一个对应的当前类(模块)存在。
如何跟踪当前类?三种方式:
- 在程序顶层,self是main,当前类是Object, main对象所属的类
- 在一个方法中,当前类是当前对象的类
- 用class关键字打开一个类(module打开一个模块)时,该类称为当前类
这里第三种方法,存在一种情况:当类名不可知时,如何打开类?比如类名是一个变量。
用class_eval method。
class_eval
类似牛逼的instance_eval可以打破类的封装一样,class_eval可以打开类,给类定义实例方法。
看这段代码:
def add_method_to(a_class)
a_class.class_eval do
def m; puts 'hi'; end
end
end
add_method_to String
'abc'.m #=> hi
这里你一定也会想到这种打开类的方法,容易引起猴子补丁的问题,当然你可以用细化的方式来处理,不过还是要慎重使用class_eval。
类似instance_eval还有一个instance_exec的双胞胎兄弟一样,class_eval也有一个class_exec,它可以接受额外的代码块作为参数。
class_eval与instance_eval
其实instance_eval也可以修改类,修改的是当前对象的单件类。后面说到单件类时,可以看到。书中也有相应的例子。不要忘了,类也是对象。
那么class_eval与instance_eval如何选择?看目的。
- 如果想要打开一个对象,用instance_eval更好
- 如果想要修改类,用class_eval打开
第六章也提到了一个eval,执行包含ruby代码的字符串,不过它有些安全性上的问题。
类实例变量和类变量
两者都是存储在类中的。不同之处在于:
- 类实例变量只能被类本身访问,定义时用@
- 类变量可以被子类或者类的实例访问,定义时用@@,类变量属于类体系结构,并不真正属于类,存在类似全局变量的风险,慎重使用。
对象也有实例变量,而且跟类实例变量看着还挺像,那么两者有什么区别?
当当前类充当self时,访问的变量是类实例变量,当类的实例充当self时,通常是调用了某个实例方法,然后拥有了该实例变量。
看例子加深理解:
class C
@var = 1
def self.read; @var; end
def read; @var; end
def write; @var = 2; end
end
obj = C.new
obj.read
obj.write
obj.read #=> 2
C.read #=> 1
这里C和C的对象obj都拥有一个实例变量@var,但是两者是不同的。C访问的是类实例变量,obj访问的是类的对象的实例变量,也就是它调用了read后获得到实例变量。
单件方法
Ruby允许给单个对象增加一个方法。看个例子:
my_array = [1,2,3,4]
def my_array.add_one
self.map{|x| x + 1}
end
my_array.add_one #=> [2,3,4,5]
my_array.methods.grep(/add_one/) #=> [:add_one]
my_array.singleton_methods #=> [:add_one]
[1,2].singleton_methods #=> []
这里my_array拥有了add_one这个method,而其他对象没有。
这种只对单个对象生效的方法,称为单件方法。
通过调用singleton_methods可以查看某个对象的单件方法。
这里就要提一下 duck typing了。
有了单件方法,那么所有的对象都可以拥有自己的方法了,这里也就可以看出,Ruby中,对象的“类型”并不严格与它的类相关,“类型”只是对象能响应的一组方法。也就是说,一个对象是不是duck类的实例并不重要,重要的是它能不能响应walk,quack的方法,也就是谚语中所说的:“If it walks like a duck and it quacks like a duck, then it must be a duck.”
类方法
类也是对象,而类名只是常量,所以在类上调用方法,就和对象调用方法一样。
类方法的实质就是:它是一个类的单件方法。单件方法存放在哪里?存放在该类的单件类里。
类方法的定义有三种:
def MyClass.a_class_method; end
def MyClass
def self.a_class_method; end
end
# 打开了该类的单件类,在单件类定义了类方法
def MyClass
class << self
def yet_another_class_method; end
end
这里,class << 打开了该类的单件类。
类宏
像attr_accessor, attr_read, attr_writer这样的方法称之为类宏。
他们看起来像是关键字,其实只是普通的方法。估计你看到这里也就知道了,这些其实是类方法。
单件类
单件类是一个对象的单件方法的存活之所,每个单件类都只有一个实例。
通过Object#singleton_class或者class << 可以获得单件类。
看个例子来更好地了解下单件类:
class C
def a_method
puts 'C#a_method()'
end
def self.a_class_method
puts 'C.a_class_method()'
end
end
class D < C;end
obj = D.new
class << obj
def a_singleton_method
puts "obj#a_singleton_method()"
end
end
obj.singleton_class #= > #<Class:#<D:0x007f9e23909f70>>
obj.singleton_class.superclass #= > D
D.singleton_class #= > #<Class:D>
D.singleton_class.superclass #= > #<Class:C>
C.singleton_class #= > #<Class:C>
C.singleton_class.superclass #= > #<Class:Object>
查看obj单件类,D单件类的祖先链,会得到:
obj.singleton_class.ancestors #=> [#<Class:#<D:0x007fc2ee041790>>,D,C,Object, Kernel, BasicObject]
D.singleton_class.ancestors #=> [#<Class:D>, #<Class:C>, #<Class:Object>, #<Class:BasicObject>, Class, Module, Object, Kernel,BasicObject]
我们来做出obj的祖先链图:
【跟书中稍稍不同,我把kernel加进来了,请无视这小学级别的英文书写】
可以发现:一个对象的单件类的超类是这个对象的类。
书中还列出了七条规则,但是我觉得理解了这个例子,能做出祖先链图,对单件类基本算是掌握了。
还记得instance_eval可以修改类?它修改的其实是对象的单件类,也就是给对象添加了单件方法,比如先前那个例子,还能这么做:
my_array = [1,2,3,4]
my_array.instance_eval do
def add_one
self.map{|x| x + 1}
end
end
my_array.add_one #=> [2,3,4,5]
my_array.methods.grep(/add_one/) #=> [:add_one]
my_array.singleton_methods #=> [:add_one]
这里你可能会想,如果单件方法与类或者对象继承的方法同名,如何调用?先调用单件方法,也就是单件方法覆写了祖先链上游的同名方法,这种猴子补丁也有风险。
看个例子:
module MyModule
def size; "size"; end
def ancestors; "ancestors"; end
end
class C
extend MyModule
end
obj = []
obj.extend MyModule
obj.size #= > 调用的是单件类中的方法, 输出"size"
C.ancestors #= > 调用的是单件类中的方法, 输出"ancestors"
如果类或者对象本身有这个方法,与扩展后的单件方法同名,调用的是自身的方法,而不是单件方法。
module MyModule
def size; "size"; end
end
class C
def self.size
"it is C#size"
end
extend MyModule
end
C.size #= > 从祖先链开始找,调用的是自己的类方法, 输出"it is C#size"
类属性
像attr_accessor, attr_read,attr_writer这样的方法可以给对象添加属性,那么如何给类添加属性?
属性实际上是一对方法,而且还是单件方法,所以针对指定类,添加类属性,则需要在它的单件类定义这个属性即可。
比如这段代码:
class C
class << self
attr_accessor :h
end
end
C.h = 'hi'
C.h #= > hi
类扩展与对象扩展
通过向类的单件类中添加模块来定义类方法,这种技巧称之为类扩展,类是对象,相应地,这种技巧也能用在对象上,故也有对象扩展。
看段代码理解下:
module MyModule
def a_method; puts "hello"; end
end
class C
class << self
include MyModule
end
end
obj = []
class << obj
include MyModule
end
C.a_method #= > hello
C.singleton_methods #= > [:a_method]
obj.a_method #= > hello
obj.singleton_methods #= > [:a_method]
类C和对象obj【这里是个空数组】,都在其对应的单件类中包含了MyModule,把MyModule的普通方法a_method变成了其单件方法。
由于类扩展和对象扩展的普遍性,Ruby中专门提供了一个方法:Object#extend,所以上面的代码还可以这么写:
module MyModule
def a_method; puts "hello"; end
end
class C
extend MyModule
end
obj = []
obj.extend MyModule
C.a_method #= > hello
C.singleton_methods #= > [:a_method]
obj.a_method #= > hello
obj.singleton_methods #= > [:a_method]
方法包装器
方法包装器,即用一个方法包装另一个方法。怎么会有这样的需求?假定目前你有一个不能直接修改的方法,比如它在一个封装的库里,你希望为这个方法包装额外的特性。比如添加异常处理什么的。
有三种方式:环绕别名,细化封装器,下包含包装器。
环绕别名
先来看看alias关键字,可以给方法起一个别名。
class C def my_method puts 'C#my_method()' end alias_method :m, :my_method end C.new.my_method #= > C#my_method() C.new.m #= > C#my_method()
如果给方法起了别名,然后又重新定义了它,会如何?继续这个例子:
class C def my_method puts 'C#my_method()' end alias_method :m, :my_method def my_method puts "redefine my_method" m # 调用了m, 也就是原来的my_method end end C.new.my_method #= > redefine my_method\n C#my_method() C.new.m #= > C#my_method()
这里重新定义了my_method,但是别名引用的仍然是原始的那个方法。
这里其实做了这样的事:
- 给方法定义了别名(my_method多了别名m)
- 重定义了这个方法(my_method)
- 在新的方法中调用了老方法(m)
新的my_method像是环绕在老的my_method方法之外,包装了新的功能,这种技巧就叫做环绕别名。
环绕别名的一个潜在危险与加载有关,特别是多次环绕别名时,所以才会有alias_method_chain的兴起与衰亡。【书中rails部分有提到】
而它最主要的问题在于它是一种猴子补丁。
细化封装器
跟细化方法一样,用refine来定义,在新的方法中使用super来调用旧方法。
细化封装器的作用范围从
using
开始,到文件末尾,这使得细化封装器比全局性的环绕别名更安全。下包含包装器
是环绕别名的一种替代方式。使用Module#prepend方法,可以覆写类的同名方法,同时可以通过super来调用旧方法。比如上面的例子,用pretend可以这么写:
module D def my_method puts "redefine my_method" super end end class C prepend D def my_method puts 'C#my_method()' end end C.new.my_method #= > redefine my_method\n C#my_method()
module D 中的my_method覆盖了class C中的实例方法my_method。
后记
这章的很多知识点,在后面rails源代码赏析中都可以寻到踪迹,特别是类扩展和方法包装器,看完这章,突然有种拿到武功秘籍的错觉:P