Ruby 中的动态方法调用
Posted
技术标签:
【中文标题】Ruby 中的动态方法调用【英文标题】:Dynamic method calling in Ruby 【发布时间】:2013-07-01 13:21:07 【问题描述】:据我所知,在 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
不允许。
所以我的问题是:有任何理由使用.send
或eval
?为什么你不总是只使用最快的方法?这些调用动态方法的方法还有哪些不同?
【问题讨论】:
很好的建设性问题:) +1.. @Abraham +1 好问题! 您可能希望包含您的基准测试结果并修复方法 2 代码示例中的错字。 “我还发现 .call 和 .send 都允许调用私有方法,而 eval 不允许。” 你可以使用.public_send
而不是.send
不允许调用私有方法。
【参考方案1】:
这是所有可能的方法调用:
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
是运行解释器,所以它是最重的。
【讨论】:
【参考方案2】:这样想:
方法一(method.call):单次运行时
如果您直接在程序上运行一次 Ruby,您就可以控制整个系统,并且可以通过“method.call”方法保持“指向您的方法的指针”。您所做的只是掌握“实时代码”的句柄,您可以随时运行。这基本上与直接从对象内调用方法一样快(但不如使用 object.send 快 - 请参阅其他答案中的基准)。
方法 2 (object.send):将方法的名称保存到数据库
但是,如果您想将要调用的方法的名称存储在数据库中,并且在将来的应用程序中您想通过在数据库中查找来调用该方法名称,该怎么办?然后您将使用第二种方法,这会导致 ruby 使用您的第二种“s.send(:dynamic_method)”方法调用任意方法名称。
方法3(eval):自修改方法代码
如果您想以一种将方法作为全新代码运行的方式将代码写入/修改/持久化到数据库中怎么办?您可能会定期修改写入数据库的代码,并希望它每次都作为新代码运行。在这种情况下(非常不寻常的情况),您可能希望使用第三种方法,它允许您将方法代码写成字符串,稍后再将其加载回来,然后完整运行它。
对于它的价值,在 Ruby 世界中,通常认为使用 Eval(方法 3)是一种不好的形式,除非在非常、非常深奥和罕见的情况下。因此,对于遇到的几乎所有问题,您都应该坚持使用方法 1 和 2。
【讨论】:
感谢您的深思熟虑和信息丰富的答案,我已投赞成票。我在下面向 Stefan 提供可接受的答案,包括一组基准、提出 method_missing 并首先响应。不过谢谢!【参考方案3】:我从@Stefan 更新了基准,以检查在保存对方法的引用时是否有一些速度改进。但同样——send
比 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
这些是结果:
user system total real
send 0.080000 0.000000 0.080000 ( 0.088685)
call 0.110000 0.000000 0.110000 ( 0.108249)
所以send
似乎是可以接受的。
【讨论】:
这是一个更准确的基准。感谢发帖。【参考方案4】:有任何理由使用
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)
【讨论】:
【参考方案5】:send
和eval
的重点在于您可以动态更改命令。如果您要执行的方法是固定的,那么您可以不使用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
调用具有显式接收器的方法。
【讨论】:
以上是关于Ruby 中的动态方法调用的主要内容,如果未能解决你的问题,请参考以下文章