Ruby 中的动态方法调用

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

方法一:

s = SomeObject.new
method = s.method(:dynamic_method)
method.call

方法二:

s = SomeObject.new
s.send(:dynamic_method)

方法三:

s = SomeObject.new
eval "s.dynamic_method"

通过对它们进行基准测试,我发现方法1是目前为止最快的,方法2是目前为止最慢的,而方法3是目前为止最慢的。

我还发现 .call.send都允许调用私有方法,而 eval不允许。

所以我的问题是: 有什么理由使用 .sendeval吗?为什么不总是用最快的方法呢?这些调用动态方法的方法还有哪些不同之处?

32890 次浏览

有什么理由使用 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)

sendeval的关键在于可以动态地更改命令。如果要执行的方法是固定的,那么可以不使用 sendeval硬连接该方法。

receiver.fixed_method(argument)

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

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

send的另一个用途是,正如您所注意到的,您可以使用 send调用具有显式接收方的方法。

这样想:

方法1(Method.call) : 单个运行时

如果你在你的程序上直接运行 Ruby 一次,你就可以控制整个系统,你可以通过“ method.call”方法保留一个“指向你的方法的指针”。您所做的一切就是抓住一个“实时代码”的句柄,可以随时运行该代码。这基本上与直接从对象内部调用方法一样快(但是不如在其他答案中使用 object.send-see 基准测试那样快)。

方法2(object.send) : 将方法的名称保存到数据库

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

方法3(eval) : 自修改方法代码

如果您希望将代码编写/修改/持久化到数据库中,并以全新代码的方式运行该方法,该怎么办?您可能会定期修改写入数据库的代码,并希望每次都以新代码的形式运行。在这种情况下(非常特殊的情况) ,您可能希望使用第三种方法,这种方法允许您将方法代码写成字符串,在以后的某个日期重新加载它,并完整地运行它。

无论如何,在 Ruby 世界中,使用 Eval (方法3)通常被认为是一种糟糕的形式,除非在非常、非常深奥和罕见的情况下。因此,对于遇到的几乎所有问题,您都应该坚持使用方法1和方法2。

我更新了@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似乎是一个采取。

下面是所有可能的方法调用:

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是运行解释器,所以它是最重的。