Méthode dynamic appelant dans Ruby

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:

Méthode 1 (method.call): exécution unique

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).

Méthode 2 (object.send): Nom persistant de la méthode à la firebase database

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)”.

Méthode 3 (eval): Code de méthode auto-modifiable

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 .