Llamada a un método dinámico en Ruby
metaprogramming dynamic-method (5)
Por lo que sé, hay tres formas de llamar dinámicamente un método en Ruby:
Método 1:
s = SomeObject.new
method = s.method(:dynamic_method)
method.call
Método 2:
s = SomeObject.new
s.send(:dynamic_method)
Método 3:
s = SomeObject.new
eval "s.dynamic_method"
Al compararlos, he establecido que el Método 1 es, con mucho, el más rápido, el Método 2 es más lento y el Método 3 es con mucho el más lento.
También encontré que .call
y .send
permiten llamar a métodos privados, mientras que eval
no.
Entonces mi pregunta es: ¿hay alguna razón para usar .send
o eval
? ¿Por qué no siempre usarías el método más rápido? ¿Qué otras diferencias tienen estos métodos para llamar a los métodos dinámicos?
¿Hay alguna razón para usar
send
?
call
necesita un objeto de método, el send
no:
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)
¿Hay alguna razón para usar alguna vez
eval
?
eval
evalúa expresiones arbitrarias, no es solo para llamar a un método.
En cuanto a los puntos de referencia, el send
parece ser más rápido que el 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
Resultado:
user system total real
send 0.210000 0.000000 0.210000 ( 0.215181)
call 0.740000 0.000000 0.740000 ( 0.739262)
Actualicé el punto de referencia de @Stefan para verificar si hay algunas mejoras de velocidad al guardar la referencia al método. Pero de nuevo, send
es mucho más rápido que 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
Estos son los resultados:
user system total real
send 0.080000 0.000000 0.080000 ( 0.088685)
call 0.110000 0.000000 0.110000 ( 0.108249)
Así que send
parece ser el que tomar.
Aquí están todas las llamadas a métodos posibles:
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
Y los resultados son:
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
Se espera que la llamada simple sea la más rápida, sin asignaciones adicionales, búsquedas de símbolos, solo búsqueda y evaluación del método.
En cuanto a send
través de un símbolo, es más rápido que a través de una cadena ya que es mucho más fácil asignar memoria para el símbolo. Una vez que está definido, se almacena a largo plazo en la memoria y no hay reasignaciones.
La misma razón se puede decir sobre el method(:name)
(1) es necesario asignar memoria para el objeto Proc
(2) estamos llamando al método en la clase que conduce a la búsqueda de métodos adicionales, que también lleva tiempo.
eval
se ejecuta intérprete por lo que es el más pesado.
El objetivo de send
y eval
es que puedes cambiar el comando de forma dinámica. Si el método que desea ejecutar es fijo, entonces puede cablear ese método sin utilizar send
o eval
.
receiver.fixed_method(argument)
Pero cuando desea invocar un método que varía o no sabe de antemano, no puede escribirlo directamente. De ahí el uso de send
o eval
.
receiver.send(method_that_changes_dynamically, argument)
eval "#{code_to_evaluate_that_changes_more_dramatically}"
El uso adicional de send
es que, como habrás notado, puedes llamar a un método con un receptor explícito usando send
.
Piénsalo de esta manera:
Método 1 (method.call): Single run-time
Si ejecuta Ruby una vez en su programa directamente, usted controla todo el sistema y puede mantener un "puntero a su método" a través del método "method.call". Todo lo que está haciendo es mantener un identificador de "código en vivo" que puede ejecutar cuando lo desee. Esto es básicamente tan rápido como llamar al método directamente desde el objeto (pero no es tan rápido como usar object.send - ver benchmarks en otras respuestas).
Método 2 (object.send): persistir el nombre del método en la base de datos
Pero, ¿qué ocurre si desea almacenar el nombre del método que desea llamar en una base de datos y, en una aplicación futura, desea llamar al nombre de ese método buscándolo en la base de datos? Luego usaría el segundo enfoque, que hace que Ruby llame a un nombre de método arbitrario usando su segundo enfoque "s.send (: dynamic_method)".
Método 3 (eval): código de método de auto modificación
¿Qué sucede si quiere escribir / modificar / persistir el código en una base de datos de una manera que ejecute el método como un código nuevo? Puede modificar periódicamente el código escrito en la base de datos y querer que se ejecute como código nuevo cada vez. En este (caso muy inusual), querrá utilizar su tercer enfoque, que le permite escribir su código de método como una cadena, cargarlo de nuevo en una fecha posterior y ejecutarlo en su totalidad.
Por lo que vale, generalmente se considera en el mundo de Ruby como una forma mala de usar Eval (método 3) excepto en casos muy, muy esotéricos y raros. Por lo tanto, debe seguir con los métodos 1 y 2 para casi todos los problemas que encuentre.