Ruby 元编程概要

前言

个人总结,方便回忆。偏向自言自语。

Ruby 的概念非常多,重点在于划分,可以让他清楚一些:

  • OOP 常规部分
  • self
  • 元编程动态修改的部分
  • 其他辅助功能

最后觉得理解 Ruby 复杂之处的关键就是 self,任何疑难杂症,确认了 self 也就能找到问题的突破口了。

“understanding self is the key to Ruby. Also the key to life”.
                                             —— Dave Thomas

一、Ruby 的面向对象基础

Ruby 的面向对象其实并不特别。基本概念和其他语言保持一致。

1)类、实例、模块

这完全是 普通的面向对象的概念

特别一点的存在:

2)作用域门:class、module、def 彼此互不可见

  • class 内部 def 通过 @ 实例变量通信
  • module、class以及 def 之间共通的只有常量

3)块、lambda

  • lambda 是匿名函数
  • 块,自带周围环境的绑定,即闭包;和 Python、JS 不同,Ruby需要有目的的使用闭包(经过我的理解,闭包是接口预留给用户的

二、Ruby 的继承

1. Ruby的模式

receiver.message

调用方法被认为是发给 receiver 的消息。

实际上,有一个 current_object 概念就是指当前的 receiver,它会被放在 self 变量里。

程序的执行过程中,遇到 module、class、def、xxx_eval、receiver.message形式显式调用 都会切换 self 执行完毕又会切换回来。

显式调用,self 会被设置为对象,方法会在 对象的链路里查找。

隐式调用,receiver 会被设置为 self ,方法查询就会在 self 的链路中查找。

理解 self 就是至关重要的。

2. receiver Ruby 查找方法的方式是固定的:

查询方式:

  • 1)单例类(如果有的话)
  • 2)类
  • 3)类的继承链条

可以通过执行的方式获得查询路径:

  • 1)单例类: xxx.singleton_class
  • 2) 类以及单继承链: xxx.class.ancestors

3. Ruby 面向对象底层逻辑:

  • 1)Ruby 的一切几乎都是对象
  • 2)Ruby 的 class 也是实例

Ruby 的对象就是一个结构体,包含:

  • 1)flags
  • 2)实例属性
  • 3)指向类

类 保存的是:

  • 1)方法
  • 2)superclass

设计与推论

树形结构

Ruby 只支持单继承;Ruby 的所有类默认继承 Object。

这两个推论不难得出:所有的类会构成一个树形彼此引用的结构;所有的类在 根 层级共享方法。

环形结构

Ruby 在文件中,即不在任何类中的方法,会被定义在 当前的对象 实例 main 上。 Ruby 的所有代码跑在了一个 Object 的实例化对象上面。

这就类似于所有的代码,定义执行在一个 Object 的 实例上下文中


main = Object.new

main.instance_eval {
	// your code
}

所有定义在 文件中的方法,最后都是 main 的 private 方法。

所有 后期复杂自定义类的 实例,寻找方法的时候,会沿着 祖先链条查找,最后会查到 Object,然后会访问到 Object 的私有方法,最后会访问到 main 上下文中定义的方法。

所有方法一个不剩,都会被查找一圈。有一个美丽的环形结构。

同时告诉我们,尽可能不要在 文件的最外层上下文中定义方法,这个和 Kernel 方法无异。

4.单例类

每一个对象除了类定义时候的方法,还有一个空间 —— 单例类

可以保存属于他自己的方法的地方

  • 1) def 定义
def instance_name.xxxx

end
  • 2) “class «” 定义形式
class << instance
	def xxx
	end
end

定义 【instance】 的 单例类

class << 【instance】, 可以用下面几种方式,来说服自己理解这个丑陋的语法:

  • 可以认为是 class << 是 单例类
  • 可以认为这种特殊语法 把 【instance】切换成了当前 self
  • 可以认为这种特殊语法,把方法注入到 【instance】的单例类中

PS: 由于 self 的存在,为了通用性 【instance】 通常会是上下文中的 self

三、Ruby 特殊的地方

Ruby 特殊的地方就是 元编程 能力,可以改变自身。 讲清楚 self 就可以讲明白 元编程

程序的执行者,就是 数据+函数;而面向对象把这两个封装成了: 类(包含:数据、方法) 元编程,是动态的塑造 类 的能力。

(笔者:没有 meta-meta programming 的原因,因为再抽象一层意义不大 :P)

我们知道了class、以及实例查找方法的固定套路、以及self

元编程由几个部分组成

1)动态调用方法

除了语法的声明式,还有内置的 API 可以调用做相同的事情。

define_method
define_class
send
# 。。。

2) 类宏: 类方法,在类声明的上下文中执行,结果是生成定义的实例方法

理解类宏的关键是:

  • 类也是实例
  • 类的声明部分,也在执行代码
  • 类方法除了被单独外部调用,也可以在类声明部分执行,用来生成实例方法

实际上只有 Ruby 精心的预留设计,才能促成这样的结果。 这些特点是有意而为之。

# 常见内置类宏举例
attr_accessor
alias

3)xxx_eval :eval的特点是可以执行字符串、block;最重要的差距是,他会改变 self

xxx_eval 的内部会切 self

  • class_eval 只在 class 中使用
  • eval 是 字符串和传入绑定
  • instance_eval 最常用,他总是在定义单例类方法 由于一切都是对象:普通对象定义的就是实例方法、如果定义在类上,就等于类方法

xxx_eval 可以接入 self 的作用域

4)modules 与 mixins

类中:

  • include:把方法放入当前 class 的继承链条,如果在 class 初始化中执行等同于拓展了 实例方法
  • extend:把方法放入 当前的单例类, class 初始化中执行等同于拓展了 类方法

至于其他的 prepend、refinement 都是辅助

5)Hooks

一些周期的钩子,比如继承

6)自省

通过读取属性,来判断 配合 =~ 内部正则来工作

7)proxy:xxx_missing

method_missing
const_missing
respond_to_missing
# 。。。

在这里可以收集失败方法,使用 其他 1)~ 6)

* 自省,来判断方法
* hooks,来判断注入阶段
* define,动态定义方法
* send,动态发送方法
* eval,求值
* 类宏,可以使用来定义方法
* super 交还给父方法,不带参数等于继承所有参数

动态生成新的类、方法

四、有效的方法 self

Ruby 的执行,用远在围绕着 self

当遇到困惑的时候:

在当前代码中,打印 self; 或者使用 instance_eval 中 打印 self

self 作为探针,可以反映出当前正在工作的对象

self 可以按图索骥:

self.class
self.class.ancestors
self.singleton_class

来查询当前的工作状态

五、DSL

self 和 instance_eval 的神奇组合

这是由 Ruby 的几个规则设定,组合下的产物:

  • 1)由于上下文中一直存在 self
  • 2)隐式调用的方法,会默认调用当前的 self 方法
  • 3)instance_eval 是会切换到 当前 self 到 instance 的上下文

这样外部的 block 可以省略 receiver 直接书写 instance 内部的方法名

这样就是 DSL 的书写范式


class Demo
  attr_accessor :cache

  def initialize(&block)
    @cache = {}
    instance_eval(&block)
  end

  def attr(name, value)
    @cache[name] = value
  end
end

d = Demo.new do
	# 这里被eval 执行,这里的 self 是对象的上下文
	# 这里 调用 attr 省略了调用者,呈现出语言形态
	# 这部分就是 DSL
	# 这部分的 程序,也可以读取文件的代码,以DSL 形式书写的文件,进行读取
  attr :hobby, "programming"
end

puts d.cache

总结

Ruby 丰富的 feature,是刻意设计,有意制造出这么多的组合效果。继承 Lisp 的基因。可以持续变化自身的语言。

Ruby 设计思想非常深邃。

  • 面向对象,强行的规定所有变量只能通过方法获取。这是使用了一种设计模式,最大可能应对未来变化。
  • 复用结构,有 class、module,可以动态的调整模块的继承顺序。环形查找结构、树形的继承结构。
  • 使用 Module管理模块,潜在为了配合:Monkey patching
  • 所有语言构建都可以改变;每一个语言组件都是对象,都可以动态的添加、删除方法。
  • DSL,eval,proxy 语言可以动态的求值,根据输入动态的生成方法
  • ……

Ruby 很适合抽象程度高的目标。

Mark24

Everything can Mix.