¿Por qué las declaraciones "if elif else" prácticamente nunca están en formato de tabla?


73
if   i>0 : return sqrt(i)  
elif i==0: return 0  
else     : return 1j * sqrt(-i)

VS

if i>0:  
   return sqrt(i)  
elif i==0:  
   return 0  
else:  
   return 1j * sqrt(-i)  

Dado los ejemplos anteriores, no entiendo por qué prácticamente nunca veo el primer estilo en bases de código. Para mí, usted convierte el código en un formato tabular que muestra claramente lo que desea. La primera columna puede ser virtualmente ignorada. La segunda columna identifica la condición y la tercera columna le proporciona el resultado que desea. Parece, al menos para mí, sencillo y fácil de leer. Sin embargo, siempre veo este tipo simple de situación de caso / cambio en el formato extendido con sangría de pestañas. ¿Porqué es eso? ¿La gente encuentra el segundo formato más legible?

El único caso donde esto podría ser problemático es si el código cambia y se alarga. En ese caso, creo que es perfectamente razonable refactorizar el código en el formato largo con sangría. ¿Todos lo hacen de la segunda manera simplemente porque siempre fue así? Siendo un defensor del diablo, supongo que otra razón podría ser porque las personas encuentran dos formatos diferentes dependiendo de la complejidad de las declaraciones if / else que sean confusas. Cualquier idea sería apreciada.


9191
¿Porque la gente encuentra la segunda opción más legible?
GrandmasterB

65
El caso de uso de ramas mutuamente excluyentes que devuelven un valor del mismo tipo a menudo no aparece en idiomas imperativos en comparación con las ramas que pueden no devolver valores, pueden abarcar varias líneas y probablemente tener efectos secundarios. Si observara los lenguajes de programación funcionales, vería un código que se parece mucho más a su primer ejemplo.
Doval

47
@horta "El único caso en el que esto podría ser problemático es si el código cambia y se alarga". - NUNCA debe asumir que un código no se cambiará. La refactorización de código ocupa una gran mayoría del ciclo de vida de un software.
Charles Addis

77
@horta: Nada que ver conmigo. Es el codigo. En el contexto de la base de código que estoy leyendo, quiero ver si las declaraciones (y otras construcciones de lenguaje) tienen un formato consistente, sin ningún caso de borde. No es que haya aprendido a leer el código de una manera particular, puedo leer bien, pero es más legible si todo es igual. Nuevamente, no es lo mismo para mí, lo mismo para el resto del código.
GManNickG

44
Además, la mayoría de los depuradores se basan en líneas. No se puede poner un punto de interrupción en una declaración que está dentro de un ifsi está en la misma línea.
isanae

Respuestas:


93

Una razón puede ser que no estás usando idiomas donde es popular.

Algunos contraejemplos:

Haskell con guardias y con patrones:

sign x |  x >  0        =   1
       |  x == 0        =   0
       |  x <  0        =  -1

take  0     _           =  []
take  _     []          =  []
take  n     (x:xs)      =  x : take (n-1) xs

Erlang con patrones:

insert(X,Set) ->
    case lists:member(X,Set) of
        true  -> Set;
        false -> [X|Set]
    end.

Emacs lisp:

(pcase (get-return-code x)
  (`success       (message "Done!"))
  (`would-block   (message "Sorry, can't do it now"))
  (`read-only     (message "The shmliblick is read-only"))
  (`access-denied (message "You do not have the needed rights"))
  (code           (message "Unknown return code %S" code)))

En general, veo que el formato de tabla es bastante popular entre los lenguajes funcionales (y en los basados ​​en expresiones generales), mientras que romper las líneas es más popular en otros (principalmente basado en declaraciones).


10
He votado a favor de esto y, en general, estoy de acuerdo, pero me siento obligado a señalar que 1. Todos estos son retornos triviales 2. Haskellers es aficionado a los identificadores cómicamente cortos además de la brevedad del lenguaje en sí y 3. Los lenguajes funcionales tienden a estar basados ​​en expresiones , e incluso los lenguajes imperativos tienen diferentes estándares para las expresiones que las declaraciones. Si el OP reescribió el ejemplo original como aplicaciones de función, podría obtener diferentes consejos ...
Jared Smith

3
@JaredSmith Gracias por la división basada en expresiones / declaraciones: creo que puede ser aún más adecuado que funcional / imperativo. De nuevo, ruby ​​se basa casi en expresiones y no usa esa convención a menudo. (excepciones a todo) Con respecto a los puntos 1 y 2, encuentro que el 50% + del código real de Haskell son "retornos triviales" que son solo partes de algo más grande, es solo cómo se escribe ese código, no solo en ejemplos aquí. Por ejemplo, cerca de la mitad de las funciones aquí son uno solo uno / dos revestimientos. (algunas líneas usan diseño de tabla)
viraptor

Si. No soy un Haskeller, pero hago algo de ocaml y descubro que la coincidencia de patrones tiende a ser mucho más concisa que los interruptores lógicamente equivalentes, y las funciones polimórficas cubren una gran parte de ese terreno de todos modos. Me imagino que las clases de tipos de Haskell ampliarían esa cobertura aún más.
Jared Smith

Creo que es la sintaxis del caso del patrón lo que promueve eso. Debido a que es más conciso y, por lo general, más cercano a una caja de interruptor corta, es más fácil de expresar como frases simples. Frecuentemente hago esto con breves declaraciones de casos de cambio también por razones similares. Sin if-elseembargo, las declaraciones literales generalmente se extienden a través de varias líneas cuando no son efectivamente un ternario simple.
Isiah Meadows

@viraptor Técnicamente, el otro 50% del código haskell son "retornos no triviales" porque todas las funciones haskell son funcionalmente puras y no pueden tener efectos secundarios. Incluso las funciones que leen e imprimen en la línea de comandos son largas declaraciones de retorno.
Pharap

134

Es más legible. Algunas razones por las cuales:

  • Casi todos los idiomas usan esta sintaxis (no todos, la mayoría ; sin embargo, su ejemplo parece ser Python)
  • Isanae señaló en un comentario que la mayoría de los depuradores se basan en líneas (no en declaraciones)
  • Comienza a verse aún más feo si tienes que poner punto y coma o llaves
  • Se lee de arriba a abajo más suavemente
  • Parece terriblemente ilegible si tiene algo más que declaraciones triviales de devolución
    • Cualquier sintaxis significativa de sangría se pierde cuando lee el código, ya que el código condicional ya no está visualmente separado (de Dan Neely )
    • Esto será particularmente malo si continúa parcheando / agregando elementos en las declaraciones if de 1 línea
  • Solo es legible si todos sus cheques si tienen aproximadamente la misma longitud
  • Esto significa que no se puede formatear complicado si negativos en mensajes de varias líneas, que van tienen que ser oneliners
  • Es mucho más probable que note errores / flujo lógico cuando leo verticalmente línea por línea, no trato de analizar varias líneas juntas
  • Nuestros cerebros leen texto más estrecho y alto MUCHO más rápido que el texto largo y horizontal

En el momento en que intente hacer algo de esto, terminará reescribiéndolo en declaraciones multilínea. ¡Lo que significa que acaba de perder el tiempo!

También la gente inevitablemente agrega algo como:

if i>0:  
   print('foobar')
   return sqrt(i)  
elif i==0:  
   return 0  
else:  
   return 1j * sqrt(-i)  

No toma mucho tiempo hacer esto antes de decidir que este formato es mucho mejor que su alternativa. ¡Ah, pero podrías incluirlo todo en una sola línea! Enderland muere por dentro .

O esto:

if   i>0 : return sqrt(i)  
elif i==0 and bar==0: return 0  
else     : return 1j * sqrt(-i)

Lo cual es muy, muy molesto. A nadie le gusta formatear cosas como esta.

Y por último, comenzará la guerra santa del problema de "cuántos espacios para pestañas". Lo que se representa perfectamente en su pantalla como un formato de tabla puede no representarse en la mía dependiendo de la configuración.

La legibilidad no debe depender de la configuración IDE de todos modos.


14
@horta porque tendrás que convertirlo si primero lo formateas, ¿inicialmente? Estoy a punto de minimizar el trabajo para el futuro enderland. Hacer bonitas tablas de espacios en blanco y contar espacios y pestañas para actualizar el formato visual cuando podría agregar algo de lógica a un if check no es nada divertido (ignorando las implicaciones de legibilidad).
enderland

14
@horta No necesariamente dice que no lo arreglará, dice que tendría que arreglarlo, y eso es mucho tiempo dedicado al formato tedioso, en lugar de a la programación.
Servy

11
@horta: En mi humilde opinión su camino es a menudo menos legible, y es definitivamente mucho más molesto al formato. Además, solo se puede usar cuando los "ifs" son pequeños, lo cual es un caso raro. Por último, de alguna manera no es bueno, en mi humilde opinión, llevar dos tipos de formatos "if" a causa de esto.
Dagnelies

9
@horta, parece haber sido bendecido con trabajar en sistemas donde los requisitos, los sistemas, las API y las necesidades del usuario nunca cambian. Tu alma con suerte.
enderland

11
Añadiría: cualquier pequeño cambio en una sola condición puede requerir reformatear los otros para que coincidan con la :posición -> haciendo una diferencia en un CVS, de repente se vuelve más difícil entender lo que realmente está cambiando. Esto también es cierto para la condición frente al cuerpo. Tenerlos en líneas separadas significa que si cambia solo una de ellas, las diferencias muestran claramente que solo cambió la condición, no el cuerpo.
Bakuriu

55

Creo firmemente en que "el código se lee muchas veces, se escribe poco, por lo que la legibilidad es muy importante".

Una cosa clave que me ayuda cuando leo el código de otras personas es que sigue los patrones 'normales' que mis ojos están entrenados para reconocer. Puedo leer la forma sangrada más fácilmente porque la he visto tantas veces que se registra casi automáticamente (con poco esfuerzo cognitivo de mi parte). No es porque sea 'más bonita', es porque sigue las convenciones a las que estoy acostumbrado. Convención late 'mejor' ...



11
Esto explica por qué las personas son conservadoras. Para empezar, no explica por qué las personas eligieron escribir su código de cierta manera.
Jørgen Fogh

8
La pregunta era "¿por qué veo esto a menudo?", No de dónde vino este estilo. Ambas preguntas son interesantes; Traté de responder la que pensé que me estaban preguntando.
Art Swri

16

Junto con los otros inconvenientes ya mencionados, el diseño tabular aumenta las probabilidades de conflictos de fusión de control de versión que requieren intervención manual.

Cuando se necesita realinear un bloque de código alineado tabularmente, el sistema de control de versiones tratará cada una de esas líneas como modificadas:

diff --git a/foo.rb b/foo.rb
index 40f7833..694d8fe 100644
--- a/foo.rb
+++ b/foo.rb
@@ -1,8 +1,8 @@
 class Foo

   def initialize(options)
-    @cached_metadata = options[:metadata]
-    @logger          = options[:logger]
+    @metadata = options[:metadata]
+    @logger   = options[:logger]
   end

 end

Ahora suponga que, mientras tanto, en otra rama, un programador ha agregado una nueva línea al bloque de código alineado:

diff --git a/foo.rb b/foo.rb
index 40f7833..86648cb 100644
--- a/foo.rb
+++ b/foo.rb
@@ -3,6 +3,7 @@ class Foo
   def initialize(options)
     @cached_metadata = options[:metadata]
     @logger          = options[:logger]
+    @kittens         = options[:kittens]
   end

 end

La fusión de esa rama fallará:

wayne@mercury:/tmp/foo$ git merge add_kittens
Auto-merging foo.rb
CONFLICT (content): Merge conflict in foo.rb
Automatic merge failed; fix conflicts and then commit the result.

Si el código que se modifica no hubiera utilizado la alineación tabular, la fusión habría tenido éxito automáticamente.

(Esta respuesta fue "plagiada" de mi propio artículo desalentando la alineación tabular en el código).


1
Interesante, pero ¿no es esto una falla en las herramientas de fusión? Específicamente git en este caso? Este es un punto de datos hacia la convención que es el camino más fácil. Para mí, es algo que podría mejorarse (desde el lado de la herramienta).
horta

77
@horta Para que la herramienta de fusión modifique el espacio en blanco de una manera que no siempre rompa el código, tendrá que entender de qué manera puede modificar el espacio en blanco sin cambiar el significado del código. También tendrá que comprender la alineación tabular particular que se utiliza. Eso no solo dependerá del idioma (Python), sino que probablemente requerirá que la herramienta entienda el código hasta cierto punto. Por otro lado, la fusión basada en líneas se puede hacer sin IA, y con frecuencia ni siquiera se rompe el código.
Wayne Conrad

Gotcha Por lo tanto, como se menciona en otra parte de los comentarios, hasta que tengamos IDE o formatos de entrada de programación que incorporen tablas directamente en el formato, los problemas de herramientas siempre estarán ahí, haciendo la vida difícil para aquellos de nosotros que preferimos las tablas.
horta

1
@horta Correcto. La mayoría de mis objeciones a la alineación tabular en el código podrían desaparecer con herramientas suficientemente avanzadas.
Wayne Conrad

8

Los formatos tabulares pueden ser muy agradables si las cosas siempre se ajustan al ancho asignado. Sin embargo, si algo excede el ancho asignado, a menudo es necesario tener una parte de la tabla que no esté alineada con el resto o ajustar el diseño de todo lo demás en la tabla para que coincida con el elemento largo .

Si los archivos de origen se editaran utilizando programas diseñados para trabajar con datos de formato tabular y pudieran manejar elementos demasiado largos utilizando un tamaño de fuente más pequeño, dividiéndolos en dos líneas dentro de la misma celda, etc., entonces podría tener sentido usar tabular los formatos son más frecuentes, pero la mayoría de los compiladores quieren archivos fuente que no tengan los tipos de marcas que dichos editores necesitarían almacenar para mantener el formato. El uso de líneas con cantidades variables de sangría pero ningún otro diseño no es tan bueno como el formato tabular en el mejor de los casos, pero no causa tantos problemas en el peor de los casos.


Cierto. He notado que el editor de texto que uso (vim) tiene un soporte horrible para el formato tabular o incluso el texto ancho. No he visto a otros editores de texto hacerlo mucho mejor.
horta

6

Existe la declaración de "cambio" que proporciona este tipo de cosas para casos especiales, pero supongo que no es eso lo que estás preguntando.

He visto sentencias if en formato de tabla, pero tiene que haber una gran cantidad de condiciones para que valga la pena. 3 si las declaraciones se muestran mejor en el formato tradicional, pero si tenía 20, entonces es mucho más fácil mostrarlas en un bloque grande que esté formateado para hacerlo más claro.

Y ahí está el punto: claridad. Si lo hace más fácil de ver (y su primer ejemplo no es fácil de ver dónde está el delimitador), formateelo para adaptarlo a la situación. De lo contrario, quédese con lo que la gente espera, ya que siempre es más fácil de reconocer.


1
OP parece estar usando Python, así que no switch.
Jared Smith

2
"3 si las declaraciones se muestran mejor en el formato tradicional, pero si tuvieras 20" ... ¡entonces tienes problemas más grandes en los que pensar! :)
Grimm El Opiner

@GrimmTheOpiner Si estás escribiendo un analizador de lenguaje o un stringtizador AST, es algo muy posible de tratar. Por ejemplo, una vez contribuí a un analizador de JavaScript donde dividí una función con 15-20 casos, uno para cada tipo de expresión. Segmenté la mayoría de los casos a sus propias funciones (para un notable aumento de rendimiento), pero el tiempo switchfue una necesidad.
Isiah Meadows

@JaredSmith: Aparentemente switches malvado, pero crear instancias de un diccionario y luego buscarlo para hacer ramificaciones triviales no es malo ...
Mark K Cowan

1
@ MarkKCowan Oh, capté el sarcasmo pero pensé que lo estabas usando para burlarte de mí. falta de contexto en internet y demás.
Jared Smith

1

Si su expresión es realmente tan fácil, la mayoría de los lenguajes de programación ofrecen el operador de ramificación?:

return  ( i > 0  ) ? sqrt( i)
      : ( i == 0 ) ? 0
        /* else */ : 1j * sqrt( -i )

Este es un formato tabular corto y legible. Pero la parte importante es: veo de un vistazo cuál es la acción "principal". Esta es una declaración de devolución! Y el valor se decide por ciertas condiciones.

Si, por otro lado, tiene ramas que ejecutan código diferente, me parece mucho más legible sangrar estos bloques. Porque ahora hay diferentes acciones "principales" dependiendo de la declaración if. En un caso tiramos, en un caso registramos y regresamos o simplemente regresamos. Hay un flujo de programa diferente según la lógica, por lo que los bloques de código encapsulan las diferentes ramas y las hacen más prominentes para el desarrollador (por ejemplo, lectura rápida de una función para captar el flujo del programa)

if ( i > 0 )
{
    throw new InvalidInputException(...);
}
else if ( i == 0 )
{
    return 0;
}
else
{
    log( "Calculating sqrt" );
    return sqrt( -i );
}

77
En realidad, considero que su "formato tabular corto y legible" es una pesadilla de leer, mientras que el formato propuesto por OP está perfectamente bien.
Matteo Italia

@MatteoItalia ¿qué tal esta versión editada?
Falco

55
Lo siento, aún peor; Creo que se trata del hecho de que el ?y :son más difíciles de detectar que los if/ elselas palabras clave y / o debido al agregado "ruido" de los símbolos.
Matteo Italia

@ MatteoItalia: He tenido casos con más de cien valores diferentes. El valor de la tabla permite verificar errores. Con multilínea, es imposible.
gnasher729

1
@ gnasher729 - Para 100 "valores" diferentes, generalmente he encontrado que es mucho mejor declarar una estructura de datos de cada "elemento" y enumerarlo todo como una tabla en la inicialización para una matriz de estas estructuras de datos. (Por supuesto, las limitaciones de idioma pueden aplicarse aquí). Si algún elemento requiere un aspecto "computacional", la estructura del elemento puede contener un puntero o una referencia a una función para realizar la acción necesaria. Para muchas aplicaciones, esto puede simplificar enormemente el código y facilitar el mantenimiento.
Michael Karas

1

Como ya dijo Enderland, estás asumiendo que solo tienes un "retorno" como acción, y que puedes etiquetar ese "retorno" al final de la condición. Me gustaría dar algunos detalles adicionales de por qué esto no va a tener éxito.

No sé cuáles son sus idiomas preferidos, pero he estado codificando en C durante mucho tiempo. Hay una serie de estándares de codificación que tienen como objetivo evitar algunos errores de codificación estándar al no permitir construcciones de código que sean propensas a errores, ya sea en la codificación inicial o durante el mantenimiento posterior. Estoy más familiarizado con MISRA-C, pero hay otros, y en general todos tienen reglas similares porque abordan los mismos problemas en el mismo idioma.

Un error popular que los estándares de codificación suelen abordar es este pequeño problema:

if (x == 10)
    do_something();
    do_something_else();

Esto no hace lo que crees que hace. En lo que respecta a C, si x es 10, usted llama do_something(), pero luego do_something_else()se llama independientemente del valor de x . Solo la acción que sigue inmediatamente al enunciado "if" es condicional. Esto puede ser lo que pretendía el codificador, en cuyo caso hay una trampa potencial para los mantenedores; o puede que no sea lo que pretendía el codificador, en cuyo caso hay un error. Es una pregunta de entrevista popular.

La solución en los estándares de codificación es ordenar llaves alrededor de todas las acciones condicionales, incluso si son de una sola línea. Ahora tenemos

if (x == 10)
{
    do_something();
    do_something_else();
}

o

if (x == 10)
{
    do_something();
}
do_something_else();

y ahora funciona correctamente y está claro para los mantenedores.

Notarás que esto es completamente incompatible con tu formato de tabla.

Algunos otros lenguajes (por ejemplo, Python) analizaron este problema y decidieron que, dado que los codificadores usaban espacios en blanco para aclarar el diseño, sería una buena idea usar espacios en blanco en lugar de llaves. Entonces en Python,

if x == 10:
    do_something()
    do_something_else()

hace las llamadas a ambos do_something()y do_something_else()condicional en x == 10, mientras que

if x == 10:
    do_something()
do_something_else()

significa que solo do_something()está condicionado a x, y do_something_else()siempre se llama.

Es un concepto válido, y encontrará que algunos idiomas lo usan. (La vi por primera vez en Occam2, hace mucho tiempo). Sin embargo, una vez más, puede ver fácilmente que su formato de tabla es incompatible con el idioma.


1
Creo que te perdiste el punto. El problema del que habla es una extraña pesadilla no estándar específica de C que causa el problema del que habla. Si codifica en C, nunca recomendaría usar el método alternativo simple if con el formato tabular que sugerí. En cambio, como estás usando C, usarías llaves todo en la única línea. Las llaves en realidad aclararían aún más el formato de la tabla porque actúan como delimitadores.
horta

Además, las declaraciones de devolución en este caso son solo un ejemplo. En general, eso puede ser código de olor en sí mismo. Solo me estoy refiriendo al formato de declaraciones simples, no necesariamente con declaraciones de devolución en absoluto.
horta

2
Mi punto fue que esto hace que un formato de tabla sea aún más torpe. Por cierto, no es específico de C: lo comparten todos los lenguajes derivados de C, por lo que C ++, C #, Java y JavaScript permiten el mismo problema.
Graham

1
No me importa que sean declaraciones de devolución; entiendo que su intención es mostrar declaraciones simples. Pero se vuelve más engorroso. Y, por supuesto, tan pronto como cualquier declaración se vuelva no simple, debe cambiar el formato porque es imposible mantener un formato de tabla. A menos que esté haciendo ofuscación de código, las largas líneas de código son un olor por derecho propio. (El límite original era de 80 caracteres, actualmente es más de 130 caracteres, pero el principio general sigue siendo que no debería tener que desplazarse para ver el final de la línea.)
Graham

1

El diseño tabular puede ser útil en algunos casos limitados, pero hay pocas veces que sea útil con if.

En casos simples?: Puede ser una mejor opción. En casos medios, un cambio suele ser mejor (si su idioma tiene uno). En casos complicados, es posible que las tablas de llamadas encajen mejor.

En muchas ocasiones, al refactorizar el código, lo reorganicé para que sea tabular y hacerlo evidente. Es raro que lo deje así, ya que en la mayoría de los casos hay una mejor manera de resolver el problema una vez que lo comprende. Ocasionalmente, una práctica de codificación o estándar de diseño lo prohíbe, en cuyo caso un comentario es útil.

Hubo algunas preguntas sobre ?:. Sí, es el operador ternario (o, como me gusta pensar, el valor si). a primera vista, este ejemplo es un poco complicado para?: (¿y el uso excesivo? solución.

if i==0: return 0
return i>0?sqrt(i):(1j*sqrt(-i))

1
Puede que tenga que dejar en claro qué es "?:" Para los no iniciados (por ejemplo, con un ejemplo, posiblemente relacionado con la pregunta).
Peter Mortensen

Supongo que ese es el operador ternario. Siento que eso se rechaza por la buena razón por la que el operador ternario tiende a reorganizar el estándar si, hace cosas, de lo contrario, hace otro formato de cosas que la gente ve día tras día y, por lo tanto, puede leer fácilmente.
horta

@PeterMortensen: Si los no iniciados no saben lo que esto significa, deben mantenerse alejados del código hasta que hagan la pregunta obvia y se enteren.
gnasher729

@horta Ternary es if ? do stuff : do other stuff. El mismo orden que un if / else.
Navin

1
@Navin Ah, tal vez sea solo una falla del lenguaje que más uso (python). stackoverflow.com/questions/394809/…
horta

-3

No veo nada malo con el formato de tabla. Preferencia personal, pero usaría un ternario como este:

return i>0  ? sqrt(i)       :
       i==0 ? 0             :
              1j * sqrt(-i)

No es necesario repetir returncada vez :)


1
Como se mencionó en algunos comentarios, las declaraciones de devolución no son ideales o el objetivo de la publicación, simplemente un fragmento de código que encontré en línea y formateé de varias maneras.
horta

Python ternaries son do_something() if condition() else do_something_else(), no condition() ? do_something() : do_something_else().
Isiah Meadows

@IsiahMeadows OP nunca mencionó Python.
Navin
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.