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


person Abraham P    schedule 03.07.2013    source источник
comment
Хорошие конструктивные вопросы :) +1..   -  person Arup Rakshit    schedule 03.07.2013
comment
@Abraham +1 отличный вопрос!   -  person feralin    schedule 03.07.2013
comment
Возможно, вы захотите включить результаты тестов и исправить опечатку в примере кода для метода 2.   -  person mu is too short    schedule 03.07.2013
comment
Я также обнаружил, что .call и .send позволяют вызывать приватные методы, а eval — нет. Вы можете использовать .public_send вместо .send, чтобы запретить вызов приватных методов.   -  person Dennis    schedule 11.09.2014


Ответы (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)
person Stefan    schedule 03.07.2013

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

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

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

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

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

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

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

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

person Steve Midgley    schedule 04.07.2013
comment
спасибо за продуманный и информативный ответ, я проголосовал. Я даю принятый ответ Стефану ниже за то, что он включил набор тестов, поднял method_missing и ответил первым. Однако, спасибо! - person Abraham P; 05.07.2013

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

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 запускает интерпретатор, поэтому он самый тяжелый.

person mpospelov    schedule 01.06.2017

Я обновил тест от @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, похоже, тот, кого нужно взять.

person Tom Freudenberg    schedule 18.03.2015
comment
Это более точный ориентир. Спасибо за публикацию. - person Andy Gauge; 11.11.2016

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

person sawa    schedule 03.07.2013