Stile di codifica: logica binaria o multiplo if () s?

5

Qualcosa che spesso compare nelle revisioni del codice è la struttura del codice, in particolare correlata all'uso delle strutture di controllo, in cui gli individui possono essere equamente valutati su ciò che è "giusto" e ciò che è "sbagliato".

Ho le mie opinioni sull'argomento, ma al momento, è proprio quello che è.

I frammenti di codice seguenti sono un esempio e provengono da una discussione recente. Preferisco uno di questi, e il mio collega preferisce l'altro.

Esempio 1:

  enum {
    failed_conn = 0,
    made_connection = 1
  };
  enum {
    is_not_in_set = 0,
    is_in_set = 1
  };
  int status = Status_Failed;
  in_set = FD_ISSET(socket_desc, &fdset);
  if (in_set) {
    sock_opt_read_result = getsockopt(socket_desc, SOL_SOCKET, SO_ERROR, &result, &result_len);
    if (sock_opt_read_result >= 0) {
      connection_to_target = (result == ECONNSUCCESS) || (result == ECONNREFUSED);
      if (connection_to_target == 1) {
        status = Status_OK;
      }
    }
    return status;
  }

Esempio 2:

  return ((FD_ISSET(socket_desc, &fdset))                                            /* Select returned the file descriptor (socket connected or failed) */   
       && (getsockopt(socket_desc, SOL_SOCKET, SO_ERROR, &result, &result_len) >= 0) /* We can get the error code (if any) from the connect call */
       && ((result == ECONNSUCCESS) || (result == ECONNREFUSED))) ? Status_OK        /* The connect() call succeded or was refused by target */
                                                                  : Status_Failed;   /* If any of the above are not true, we failed to connect */

Questi sono solo un esempio di struttura. Per quelli di voi che sono preoccupati per gli effetti collaterali (ad esempio la variabile result ) non lo sono, nessuna delle variabili in uso qui viene utilizzata altrove.

Quali sono gli (se ci sono) argomenti per la codifica in uno di questi stili (se ne ho abbastanza di quelli già esistenti), che potrebbero aiutare a cementare uno stile di codifica che può essere generalmente concordato, o uno stile, l'altro o qualche altra forma?

    
posta Baldrickk 11.04.2016 - 20:29
fonte

4 risposte

19

Non sono molto entusiasta di nessuna delle due versioni. La seconda (singola grande espressione in return statement) rende difficile seguire la logica di controllo. Il fatto che l'autore abbia sentito il bisogno di commentare il codice per chiarire che cosa fa e che non si adatti a qualsiasi lunghezza ragionevole della linea sono indicatori che l'espressione è troppo complessa e dovrebbe essere spezzata.

Il primo esempio, al contrario, mi sembra troppo complicato. Ad esempio, come è

enum {
    is_not_in_set = 0,
    is_in_set = 1
};
in_set = FD_ISSET(socket_desc, &fdset);
if (in_set) {
    // …
}

più leggibile del semplice utilizzo immediato del risultato della "chiamata di funzione" come condizionale?

if (FD_ISSET(socket_desc, &fdset)) {
    // …
}

Ogni nome che introduci nello scope corrente aggiunge ulteriore carico cognitivo al lettore. Ciò potrebbe essere giustificato se aggiunge ulteriori informazioni, ma non vedo come l'uso di enum aiuti qui. È chiaro che una chiamata a FD_ISSET mi dice se il descrittore di file è nell'insieme o no. Non riesco a vedere come il enum sia utile qui. E l'altro enum ?

Allo stesso modo qui.

connection_to_target = (result == ECONNSUCCESS) || (result == ECONNREFUSED);
if (connection_to_target == 1) {
    // …
}

C'è molta ridondanza qui. Dove viene dichiarato connection_to_target ? Qual è il suo tipo? Perché è rispetto a 1? Perché non è const ? Perché è lì?

Ancora, usando direttamente l'espressione

if ((result == ECONNSUCCESS) || (result == ECONNREFUSED)) {
    // …
}

migliorerebbe notevolmente la leggibilità e ridurrebbe la complessità.

Nel terzo caso

sock_opt_read_result = getsockopt(socket_desc, SOL_SOCKET, SO_ERROR, &result, &result_len);
if (sock_opt_read_result >= 0) {
    // …
}

potrebbe essere proposto un argomento per l'uso di una variabile denominata perché la chiamata alla funzione è molto lunga. Ma ancora una volta, ridurrebbe il carico cognitivo se la variabile fosse dichiarata const e inizializzata immediatamente. Inoltre, il nome sock_opt_read_result non è davvero utile. Il nome di una variabile dovrebbe indicare cosa significa, non da dove viene.

Infine, trovo il codice molto più leggibile quando si concentra sul "percorso del successo" e ne esce in caso di fallimento. Nei linguaggi di programmazione che hanno eccezioni, questo stile emerge in modo naturale. Ti aiuta a ridurre la profondità dei tuoi condizionali nidificati e rende più facile vedere a che punto abbiamo successo o fallimento.

Quindi, vorrei riscrivere il codice in qualche modo come questo.

if (!FD_ISSET(socket_desc, &fdset)) {
    return Status_Failed;
}
if (getsockopt(socket_desc, SOL_SOCKET, SO_ERROR, &result, &result_len) < 0) {
    return Status_Failed;
}
if ((result != ECONNSUCCESS) && (result != ECONNREFUSED)) {
    return Status_Failed;
}
return Status_OK;

Se senti il bisogno di spiegare perché una certa condizione indica un fallimento, l'inizio del rispettivo blocco "allora" è un buon posto per inserire un breve commento.

L'ultimo condizionale nel codice suggerito sopra potrebbe essere controverso. L'ho scritto in questo modo per rimanere fedele al principio suggerito di mantenere dritto il "percorso del successo". Alternative come

return ((result == ECONNSUCCESS) || (result == ECONNREFUSED)) ? Status_OK : Status_Failed;

o

switch (result) {
  case ECONNSUCCESS:
  case ECONNREFUSED:
    return Status_OK;
  default:
    return Status_Failed;
}

potrebbe anche essere una scelta ragionevole.

Sono abbastanza sicuro di poter essere d'accordo sul fatto che la sostituzione suggerita in questa risposta sia più leggibile rispetto a una delle alternative originali, ma questa risposta potrebbe non essere ancora basata sulla non opinione come speravate. Ho paura che non ci sia l'unico modo giusto per scrivere il tuo codice. Alla fine, penso che sia anche importante rispettare la personalità dei tuoi colleghi durante la revisione del codice. Se non è perfettamente chiaro in quale modo strutturare il codice è superiore, lascia che abbiano la sua versione e usa il tuo modo preferito per farlo nel codice che scrivi. Questi dettagli di implementazione sono locali per ciascuna funzione e non influiscono sulla capacità di manutenzione del software a un livello superiore. Se cambi la tua opinione in un secondo momento, puoi sempre rifattare in modo sicuro gli interni di qualsiasi funzione senza rischiare di rompere nulla.

A proposito, dai un'occhiata a Code Review se vuoi che altre persone discutano qualità di un pezzo del tuo codice.

    
risposta data 11.04.2016 - 22:07
fonte
4

Questo codice:

  return ((FD_ISSET(socket_desc, &fdset))                                            /* Select returned the file descriptor (socket connected or failed) */   
       && (getsockopt(socket_desc, SOL_SOCKET, SO_ERROR, &result, &result_len) >= 0) /* We can get the error code (if any) from the connect call */
       && ((result == ECONNSUCCESS) || (result == ECONNREFUSED))) ? Status_OK        /* The connect() call succeded or was refused by target */
                                                                  : Status_Failed;   /* If any of the above are not true, we failed to connect */

è una programmazione C idiomatica perfettamente chiara. L'ho trovato immediatamente comprensibile e ovviamente corretto. I programmatori C più esperti scriverebbero in questo stile, ma senza i commenti esplicativi, che non aggiungono nulla che non sia nella documentazione di FD_ISSET e getsockopt. I "miglioramenti" del recensore fanno sì che i buoni programmatori dica "WTFF ?? Perché non ha appena scritto 'return (A & & B & & C)? Success: failure'?"

L'uso di enumerazioni e variabili temporanee nel primo blocco rallenta solo la comprensione e aumenta la probabilità di errori. Questo è il tipo di codice che vedi dai programmatori che non amano C e desiderano che stessero programmando in COBOL.

    
risposta data 11.04.2016 - 21:33
fonte
2

La priorità nella scrittura del codice è la manutenibilità / leggibilità. Ci sono un gran numero di fattori che possono influenzare questo.

Rompendo per esempio:

Esempio 1:

  • Positivi:
    • variabili nominate: nominando ciascuna variabile con un nome leggibile, è più facile capire l'intento che sta dietro l'uso della variabile
    • strutture rinforzate: è chiaro quali operazioni sono collegate a quali istruzioni di controllo
  • Aspetti negativi:
    • "gonfia": ci sono molte variabili in uso. Più ci sono, più è difficile che il lettore tenga traccia. Questo link (sì, è è wikipedia ...) fornisce sia 7 o 4 come "limiti cognitivi", il che significa che provare a ricordare molti valori mentre il codice è letto diventa più difficile.
      • questo significa che un lettore che deve verificare quale valore rappresenta una variabile o aggiornare la memoria su ciò che è stato fatto a quella variabile dovrà interromperlo per ricontrollare
    • i risultati finali sono separati: il valore di ritorno è impostato in una posizione, (eventualmente) modificato in un'altra (devi leggere e comprendere ogni if separatamente per confermare) e restituito alla fine. Anche se non è terribile, è più da tenere traccia di.

Esempio 2:

  • positivi:

    • le variabili temporanee sono temporanee: non è necessario memorizzare questi valori per un uso successivo, poiché non verranno utilizzati in seguito. Dove il valore di ritorno di una funzione è usato come argomento, il nome della funzione è sufficiente per identificare l'uso del valore senza nominarlo esplicitamente
    • conciso: minimo bloat / densità informativa superiore, c'è una piccola piastra della caldaia intorno (e tra) il codice, quindi ciò che rimane è più significativo (sì, questo può essere preso troppo lontano, offuscando il codice. / li>
    • clear meaning: l'unico lavoro del snippet (ad alto livello) è return un valore, invece di crearne altri.
  • Negativi

    • affidamento sulla comprensione degli operatori booleani e sulla loro precedenza
    • la corrispondenza delle parentesi può essere un problema visivo

Sono sicuro che ci sono più argomenti a favore e contro. Questi sono solo quelli che vengono in mente.

    
risposta data 11.04.2016 - 20:29
fonte
2

In primo luogo, C99 introduce stdbool.h e supporto per bool ; Penso che tu possa rendere il tuo codice più leggibile e comprensibile usandolo.

5gon12eder ha già abbozzato un approccio accattivante usando successive istruzioni if ; usando bool potresti riformulare quello come il seguente snippet - un pattern che applico abbastanza spesso nel mio codice C ++.

bool ok = false;

ok = (FD_ISSET(socket_desc, &fdset) == 1);

if(ok)
    ok = (getsockopt(socket_desc, SOL_SOCKET, SO_ERROR, &result, &result_len) >= 0);

if(ok)
    ok = (result == ECONNSUCCESS) || (result == ECONNREFUSED);

return (ok ? Status_OK : Status_Failed);

Ciò semplificherebbe anche l'aggiunta della gestione degli errori, i diversi valori di ritorno, i blocchi multilinea e il codice di pulizia comune senza compromettere la comprensibilità:

bool ok = false;
enum Status status = Status_Failed;

ok = (FD_ISSET(socket_desc, &fdset) == 1);

if(ok)
    ok = (getsockopt(socket_desc, SOL_SOCKET, SO_ERROR, &result, &result_len) >= 0);

if(!ok) {
    //Error handling for getsockopt() goes here
}
else
    ok = (result == ECONNSUCCESS) || (result == ECONNREFUSED);

if(!ok)
    status = Status_Connection_Failed;
else {
    //Multiple lines of peer sanity checks here
    if(!ok)
        status = Status_Peer_Error;
}

//Cleanup code goes here

assert(ok || status != Status_OK);
return (ok ? Status_OK : status);
    
risposta data 13.04.2016 - 16:39
fonte

Leggi altre domande sui tag