Cuando ejecuté ReSharper en mi código, por ejemplo:
if (some condition)
{
Some code...
}
ReSharper me dio la advertencia anterior (Invierta la declaración "if" para reducir el anidamiento) y sugirió la siguiente corrección:
if (!some condition) return;
Some code...
Me gustaría entender por qué eso es mejor. Siempre pensé que usar "return" en medio de un método problemático, algo así como "goto".
ASSERT( exampleParam > 0 )
.Respuestas:
Un retorno en el medio del método no es necesariamente malo. Sería mejor regresar de inmediato si aclara la intención del código. Por ejemplo:
En este caso, si
_isDead
es cierto, podemos salir inmediatamente del método. Podría ser mejor estructurarlo de esta manera:Elegí este código del catálogo de refactorización . Esta refactorización específica se llama: Reemplazar condicional anidado con cláusulas de guardia.
fuente
No solo es estética , sino que también reduce el nivel máximo de anidación dentro del método. Esto generalmente se considera como una ventaja porque hace que los métodos sean más fáciles de entender (y, de hecho, muchas herramientas de análisis estático proporcionan una medida de esto como uno de los indicadores de la calidad del código).
Por otro lado, también hace que su método tenga múltiples puntos de salida, algo que otro grupo de personas cree que es un no-no.
Personalmente, estoy de acuerdo con ReSharper y el primer grupo (en un lenguaje que tiene excepciones, me parece absurdo discutir "puntos de salida múltiples"; casi cualquier cosa puede arrojar, por lo que hay numerosos puntos de salida potenciales en todos los métodos).
En cuanto al rendimiento : ambas versiones deben ser equivalentes (si no en el nivel de IL, entonces ciertamente después de que el jitter haya terminado con el código) en todos los idiomas. Teóricamente, esto depende del compilador, pero prácticamente cualquier compilador ampliamente utilizado en la actualidad es capaz de manejar casos mucho más avanzados de optimización de código que este.
fuente
Este es un argumento un poco religioso, pero estoy de acuerdo con ReSharper en que debería preferir menos anidamiento. Creo que esto supera los aspectos negativos de tener múltiples rutas de retorno desde una función.
La razón clave para tener menos anidamiento es mejorar la legibilidad y la facilidad de mantenimiento del código . Recuerde que muchos otros desarrolladores necesitarán leer su código en el futuro, y el código con menos sangría es generalmente mucho más fácil de leer.
Las condiciones previas son un gran ejemplo de dónde está bien regresar temprano al comienzo de la función. ¿Por qué la legibilidad del resto de la función debería verse afectada por la presencia de una verificación de precondición?
En cuanto a los aspectos negativos de regresar varias veces de un método, los depuradores son bastante potentes ahora, y es muy fácil averiguar exactamente dónde y cuándo regresa una función en particular.
Tener múltiples retornos en una función no va a afectar el trabajo del programador de mantenimiento.
Mala legibilidad del código.
fuente
Como otros han mencionado, no debería haber un impacto en el rendimiento, pero hay otras consideraciones. Aparte de esas inquietudes válidas, esto también puede abrirlo en algunas circunstancias. Supongamos que se trata de un
double
lugar:Contrasta eso con la inversión aparentemente equivalente:
Entonces, en ciertas circunstancias, lo que parece estar correctamente invertido
if
podría no serlo.fuente
La idea de regresar solo al final de una función surgió de los días anteriores a que los idiomas admitieran excepciones. Permitió que los programas confiaran en poder poner el código de limpieza al final de un método, y luego estar seguro de que se llamaría y algún otro programador no ocultaría un retorno en el método que hizo que se omitiera el código de limpieza . El código de limpieza omitido podría provocar una pérdida de memoria o recursos.
Sin embargo, en un lenguaje que admite excepciones, no ofrece tales garantías. En un lenguaje que admite excepciones, la ejecución de cualquier declaración o expresión puede causar un flujo de control que hace que el método finalice. Esto significa que la limpieza debe hacerse mediante el uso de finalmente o mediante palabras clave.
De todos modos, digo que creo que muchas personas citan la directriz de 'solo retorno al final de un método' sin entender por qué fue algo bueno hacer, y que reducir la anidación para mejorar la legibilidad es probablemente un mejor objetivo.
fuente
If you've got deep nesting, maybe your function is trying to do too many things.
=> esta no es una consecuencia correcta de tu frase anterior. Porque justo antes de decir que puede refactorizar el comportamiento A con el código C en el comportamiento A con el código D. el código D es más limpio, concedido, pero "demasiadas cosas" se refieren al comportamiento, que NO ha cambiado. Por lo tanto, no tiene ningún sentido con esta conclusión.Me gustaría agregar que hay un nombre para esos si invertidos: cláusula de guardia. Lo uso siempre que puedo.
Odio leer el código donde hay si al principio, dos pantallas de código y nada más. Simplemente invierta si y regrese. De esa manera nadie perderá el tiempo desplazándose.
http://c2.com/cgi/wiki?GuardClause
fuente
if
so no. lolNo solo afecta la estética, sino que también evita el anidamiento de código.
En realidad, puede funcionar como una condición previa para garantizar que sus datos también sean válidos.
fuente
Por supuesto, esto es subjetivo, pero creo que mejora mucho en dos puntos:
condition
mantiene.fuente
Múltiples puntos de retorno fueron un problema en C (y en menor medida en C ++) porque te obligaron a duplicar el código de limpieza antes de cada uno de los puntos de retorno. Con la recolección de basura, el
try
|finally
construir yusing
bloquear, realmente no hay razón por la que debas tenerles miedo.En última instancia, todo se reduce a lo que usted y sus colegas encuentran más fácil de leer.
fuente
a loop that is not vectorizable due to a second data-dependent exit
En cuanto al rendimiento, no habrá una diferencia notable entre los dos enfoques.
Pero la codificación es algo más que rendimiento. La claridad y la facilidad de mantenimiento también son muy importantes. Y, en casos como este donde no afecta el rendimiento, es lo único que importa.
Hay escuelas de pensamiento competidoras sobre qué enfoque es preferible.
Una vista es la que otros han mencionado: el segundo enfoque reduce el nivel de anidación, lo que mejora la claridad del código. Esto es natural en un estilo imperativo: cuando no tiene nada que hacer, es mejor que regrese temprano.
Otra opinión, desde la perspectiva de un estilo más funcional, es que un método debe tener solo un punto de salida. Todo en un lenguaje funcional es una expresión. Entonces, las declaraciones siempre deben tener cláusulas else. De lo contrario, la expresión if no siempre tendría un valor. Entonces, en el estilo funcional, el primer enfoque es más natural.
fuente
Las cláusulas de protección o las condiciones previas (como probablemente pueda ver) verifican si se cumple una determinada condición y luego interrumpe el flujo del programa. Son ideales para lugares donde realmente solo le interesa un resultado de una
if
declaración. Entonces, en lugar de decir:Invierte la condición y se rompe si se cumple esa condición inversa
return
no está tan sucio comogoto
. Le permite pasar un valor para mostrar el resto de su código que la función no pudo ejecutarse.Verá los mejores ejemplos de dónde se puede aplicar esto en condiciones anidadas:
vs:
Encontrará que pocas personas argumentan que la primera es más limpia pero, por supuesto, es completamente subjetiva. A algunos programadores les gusta saber en qué condiciones está operando algo por sangrado, mientras que prefiero mantener el flujo del método lineal.
No voy a sugerir ni por un momento que las preconfiguraciones cambiarán tu vida o te acostarán, pero es posible que tu código sea un poco más fácil de leer.
fuente
Aquí se hacen varios puntos buenos, pero también pueden ser ilegibles múltiples puntos de retorno , si el método es muy largo. Dicho esto, si va a utilizar múltiples puntos de retorno, asegúrese de que su método sea corto, de lo contrario, se puede perder la bonificación de legibilidad de múltiples puntos de retorno.
fuente
El rendimiento se divide en dos partes. Tiene rendimiento cuando el software está en producción, pero también desea tener rendimiento durante el desarrollo y la depuración. Lo último que quiere un desarrollador es "esperar" algo trivial. Al final, compilar esto con la optimización habilitada dará como resultado un código similar. Por lo tanto, es bueno conocer estos pequeños trucos que dan resultado en ambos escenarios.
El caso en la pregunta es claro, ReSharper es correcto. En lugar de anidar
if
declaraciones y crear un nuevo alcance en el código, está estableciendo una regla clara al comienzo de su método. Aumenta la legibilidad, será más fácil de mantener y reduce la cantidad de reglas que uno debe examinar para encontrar a dónde quiere ir.fuente
Personalmente prefiero solo 1 punto de salida. Es fácil de lograr si mantiene sus métodos cortos y al grano, y proporciona un patrón predecible para la próxima persona que trabaje en su código.
p.ej.
Esto también es muy útil si solo desea verificar los valores de ciertas variables locales dentro de una función antes de que salga. Todo lo que necesita hacer es colocar un punto de interrupción en el retorno final y se garantiza que lo alcanzará (a menos que se produzca una excepción).
fuente
Muchas buenas razones sobre cómo se ve el código . ¿Pero qué hay de los resultados? ?
Echemos un vistazo a un código C # y su forma compilada IL:
Este fragmento simple se puede compilar. Puede abrir el archivo .exe generado con ildasm y verificar cuál es el resultado. No publicaré todo lo relacionado con el ensamblador, pero describiré los resultados.
El código IL generado hace lo siguiente:
Entonces parece que el código saltará al final. ¿Qué pasa si hacemos un normal si con código anidado?
Los resultados son bastante similares en las instrucciones de IL. La diferencia es que antes había saltos por condición: si es falso, vaya al siguiente fragmento de código, si es verdadero, vaya al final. Y ahora el código IL fluye mejor y tiene 3 saltos (el compilador optimizó esto un poco): 1. Primer salto: cuando Longitud es 0 a una parte donde el código salta nuevamente (Tercer salto) hasta el final. 2. Segundo: en medio de la segunda condición para evitar una instrucción. 3. Tercero: si la segunda condición es falsa, salta al final.
De todos modos, el contador del programa siempre saltará.
fuente
En teoría, la inversión
if
podría conducir a un mejor rendimiento si aumenta la tasa de aciertos de predicción de rama. En la práctica, creo que es muy difícil saber exactamente cómo se comportará la predicción de bifurcación, especialmente después de la compilación, por lo que no lo haría en mi desarrollo diario, excepto si estoy escribiendo código de ensamblaje.Más sobre predicción de ramas aquí .
fuente
Eso es simplemente controvertido. No existe un "acuerdo entre los programadores" sobre la cuestión del retorno temprano. Siempre es subjetivo, que yo sepa.
Es posible hacer un argumento de rendimiento, ya que es mejor tener condiciones escritas para que con frecuencia sean ciertas; También se puede argumentar que es más claro. Por otro lado, crea pruebas anidadas.
No creo que obtenga una respuesta concluyente a esta pregunta.
fuente
Ya hay muchas respuestas perspicaces, pero aún así, me gustaría dirigirme a una situación ligeramente diferente: en lugar de una condición previa, que debería ponerse sobre una función, piense en una inicialización paso a paso, donde tiene que verificar cada paso para tener éxito y luego continuar con el siguiente. En este caso, no puede verificar todo en la parte superior.
Encontré mi código realmente ilegible al escribir una aplicación host ASIO con ASIOSDK de Steinberg, ya que seguí el paradigma de anidamiento. Fue como ocho niveles de profundidad, y no puedo ver una falla de diseño allí, como lo mencionó Andrew Bullock anteriormente. Por supuesto, podría haber empaquetado algún código interno para otra función, y luego anidar los niveles restantes allí para hacerlo más legible, pero esto me parece bastante aleatorio.
Al reemplazar la anidación con cláusulas de protección, incluso descubrí una idea errónea mía con respecto a una parte del código de limpieza que debería haber ocurrido mucho antes dentro de la función en lugar de al final. Con ramas anidadas, nunca habría visto eso, incluso se podría decir que condujeron a mi error.
Entonces, esta podría ser otra situación en la que los if invertidos pueden contribuir a un código más claro.
fuente
Evitar múltiples puntos de salida puede conducir a ganancias de rendimiento. No estoy seguro acerca de C #, pero en C ++ la optimización del valor de retorno con nombre (Copy Elision, ISO C ++ '03 12.8 / 15) depende de tener un único punto de salida. Esta optimización evita que la copia construya su valor de retorno (en su ejemplo específico no importa). Esto podría generar ganancias considerables en el rendimiento en bucles cerrados, ya que está guardando un constructor y un destructor cada vez que se invoca la función.
Pero para el 99% de los casos, guardar las llamadas adicionales de constructor y destructor no vale la pérdida de legibilidad que
if
introducen los bloques anidados (como otros han señalado).fuente
Es una cuestión de opinión.
Mi enfoque normal sería evitar ifs de una sola línea y retornos en medio de un método.
No querrá líneas como sugiere en todas partes en su método, pero hay algo que decir para verificar un montón de suposiciones en la parte superior de su método, y solo hacer su trabajo real si todas pasan.
fuente
En mi opinión, el retorno temprano está bien si solo está volviendo vacío (o algún código de retorno inútil que nunca verificará) y podría mejorar la legibilidad porque evita anidar y al mismo tiempo hace explícito que su función está hecha.
Si realmente está devolviendo un returnValue, la anidación suele ser una mejor manera de hacerlo porque devuelve su returnValue solo en un lugar (al final, duh), y puede hacer que su código sea más fácil de mantener en muchos casos.
fuente
No estoy seguro, pero creo que R # intenta evitar saltos lejanos. Cuando tienes IF-ELSE, el compilador hace algo como esto:
Condición falsa -> salto lejano a etiqueta_condición_ falso
etiqueta_condición_verdadera: instrucción1 ... instrucción_n
false_condition_label: instrucción1 ... instrucción_n
bloque final
Si la condición es verdadera, no hay salto ni despliegue de caché L1, pero saltar a false_condition_label puede estar muy lejos y el procesador debe desplegar su propio caché. Sincronizar caché es costoso. R # intenta reemplazar saltos lejanos en saltos cortos y en este caso hay una mayor probabilidad de que todas las instrucciones ya estén en caché.
fuente
Creo que depende de lo que prefiera, como se mencionó, no hay ningún acuerdo general afaik. Para reducir la molestia, puede reducir este tipo de advertencia a "Sugerencia"
fuente
Mi idea es que el retorno "en medio de una función" no debería ser tan "subjetivo". La razón es bastante simple, tome este código:
Tal vez el primer "retorno" no sea TAN intuitivo, pero eso realmente está ahorrando. Hay demasiadas "ideas" sobre códigos limpios, que simplemente necesitan más práctica para perder sus malas ideas "subjetivas".
fuente
Hay varias ventajas para este tipo de codificación, pero para mí la gran victoria es que, si puede regresar rápidamente, puede mejorar la velocidad de su aplicación. Es decir, sé que debido a la Precondición X, puedo regresar rápidamente con un error. Esto elimina primero los casos de error y reduce la complejidad de su código. En muchos casos, debido a que la tubería de la CPU ahora puede estar más limpia, puede detener los bloqueos o interruptores de la tubería. En segundo lugar, si está en un bucle, romper o regresar rápidamente puede ahorrarle mucha CPU. Algunos programadores usan invariantes de bucle para hacer este tipo de salida rápida, pero en esto puede romper su canal de CPU e incluso crear problemas de búsqueda de memoria y significa que la CPU necesita cargarse desde el caché externo. Pero básicamente creo que deberías hacer lo que pretendías, es decir, finalizar el ciclo o la función no crear una ruta de código compleja solo para implementar alguna noción abstracta de código correcto. Si la única herramienta que tienes es un martillo, entonces todo parece un clavo.
fuente