笔记:Ruby的答疑

导航关联

Ruby特性和实验分析汇总

答疑

Ruby的问题比较动态,而经典的几本书《Ruby元编程(第二版)》、《Ruby程序员修炼之道(第二版)》、《Ruby原理剖析》除了最后一本,都喜欢用比喻。

我不喜欢用比喻。使用比喻你总觉得你懂了,但是偏偏 Ruby 富于变化,可以嵌套、mixin,这种让比喻带来的理解很容易破碎。

这里使用一些自省实验的方式,从反向理解。

我们可以建立心理模型,亦或是根本不关心工作原理,毕竟在超级复杂的比如Rails下,类太多了。我们已经无从确认。 那么我们就使用最终的计算结果,来校准我们的程序行为好了 —— 这也很符合 元编程的现状。很工程的方法,他很奏效。

难点一 方法查找

一旦理解Ruby的单继承,那么复杂继承就最终会变成一个链条。

《Ruby原理剖析》 简单的说就是实例先找自己的类,找到不就顺着 super指针指向的父类一直这样找,找不到就 method_missing

obj.ancestors 可以获得继承链

obj.singleton_class.ancestors 可以获得类的继承链

我们也可以不关心 他是如何工作的,也能靠 ruby的这个打印出他如何搜索方法。

难点二 常量查找

常量查找怎么找?

根据 《Ruby原理剖析》

常量有两种层次的查找

  1. 顺着词法作用域查找

这里参考《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采用的就是很朴素的算法。把问题设计的简单。我们可以站在这个角度补全一些资料不足,依然可以做一些猜测和判断。

小结

我们就算是明白了两个最难得部分。

  1. 方法如何查找 —— 超类链
  2. 常量如何查找 —— 词法作用域优先,超类链靠后,特别像文件系统

而且都可以用自省的方式去逐层确认

方法、常量这两个可以帮助我们去写一个 互不往来的类和方法了。也能工作。

但是想打破所谓的作用域门,作用域、作用域门、块,他们之间又是怎么工作的?

难点三 作用域和作用域门

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会遵循三个步骤

  1. 把每个方法体编译成独立的YARV指令片段(这个情况会发生在Ruby解析和编译程序的时候)
  2. 使用当前的词法作用域,来获取类或者模块指针(这种情况发生在Ruby执行程序的时候遇到了def关键字)
  3. 在该类的方法表中保存新的方法名 —— 实际上是保存对应方法名的整数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.newModule.newModule#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语言源码 (实在不想)。。。

Mark24

Everything can Mix.