Поиск метода Ruby (сравнение с JavaScript)

Я хочу лучше понять, как объекты в 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'

В примере с JavaScript и obj1, и obj2 не имеют функции someMethod в самом объекте. Ключевым моментом является то, что:

  1. В объекте child есть одна копия функции someMethod, и обе функции obj1 и obj2 делегируют объекту 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'

Вот мои вопросы:

  1. Имеют ли obj1 и obj2 в коде Ruby каждый экземпляр метода some_method? Или это похоже на JavaScript, где оба объекта имеют доступ к some_method через другой объект (в данном случае через класс Child)?
  2. Точно так же, когда в Ruby учитывается наследование, имеет ли каждый объект Ruby копию всех одноименных методов класса и суперкласса?

Моя интуиция подсказывает мне, что объекты Ruby НЕ имеют отдельные копии методов, унаследованных от их класса, смешанных модулей и суперклассов. Вместо этого я считаю, что Ruby обрабатывает поиск метода аналогично JavaScript, где объекты проверяют, есть ли у самого объекта метод, и если нет, он ищет метод в классе объекта, смешанных модулях и суперклассах, пока поиск не достигнет BasicObject .


person wmock    schedule 06.02.2015    source источник


Ответы (3)


  1. Имеют ли obj1 и obj2 в коде Ruby каждый экземпляр метода some_method? Или это похоже на JavaScript, где оба объекта имеют доступ к some_method через другой объект (в данном случае через класс Child)?

Вы не знаете. Спецификация языка Ruby просто говорит: «Если вы делаете это, происходит то». Однако он не предписывает какой-либо конкретный способ, чтобы это произошло. Каждая реализация Ruby может реализовывать ее так, как считает нужным, пока результаты совпадают со спецификацией, спецификации не важно, как были получены эти результаты.

Вы не можете сказать. Если реализация поддерживает правильную абстракцию, вы не сможете сказать, как они это делают. Такова природа абстракции. (На самом деле это почти определение абстракции.)

  1. Точно так же, когда в Ruby учитывается наследование, имеет ли каждый объект Ruby копию всех одноименных методов класса и суперкласса?

То же, что и выше.

В настоящее время существует много реализаций Ruby, а в прошлом их было еще больше, на разных стадиях (не)завершенности. Некоторые из них реализуют свои собственные объектные модели (например, MRI, YARV, Rubinius, MRuby, Topaz, tinyrb, RubyGoLightly), некоторые располагаются поверх существующей объектной модели, в которую они пытаются вписаться (например, XRuby и JRuby на Java, Ruby.NET и IronRuby в CLI, SmallRuby, smalltalk.rb, Alumina и MagLev в Smalltalk, MacRuby и RubyMotion в Objective-C/Cocoa, Cardinal в Parrot, Red Sun в ActionScript/Flash, BlueRuby в SAP/ABAP , HotRuby и Opal.rb на ECMAScript)

Кто сказал, что все эти реализации работают одинаково?

Моя интуиция подсказывает мне, что объекты Ruby НЕ имеют отдельные копии методов, унаследованных от их класса, смешанных модулей и суперклассов. Вместо этого я считаю, что Ruby обрабатывает поиск метода аналогично JavaScript, где объекты проверяют, есть ли у самого объекта метод, и если нет, он ищет метод в классе объекта, смешанных модулях и суперклассах, пока поиск не достигнет BasicObject .

Несмотря на то, что я написал выше, это является разумным предположением, и на самом деле это то, как работают известные мне реализации (MRI, YARV, Rubinius, JRuby, IronRuby, MagLev, Topaz).

Просто подумайте, что бы это значило, если бы это было не так. Каждый экземпляр класса String должен иметь собственную копию всех 116 методов String. Подумайте, сколько String в типичной программе Ruby!

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

Даже в этой простейшей программе, которая не использует require никаких библиотек и сама создает только одну строку (для вывода числа на экран), уже содержится более 10000 строк. Каждый из них будет иметь свои собственные копии более 100 методов String. Это было бы огромной тратой памяти.

Это также был бы кошмар синхронизации! Ruby позволяет вам в любое время изменять методы. Что, если я переопределю метод в классе String? Теперь Ruby придется обновлять каждую копию этого метода, даже в разных потоках.

И на самом деле я учитывал только общедоступные методы, определенные непосредственно в String. С учетом приватных методов количество методов еще больше. И, конечно же, есть наследование: строкам потребуется не только копия каждого метода в String, но и копия каждого метода в Comparable, Object, Kernel и BasicObject. Можете ли вы представить, что каждый объект в системе имеет копию require?

Нет, так это работает в большинстве реализаций Ruby. У объекта есть идентификатор, переменные экземпляра и класс (в псевдо-Ruby со статическим типом):

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

Модуль имеет словарь методов, словарь констант и словарь переменных класса:

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

Класс подобен модулю, но у него также есть суперкласс:

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

Когда вы вызываете метод для объекта, Ruby ищет указатель объекта class и пытается найти там метод. Если это не так, он будет смотреть на указатель класса superclass и так далее, пока не достигнет класса, у которого нет суперкласса. В этот момент он на самом деле не сдастся, а попытается вызвать метод method_missing для исходного объекта, передав имя метода, который вы пытались вызвать, в качестве аргумента, но это тоже обычный вызов метода, поэтому он следует всем те же правила (за исключением того, что если вызов method_missing достигает вершины иерархии, он не будет пытаться вызвать его снова, что приведет к бесконечному циклу).

О, но мы проигнорировали одну вещь: одноэлементные методы! Каждый объект также должен иметь свой собственный словарь методов. На самом деле, скорее, у каждого объекта есть свой собственный класс singleton в дополнение к своему классу:

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

Таким образом, поиск метода начинается сначала в классе синглтона и только затем переходит к классу.

А как же миксины? Ах да, каждому модулю и классу тоже нужен список включенных миксинов:

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

Теперь алгоритм следующий: посмотрите сначала в одноэлементном классе, затем в классе, а затем в суперклассе (ах), где, однако, «смотреть» также означает «после того, как вы просмотрите словарь методов, также просмотрите все словари методов класса». включенные примеси (и включенные примеси включенных примесей и т. д., рекурсивно) перед переходом к суперклассу".

Звучит сложно? Это! И это нехорошо. Поиск метода — единственный наиболее часто выполняемый алгоритм в объектно-ориентированной системе, он должен быть простым и молниеносным. Итак, то, что делают некоторые реализации Ruby (например, MRI, YARV), заключается в том, чтобы отделить внутреннее представление интерпретатора о том, что означают «класс» и «суперкласс», от взгляда программиста на те же самые концепции.

У объекта больше нет и одноэлементного класса, и класса, у него просто есть класс:

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

У класса больше нет списка включенных примесей, только суперкласс. Однако это может быть скрыто. Обратите также внимание, что словари становятся указателями, и вы вскоре поймете, почему:

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

Теперь указатель класса объекта всегда будет указывать на класс-одиночку, а указатель надкласса класса-одиночки всегда будет указывать на фактический класс объекта. Если вы включите примесь M в класс C, Ruby создаст новый невидимый класс M′, который будет использовать свой метод, константы и словари переменных вместе с примесью. Этот класс миксина станет суперклассом C, а старый суперкласс C станет суперклассом класса миксина:

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

C->superclass = *M'

На самом деле, это немного сложнее, так как он также должен создавать классы для миксинов, которые включены в M (и рекурсивно), но в итоге мы получаем хороший линейный путь поиска метода без обходных шагов. в одноэлементные классы и включены миксины.

Алгоритм поиска метода таков:

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

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

  res
end

Красиво и чисто, скудно и быстро.

В качестве компромисса, узнать класс объекта или суперкласс класса немного сложнее, потому что вы не можете просто вернуть указатель класса или суперкласса, вы должны пройти по цепочке, пока не найдете класс, который не спрятанный. Но как часто вы звоните Object#class или Class#superclass? Вы вообще его вызываете, кроме отладки?

К сожалению, Module#prepend не очень хорошо вписывается в эту картину. А уточнения на самом деле все портят, поэтому многие реализации Ruby их даже не реализуют.

person Jörg W Mittag    schedule 06.02.2015
comment
Спасибо @Jorg W Mittag за подробный ответ! Ваше объяснение было действительно полезным - по крайней мере, оно подтверждает мою ментальную модель того, как объекты Ruby ищут методы. - person wmock; 06.02.2015

Давайте продолжим работать с вашим примером в сеансе 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]

Итак, два объекта одного класса имеют один и тот же метод. Интересно, всегда ли это так...

> 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]

Что ж, это интересно.

У Джеймса Коглана есть хороший пост в блоге, который предлагает лучшее объяснение большей части этого, чем я на https://blog.jcoglan.com/2013/05/08/how-ruby-method-dispatch-работает/

Также может быть интересно рассмотреть, когда что-то из этого важно. Подумайте, какая часть этой системы является деталью реализации интерпретатора и может обрабатываться по-разному в MRI, JRuby и Rubinius, и что на самом деле должно быть согласованным, чтобы программа на Ruby выполнялась последовательно во всех них.

person Jonah    schedule 06.02.2015
comment
Спасибо @Jonah за ответ! Что касается вашего второго примера кода, добавляет ли instance_eval одноэлементный метод к obj1? Если это так, то поведение obj1 будет отличаться от поведения obj2, поскольку его функция 'some_method' затенена. Это правильный способ думать об этом? - person wmock; 06.02.2015
comment
Это кажется мне разумной ментальной моделью. Я не могу обещать, что данный интерпретатор реализует это. - person Jonah; 07.02.2015

Больше пищи для размышлений

> obj1.instance_eval do
>  def some_method
>    puts "Inside Instance"
>    super
>  end
> end 
=> :some_method
Inside Instance
Inside Child
person Steve Wilhelm    schedule 06.02.2015
comment
Спасибо за комментарий @Steve Wilhelm! Я предполагаю, что instance_eval добавляет одноэлементный метод к obj1, верно? В этом случае выполнение some_method для obj1 сначала запустит метод singleton, а затем вызовет super для метода, определенного в классе Child. - person wmock; 06.02.2015