Mark24
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 很适合抽象程度高的目标。