¿Cómo llamar a los métodos dinámicamente en función de su nombre?


¿Cómo puedo llamar a un método dinámicamente cuando su nombre está contenido en una variable string? Por ejemplo:

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

¿Cómo puedo hacer esto? ¿Es un riesgo para la seguridad?

Author: A. Romeu, 2011-03-18

5 answers

Lo que quieres hacer se llama despacho dinámico. Es muy fácil en Ruby, solo tiene que usar public_send:

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

Si el método es privado/protegido, use send en su lugar, pero prefieren public_send.

Este es un riesgo de seguridad potencial si el valor de method_name proviene del usuario. Para evitar vulnerabilidades, debe validar qué métodos se pueden llamar realmente. Por ejemplo:

if obj.respond_to?(method_name) && %w[foo bar].include?(method_name)
  obj.send(method_name)
end
 117
Author: David,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2015-08-09 19:33:11

Hay múltiples maneras de lograr el despacho dinámico en Ruby, cada una con sus propias ventajas y desventajas. Se debe tener cuidado de seleccionar el método más apropiado para la situación.

La siguiente tabla desglosa algunas de las técnicas más comunes:

+---------------+-----------------+-----------------+------------+------------+
|    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        |
+---------------+-----------------+-----------------+------------+------------+

Código arbitrario

Algunas técnicas se limitan a llamar a métodos solamente, mientras que otras pueden ejecutar básicamente cualquier cosa. Los métodos que permiten la ejecución de código arbitrario deben ser utilizados con extrema precaución, si no se evita por completo .

Acceso privado

Algunas técnicas se limitan a llamar solo a métodos públicos, mientras que otras pueden llamar tanto a métodos públicos como privados. Idealmente, debe esforzarse por usar el método con la menor cantidad de visibilidad que cumpla con sus requisitos.

Nota: Si una técnica puede ejecutar código arbitrario, se puede usar fácilmente para acceder a métodos privados que de otro modo no tendría acceso a.

Peligroso

Solo porque una técnica no puede ejecutar código arbitrario o llamar a un método privado no significa que sea seguro, particularmente si está utilizando valores proporcionados por el usuario. Eliminar es un método público.

Más rápido en

Algunas de estas técnicas pueden tener más rendimiento que otras, dependiendo de su versión de Ruby. Puntos de referencia para seguir....


Ejemplos

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 

Enviar

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

Método

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
 71
Author: Brad Werth,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-05-23 11:47:19

Usted es realmente va a querer tener cuidado con esto. El uso de datos de usuario para llamar a cualquier método a través de send podría dejar espacio abierto para que los usuarios ejecuten cualquier método que deseen. send se usa a menudo para llamar a los nombres de los métodos dinámicamente, pero asegúrese de que los valores de entrada sean de confianza y no puedan ser manipulados por los usuarios.

La regla de oro es nunca confiar en ninguna entrada que venga del usuario.

 13
Author: nzifnab,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2014-08-11 17:55:45

Uso send para llamar a un método dinámicamente:

obj.send(str)
 10
Author: sawa,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2014-08-10 23:29:44

Puede comprobar la disponibilidad del método usando respond_to?. Si está disponible, llama a send. Por ejemplo:

if obj.respond_to?(str)
  obj.send(str)
end
 7
Author: RameshVel,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2014-08-10 23:30:26