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。在这三种形式下,定义的语法看起来有些不同,但实际上,底层机制是一样的。

单件类

单件类也成为元类、本征类

如何获取单件类

  1. 方式一:通过class << object语法
class << an_object
  # 自定义代码
end
# 如果像获得这个单件类的引用,可以在离开作用域的时候返回self:
obj = Object.new

singleton_class = class << obj
  self
end

singleton_class.class #=> Class
  1. 方式二:直接用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

环境别名定义的三个步骤:

  1. 给原始方法起一个别名
  2. 重定义原始方法
  3. 在新的方法中调用老的方法(通过之前定义的别名来进行)

细化包装器

细化除了可以把一段代码直接加入一个类中,还可以用来替换环绕别名。 如果在细化的方法中,调用了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"

这种技术也被称为下包含包装器。和细化包装器相比,它不是一种局部化方法,但是一般认为其比细化包装器和环绕别名都更清晰。