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