2010-06-28 10 views
23

Al escribir una API u objeto reutilizable, ¿hay alguna razón técnica por la cual todas las llamadas a métodos que devuelven 'nulo' no deberían simplemente devolver 'esto' (* esto en C++)?¿Por qué debería una API devolver 'nulo'?

Por ejemplo, el uso de la clase string, podemos hacer este tipo de cosas:

string input= ...; 
string.Join(input.TrimStart().TrimEnd().Split("|"), "-"); 

pero no podemos hacer esto:

string.Join(input.TrimStart().TrimEnd().Split("|").Reverse(), "-"); 

..because Array.reverse() devuelve vacío

Hay muchos otros ejemplos en los que una API tiene una gran cantidad de operaciones de retorno de vacío, por lo que el código termina pareciéndose a:

api.Method1(); 
api.Method2(); 
api.Method3(); 

..pero sería perfectamente posible escribir:

api.Method1().Method2().Method3() 

..si el diseñador de API lo permitió.

¿Hay alguna razón técnica para seguir esta ruta? ¿O es solo una cuestión de estilo indicar mutabilidad/nuevo objeto?

(x-ref Stylistic question concerning returning void)


Epílogo

He aceptado la respuesta de Luvieere ya que creo que mejor representa la intención/diseño, pero parece que hay ejemplos de API populares hacia fuera allí que se desvía de esto:

En C++ cout << setprecision(..) << number << setwidth(..) << othernumber; parece alterar el objeto cout para modificar el siguiente datum insertado.

En .NET, Stack.Pop() y Queue.Dequeue() ambos devuelven un elemento pero también cambian la colección.

Apoyos a ChrisW y otros para obtener información detallada sobre los costos reales de rendimiento.

+1

El patrón que está describiendo también se conoce como * method chaining * (http://martinfowler.com/dslwip/MethodChaining.html). –

+0

Método de encadenamiento, o interfaces fluidas (¡aunque los puristas afirman que se requiere algo más para este último, creo!). –

+0

Ahhh - ¡tiene un nombre! Parece bastante legible y popular en cosas como JQuery – JBRWilkinson

Respuesta

26

Los métodos que vuelven vacíos declaran más claramente que tienen efectos secundarios. Se supone que los que devuelven el resultado modificado no tienen efectos secundarios, incluida la modificación de la entrada original. Hacer que un método devuelva vacío implica que cambia su entrada o algún otro estado interno de la API.

+0

En C++, hay otras formas de expresar esto: funciones de miembros de const, por ejemplo ...? – JBRWilkinson

+0

@JBRWilkinson El alcance de la pregunta es más amplio que C++, también tiene una etiqueta C#. Lo que dije fue una práctica API agnóstica. – luvieere

+0

Bien - punto tomado. – JBRWilkinson

19

Si tuviera un retorno Reverse()string, entonces no sería obvio para un usuario de la API si devuelve una nueva cadena deo igual a uno, invertido en el lugar.

string my_string = "hello"; 
string your_string = my_string.reverse(); // is my_string reversed or not? 

Esa es la razón, por ejemplo, en Python, list.sort() vuelve None; distingue el ordenamiento in situ de sorted(my_list).

5

¿Hay algún motivo técnico para seguir esta ruta?

Una de las pautas de diseño de C++ es "no pague por las funciones que no utiliza"; devolver this tendría alguna (leve) penalización de rendimiento, por una característica que mucha gente (yo, por ejemplo) no estaría inclinada a utilizar.

+0

¿Cuál es la penalización de rendimiento en C++? ¿No es este el puntero en la pila de todos modos? – JBRWilkinson

+2

JBRWilkinson Donde quiera que esté este puntero, debe ser este puntero del proceso de llamada para cuando regrese al proceso de llamada. En realidad, usando MSVC, el puntero 'this' está en el registro' ecx', y el código de retorno está en el registro 'eax': entonces la penalización de rendimiento sería una instrucción extra' mov ecx, eax' al final de/cada llamada subrutina. – ChrisW

+2

@JBRWilkinson El código de operación adicional podría optimizarse cuando no se usa, solo cuando la rutina llamada sepa de dónde se está llamando, es decir, cuando está en línea (pero al interior todo tendrá sus propios problemas de rendimiento). – ChrisW

0

Además de las razones de diseño, también hay un pequeño costo de rendimiento (tanto en velocidad como en espacio) para devolver esto.

+0

¿Puedes darnos algunos detalles? – JBRWilkinson

+1

ChrisW proporcionó algunos excelentes comentarios en su respuesta. Cada idioma/plataforma debería ser bastante similar a este respecto. Por ejemplo, en C#/.NET, el compilador tendrá que agregar una instrucción de retorno IL al ensamblado y el compilador JIT en la mayoría de los casos debe convertir eso a una instrucción 'mov eax, ecx' como el compilador C++ en su ejemplo. –

4

El principio técnico que muchos otros han mencionado (que void enfatiza el hecho de que la función tiene un efecto secundario) se conoce como Command-Query Separation.

Si bien existen ventajas y desventajas con respecto a este principio, por ejemplo, intenciones (subjetivamente) más claras frente a API más concisas, la parte más importante es sea consistente.

+0

El artículo de Wiki parece un poco embarrado.Es completamente normal que los comandos devuelvan una indicación de si sucedió algo inesperado. Algo así como la función incrementAndReturnValue se comporta como un comando, pero devuelve un valor con el fin de permitir que la persona que llama sepa qué sucedió. Uno podría pasar un valor "esperado" y hacer que la función devuelva un booleano que diga si las cosas fueron "como se esperaba", pero es más fácil para el que llama hacer la comparación. – supercat

4

Me imagino que una de las razones podría ser la simplicidad. Simplemente, una API generalmente debe ser lo más mínima posible. Debería quedar claro con cada aspecto, para qué sirve.

Si veo una función que devuelve vacío, sé que el tipo de devolución no es importante. Cualquiera que sea la función, no me devuelve nada para que trabaje.

Si una función devuelve algo que no es void, tengo que detenerme y preguntarme por qué. ¿Qué es este objeto que puede ser devuelto? ¿Por qué se devuelve? ¿Puedo suponer que this es siempre devuelto, o a veces será nulo? ¿O un objeto completamente diferente? Y así.

En una API de terceros, preferiría que ese tipo de preguntas simplemente nunca surjan.

Si la función no es necesita para devolver algo, no debe devolver nada.

+3

+1. Sería bastante fácil escribir un método encadenando envoltorio sobre él si se desea una sintaxis conveniente. Prefiero que pesimizing todo tipo de funciones devolviendo esto/* esto cuando no sea necesario a favor de una taquigrafía estilística debatible. – stinky472

3

Si tiene la intención de su API para llamarse desde F #, por favor hacervoid retorno a menos que esté convencido de que esta llamada a un método en particular va a estar encadenado con otro casi cada vez que se usa.

Si no le importa hacer que su API sea fácil de usar desde F #, puede dejar de leer aquí.

F # es más estricto que C# en ciertas áreas: quiere que sea explícito acerca de si está llamando a un método para obtener un valor, o simplemente por sus efectos secundarios. Como resultado, llamar a un método para sus efectos secundarios cuando ese método también devuelve un valor se torna incómodo, porque el valor devuelto debe ignorarse explícitamente para evitar errores del compilador. Esto hace que las "interfaces fluidas" sean algo incómodas de usar desde F #, que tiene su propia sintaxis superior para encadenar una serie de llamadas juntas.

Por ejemplo, supongamos que tenemos un sistema de registro con un método Log que devuelve this para permitir algún tipo de método de encadenamiento:

let add x y = 
    Logger.Log(String.Format("Adding {0} and {1}", x, y)) // #1 
    x + y             // #2 

En F #, porque la línea # 1 es una llamada a un método que devuelve una valor, y no estamos haciendo nada con ese valor, se considera que la función add toma dos valores y devuelve esa instancia de Logger.Sin embargo, la línea # 2 no solo devuelve un valor, sino que aparece después de lo que F # considera que es la declaración de "retorno" para la función, haciendo efectivamente dos declaraciones de "devolución". Esto causará un error del compilador, y necesitamos ignorar explícitamente el valor de retorno del método Log para evitar este error, de modo que nuestro método add tenga solo una declaración de retorno.

let add x y = 
    Logger.Log(String.Format("Adding {0} and {1}", x, y)) |> ignore 
    x + y 

Como se puede adivinar, hacer un montón de llamadas de la API "Fluido" que son principalmente acerca de los efectos secundarios se convierte en un ejercicio un tanto frustrante para rociar un montón de ignore declaraciones por todo el lugar.

Puede, por supuesto, tener lo mejor de ambos mundos y complacer a los desarrolladores C# y F # al proporcionar una API fluida y un módulo F # para trabajar con su código. Pero si no va a hacer eso, y tiene la intención de utilizar su API para el consumo público, , por favor,, piense dos veces antes de devolver this de cada método.

+1

Interesante para ver cómo otros idiomas realmente dependen de este patrón. ¡Gracias por la gran respuesta! – JBRWilkinson

+0

Esto me recuerda de Yegge "rinocerontes y tigres", que discute (entre otras cosas) si y cómo las máquinas virtuales puede soportar llamadas entre varios idiomas. "Y son como (agitando las manos) 'Ooooh, vamos a pasar por alto, pasar el brillo, alisarlo'. Y la respuesta es: "No se puede. Esto es fundamental. ¡Estos idiomas funcionan de manera diferente!" – Ken

Cuestiones relacionadas