Как динамически вызывать методы на основе их имени?

Как я могу вызвать метод динамически, когда его имя содержится в строковой переменной? Например:

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`.

Как я могу это сделать? Это угроза безопасности?

Ответ 1

Что вы хотите сделать, называется динамическая отправка. Его очень просто в Ruby, просто используйте public_send:

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

Если этот метод является частным/защищенным, используйте send, но предпочитайте public_send.

Это потенциальный риск безопасности, если значение method_name исходит от пользователя. Чтобы предотвратить уязвимости, вы должны проверить, какие методы могут быть фактически вызваны. Например:

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

Ответ 2

Существует несколько способов выполнения динамической отправки в 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        |
+---------------+-----------------+-----------------+------------+------------+

Произвольный код

Некоторые методы ограничены только вызывающими методами, в то время как другие могут выполнять в основном что угодно. Методы, позволяющие выполнять произвольный код, должны быть использованы с особой осторожностью, если вообще не избегать.

Доступ к приватным

Некоторые методы ограничены только вызовом общедоступных методов, в то время как другие могут вызывать как публичные, так и частные методы. В идеале вы должны стремиться использовать метод с наименьшим количеством видимости, отвечающей вашим требованиям.

Примечание. Если метод может выполнять произвольный код, его можно легко использовать для доступа к закрытым методам, к которым у него, возможно, нет доступа.

Dangerous

Просто потому, что метод не может выполнить произвольный код или вызвать частный метод, не означает, что он безопасен, особенно если вы используете предоставленные пользователем значения. Удалить - общедоступный метод.

Самый быстрый на

Некоторые из этих методов могут быть более эффективными, чем другие, в зависимости от вашей версии 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 

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

Метод

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

Ответ 3

Вы действительно хотите быть осторожным с этим. Использование пользовательских данных для вызова любого метода с помощью send может оставлять пространство открытым для пользователей для выполнения любого метода, который они хотят. send часто используется для динамического вызова имен методов, но убедитесь, что входные значения являются надежными и не могут быть обработаны пользователями.

Золотое правило никогда не доверяет никаким данным, поступающим от пользователя.

Ответ 4

Используйте send для динамического вызова метода:

obj.send(str)

Ответ 5

Вы можете проверить доступность метода с помощью respond_to?. Если он доступен, вы вызываете send. Например:

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