C'è un modo per avere quattro risultati possibili senza ripetere le due condizioni? [duplicare]

2

In senso generico, la mia domanda è simile a questa:

if (p) {
    if (q) {
        a();
    } else {
        b();
    }
} else {
    if (q) {
        c();
    } else {
        d();
    }
}

Ci sono quattro risultati a() , b() , c() e d() a seconda delle condizioni p e q . Anche se p e q saranno valutati solo una volta in ogni caso, q è ancora ripetuto nel codice. Mi chiedo se c'è un modo per evitare questa ridondanza.

Nel mio caso specifico, mi trovo in una situazione in cui un risultato in realtà non sta accadendo nulla, e un altro risultato è un fatto aggiuntivo. Quindi sembra questo:

if (p) {
    a();
    if (q) {
        b();
    }
} else {
    if (q) {
        c();
    }
}

C'è un modo per scrivere questo in modo che p e q vengano visualizzati solo una volta? Di solito sono bravo a riordinare le cose per evitare la ridondanza, ma non so se sia possibile in questo caso. Se non è possibile, esiste un modo consigliato per esprimere la struttura logica in questo modo?

Qui sto usando C # ma accetterò le risposte in qualsiasi lingua.

EDIT: La mia domanda è sicuramente simile a Come trasformare la tabella della verità in un blocco / se possibile il più piccolo possibile e trovo le sue risposte utili, ma la mia domanda mi sembra diversa, nel senso che sto chiedendo specificamente come evitare la condizione ridondante.

    
posta Kyle Delaney 18.08.2017 - 21:46
fonte

3 risposte

5

Sì, è fattibile e potrebbe essere una buona idea

Certamente questo è fattibile e può spesso generare una logica più chiara, a seconda di quali parametri stai usando. Speriamo che i parametri siano collegati in qualche modo a un concetto di business ben compreso.

Identifica il concetto di business nascosto

Nei casi in cui i due parametri sono mappati su un singolo concetto di business (nascosto), è necessario rendere esplicito tale concetto, utilizzando una funzione di mappatura che prende p e q e restituisce il caso.

Questo esempio corrisponde al tuo primo esempio:

z = map(p, q)
switch (z)
    case z1:
       a();
       break;
    case z2:
       b();
       break;
    case z3:
       c();
       break;
    case z4:
       d();
       break;
}

O per il tuo secondo esempio:

z = map(p, q)
switch (z)
    case z1:
       a();
       break;
    case z2:
       a();
       b();
       break;
    case z3:
       c();
       break;
    case z4:
       //No operation
       break;
}

Qual è la funzione di mappatura?

La funzione di mappatura può essere semplice come combinare p e q in una maschera di bit (se sono booleani) o combinare stringhe (se si tratta, ad esempio, di codici prodotto e sottoprodotto). Questo potrebbe essere un allungamento in alcune circostanze, ma è una soluzione semplice e ovvia in altre circostanze. Ad esempio, prendi in considerazione queste domande che potrebbero apparire in una domanda di assicurazione sanitaria:

  1. sei sposato? Sì o no
  2. Hai figli? Sì o no

In questo caso, la tua azienda potrebbe offrire quattro prodotti (il concetto di business nascosto): single, sposato, single con figli e sposato con figli. In questo tipo di scenario è perfettamente giustificato combinare le risposte dell'utente in un prodotto virtuale (dato dalla funzione di mappatura) e scegliere un percorso logico basato sul prodotto anziché sulle singole bandiere. In effetti è un codice molto migliore, IMO.

Sbarazzarsi del caso interruttore

Switch / case è brutto. Un altro approccio continuerebbe a utilizzare il concetto di business nascosto ma in uno schema di codice più pulito:

IProduct z = ProductFactory.GetProduct(q, p);
z.Execute();

... dove la factory può restituire uno dei quattro prodotti, ognuno dei quali implementa IProduct che contiene un metodo Execute specifico del prodotto che eseguirà a() , b() , c() e / o d() se necessario.

La fabbrica non avrà lo stesso problema (originale)?

La factory potrebbe contenere dichiarazioni% co_de nidificate (che riportano indietro il problema originale) oppure potrebbe utilizzare una tabella di ricerca per l'istanziazione, come questa:

class ProductFactory
{
    private static readonly Dictionary<bool, Dictionary<bool, Func<IProduct>>> _products  
                      = new Dictionary<bool, Dictionary<bool, Func<IProduct>>>();

    static ProductFactory()
    {
        _products.Add(false, new Dictionary<bool, Func<IProduct>> {
            { true,  () => new ProductSingleWithChildren() },
            { false, () => new ProductSingle() }
        });
        _products.Add(true, new Dictionary<bool, Func<IProduct>> {
            { true,  () => new ProductMarriedWithChildren() },
            { false, () => new ProductMarried() }
        });

     }
    public IProduct GetProduct(bool isMarried, bool hasChildren)
    {
        return _products[isMarried][hasChildren]();
    }
}

... dove if è un elenco di elenchi, ogni elemento contenente un _products che istanzia il tipo appropriato.

I dizionari con una chiave di Func<IProduct> sembrano un po 'strani, quindi in un vero pezzo di codice potresti inclinarti a utilizzare bool , oppure possibile a enum contenente un codice. Suggerirei di utilizzare un identificatore allineato con il business / dominio; rendere facile per gli altri sviluppatori capire cosa sta succedendo.

Per completezza, ecco un'idea di come sarebbero le classi:

interface IProduct
{
    void Execute();
}
class ProductSingle : IProduct
{
    public void Execute()
    {
        a();
    }
}
class ProductSingleWithChildren : IProduct 
{
    public void Execute()
    {
        a();
        b();
    }
}
class ProductMarried: IProduct 
{
    public void Execute()
    {
        c();
    }
}
class ProductMarriedWithChildren : IProduct 
{
    public void Execute()
    {
        //No operation
    }
}

Noterai che alla fine non ci siamo solo sbarazzati della condizione string duplicata, ci sono infatti dichiarazioni di% ZERO% co_de nell'intera soluzione !!! :) Abbastanza pulito, e ti fornirà un punteggio di complessità ciclomatica basso e una buona pipeline pulita della CPU. Più codice molto più leggibile.

Poi di nuovo, abbiamo appena scritto un sacco di più righe di codice rispetto all'esempio originale. Quindi, a seconda del contesto, forse le dichiarazioni% co_de nidificate sarebbero migliori, in alcuni casi, ad es. la combinazione dei flag è arbitraria o non si associa ad un concetto logico all'interno del problema aziendale, o la leggibilità extra non è necessaria per un certo piccolo problema. Dipende da te, naturalmente.

    
risposta data 19.08.2017 - 00:32
fonte
3

Risposta filosofica breve

Si sta tentando di presentare un bel trucco. Lo farò più tardi nella risposta. Ma preferirei innanzitutto affrontare la causa principale del tuo problema, anche se potrebbe non piacerti:

I'm usually good at rearranging things to avoid redundancy
- Kyle Delaney

Sì, certamente. Ma sei bravo come il tuo compilatore di ottimizzazione? Lascia che questo tipo di livello basso riordini il tuo compilatore e concentrati sul codice corretto. Il compilatore ha molto più trucco, in quanto può funzionare a livello di assemblatore.

Premature optimisation is the root of all evil
- Donald Knuth

Don't diddle code to make it faster; find a better algorithm.
- B.W.Kernighan & P.J.Plauger

Risposta lunga: sì è possibile ma non necessariamente migliore

Scriverò in C ++ mentre accetti le soluzioni in qualsiasi lingua.

Il tuo codice iniziale con 10 istruzioni genererà un codice diverso a seconda di come inizializzi p e q , e cosa sa il compilatore di a() , b() e c()

Qui il risultato dell'assieme se p e q sono inizializzati con un numero casuale. Il compilatore li manterrà nei registri il più a lungo possibile:

    call    rand         ;  p is returned in register eax
    mov     ebx, eax     ;  ultrafast move to another register
    call    rand         ;  so that q can be kept in register eax
    test    ebx, ebx     ;  check if p is null 
    jne     .L11         ;  if not null continue at .L11 to call a() etc...
    test    eax, eax     ;  here we're in the "else": check if q is null 
    jne     .L12         ;  if not null go to .L12 to call c() and finish
.L4:
    ...                  ; function exit code, not relevant for us
    ret 
.L12:
    call    c()
    jmp     .L4
.L11:
    mov     DWORD PTR [rsp+12], eax   ; save q : register might be overwritten
    call    a()                       ; execute a() 
    mov     eax, DWORD PTR [rsp+12]   ; restore q
    test    eax, eax                  ; and test if it's null
    je      .L4                       
    call    b()                       ; if it's not call b(). 
    jmp     .L4

Come vedi, ci sono due accessi a q come nel codice sorgente. Assolutamente no, a quanto pare. Tuttavia, il codice è per lo più basato sulla registrazione, quindi molto veloce.

Ora una variante, in cui p e q vengono inizializzati dallo standard input. Per fare ciò, l'indirizzo di p e q deve essere "trapelato" a una funzione di libreria e una volta trapelato, poiché il compilatore non sa cosa succede in a() , b() e c() , assume il peggio (cioè, ciascuna di queste funzioni potrebbe modificare maliziosamente i booleani riutilizzando l'indirizzo "trapelato").

Per farla breve, sarà 17 istruzioni di assemblaggio pertinenti . Di nuovo, q verrà letto in due istruzioni. Purtroppo qui p e q verranno scritti e letti dalla memoria dello stack, a causa del vincolo sopra menzionato. L'accesso alla memoria è leggermente più lento dell'accesso al registro. Quindi questa versione sarà meno performante. Ma vedi, il compilatore prende in considerazione molte cose. Ritiene che il rischio di q sia stato modificato da a() ?

Infine, propongo una soluzione utilizzando un puntatore a funzione. Probabilmente può essere implementato in modo simile in altre lingue che supportano le funzioni lambda.

void (*x)() = c;   // function pointer pointing by default to c() function
if (p) {          // if it's p then we will execute a()
    a();
    x = b;        // and depending on q, we could have to execute b() later instead of c()
}
if (q) {
    x();          // call the function pointed to
}

Non sono sicuro che aggiungerà alla leggibilità del codice, ma p e q sono accessibili solo in un posto. Il codice è anche più breve: 6 istruzioni nel codice sorgente e 14 istruzioni pertinenti nell'assembly . Potrei sentirmi soddisfatto, ma è davvero una buona idea?

Sfortunatamente, la chiamata al puntatore della funzione sarà più lenta di una chiamata diretta all'indirizzo della funzione (a causa della indiretta). E la CPU dovrà eseguire una o due istruzioni aggiuntive per caricare e aggiornare il puntatore della funzione. Quindi, pur essendo di dimensioni più ridotte e ben organizzato, si tradurrà in un codice più lento e meno leggibile.

Pertanto, vorrei strongmente ribadire la mia prima risposta filosofica.

    
risposta data 18.08.2017 - 22:08
fonte
-1

Sì, concatena semplicemente P e Q e metti il risultato al tuo condizionale.

La domanda è: perché lo faresti?

    
risposta data 19.08.2017 - 18:01
fonte