Динамический вызов метода в 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 66

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 (метод.call): Single run-time

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

Способ 2 (Объект.send): сохранение имени метода в базе данных

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

Метод 3 (eval): Самомодифицирующийся код метода

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

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

я обновил тест от @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 Кажется, тот, чтобы взять.

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

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 работает переводчик, так что это самый тяжелый.

смысл 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.