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

use*_*052 94 ruby metaprogramming dynamic

当名称包含在字符串变量中时,如何动态调用方法?例如:

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`.
Run Code Online (Sandbox Code Playgroud)

我怎样才能做到这一点?这样做是否存在安全风险?

Dav*_*vid 132

您想要做的是称为动态调度.在Ruby中它很容易,只需使用public_send:

method_name = 'foobar'
obj.public_send(method_name) if obj.respond_to? method_name
Run Code Online (Sandbox Code Playgroud)

如果该方法是私有/受保护的,请send改用,但更喜欢public_send.

如果价值method_name来自用户,则存在潜在的安全风险.要防止漏洞,您应该验证哪些方法可以实际调用.例如:

if obj.respond_to?(method_name) && %w[foo bar].include?(method_name)
  obj.send(method_name)
end
Run Code Online (Sandbox Code Playgroud)


Bra*_*rth 88

在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        |
+---------------+-----------------+-----------------+------------+------------+
Run Code Online (Sandbox Code Playgroud)

任意代码

有些技术仅限于调用方法,而其他技术基本上只能执行任何操作.如果不能完全避免,应该极其谨慎使用允许执行任意代码的方法.

访问私人

有些技术仅限于调用公共方法,而其他技术可以调用公共方法和私有方法.理想情况下,您应该努力使用满足您要求的最低可见性的方法.

注意:如果某种技术可以执行任意代码,则可以轻松地使用它来访问它可能无法访问的私有方法.

危险

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

最快的

根据您的Ruby版本,其中一些技术可能比其他技术更高效.基准跟随......


例子

class MyClass
  def foo(*args); end

  private

  def bar(*args); end
end

obj = MyClass.new
Run Code Online (Sandbox Code Playgroud)

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
Run Code Online (Sandbox Code Playgroud)

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 
Run Code Online (Sandbox Code Playgroud)

发送

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

# With arguments:
obj.send('foo', :arg1, :arg2) #=> nil 
obj.send('bar', :arg1, :arg2) #=> nil 
Run Code Online (Sandbox Code Playgroud)

public_send

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
Run Code Online (Sandbox Code Playgroud)

方法

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
Run Code Online (Sandbox Code Playgroud)

  • 我对这个答案很感兴趣。我不太在乎基准测试,但是我想知道您是否可以从上面的描述中假设`public_send`在这些可能性中风险最小。 (2认同)
  • 特别值得注意的是,如果用户同时提供方法名称和参数,则可以使用 `public_send` 来调用 `send`,而后者又可以用于调用 `eval` 或 `system`: `obj.public_send('send','system','rm','-r','-f','/')` 会毁了你的一天。 (2认同)

nzi*_*nab 13

真的要小心这个.使用用户数据来调用任何方法send可以为用户打开空间以执行他们想要的任何方法. send通常用于动态调用方法名称,但要确保输入值是可信的,用户不能操作.

黄金法则永远不会信任来自用户的任何输入.


saw*_*awa 10

用于send动态调用方法:

obj.send(str)
Run Code Online (Sandbox Code Playgroud)


Ram*_*Vel 7

您可以使用检查方法可用性respond_to?.如果它可用,那么你打电话send.例如:

if obj.respond_to?(str)
  obj.send(str)
end
Run Code Online (Sandbox Code Playgroud)