Ruby方法查找(与JavaScript进行比较)

wmo*_*ock 5 javascript ruby inheritance prototypal-inheritance

我想更好地了解Ruby中的对象如何具有在类和模块中定义的访问方法。具体来说,我想将其与JavaScript(我比较熟悉)进行比较和对比。

在JavaScript中,对象会在对象本身上查找方法,如果找不到,则会在原型对象上查找方法。此过程将一直持续到Object.prototype

// JavaScript Example
var parent = {
  someMethod: function () {
    console.log( 'Inside Parent' );
  }
};

var child = Object.create( parent );
child.someMethod = function () {
  console.log( 'Inside Child' );
};

var obj1 = Object.create( child );
var obj2 = Object.create( child );

obj1.someMethod(); // 'Inside Child'
obj2.someMethod(); // 'Inside Child'
Run Code Online (Sandbox Code Playgroud)

在JavaScript示例中,obj1obj2都不具有someMethod对象本身的功能。需要注意的关键是:

  1. someMethodchild对象中有一个函数的副本,并且两者都有obj1obj2委托给该child对象。
  2. 这意味着在对象本身上既obj没有功能也obj2没有副本someMethod
  3. 如果child对象没有someMethod定义函数,则委托将继续到该parent对象。

现在,我想将其与Ruby中的类似示例进行对比:

# Ruby Example
class Parent
  def some_method
    put 'Inside Parent'
  end
end

class Child < Parent
  def some_method
    puts 'Inside Child'
  end
end

obj1 = Child.new
obj2 = Child.new

obj1.some_method  # 'Inside Child'
obj2.some_method  # 'Inside Child'
Run Code Online (Sandbox Code Playgroud)

这是我的问题:

  1. 是否obj1obj2在Ruby代码的每个中的自己的副本some_method的方法?还是类似于JavaScript,其中两个对象都可以some_method通过另一个对象(在这种情况下,通过Child类)进行访问?
  2. 同样,当在Ruby中考虑继承时,每个Ruby对象是否都具有相同名称的所有类和超类方法的副本

我的直觉告诉我,Ruby对象没有从其类,混合模块和超类继承的方法的单独副本。相反,我的直觉是Ruby与JavaScript相似地处理方法查找,即对象检查对象本身是否具有该方法,如果不是,则在对象的类,混合模块和超类中查找该方法,直到查找达到为止BasicObject

Jör*_*tag 2

\n
    \n
  1. Ruby 代码中obj1和是否各自拥有该方法的副本?或者它是否类似于 JavaScript,其中两个对象都可以通过另一个对象访问(在本例中,通过 Child 类)?obj2some_methodsome_method
  2. \n
\n
\n\n

你不知道。Ruby 语言规范简单地说“如果你这样做,就会发生这种情况”。然而,它并没有规定实现这一目标的特定方法。每个 Ruby 实现都可以自由地以它认为合适的方式实现它,只要结果与规范相匹配,规范并不关心这些结果是如何获得的。

\n\n

你说不出来。如果实现保持适当的抽象,您将不可能知道他们是如何做到的。这就是抽象的本质。(事实上​​,这几乎就是抽象的定义。)

\n\n
\n
    \n
  1. 同样,当在 Ruby 中考虑继承时,每个 Ruby 对象是否都具有所有同名类和超类方法的副本?
  2. \n
\n
\n\n

与上面相同。

\n\n

很多实现,过去甚至有更多,处于不同(不)完整的阶段。其中一些实现了自己的对象模型(例如 MRI、YARV、Rubinius、MRuby、Topaz、tinyrb、RubyGoLightly),一些位于它们试图适应的现有对象模型之上(例如 XRuby 和 JRuby) CLI 上的 Java、Ruby.NET 和 IronRuby,SmallRuby 上的 SmallRuby、smalltalk.rb、Alumina 和 MagLev,Objective-C/Cocoa 上的 MacRuby 和 RubyMotion,Parrot 上的 Cardinal,ActionScript/Flash 上的 Red Sun,SAP/ABAP 上的 BlueRuby 、ECMAScript 上的 HotRuby 和 Opal.rb)

\n\n

谁敢说所有这些实现的工作原理完全相同?

\n\n
\n

我的直觉告诉我,Ruby 对象没有从其类、混合模块和超类继承的方法的单独副本。相反,我的直觉是,Ruby 处理方法查找的方式与 JavaScript 类似,其中对象检查对象本身是否具有该方法,如果没有,它将在对象的类、混合模块和超类中查找该方法,直到查找到达BasicObject.

\n
\n\n

不管我上面写了什么,这一个合理的假设,事实上,我所知道的实现(MRI、YARV、Rubinius、JRuby、IronRuby、MagLev、Topaz)是如何工作的。

\n\n

想想如果不是这样的话那意味着什么。该类的每个实例都String需要有自己的String116 个方法的副本。想想String一个典型的 Ruby 程序中有多少个 s!

\n\n
ruby -e \'p ObjectSpace.each_object(String).count\'\n# => 10013\n
Run Code Online (Sandbox Code Playgroud)\n\n

即使在这个最简单的程序中,它没有require任何库,并且本身只创建一个字符串(用于将数字打印到屏幕上),也已经有超过10000 个字符串。其中每一个都会有 100 多个String方法的自己的副本。这将是巨大的内存浪费。

\n\n

这也将是一场同步噩梦!Ruby 允许您随时对方法进行猴子修补。如果我在String类中重新定义一个方法怎么办?Ruby 现在必须更新该方法的每个副本,即使跨不同的线程也是如此。

\n\n

而且我实际上只计算了直接在String. 考虑到私有方法,方法的数量就更多了。当然,还有继承:字符串不仅需要 中每个方法的副本,还需要、、和String中每个方法的副本。你能想象系统中的每个对象都有一个副本吗ComparableObjectKernelBasicObjectrequire吗?

\n\n

不,它在大多数 Ruby 实现中的工作方式是这样的。一个对象有一个标识、实例变量和一个类(在静态类型的伪 Ruby 中):

\n\n
struct Object\n  object_id: Id\n  ivars: Dictionary<Symbol, *Object>\n  class: *Class\nend\n
Run Code Online (Sandbox Code Playgroud)\n\n

一个模块有一个方法字典、一个常量字典和一个类变量字典:

\n\n
struct Module\n  methods: Dictionary<Symbol, *Method>\n  constants: Dictionary<Symbol, *Object>\n  cvars: Dictionary<Symbol, *Object>\nend\n
Run Code Online (Sandbox Code Playgroud)\n\n

类就像一个模块,但它也有一个超类:

\n\n
struct Class\n  methods: Dictionary<Symbol, *Method>\n  constants: Dictionary<Symbol, *Object>\n  cvars: Dictionary<Symbol, *Object>\n  superclass: *Class\nend\n
Run Code Online (Sandbox Code Playgroud)\n\n

当您调用对象的方法时,Ruby 将查找该对象的class指针并尝试找到该方法。如果没有,它将查看类的superclass指针,依此类推,直到找到没有超类的类。此时它实际上不会放弃,而是尝试调用method_missing原始对象上的方法,并传递您尝试调用的方法的名称作为参数,但这也只是一个普通的方法调用,所以它遵循所有相同的规则(除了如果调用到达method_missing层次结构的顶部,它将不会尝试再次调用它,这将导致无限循环)。

\n\n

哦,但是我们忽略了一件事:单例方法!每个对象也需要有自己的方法字典。实际上,每个对象除了它的类之外还有它自己的私有单例类:

\n\n
struct Object\n  object_id: Id\n  ivars: Dictionary<Symbol, *Object>\n  class: *Class\n  singleton_class: Class\nend\n
Run Code Online (Sandbox Code Playgroud)\n\n

因此,方法查找首先在单例类中开始,然后才转到该类。

\n\n

那么 mixins 呢?哦,对了,每个模块和类还需要一个包含的 mixin 列表:

\n\n
struct Module\n  methods: Dictionary<Symbol, *Method>\n  constants: Dictionary<Symbol, *Object>\n  cvars: Dictionary<Symbol, *Object>\n  mixins: List<*Module>\nend\n\nstruct Class\n  methods: Dictionary<Symbol, *Method>\n  constants: Dictionary<Symbol, *Object>\n  cvars: Dictionary<Symbol, *Object>\n  superclass: *Class\n  mixins: List<*Module>\nend\n
Run Code Online (Sandbox Code Playgroud)\n\n

现在,算法是这样的:首先查看单例类,然后是类,然后是超类,但是,“查看”也意味着“在查看方法字典之后,还要查看该类的所有方法字典”。在进入超类之前包含 mixins(以及包含的 mixin 的包含的 mixin,等等,递归地) ”。

\n\n

听起来很复杂吗?这是!那可不好。方法查找是面向对象系统中最常执行的算法,它需要简单且快速。因此,一些 Ruby 实现(例如 MRI、YARV)所做的就是将解释器关于“类”和“超类”含义的内部概念与程序员对这些相同概念的看法分开。

\n\n

一个对象不再同时具有单例类和类,它只具有一个类:

\n\n
struct Object\n  object_id: Id\n  ivars: Dictionary<Symbol, *Object>\n  class: *Class\n  singleton_class: Class\nend\n
Run Code Online (Sandbox Code Playgroud)\n\n

一个类不再有包含的 mixins 列表,而只是一个超类。然而,它可能是隐藏的。另请注意,字典变成了指针,稍后您就会明白为什么:

\n\n
struct Class\n  methods: *Dictionary<Symbol, *Method>\n  constants: *Dictionary<Symbol, *Object>\n  cvars: *Dictionary<Symbol, *Object>\n  superclass: *Class\n  visible?: Bool\nend\n
Run Code Online (Sandbox Code Playgroud)\n\n

现在,对象的类指针将始终指向单例类,而单例类的超类指针将始终指向对象的实际类。如果您将 mixin 包含M到类中C,Ruby 将创建一个新的不可见类M\xe2\x80\xb2,该类与 mixin 共享其方法、常量和 cvar 字典。这个 mixin 类将成为 的超类C,以及 的旧超类C将成为 mixin 类的超类:

\n\n
M\xe2\x80\xb2 = Class.new(\n  methods = M->methods\n  constants = M->constants\n  cvars = M->cvars\n  superclass = C->superclass\n  visible? = false\n)\n\nC->superclass = *M\'\n
Run Code Online (Sandbox Code Playgroud)\n\n

实际上,它涉及更多一点,因为它还必须为包含在中的 mixin 创建类M(并且递归地)中的 mixins 创建类,但最终,我们最终得到的是一个很好的线性方法查找路径,没有边-进入单例类并包含混合。

\n\n

现在,方法查找算法如下:

\n\n
def lookup(meth, obj)\n  c = obj->class\n\n  until res = c->methods[meth]\n    c = c->superclass\n    raise MethodNotFound, meth if c.nil?\n  end\n\n  res\nend\n
Run Code Online (Sandbox Code Playgroud)\n\n

漂亮、干净、精简、快速。

\n\n

作为权衡,找出对象的类或类的超类稍微困难一些,因为你不能简单地返回类或超类指针,你必须遍历整个链,直到找到一个类没有隐藏。但你多久打电话Object#class一次Class#superclass?除了调试之外,您是否还调用过它?

\n\n

不幸的是,Module#prepend它并不完全符合这张图片。细化确实会把事情搞砸,这就是为什么许多 Ruby 实现甚至不实现它们。

\n