Autant que je sache, il existe trois manières d’appeler dynamicment une méthode dans Ruby:
Méthode 1:
s = SomeObject.new method = s.method(:dynamic_method) method.call
Méthode 2:
s = SomeObject.new s.send(:dynamic_method)
Méthode 3:
s = SomeObject.new eval "s.dynamic_method"
En les comparant, j’ai établi que la méthode 1 est de loin la plus rapide, que la méthode 2 est plus lente et que la méthode 3 est de loin la plus lente.
J’ai également constaté que .send
et .send
permettent tous deux d’appeler des méthodes privées, alors que eval
ne le permet pas.
Donc, ma question est la suivante: y a-t-il une raison d’utiliser jamais .send
ou eval
? Pourquoi ne pas toujours utiliser la méthode la plus rapide? Quelles sont les autres différences entre ces méthodes d’appel de méthodes dynamics?
Y a-t-il une raison d’utiliser jamais
send
?
call
nécessite un object méthode, send
not:
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)
Y a-t-il une raison d’utiliser
eval
?
eval
évalue les expressions arbitraires, ce n’est pas juste pour appeler une méthode.
En ce qui concerne les tests de performance, l’ send
semble être plus rapide que la 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
Résultat:
user system total real send 0.210000 0.000000 0.210000 ( 0.215181) call 0.740000 0.000000 0.740000 ( 0.739262)
Pense-y de cette façon:
Si vous exécutez Ruby une fois sur votre programme, vous contrôlez l’ensemble du système et vous pouvez conserver un “pointeur sur votre méthode” via l’approche “method.call”. Tout ce que vous faites, c’est garder un pseudo pour “live code” que vous pouvez exécuter quand vous voulez. Ceci est fondamentalement aussi rapide que d’appeler la méthode directement depuis l’object (mais ce n’est pas aussi rapide que d’utiliser object.send – voir les tests dans d’autres réponses).
Mais que se passe-t-il si vous souhaitez stocker le nom de la méthode que vous souhaitez appeler dans une firebase database et dans une future application que vous souhaitez appeler le nom de cette méthode en la recherchant dans la firebase database? Ensuite, vous utiliseriez la seconde approche, qui fait que ruby appelle un nom de méthode arbitraire en utilisant votre deuxième approche “s.send (: dynamic_method)”.
Que faire si vous voulez écrire / modifier / persister du code dans une firebase database de manière à exécuter la méthode en tant que tout nouveau code? Vous pouvez modifier périodiquement le code écrit dans la firebase database et souhaitez qu’il s’exécute en tant que nouveau code à chaque fois. Dans ce cas (très inhabituel), vous voudriez utiliser votre troisième approche, qui vous permet d’écrire votre code de méthode sous forme de chaîne, de le recharger ultérieurement et de l’exécuter intégralement.
Pour ce que cela vaut, généralement, dans le monde Ruby, il est considéré comme une mauvaise forme pour utiliser Eval (méthode 3) sauf dans des cas très, très ésotériques et rares. Donc, vous devriez vraiment vous en tenir aux méthodes 1 et 2 pour presque tous les problèmes que vous rencontrez.
J’ai mis à jour le benchmark de @Stefan pour vérifier s’il existe des améliorations de vitesse lors de la sauvegarde de la référence à la méthode. Mais encore une fois, l’ send
est beaucoup plus rapide que l’ 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
Ce sont les résultats:
user system total real send 0.080000 0.000000 0.080000 ( 0.088685) call 0.110000 0.000000 0.110000 ( 0.108249)
Donc, send
semble être celui à prendre.
Voici tous les appels de méthode possibles:
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
Et les résultats sont:
Warming up -------------------------------------- plain 236.448ki/100ms eval 20.743ki/100ms method call 131.408ki/100ms send sym 205.491ki/100ms send str 168.137ki/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
Il est prévu que l’appel simple soit le plus rapide, aucune allocation supplémentaire, la recherche de symboles, la recherche et l’évaluation de la méthode.
En ce qui concerne l’ send
via le symbole, il est plus rapide que via la chaîne car il est beaucoup plus facile d’allouer de la mémoire pour le symbole. Une fois défini, il est stocké en mémoire à long terme et il n’y a pas de réaffectation.
On peut en dire autant de la method(:name)
(1) nécessaire pour allouer de la mémoire pour l’object Proc
(2). Nous appelons la méthode en classe qui conduit à une recherche de méthode supplémentaire qui prend du temps.
eval
est un interprète, c’est le plus lourd.
Le point entier de send
et eval
est que vous pouvez changer la commande dynamicment. Si la méthode à exécuter est fixe, vous pouvez câbler cette méthode sans utiliser send
ou eval
.
receiver.fixed_method(argument)
Mais lorsque vous souhaitez appeler une méthode qui varie ou que vous ne connaissez pas à l’avance, vous ne pouvez pas l’écrire directement. D’où l’utilisation de send
ou eval
.
receiver.send(method_that_changes_dynamically, argument) eval "#{code_to_evaluate_that_changes_more_dramatically}"
L’utilisation supplémentaire de send
est que, comme vous l’avez remarqué, vous pouvez appeler une méthode avec récepteur explicite en utilisant send
.