如何根据方法的名称动态调用它们?

当一个方法的名字包含在一个字符串变量中时,我怎样才能动态地调用它呢? 例如:

class MyClass
def foo; end
def bar; end
end


obj = MyClass.new
str = get_data_from_user  # e.g. `gets`, `params`, DB access, etc.
str  #=> "foo"
# somehow call `foo` on `obj` using the value in `str`.

我怎么能这么做? 这么做有安全风险吗?

85958 次浏览

您可以使用 respond_to?检查方法的可用性。如果可用,则调用 send。例如:

if obj.respond_to?(str)
obj.send(str)
end

使用 send动态调用方法:

obj.send(str)

你是 真的要小心处理这个。使用用户数据通过 send调用任何方法可以为用户留出空间来执行任何他们想要的方法。send通常用于动态调用方法名,但要确保输入值是 值得信任,不能被用户操纵。

黄金法则是永远不要相信来自用户的任何输入。

你要做的是叫做 克劳斯·福尔曼,在 Ruby 中很简单,只要使用 public_send:

method_name = 'foobar'
obj.public_send(method_name) if obj.respond_to? method_name

如果该方法是 private/protected,则使用 send,但更喜欢使用 public_send

如果 method_name的值来自用户,那么这是一个潜在的安全风险。为了防止漏洞,您应该验证哪些方法可以实际调用。例如:

if obj.respond_to?(method_name) && %w[foo bar].include?(method_name)
obj.send(method_name)
end

在 Ruby 中有多种方法来实现克劳斯·福尔曼,每种方法都有自己的优缺点。应谨慎选择最适合情况的方法。

下表列出了一些较为常见的技术:

+---------------+-----------------+-----------------+------------+------------+
|    Method     | Arbitrary Code? | Access Private? | Dangerous? | Fastest On |
+---------------+-----------------+-----------------+------------+------------+
| eval          | Yes             | No              | Yes        | TBD        |
| instance_eval | Yes             | No              | Yes        | TBD        |
| send          | No              | Yes             | Yes        | TBD        |
| public_send   | No              | No              | Yes        | TBD        |
| method        | No              | Yes             | Yes        | TBD        |
+---------------+-----------------+-----------------+------------+------------+

任意代码

有些技术仅限于调用方法,而其他技术基本上可以执行任何内容。允许执行任意代码的方法应该是 如果不能完全避免的话,要极其小心地使用

私人访问

有些技术仅限于调用公共方法,而其他技术可以同时调用公共方法和私有方法。理想情况下,您应该努力使用具有最少可见性的方法来满足您的需求。

注意 : 如果一种技术可以执行任意代码,那么它可以很容易地用于访问它可能无法访问的私有方法。

危险

仅仅因为技术不能执行任意代码或调用私有方法并不意味着它是安全的,特别是如果您使用用户提供的值。删除是一个公共方法。

最快的

根据您的 Ruby 版本,这些技术中的一些可能比其他的更高效。


例子

class MyClass
def foo(*args); end


private


def bar(*args); end
end


obj = MyClass.new

Eval

eval('obj.foo') #=> nil
eval('obj.bar') #=> NoMethodError: private method `bar' called


# With arguments:
eval('obj.foo(:arg1, :arg2)') #=> nil
eval('obj.bar(:arg1, :arg2)') #=> NoMethodError: private method `bar' called

Instance _ eval

obj.instance_eval('foo') #=> nil
obj.instance_eval('bar') #=> nil


# With arguments:
obj.instance_eval('foo(:arg1, :arg2)') #=> nil
obj.instance_eval('bar(:arg1, :arg2)') #=> nil

发送

obj.send('foo') #=> nil
obj.send('bar') #=> nil


# With arguments:
obj.send('foo', :arg1, :arg2) #=> nil
obj.send('bar', :arg1, :arg2) #=> nil

公开发送

obj.public_send('foo') #=> nil
obj.public_send('bar') #=> NoMethodError: private method `bar' called


# With arguments:
obj.public_send('foo', :arg1, :arg2) #=> nil
obj.public_send('bar', :arg1, :arg2) #=> NoMethodError: private method `bar' called

方法

obj.method('foo').call #=> nil
obj.method('bar').call #=> nil


# With arguments:
obj.method('foo').call(:arg1, :arg2) #=> nil
obj.method('bar').call(:arg1, :arg2) #=> nil