web-dev-qa-db-ja.com

Rubyで呼び出す動的メソッド

私の知る限り、Rubyでメソッドを動的に呼び出すには3つの方法があります。

方法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?常に最速の方法を常に使用するとは限らないのはなぜですか?動的メソッドを呼び出すこれらのメソッドには、他にどのような違いがありますか?

68
Abraham P

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 は、任意の式を評価します。メソッドを呼び出すためだけではありません。


ベンチマークに関して、sendmethod + 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)
61
Stefan

このように考えてください:

方法1(method.call):単一のランタイム

プログラムでRubyを1回実行すると、システム全体を制御し、「method.call」アプローチを介して「メソッドへのポインタ」を保持できます。これは、基本的にオブジェクト内から直接メソッドを呼び出すのと同じくらい高速です(ただし、object.sendを使用するほど高速ではありません-他のベンチマークを参照してください)回答)。

方法2(object.send):メソッドの名前をデータベースに永続化する

しかし、呼び出したいメソッドの名前をデータベースに保存し、将来のアプリケーションでデータベースで検索してそのメソッド名を呼び出したい場合はどうでしょうか?次に、2番目のアプローチを使用します。これにより、Rubyは2番目の「s.send(:dynamic_method)」アプローチを使用して任意のメソッド名を呼び出します。

方法3(評価):自己修正メソッドコード

メソッドをまったく新しいコードとして実行する方法で、データベースにコードを書き込み/変更/永続化したい場合はどうでしょうか?データベースに書き込まれたコードを定期的に変更し、毎回新しいコードとして実行したい場合があります。この場合(非常にまれなケース)、3番目のアプローチを使用します。これにより、メソッドコードを文字列として記述し、後日ロードし直して、完全に実行できます。

価値のあるものとしては、一般的にRuby世界ではEval(方法3)を使用するのは悪い形式であると見なされます。そして、あなたが遭遇するほとんどすべての問題のための2。

12
Steve Midgley

すべての可能なメソッド呼び出しは次のとおりです。

_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はインタープリターを実行するため、最も重いです。

4
mpospelov

@Stefanのベンチマークを更新して、メソッドへの参照を保存する際に速度の改善があるかどうかを確認しました。しかし、再び-sendcallよりもはるかに高速です

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を使用するようです。

3
Tom Freudenberg

sendevalのポイントは、コマンドを動的に変更できることです。実行するメソッドが固定されている場合は、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を使用して明示的なレシーバーでメソッドを呼び出すことができるということです。

0
sawa