web-dev-qa-db-fra.com

Méthode dynamique appelant Ruby

Pour autant que je sache, il existe trois façons d'appeler dynamiquement 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, la méthode 2 est plus lente et la méthode 3 est de loin la plus lente.

J'ai également constaté que .call et .send les deux permettent d'appeler des méthodes privées, contrairement à eval.

Ma question est donc la suivante: y a-t-il une raison de jamais utiliser .send ou eval? Pourquoi n'utilisez-vous pas toujours la méthode la plus rapide? Quelles autres différences ces méthodes d'appel de méthodes dynamiques ont-elles?

68
Abraham P

y a-t-il une raison pour utiliser send?

call a besoin d'un objet méthode, send ne fait pas:

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 pour utiliser eval?

eval évalue les expressions arbitraires, ce n'est pas seulement pour appeler une méthode.


Concernant les benchmarks, send semble être plus rapide que 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)
61
Stefan

Pense-y de cette façon:

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

Si vous exécutez Ruby une fois directement sur votre programme, vous contrôlez l'ensemble du système et vous pouvez conserver un "pointeur vers votre méthode" via l'approche "method.call". Tout ce que vous faites tient un descripteur de "code en direct" que vous pouvez exécuter quand vous le souhaitez. C'est fondamentalement aussi rapide que d'appeler la méthode directement à partir de l'objet (mais ce n'est pas aussi rapide que d'utiliser object.send - voir les tests de performances dans d'autres réponses).

Méthode 2 (object.send): conserver le nom de la méthode dans la base de données

Mais que se passe-t-il si vous souhaitez stocker le nom de la méthode que vous souhaitez appeler dans une base de données et dans une future application, vous voulez appeler ce nom de méthode en le recherchant dans la base de données? Ensuite, vous utiliseriez la deuxième approche, ce qui provoque Ruby pour appeler 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 souhaitez écrire/modifier/conserver du code dans une base de données de manière à exécuter la méthode en tant que nouveau code? Vous pouvez modifier périodiquement le code écrit dans la base de données et souhaiter qu'il s'exécute en tant que nouveau code à chaque fois. Dans ce cas (très inhabituel), vous souhaitez utiliser votre troisième approche, qui vous permet d'écrire votre code de méthode sous forme de chaîne, de le charger à une date ultérieure et de l'exécuter dans son intégralité.

Pour ce que ça vaut, il est généralement considéré dans le monde Ruby 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.

12
Steve Midgley

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

On s'attend à ce que l'appel simple soit le plus rapide, aucune allocation supplémentaire, recherche de symbole, juste recherche et évaluation de la méthode.

Quant à send via symbole, il est plus rapide que via string car il est beaucoup plus facile d'allouer de la mémoire pour le symbole. Une fois défini, il est stocké à long terme dans la mémoire et il n'y a pas de réaffectation.

La même raison peut être dite à propos de method(:name) (1) il faut allouer de la mémoire pour Proc objet (2) nous appelons la méthode en classe qui mène à une recherche de méthode supplémentaire qui prend aussi du temps .

eval is exécute l'interpréteur donc c'est le plus lourd.

4
mpospelov

J'ai mis à jour le benchmark de @Stefan pour vérifier s'il y a des améliorations de vitesse lors de l'enregistrement de la référence à la méthode. Mais encore une fois - send est beaucoup plus rapide 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

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

3
Tom Freudenberg

L'intérêt de send et eval est que vous pouvez modifier la commande dynamiquement. Si la méthode que vous souhaitez exécuter est fixe, vous pouvez câbler cette méthode sans utiliser send ou eval.

receiver.fixed_method(argument)

Mais lorsque vous souhaitez invoquer 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 un récepteur explicite en utilisant send.

0
sawa