Динамический вызов метода в 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? Почему бы вам не всегда использовать самый быстрый метод? Какие еще различия есть у этих методов вызова динамических методов?

5 ответов

Решение

есть ли причина когда-либо использовать 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)

Думайте об этом так:

Метод 1 (method.call): одиночная среда выполнения

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

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

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

Метод 3 (eval): самоизменяющийся код метода

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

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

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

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 object (2) мы вызываем метод в классе, который ведет к поиску дополнительного метода, который тоже требует времени.

eval Это работает переводчик, так что это самый тяжелый.

Я обновил тест от @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 кажется, тот, который нужно взять.

Весь смысл 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,

Другие вопросы по тегам