Динамический метод вызова в Ruby

Насколько мне известно, существует три способа динамического вызова метода в Ruby:

Способ 1:

s = SomeObject.new
method = s.method(:dynamic_method)
method.call

Способ 2:

s = SomeObject.new
s.send(:dynamic_method)

Способ 3:

s = SomeObject.new
eval "s.dynamic_method"

Сравнивая их, я установил, что метод 1, безусловно, самый быстрый, метод 2 медленнее, а метод 3 на сегодняшний день является самым медленным.

Я также обнаружил, что .call и .send позволяют разрешать частные методы, а eval - нет.

Итак, мой вопрос: есть ли какая-либо причина когда-либо использовать .send или eval? Почему бы вам не всегда использовать самый быстрый метод? Какие еще различия имеют эти методы вызова динамических методов?

Ответ 1

Есть ли какая-либо причина когда-либо использовать send?

call требуется объект метода, send не:

class Foo
  def method_missing(name)
    "#{name} called"
  end
end

Foo.new.send(:bar)         #=> "bar called"
Foo.new.method(:bar).call  #=> undefined method `bar' for class `Foo' (NameError)

Есть ли какая-либо причина когда-либо использовать eval?

eval оценивает произвольные выражения, это не только для вызова метода.


Что касается тестов, send кажется быстрее, чем method + call:

require 'benchmark'

class Foo
  def bar; end
end

Benchmark.bm(4) do |b|
  b.report("send") { 1_000_000.times { Foo.new.send(:bar) } }
  b.report("call") { 1_000_000.times { Foo.new.method(:bar).call } }
end

Результат:

           user     system      total        real
send   0.210000   0.000000   0.210000 (  0.215181)
call   0.740000   0.000000   0.740000 (  0.739262)

Ответ 2

Подумайте об этом так:

Способ 1 (метод .call): одиночное время выполнения

Если вы запускаете Ruby один раз в своей программе прямо, вы управляете всей системой, и вы можете удерживать "указатель на свой метод" с помощью метода "method.call". Все, что вы делаете, это держать ручку "живой код", который вы можете запускать, когда захотите. Это в основном так же быстро, как вызов метода непосредственно из объекта (но это не так быстро, как использование object.send - см. Тесты в других ответах).

Способ 2 (object.send): сохранить имя метода в базе данных

Но что, если вы хотите сохранить имя метода, который хотите вызвать в базе данных, и в будущем приложении, которое вы хотите назвать этим именем метода, просмотрев его в базе данных? Затем вы будете использовать второй подход, который заставляет ruby ​​вызывать произвольное имя метода, используя ваш второй подход "s.send(: dynamic_method)".

Метод 3 (eval): код метода самомодификации

Что делать, если вы хотите написать/изменить/перенести код в базу данных таким образом, чтобы запустить этот метод как новый код? Вы можете периодически изменять код, записанный в базу данных, и каждый раз запускать его как новый. В этом (очень необычном случае) вы хотели бы использовать свой третий подход, который позволяет вам написать код метода в виде строки, загрузить его в какой-то более поздний срок и запустить его целиком.

Для чего это стоит, как правило, в мире Ruby рассматривается как плохая форма использования Eval (метод 3), за исключением очень, очень эзотерических и редких случаев. Поэтому вы должны придерживаться методов 1 и 2 для почти всех проблем, с которыми вы сталкиваетесь.

Ответ 3

Я обновил бенчмарк от @Stefan, чтобы проверить, есть ли некоторые улучшения скорости при сохранении ссылки на метод. Но снова - send намного быстрее, чем call

require 'benchmark'

class Foo
  def bar; end
end

foo = Foo.new
foo_bar = foo.method(:bar)

Benchmark.bm(4) do |b|
  b.report("send") { 1_000_000.times { foo.send(:bar) } }
  b.report("call") { 1_000_000.times { foo_bar.call } }
end

Вот результаты:

           user     system      total        real
send   0.080000   0.000000   0.080000 (  0.088685)
call   0.110000   0.000000   0.110000 (  0.108249)

Так что send, похоже, будет тем, что нужно сделать.

Ответ 4

Вот все возможные вызовы методов:

require 'benchmark/ips'

class FooBar
  def name; end
end

el = FooBar.new

Benchmark.ips do |x|
  x.report('plain') { el.name }
  x.report('eval') { eval('el.name') }
  x.report('method call') { el.method(:name).call }
  x.report('send sym') { el.send(:name) }
  x.report('send str') { el.send('name') }
  x.compare!
end

И результаты:

Warming up --------------------------------------
               plain   236.448k i/100ms
                eval    20.743k i/100ms
         method call   131.408k i/100ms
            send sym   205.491k i/100ms
            send str   168.137k i/100ms
Calculating -------------------------------------
               plain      9.150M (± 6.5%) i/s -     45.634M in   5.009566s
                eval    232.303k (± 5.4%) i/s -      1.162M in   5.015430s
         method call      2.602M (± 4.5%) i/s -     13.009M in   5.010535s
            send sym      6.729M (± 8.6%) i/s -     33.495M in   5.016481s
            send str      4.027M (± 5.7%) i/s -     20.176M in   5.027409s

Comparison:
               plain:  9149514.0 i/s
            send sym:  6729490.1 i/s - 1.36x  slower
            send str:  4026672.4 i/s - 2.27x  slower
         method call:  2601777.5 i/s - 3.52x  slower
                eval:   232302.6 i/s - 39.39x  slower

Ожидалось, что простой вызов будет самым быстрым, никаких дополнительных распределений, поиска по символам, просто поиска и оценки метода.

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

То же самое можно сказать и о method(:name) (1), чтобы выделить память для объекта Proc (2), мы вызываем метод в классе, который приводит к дополнительному поиску метода, который также требует времени.

eval запускает интерпретатор, поэтому он самый тяжелый.

Ответ 5

Вся точка send и eval заключается в том, что вы можете изменить команду динамически. Если метод, который вы хотите выполнить, исправлен, вы можете жестко связать этот метод без использования send или eval.

receiver.fixed_method(argument)

Но если вы хотите вызывать метод, который изменяется или вы не знаете заранее, то вы не можете написать это напрямую. Следовательно, использование send или eval.

receiver.send(method_that_changes_dynamically, argument)
eval "#{code_to_evaluate_that_changes_more_dramatically}"

Дополнительное использование send заключается в том, что, как вы заметили, вы можете вызвать метод с явным приемником, используя send.