读《Ruby元编程》之类定义

《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