2009-08-25 6 views
5

Esto es para una API pública ya existente que no puedo romper, pero deseo extenderla.¿Manera elegante de escribir cadenas de pato, símbolos y matrices?

Actualmente, el método toma una cadena o un símbolo o cualquier otra cosa que tenga sentido cuando se pasa como primer parámetro a send

me gustaría añadir la posibilidad de enviar una lista de cadenas, símbolos, etc. . Podría simplemente usar is_a? Array, pero hay otras maneras de enviar listas, y eso no es muy ruby-ish.

Llamaré map en la lista, por lo que la primera inclinación es usar respond_to? :map. Pero una cadena también responde al :map, por lo que no funcionará.

+3

Nada que ver con su pregunta, pero espero que esté haciendo algunos controles antes de pasar la información del usuario al método de envío. Yikes. –

+0

buen punto. En nuestro caso, los documentos indican claramente que el parámetro es un nombre de método. Es una API lo suficientemente profunda que esto debería ser suficiente ... –

+0

No es una cuestión de documentación, es un agujero de seguridad. Si pasan en un backtick, el segundo parámetro de send se ejecuta en un shell. ¿Es eso realmente lo que quieres? Es mucho mejor que examines la entrada en una declaración de caso y luego envíes símbolos de tu propia construcción para enviar. –

Respuesta

6

¿Qué les parece tratarlos a todos como Arrays? El comportamiento que desea para String s es el mismo que para un Array que contiene sólo eso String:

def foo(obj, arg) 
    [*arg].each { |method| obj.send(method) } 
end 

El truco [*arg] funciona porque el operador splat (*) convierte un solo elemento en sí mismo o un Array en una lista en línea de sus elementos.

tarde

Ésta es básicamente una versión azucarada o sintácticamente Arnaud's answer, aunque hay diferencias sutiles si pasa un Array contiene otros Array s.

tarde aún

Hay una diferencia adicional que tiene que ver con el valor de retorno foo 's. Si llama al foo(bar, :baz), es posible que se sorprenda al obtener [baz]. Para solucionar esto, se puede añadir un Kestrel:

def foo(obj, arg) 
    returning(arg) do |args| 
    [*args].each { |method| obj.send(method) } 
    end 
end 

que siempre volverá arg tal como fue aprobado. O puede hacer returning(obj) para poder llamar en cadena al foo. Depende de usted qué tipo de comportamiento de valor de retorno desea.

+0

Mucho más hermosa que mi publicación original. Nunca me acostumbro al operador *. – Arnaud

+0

[* arg] brilliant! – Dmitry

0

¿Puedes simplemente cambiar el comportamiento basado en parameter.class.name? Es feo, pero si entiendo correctamente, tienes un único método al que le pasarás varios tipos: tendrás que diferenciar de alguna manera.

Como alternativa, simplemente agregue un método que maneje un parámetro de tipo de matriz. Es un comportamiento ligeramente diferente, por lo que un método extra podría tener sentido.

+0

Su segundo comentario es una mejor solución, pero espero obtener más de los gurús de stackoverflow. El primero tiene el mismo problema que 'is_a? Array': un usuario puede enviar una cadena que no sea una "Cadena" o una lista que no sea una "Matriz". –

+0

No estoy seguro de cuán comunes son las cadenas que no son '' String'.las listas que no son ''Array' son bastante comunes, muchos objetos son' Enumerable'. Desafortunadamente, también lo son las cuerdas ... –

0

Utilice Marshal para serializar sus objetos antes de enviarlos.

+0

Estoy confundido. 'send' es el método que llama a una función por nombre en Ruby. ¿Cómo ayudaría Marshal? –

1

Desde Array y String son tanto Enumerables, no hay una manera elegante de decir "una cosa que es un Enumberable, pero no una cadena", al menos no de la manera en discusión.

Lo que yo haría es de tipo pato para Enumerable (responds_to? :[]) y luego utilizar una declaración case, así:

def foo(obj, arg) 
    if arg.respond_to?(:[]) 
    case arg 
    when String then obj.send(arg) 
    else arg.each { |method_name| obj.send(method_name) } 
    end 
    end 
end 

o incluso más limpio:

def foo(obj, arg) 
    case arg 
    when String then obj.send(arg) 
    when Enumerable then arg.each { |method| obj.send(method) } 
    else nil 
    end 
end 
+0

Gracias por la explicación extendida, pero en la pregunta original pedí algo más limpio que 'is_a? Array'. Usando 'is_a? String' (o una variante como la que has mostrado) rompe la API original para cualquiera que esté usando algo que grazna como una Cadena para 'enviar'. De hecho, la mayoría de los usuarios probablemente use 'Symbol' en su lugar. Podría usar 'x.is_a? (String) || x.is_a? (Symbol) ', pero eso no es muy ruby-ish, y puede romper la API para algunos de mis usuarios. –

0

Si no lo hace Desea parchear, simplemente masajee la lista a una cadena apropiada antes del envío. Si no te importa monkeypatching o heredar, pero desea mantener el mismo método de firma:

class ToBePatched 
    alias_method :__old_takes_a_string, :takes_a_string 

    #since the old method wanted only a string, check for a string and call the old method 
    # otherwise do your business with the map on things that respond to a map. 
    def takes_a_string(string_or_mappable) 
     return __old_takes_a_string(string_or_mappable) if String === string_or_mappable 
     raise ArgumentError unless string_or_mappable.responds_to?(:map) 
     # do whatever you wish to do 
    end 
end 
0

Tal vez la pregunta no era lo suficientemente claro, pero una noche de sueño me mostró dos formas limpias de responder a esta pregunta.

1: to_sym está disponible en String y Symbol y debe estar disponible en cualquier cosa que grazna como una cadena.

if arg.respond_to? :to_sym 
    obj.send(arg, ...) 
else 
    # do array stuff 
end 

2: enviar arroja TypeError cuando pasa una matriz.

begin 
    obj.send(arg, ...) 
rescue TypeError 
    # do array stuff 
end 

Me gusta especialmente # 2. Dudo mucho que alguno de los usuarios de la antigua API espere que TypeError se genere con este método ...

+1

La primera solución tiene sentido, ya que el envío realmente espera un símbolo. Pero espero que no te importe la segunda solución. Las excepciones no son flujo de control. –

+0

@Sarah: al azar, pero creo que aproximadamente el 40% de todos tus comentarios y respuestas sobre SO incluyen la oración "Las excepciones no son flujo de control". – Telemachus

+0

@Telemachus - al menos recientemente. :) –

1

Digamos que su función se llama func

Me gustaría hacer una matriz a partir de los parámetros con

def func(param) 
    a = Array.new 
    a << param 
    a.flatten! 
    func_array(a) 
end 

Se termina con la implementación de su func_array función para matrices única

con func (" hola mundo ") obtendrá a.flatten! => ["hello world"] con func (["hello", "world"]) obtendrás a.flatten! => ["hello", "world"]

2

Un detalle importante que se pasa por alto en todas las respuestas: cuerdas hacen no responden a :map, así que la respuesta es más simple en la pregunta original: sólo tiene que utilizar respond_to? :map.

Cuestiones relacionadas