Ruby中的动态方法调用


71

据我所知,有三种方法可以在Ruby中动态调用方法:

方法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不会。

所以我的问题是:有没有理由使用.sendeval?为什么您不总是使用最快的方法?这些调用动态方法的方法还有什么其他区别?


1
好建设性的问题:) +1 ..
Arup Rakshit

@亚伯拉罕+1伟大的问题!
feralin

2
您可能需要包括基准测试结果并解决方法2代码示例中的拼写错误。
亩太短

3
“我还发现.call和.send都允许调用私有方法,而eval不允许。” 您可以使用.public_send而不是.send禁止调用私有方法。
丹尼斯

Answers:


65

有什么理由要使用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 计算任意表达式,不仅仅用于调用方法。


关于基准,send似乎比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

结果:

           user     system      total        real
send   0.210000   0.000000   0.210000 (  0.215181)
call   0.740000   0.000000   0.740000 (  0.739262)

12

这样想:

方法1(方法调用):单个运行时

如果您直接在程序上运行Ruby,则可以控制整个系统,并且可以通过“ method.call”方法抓住“方法的指针”。您所做的只是握住“实时代码”的句柄,您可以随时运行它。这基本上和直接从对象内部调用方法一样快(但不如使用object.send快(请参阅其他答案中的基准测试)。

方法2(object.send):方法的名称持久化到数据库

但是,如果要在数据库中存储要调用的方法的名称,而在将来的应用程序中要通过在数据库中查找来调用该方法的名称,该怎么办?然后,您将使用第二种方法,这将使ruby使用第二种“ s.send(:dynamic_method)”方法调用任意方法名称。

方法3(评估):自修改方法代码

如果您想以一种将方法作为全新代码运行的方式向数据库写入/修改/持久化代码,该怎么办?您可能会定期修改写入数据库的代码,并希望它每次都作为新代码运行。在这种情况下(非常不寻常的情况),您可能希望使用第三种方法,该方法允许您将方法代码作为字符串写出,在以后再加载回去,然后完整地运行它。

对于它的价值,通常在Ruby世界中,使用Eval(方法3)是一种不好的形式,除非在非常,非常神秘和罕见的情况下使用。因此,对于遇到的几乎所有问题,您都应该坚持使用方法1和2。


1
谢谢您的深思熟虑和有益的回答,我已投票赞成。对于下面的Stefan,我给出了一个公认的答案,其中包括一组基准,提出method_missing并首先进行响应。不过谢谢你!
亚伯拉罕·P

4

这是所有可能的方法调用:

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 是运行翻译程序,因此最重。


3

我从@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似乎是要采取的措施。


这是一个更准确的基准。感谢您的发布。
安迪·

0

整点sendeval是可以动态改变的命令。如果您要执行的方法是固定的,则无需使用send或即可硬连线该方法eval

receiver.fixed_method(argument)

但是,当您要调用变化的方法或事先不知道时,就无法直接编写该方法。因此使用sendeval

receiver.send(method_that_changes_dynamically, argument)
eval "#{code_to_evaluate_that_changes_more_dramatically}"

附加使用 send如您所注意到的,的是您可以使用来使用显式接收器调用方法send

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.