Perché le istruzioni "if elif else" non sono mai virtualmente in formato tabella?

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)  

Considerati gli esempi precedenti, non capisco perché virtualmente non vedo mai il primo stile nelle basi di codice. Per me, trasformi il codice in un formato tabulare che mostri chiaramente ciò che desideri. La prima colonna può essere praticamente ignorata. La seconda colonna identifica la condizione e la terza colonna fornisce l'output desiderato. Sembra, almeno per me, semplice e facile da leggere. Tuttavia, vedo sempre questo semplice tipo di caso / situazione di commutazione che emerge nel formato esteso, con tabulazione indentata. Perché? Le persone trovano il secondo formato più leggibile?

L'unico caso in cui ciò potrebbe essere problematico è se il codice cambia e si allunga. In quel caso, penso che sia perfettamente ragionevole rifattorizzare il codice nel formato lungo e indentato. Lo fanno tutti in secondo luogo semplicemente perché è sempre stato fatto? Essendo un avvocato del diavolo, credo che un altro motivo potrebbe essere perché le persone trovano due diversi formati a seconda della complessità delle dichiarazioni if / else da confondere? Qualsiasi intuizione sarebbe apprezzata.

    
posta horta 26.07.2016 - 21:31
fonte

10 risposte

93

Un motivo potrebbe essere che non stai utilizzando le lingue in cui è popolare.

Alcuni contro-esempi:

Haskell con guardie e con schemi:

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

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

Erlang with patterns:

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

In genere vedo che il formato della tabella è molto popolare con i linguaggi funzionali (e quelli basati su espressioni generali), mentre la rottura delle linee è più popolare in altri (principalmente basata su istruzioni).

    
risposta data 27.07.2016 - 01:47
fonte
134

È più leggibile. Alcuni motivi per cui:

  • Quasi tutte le lingue usano questa sintassi (non tutte, la maggior parte - il tuo esempio sembra essere Python, comunque)
  • isanae ha sottolineato in un commento che la maggior parte dei debugger sono basati su linee (non basate su istruzioni)
  • Inizia ad apparire ancora più brutto se devi inserire punti e virgola in linea
  • Legge dall'alto verso il basso più agevolmente
  • Sembra orribilmente illeggibile se hai qualcosa di diverso da dichiarazioni di ritorno banali
    • Qualsiasi sintassi significativa del rientro viene persa quando si scremano i codici poiché il codice condizionale non è più visivamente separato (da Dan Neely )
    • Questo sarà particolarmente negativo se continui a applicare patch / aggiungi elementi nelle istruzioni di riga 1 se
  • È leggibile solo se tutti i tuoi controlli if hanno circa la stessa lunghezza
  • Significa che non puoi formattare complicate se le istruzioni sono in istruzioni multilinea, avranno per essere oneliner
  • Ho molta più probabilità di notare bug / flusso logico durante la lettura verticale riga per riga, non cercando di analizzare più righe insieme
  • I nostri cervelli leggono il testo più stretto e più alto MOLTO più veloce del testo orizzontale lungo

Nel momento in cui proverai a fare ciò, finirai per riscriverlo in istruzioni multiline. Il che significa che hai appena perso tempo!

Anche le persone inevitabilmente aggiungono qualcosa del tipo:

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

Non ci vuole molto spesso farlo prima di decidere che questo formato è molto meglio della tua alternativa. Ah, ma potresti metterlo in linea tutto in una riga! enderland muore all'interno .

O questo:

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

Che è davvero, davvero fastidioso. A nessuno piace formattare cose come questa.

E per ultimo, inizierai la guerra santa del problema "quanti spazi per le schede". Ciò che rende perfettamente il tuo schermo come formato di una tabella potrebbe non essere visualizzato sulla mia in base alle impostazioni.

La leggibilità non dovrebbe dipendere comunque dalle impostazioni IDE.

    
risposta data 26.07.2016 - 21:52
fonte
55

Sono fermamente convinto che "il codice venga letto molte volte, scritto in pochi - quindi la leggibilità è molto importante."

Una cosa fondamentale che mi aiuta quando leggo il codice di altre persone è che segue i modelli "normali" che i miei occhi sono addestrati a riconoscere. Riesco a leggere la forma rientrata più facilmente perché l'ho vista tante volte che si registra quasi automaticamente (con un minimo sforzo cognitivo da parte mia). Non è perché è "più bello" - è perché segue le convenzioni a cui sono abituato. La convenzione batte "meglio" ...

    
risposta data 26.07.2016 - 22:31
fonte
16

Insieme agli altri inconvenienti già citati, il layout tabellare aumenta le probabilità di conflitti di unione del controllo della versione che richiedono l'intervento manuale.

Quando un blocco di codice tabellare deve essere riallineato, il sistema di controllo della versione considera ciascuna di queste linee come modificata:

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

Ora supponiamo che nel frattempo, in un altro ramo, un programmatore abbia aggiunto una nuova riga al blocco del codice allineato:

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

L'unione di quel ramo fallirà:

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.

Se il codice in fase di modifica non avesse utilizzato l'allineamento tabellare, l'unione sarebbe riuscita automaticamente.

(Questa risposta è stata "plagiata" da mio articolo scoraggiante allineamento tabulare nel codice) .

    
risposta data 28.07.2016 - 23:57
fonte
8

I formati tabulari possono essere molto belli se le cose rientrano sempre nella larghezza assegnata. Se qualcosa supera la larghezza assegnata, tuttavia, spesso diventa necessario avere una parte della tabella che non è allineata con il resto, oppure regolare il layout di tutto il resto della tabella per adattarlo all'elemento lungo .

Se i file sorgente sono stati modificati utilizzando programmi progettati per funzionare con dati in formato tabellare e in grado di gestire elementi troppo lunghi utilizzando una dimensione del carattere più piccola, suddividendoli in due righe all'interno della stessa cella, ecc., potrebbe avere senso utilizzare i formati tabulari più spesso, ma la maggior parte dei compilatori desidera file di origine privi dei tipi di markup che tali editor avrebbero bisogno di archiviare per mantenere la formattazione. Usare linee con quantità variabili di rientro ma nessun altro layout non è bello come la formattazione tabellare nel migliore dei casi, ma nel peggiore dei casi non causa quasi altrettanti problemi.

    
risposta data 27.07.2016 - 01:38
fonte
6

Esiste l'istruzione "switch" che fornisce questo genere di cose per casi speciali, ma suppongo che non sia quello che stai chiedendo.

Ho visto se le istruzioni in formato tabella, ma ci deve essere un gran numero di condizioni per renderlo utile. 3 se le istruzioni sono mostrate nel formato tradizionale, ma se ne hai 20, è molto più facile visualizzarle in un blocco grande formattato per renderlo più chiaro.

E c'è il punto: chiarezza. Se rende più facile vedere (e il tuo primo esempio non è facile da vedere dove si trova il delimitatore), formattalo in base alla situazione. Altrimenti, mantieni quello che le persone si aspettano, perché è sempre più facile da riconoscere.

    
risposta data 27.07.2016 - 09:35
fonte
1

Se la tua espressione è davvero così semplice, la maggior parte dei linguaggi di programmazione offre l'operatore?: branching:

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

Questo è un formato tabulare leggibile breve. Ma la parte importante è: vedo a prima vista qual è l'azione "principale". Questa è una dichiarazione di ritorno! E il valore è deciso in base a determinate condizioni.

Se d'altra parte ci sono rami che eseguono codice diverso, trovo molto più leggibile il rientro di questi blocchi. Perché ora ci sono diverse azioni "principali" a seconda dell'istruzione if. In un caso gettiamo, in un caso registriamo e restituiamo o semplicemente restituiamo. Esiste un flusso di programma diverso a seconda della logica, quindi i blocchi di codice incapsulano i diversi rami e li rendono più prominenti per lo sviluppatore (ad esempio, velocità di lettura di una funzione per afferrare il flusso del programma)

if ( i > 0 )
{
    throw new InvalidInputException(...);
}
else if ( i == 0 )
{
    return 0;
}
else
{
    log( "Calculating sqrt" );
    return sqrt( -i );
}
    
risposta data 27.07.2016 - 13:00
fonte
1

Come già detto da Enderland, stai assumendo che tu abbia sempre un "ritorno" come azione e che puoi taggare quel "ritorno" alla fine della condizione. Mi piacerebbe dare qualche dettaglio in più sul perché questo non avrà successo.

Non so quali siano le tue lingue preferite, ma sono stato codificato in C per molto tempo. Esistono numerosi standard di codifica attorno ai quali si mira ad evitare alcuni errori di codifica standard non consentendo la costruzione di codici soggetti a errori, sia nella codifica iniziale che durante la manutenzione successiva. Sono più familiare con MISRA-C, ma ce ne sono altri, e in generale hanno tutti regole simili perché stanno affrontando gli stessi problemi nella stessa lingua.

Un errore popolare a cui spesso si indirizzano gli standard di codifica è questo piccolo trucchetto: -

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

Questo non fa quello che pensi che faccia. Per quanto riguarda C, se x è 10, allora chiami do_something() , ma poi do_something_else() viene chiamato indipendentemente dal valore di x . Solo l'azione immediatamente successiva all'istruzione "if" è condizionata. Questo potrebbe essere ciò che intendeva il programmatore, nel qual caso c'è una potenziale trappola per i manutentori; o potrebbe non essere ciò che intendeva il programmatore, nel qual caso c'è un bug. È una domanda di intervista molto popolare.

La soluzione negli standard di codifica è quella di imporre le parentesi su tutte le azioni condizionali, anche se sono a linea singola. Ora otteniamo

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

o

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

e ora funziona correttamente ed è chiaro per i manutentori.

Noterai che questo è completamente incompatibile con il tuo formato in stile tabella.

Alcuni altri linguaggi (ad esempio Python) hanno esaminato questo problema e hanno deciso che, poiché i codificatori utilizzavano spazi bianchi per rendere chiaro il layout, sarebbe una buona idea utilizzare spazi bianchi anziché parentesi. Quindi in Python,

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

effettua le chiamate sia a do_something() sia a do_something_else() condizionale su x == 10, mentre

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

significa che solo do_something() è condizionale su x e do_something_else() è sempre chiamato.

È un concetto valido, e troverai che alcune lingue lo usano. (L'ho visto per la prima volta in Occam2, molto indietro quando). Ancora una volta, puoi facilmente vedere che il tuo formato in stile tabella è incompatibile con la lingua.

    
risposta data 27.07.2016 - 13:53
fonte
1

Il layout tabellare può essere utile in alcuni casi limitati, ma ci sono poche volte che è utile con if.

In casi semplici?: potrebbe essere una scelta migliore. Nei casi medi un interruttore è spesso una soluzione migliore (se la lingua ne ha uno). In casi complicati potresti scoprire che le tabelle di chiamata sono più adatte.

Ci sono state molte volte in cui refactoring codice che ho riorganizzato per essere tabellare per renderlo patern evidente. Raramente succede che lo lascio in questo modo in quanto nella maggior parte dei casi esiste un modo migliore per risolvere il problema una volta compreso. Occasionalmente una pratica di codifica o standard di layout lo proibiscono, nel qual caso è utile un commento.

C'erano alcune domande su ?: . Sì, è l'operatore ternario (o come mi piace pensarne il valore se). a prima vista questo esempio è un po 'complicato per?: (ed eccessivamente utile?: non aiuta la leggibilità ma lo fa male), ma con qualche pensiero L'esempio può essere riorganizzato come di seguito, ma penso che in questo caso un interruttore sia il più leggibile soluzione.

if i==0: return 0
return i>0?sqrt(i):(1j*sqrt(-i))
    
risposta data 27.07.2016 - 02:40
fonte
-3

Non vedo nulla di sbagliato nel formato tabella. Preferenza personale, ma vorrei usare un ternario come questo:

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

Non è necessario ripetere return ogni volta:)

    
risposta data 27.07.2016 - 17:33
fonte

Leggi altre domande sui tag