参考书籍《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"

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

更新于 阅读次数