Ruby 方法查找(与 JavaScript 比较)

Ruby Method Lookup (comparison with JavaScript)

我想更好地了解 Ruby 中的对象如何具有在 classes 和模块中定义的访问方法。具体来说,我想将其与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'

在 JavaScript 示例中,obj1obj2 都没有针对对象本身的 someMethod 函数。需要注意的关键是:

  1. child 对象中有一个 someMethod 函数的副本,obj1obj2 都委托给 child 对象。
  2. 这意味着 objobj2 都没有对象本身的 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'

这是我的问题:

  1. Ruby 代码中的 obj1obj2 每个 是否拥有 some_method 方法的副本?或者它是否类似于 JavaScript,其中两个对象都可以通过另一个对象访问 some_method(在本例中,通过子对象 class)?
  2. 类似地,当在 Ruby 中考虑继承时,是否每个 Ruby 对象都具有所有 class 对象的 副本 和 superclass 同名方法?

我的直觉告诉我 Ruby 对象 不要 具有从它们的 class 继承的方法的单独副本、混合模块和 super classes。相反,我的直觉是 Ruby 处理方法查找类似于 JavaScript,其中对象检查对象本身是否有方法,如果没有,它会在对象的 class 中查找方法,混合模块,并超级classes直到查找到达BasicObject.

让我们在 IRB 会话中继续使用您的示例,看看我们可能学到什么:

> obj1.method(:some_method)
=> #<Method: Child#some_method>
> obj1.method(:some_method).source_location
=> ["(irb)", 8]
> obj2.method(:some_method)
=> #<Method: Child#some_method>
> obj2.method(:some_method).source_location
=> ["(irb)", 8]

啊好的,所以相同 class 的两个对象具有相同的方法。我想知道这是否总是正确的...

> obj1.instance_eval do
>   def some_method
>     puts 'what is going on here?'
>   end
> end
=> nil
> obj1.some_method
what is going on here?
=> nil
> obj2.some_method
Inside Child
=> nil
> obj1.method(:some_method)
=> #<Method: #<Child:0x2b9c128>.some_method>
> obj1.method(:some_method).source_location
=> ["(irb)", 19]

嗯,这很有趣。

James Coglan 有一个不错的博客 post,它提供了比我在 https://blog.jcoglan.com/2013/05/08/how-ruby-method-dispatch-works/

上更好的解释

考虑这些因素何时重要也可能很有趣。想想这个系统有多少是解释器的实现细节,可以在 MRI、JRuby 和 Rubinius 中以不同方式处理,以及 Ruby 程序在全部。

更多深思

> obj1.instance_eval do
>  def some_method
>    puts "Inside Instance"
>    super
>  end
> end 
=> :some_method
Inside Instance
Inside Child
  1. Does obj1 and obj2 in the Ruby code each own a copy of the some_method method? Or is it similar to JavaScript where both objects have access to some_method via another object (in this case, via the Child class)?

你不知道。 Ruby 语言规范简单地说 "if you do this, that happens"。但是,它确实没有规定使 that 发生的特定方法。每个 Ruby 实现都可以按照它认为合适的方式自由实现,只要结果符合规范,规范不关心 如何 这些结果是获得。

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

  1. Similarly, when inheritance is taken into account in Ruby, does each Ruby object have a copy of all of the class and superclass methods of the same name?

同上

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

谁能说所有这些实现的工作方式完全相同?

My gut tells me that Ruby objects DO NOT have separate copies of the methods inherited from their class, mixed-in modules, and superclasses. Instead, my gut is that Ruby handles method lookup similarly to JavaScript, where objects check if the object itself has the method and if not, it looks up the method in the object's class, mixed-in modules, and superclasses until the lookup reaches BasicObject.

尽管我在上面写过,一个合理的假设,事实上,我所知道的实现方式(MRI、YARV、Rubinius、JRuby、铁Ruby、MagLev、Topaz) 工作。

想想如果不是会意味着什么。 String class 的每个实例都需要有自己的 String 的 116 个方法的副本。想想一个典型的 Ruby 程序中有多少 String

ruby -e 'p ObjectSpace.each_object(String).count'
# => 10013

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

这也将是一场同步噩梦! Ruby 允许您随时修改方法。如果我在 String class 中重新定义一个方法会怎样? Ruby 现在必须更新 该方法的每个 副本,甚至跨不同的线程。

而我实际上只统计了直接定义在String中的public个方法。考虑到私有方法,方法的数量就更多了。当然,还有继承:字符串不仅需要 String 中每个方法的副本,还需要 ComparableObject、[=35= 中每个方法的副本],以及 BasicObject。您能想象 系统中的每个对象 都有一个 require 的副本吗?

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

struct Object
  object_id: Id
  ivars: Dictionary<Symbol, *Object>
  class: *Class
end

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

struct Module
  methods: Dictionary<Symbol, *Method>
  constants: Dictionary<Symbol, *Object>
  cvars: Dictionary<Symbol, *Object>
end

一个class就像一个模块,但是它还有一个superclass:

struct Class
  methods: Dictionary<Symbol, *Method>
  constants: Dictionary<Symbol, *Object>
  cvars: Dictionary<Symbol, *Object>
  superclass: *Class
end

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

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

之外,还有自己的私有单例 class
struct Object
  object_id: Id
  ivars: Dictionary<Symbol, *Object>
  class: *Class
  singleton_class: Class
end

因此,方法查找在单例class中首先开始,只有然后进入class.

mixins 呢?哦,对了,每个模块和 class 还需要一个包含它的 mixins 的列表:

struct Module
  methods: Dictionary<Symbol, *Method>
  constants: Dictionary<Symbol, *Object>
  cvars: Dictionary<Symbol, *Object>
  mixins: List<*Module>
end

struct Class
  methods: Dictionary<Symbol, *Method>
  constants: Dictionary<Symbol, *Object>
  cvars: Dictionary<Symbol, *Object>
  superclass: *Class
  mixins: List<*Module>
end

现在,算法开始:首先查看单例 class,然后是 class,然后是超级 class(es),但是 "look"也意味着 "after you look at the method dictionary, also look at all the method dictionaries of the included mixins (and the included mixins of the included mixins, and so forth, recursively) before going up to the superclass".

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

一个对象不再有单例 class 和 class,它只有 class:

struct Object
  object_id: Id
  ivars: Dictionary<Symbol, *Object>
  class: *Class
  singleton_class: Class
end

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

struct Class
  methods: *Dictionary<Symbol, *Method>
  constants: *Dictionary<Symbol, *Object>
  cvars: *Dictionary<Symbol, *Object>
  superclass: *Class
  visible?: Bool
end

现在,对象的class指针将始终指向单例class,而单例class的超class指针将始终指向对象的实际class。如果你将 mixin M 包含到 class C 中,Ruby 将创建一个新的不可见的 class M′,它共享其方法、常量和带有 mixin 的 cvar 字典。这个mixinclass会变成C的superclass,而C的旧superclass会变成mixin的superclass class:

M′ = Class.new(
  methods = M->methods
  constants = M->constants
  cvars = M->cvars
  superclass = C->superclass
  visible? = false
)

C->superclass = *M'

实际上,它有点复杂,因为它还必须为包含在 M 中的混合创建 classes(并且递归),但最后,我们结束up with 是一个很好的线性方法查找路径,没有侧步进入单例 classes 和包含的 mixins。

现在,方法查找算法就是这样:

def lookup(meth, obj)
  c = obj->class

  until res = c->methods[meth]
    c = c->superclass
    raise MethodNotFound, meth if c.nil?
  end

  res
end

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

作为权衡,找出对象的 class 或 class 的超 class 稍微困难一些,因为您不能简单地 return class 或 superclass 指针,您必须遍历链,直到找到未隐藏的 class。但是您多久调用一次 Object#classClass#superclass?除了调试之外,你甚至会调用它吗?

不幸的是,Module#prepend 与这张图片不完全吻合。并且改进 确实 把事情搞砸了,这就是为什么许多 Ruby 实现甚至不实现它们的原因。