跳到主要内容

Ruby-元编程之对象模型

· 阅读需 14 分钟

最后更新于 2016-08-01 18:39:00

本文的代码均在 Ruby 2.3.0 环境下测试通过。

这篇文章讲述关于 Ruby 中对象模型相关的知识。事实上,有很多编程语言都具备元编程的能力,而 Ruby 则是将元编程发挥到极致的语言。元编程这个概念是模糊的,很多人都说是用代码生成代码的方式,不过我们不用去纠结这个概念。元编程最具有魅力的地方就是,原来我们还可以在程序运行时去操控代码。

对象模型

Ruby 是一个纯面向对象的语言,之所以这么说,是因为 Ruby 的设计哲学将面向对象(OO)这个思想完美的展现出来。接下来我们就要了解一个很重要的概念,对象模型。

类(Class)

Ruby 的代码是简洁明了的,每一个对象都是相应类的一个实例,目前来看 Ruby 的类并没有什么特色。

class A
# 这里定义一个类
end

class B < A
# 该类(B)继承自类A
end

class 关键字声明了一个类,并使用 < 符号标识继承关系。要特别注意的是,Ruby 的类并没有所谓的构造方法和析构函数,当然这并不意味着一个实例化对象在创建之后我们才能对其进行修改。

class A
# 每一个实例化对象在创建时都会自动调用这个方法
def initialize
@a = 1
end
end

a = A.new

initialize() 方法是对象实例化时默认调用的,于是我们可以在此方法内部完成一些事情(初始化变量,调用方法等),该方法是可以传递参数的。看见了吧,Ruby 在创建实例对象时的方式也是不同的,是通过调用 new() 方法来完成的,这么做自有它的道理。

打开类(Open Class)

Ruby 中有许多内置类,例如 Object、String、Array、Hash 等等,这些类中都定义了许多内置方法供我们使用。但是,Ruby 允许我们自己去修改这些内置类,这在某些方面来说是很重要的。

class String
def say
p "Hello,World!"
end
end

"a".say # ==> "Hello,World!"

我们给 String 类添加了一个实例方法,这时所有的字符串都拥有这个 say() 方法了。class 关键字的另一个作用就是,打开已经存在的一个类对其进行修改。这是 Ruby 的特色,很方便我们开发者,但是我们不要频繁的打开类,因为到处打开同一个类并不方便我们进行后期维护,而且打开类很容易覆盖内建方法,极有可能导致 Ruby 崩溃。

对象(object)

一个实例化对象拥有什么呢?所属类(class)、超类(superclass)、方法(methods)、实例变量(instance_variables),这些都是一个对象所拥有的属性或者说特征。

class A
def speak
end
end

class B < A
def initialize(n)
@a = n
end

def say
end
end

obj = B.new(5)
obj.class # ==> B
obj.methods # ==> [:speak,:say...]
obj.instance_variables # ==> [:@a]
B.class # ==> Class
B.superclass # ==> A

一个对象拥有自己的实例变量,这些变量存放在对象自身中,由对象自己独立修改,不影响其他对象;同一个类中不同的实例对象共享类中定义的实例方法,这些方法存放在类中,各个对象不过是对这些方法存放着一个引用,这样更节省内存。

我们可以看出来,不仅仅类的实例化对象是对象,其实类自身也是一个对象,每一个类都是从 Class 类中实例化出来的一个对象。在 Ruby 中任何东西都是对象,包括数字、true、nil,甚至连一段代码都是一个对象。

B.ancestors  # ==> [B, A, Object, Kernel, BasicObject]

我们可以使用类对象的 ancestors 属性获得自身的继承链,BasicObject 类是所有类的根节点。

模块(Module)

Ruby 中的类也是不支持多重继承的,因为多重继承会引发很多严重的问题,但是多重继承却具有很大的作用,于是 Ruby 用更优雅的方式解决了多重继承的问题,也就是模块(Module)。当然,模块的作用不仅仅局限于解决多重继承的问题,它还提供了命名空间(namespace)。将不同的代码分别放在不同的模块中,不仅解决了变量名,方法名容易重复的问题,代码结构也更加清晰,更便于以后代码的维护。

Class.superclass  # ==> Module

然而,我们会发现所有类对象的所属类(Class)竟继承自 Module,其实根本不用惊讶,我们只需接受这个事实即可。至于为什么,这可能是一个更复杂的问题,不过它并不影响我们理解对象模型。

由此看来,一个类也只不过是增强的模块,为什么会这么说呢?因为模块是不允许实例化的,而类可以拥有实例化对象。我们可以在类中通过 include 方法引入多个模块,从而实现多重继承。

module M
def say
end
end

class C
include M
end

c = C.new
c.methods # ==> [:say,...]

常量(Constant)

常量是相对于变量来说的,一旦定义就不可改变。不过, Ruby 中的常量却是可以改变的,解释器会发出警告但不会阻止,所以我们要格外注意。不过,话说回来我们既然定义的是常量,就没有理由去修改它。常量是以大写字母开头的,是为了和变量进行区分;常量的作用域也和变量不同。

module M
CONST = "out"

class C
CONST = "in"
Module.nesting
end
end
# ==> [M::C, M]

M::CONST # ==> "out"
M::C::CONST #==> "in"

模块与类就像文件目录系统,在不同目录下可以拥有相同文件;同样地,在不同的类(模块)中可以有命名相同的常量,当然这样我们就不能直接通过常量名访问了。我们通过 Module.nesting() 方法来获取当前所在类(模块)的常量路径,然后通过这些路径我们就可以在任何位置访问到相应常量。其实,类名、模块名也都是普通常量。

方法查找(Method Lookup)

在面向对象编程中,子类可以覆盖父类中的同名方法,同时子类也可以继承自己没有而父类中有的方法。所以说,当我们在调用一个方法时,方法查找(Method Lookup)过程却是很关键的。通常来说,方法查找遵循就近原则,先从所属类中查找,如果没找到会从父类中继续查找,依次沿着继承链的顺序直至根节点为止。我们可以通过 ancestors 方法得到一个类对象的继承链。

在这里我们需要明白的是,继承链是对于类来说的,而不是类中实例对象,为什么这么说呢?因为我们说过,只有实例变量是存在于实例对象自身的,而方法是存在于其所在类中的。事实上,方法查找(Method Lookup)的起点并非是其所属类,而是对象自身的单例类(singleton_class),单例类中所有的东西都是对象自身独有的。单例类也称为特征类、元类,这个概念还是比较复杂的,需单独详细介绍,但这并不影响我们去理解方法查找的大致过程。

self

在面向对象编程中,对象之间是通过消息传递来实现交流的,而我们所谓的方法调用其实就是消息传递的过程。所在当前对象就是消息发送者(sender),而接受者(receiver)则是被调用对象,那么方法就是消息了。

class A
def say
p "Hello!"
end
end

class B
a = A.new
a.say
end

在以上示例中,发送者就是类 B,接受者就是类 A 的实例对象 a,而消息则是 say()方法。在 Ruby 中,接受者(receiver)会用 self 关键字代替,也就是说 self 永远指向当前消息的接受者,其实它与其他面向对象语言中的 this 关键字很像,但也不完全相同。

class A
def initialize(n)
@a = n
end

def say
p @a
end
end

obj1 = A.new(10)
obj2 = A.new(20)
obj1.say # ==> 10
obj2.say # ==> 20

当我们处于一个消息的上下文中时,所有没有指出引用对象的实例变量都是属于 self 所引用对象的实例变量,所以说两个不同的对象被调用同一个方法时,并不会因为实例变量而发生冲突。

我们还需要知道的是,当我们没有在方法中,而是在一个类(模块)中时 self 指向当前类(模块);当我们既没有在类(模块)中,也没有在方法中时,self 指向顶级对象 main如何理解这句话,我们可以用定义类方法的形式来很形象的说明。

class A
def A.say
end

def self.say
end
end

其实这两种形式都是定义了同一个类方法 say(),由此可以看到 self 其实指向的就是类 A。所以,在开发过程中我们更推荐使用 self 来指定类方法,因为若改变类名我们就不需要做任何改变。

私有方法是默认接受者为 self 的,如果显式指出则会报错,也就是说私有方法调用时直接使用方法名即可,若加上接受者就会报错。

结语

我们可以简单的将对象模型总结为以下几点:

  • 对象是由类实例化所得到的,所有的类其实都是从 Class 类实例化得到的,Ruby 中所有的东西都是对象,包括方法。
  • class 关键字不仅声明了一个类,并且可以打开一个已存在的类进行修改。
  • 对象自身只保存实例变量,其方法都存在于所属类及其继承链中,这些方法对于类来说都是实例方法。
  • 类是一个增强的模块,模块不可以被实例化,模块可以实现多重继承,添加新的命名空间。
  • 常量的作用域类似于文件目录系统,不同的文件目录中可以存在相同常量但不会冲突。
  • 所有类对象都有其继承链,可以通过 ancestors 方法查询,根节点是 BasicObject 类。
  • 方法查找(Method Lookup)的过程遵循就近原则,沿继承链向上查找直至根节点为止。
  • self 关键字指向接受者,或者当前类(模块),没有明确指出接受者的实例变量、方法,均默认为 self 所属。

参考

  • 《Ruby 元编程》,[意] Paolo Perrotta,廖志刚 译
  • 《Ruby 基础教程》,[日] 高桥征义 后藤裕藏,何文斯 译