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