Mark24
笔记:Ruby的答疑
导航关联
答疑
Ruby的问题比较动态,而经典的几本书《Ruby元编程(第二版)》、《Ruby程序员修炼之道(第二版)》、《Ruby原理剖析》除了最后一本,都喜欢用比喻。
我不喜欢用比喻。使用比喻你总觉得你懂了,但是偏偏 Ruby 富于变化,可以嵌套、mixin,这种让比喻带来的理解很容易破碎。
这里使用一些自省实验的方式,从反向理解。
我们可以建立心理模型,亦或是根本不关心工作原理,毕竟在超级复杂的比如Rails下,类太多了。我们已经无从确认。 那么我们就使用最终的计算结果,来校准我们的程序行为好了 —— 这也很符合 元编程的现状。很工程的方法,他很奏效。
难点一 方法查找
一旦理解Ruby的单继承,那么复杂继承就最终会变成一个链条。
《Ruby原理剖析》 简单的说就是实例先找自己的类,找到不就顺着 super指针指向的父类一直这样找,找不到就 method_missing
obj.ancestors 可以获得继承链
obj.singleton_class.ancestors 可以获得类的继承链
我们也可以不关心 他是如何工作的,也能靠 ruby的这个打印出他如何搜索方法。
难点二 常量查找
常量查找怎么找?
根据 《Ruby原理剖析》
常量有两种层次的查找
- 顺着词法作用域查找
这里参考《Ruby程序员修炼之道》 他跟书写层及有关,像文件目录一样,先搜索本层。绝对层级请用 ::开头
# 自我实验
module B
NAME = "aaaaaaaa"
end
class A
module B
NAME ='bbbb'
end
puts B::NAME # 在自己的词法作用域向下搜索到 bbbb
puts ::B::NAME # aaaa 可以访问到外部
end
1.1 如果遇到 autoload 就载入文件展开,进行搜索。
2.如果词法作用域找不到,则进入超类链中搜索
2.1 如果遇到 autoload 就载入文件展开,进行搜索。
最后调用 const_missing
不光是自定义的大写开头的叫常量。使用类、Module 都是常量。遵循搜索。
实验:通过《原理剖析》我们知道,常量其实是保存在RClass上面的
basic_consts = self.class.constants
module B
NAME = "aaaaaaaa"
end
class A
module B
NAME ='bbbb'
puts '--In class B'
puts self.constants
end
puts '-In class A---'
puts self.constants
end
puts '---outer---'
# 默认的top level 是一个 main实例,想访问constans需要找他的 class
puts self.class.constants - basic_consts
# output>>>>>
# --In class B
# NAME
# -In class A---
# B
# ---outer---
# B
# A
我们也可以用自省,获得每个 Class、Module 即 RClass下的 常量。
在这里,其实我们把每层的constans打出来,再叠加试验等于模拟了 词法作用域中 寻找变量的方式。就是往上找(上面的代码是运行过得,会被注册。下面的代码没有真正运行所以不可见)。
Module.nesting 可以返回词法作用域栈的数组 —— 《Ruby原理剖析》 9.1.8 Page250
module C
module B
class A
pp ::Module.nesting
end
end
end
# output>>>
# [C::B::A, C::B, C]
其实这个就很容易理解了。基本上就是书写层面上 Module、Class
Ruby里比较晦涩的,就是在这里。个人觉得。他的动态性到底如何工作。
但是实际上,Ruby采用的就是很朴素的算法。把问题设计的简单。我们可以站在这个角度补全一些资料不足,依然可以做一些猜测和判断。
小结
我们就算是明白了两个最难得部分。
- 方法如何查找 —— 超类链
- 常量如何查找 —— 词法作用域优先,超类链靠后,特别像文件系统
而且都可以用自省的方式去逐层确认
方法、常量这两个可以帮助我们去写一个 互不往来的类和方法了。也能工作。
但是想打破所谓的作用域门,作用域、作用域门、块,他们之间又是怎么工作的?
难点三 作用域和作用域门
Class、Module、def 都会创建新的作用域。
一下实验说明,无法跨级访问,作用域真实存在。
test_a = 'aaaa'
class A
def run
test_b = 'cccc'
puts test_b
puts test_a
end
end
A.new.run
# output:>>>
# cccc
# Traceback (most recent call last):
# 1: from test.rb:12:in `<main>'
# test.rb:8:in `run': undefined local variable or method `test_a' for #<A:0x00007fc402036538> (NameError)
# Did you mean? test
# test_b
Class、Module 都会在内存中创建独立的 RClass、RClass:Module 结构体,其实这很容易理解。RClass不同于用户生成的对象RObject; RClass更复杂,并且保存了一些变量方法,彼此隔离很容易理解。
这里重点提一下 def
《Ruby原理剖析》9.1.1节 Page239
程序中使用def关键字定义方法,Ruby会遵循三个步骤
- 把每个方法体编译成独立的YARV指令片段(这个情况会发生在Ruby解析和编译程序的时候)
- 使用当前的词法作用域,来获取类或者模块指针(这种情况发生在Ruby执行程序的时候遇到了def关键字)
- 在该类的方法表中保存新的方法名 —— 实际上是保存对应方法名的整数ID值
编译那段我们可以不关心。这个对于程序执行是透明的。我们就当做我们的程序在直接执行,时序上是一致的。
这段的意思其实是,Ruby会将这个方法(编译的,拥有ID的)分配给当前词法作用域对应的类。
还有特殊情况
def self.display
end
这种书写方式与标准方法不同,它使用了前缀,这个前缀告诉Ruby将方法添加到 被指定前缀的——对象的——类中。
仔细品味这句话。 所以如果我们这样定义类方法(类是个对象)所以这个方法其实添加到了 元类中。
好了。我们知道了,总之 def 是运行的时候,他会做的事情就是绑定到 当前词法作用域类(或者指定) 的方法列表中注册。
闭包和块
闭包是一种计算机科学理论,块 是一个闭包的具体实现。 块在底层是一个结构体,保存了词法作用域的环境指针 EP(Environment Pointer)。
保存环境指针意味着可以看到外部的变量。
a = 'aaa'
b = 'bbbb'
puts '---outer---'
pp local_variables
def test
c = 'ccc'
puts '---in test'
pp local_variables
end
test
def self.test2
d = 'dddd'
puts '---in test2'
pp local_variables
end
test2
define_method :test3 do
e = "eeeeeee"
puts "---in test3"
pp local_variables
end
test3
# output >>>>>
# ---outer---
# [:a, :b]
# ---in test
# [:c]
# ---in test2
# [:d]
# ---in test3
# [:e, :a, :b]
可以看到 test3 看到了外部的变量
那么块到底能看到多少层次呢?
a = 'aaa'
b = 'bbbb'
puts '---outer---'
pp local_variables
class B
ba = 'baaaa'
bb = 'bbbb'
puts "---in ClassB"
pp local_variables
class A
c = "cccc"
d = "dddd"
puts "---in ClassA"
pp local_variables
define_method :test3 do
e = "eeeeeee"
puts "---in test3"
pp local_variables
end
end
end
B::A.new.test3
# output >>>>>
# ---outer---
# [:a, :b]
# ---in ClassB
# [:ba, :bb]
# ---in ClassA
# [:c, :d]
# ---in test3
# [:e, :c, :d]
可以看到 基本上 def都在class中声明的。在module中声明的无法运行。
简单的说 块,只能带上词法作用域最近一层(Class\Module)的 EP(环境指针)罢了。
EP 是调用lambda或者函数时 所用环境的指针——指上下文栈帧的指针。 —— 《Ruby原理剖析》8.1.2 Page 214 块是函数与调用该函数时所用环境的组合。 —— 《Ruby原理剖析》8.3 Page 234
可以用 pp local_variables
确认下当前环境。local_variables 也是自省,可以用他的结果,验证词法作用域问题。block中和外部变量比较。这个在元编程中也可以验证, 一些元编程方法的内部情况。
技巧: 扁平化作用域
参考《Ruby元编程(第二版)》 4.3.4 Page 84
我们的实验内容比较浅显,比如前面只用了 A里面动态定义 def的时候,观察了下 local_variables.
实际上 Class.new
、Module.new
、Module#define_method
可以嵌套使用,作用域会进行传递。相当于压平了作用域、嵌套的话相当于串联了作用域。如下所示。
my_car = 'ssss'
MyClass = Class.new do
puts '---MyClass new'
pp local_variables
define_method :my_method do
puts '--in my method'
pp local_variables
end
end
MyClass.new.my_method
# output >>>>>
# ---MyClass new
# [:my_car]
# --in my method
# [:my_car]
他们都可以看到 最外部的 my_car
技巧: 共享作用域
def define_shared_methods
shared = 0
Kernel.send :define_method, :counter do
shared
end
Kernel.send :define_method, :inc do |x|
shared +=x
end
end
define_shared_methods
puts counter
puts inc(4)
puts counter
# output >>>>
# 0
# 4
# 4
掌握了扁平作用域,就拥有了控制作用域的能力。 比如想在一组方法之间共享一个变量,但是又不希望其他方法访问这个变量。就可以把这些方法定义在那个变量所在的扁平作用域里。
而且这两个方法还被 def 作用域门保护着,这种共享变量的技巧叫做 —— 共享作用域。
掌握了,作用域门、扁平作用域 和 共享作用域之后,就可以在任何地方看到你希望看到的变量了。
个人评论: 别忘了Ruby是一个可以重复定义、打开类、重定义方法的。他并不是静态的。所以当我们需要的时候,就开始定义,完全没问题。这里面不会有阻碍。
TODO 元编程
元编程给我的感觉更像是,内部原理剖析针对了不同情况去处理了指针。
使用者是无感的。
使用具体的元编程接口,就是继承了EP(环境指针)也叫绑定。
总结
基本的Ruby语法意味着可以构建逻辑
Ruby的方法查找部分,意味着理解了 Ruby单继承、Mixin 包括用自省查看。
Ruby的常量查找,意味着可以使用模块中常量。
Ruby的作用域门、块打破作用域门(还有一些技巧),意味着你可以构建模块、类的嵌套程序,并且可以控制对变量的可见。作为上面程序的补充。
然后就是动态的元编程。
我个人感觉是
Ruby比较特别在与动态性,而这部分内容,如果我们光是研究书本的话,大多数都得不到理想的答案。尤其是我已经在这里浪费了很久的时间。
《Ruby原理剖析》是值得一读的书(虽然和最新的Ruby3源码偏离了),值的原因是它提供了一个比较靠谱的模型帮助你尽可能的理解了Ruby底层的工作原理。而不是模糊的口号、比喻。这对于时刻变化的Ruby很重要。
然后应该鼓励在实践中做实验去查找结论。
Ruby的一些技巧性,据我观察是一些 作用域隔离和扁平、共享的技巧。还有元编程访问的绑定。对环境绑定、对方法的搜索排序甚至剪切。 不过这些都可以通过自省来获得。对于使用方面其实没有多少影响。
只能说期待更完备的理论,实在不行就啃C语言源码 (实在不想)。。。