Come evitare la catena if / else if quando si classifica un'intestazione in 8 direzioni?


111

Ho il codice seguente:

if (this->_car.getAbsoluteAngle() <= 30 || this->_car.getAbsoluteAngle() >= 330)
  this->_car.edir = Car::EDirection::RIGHT;
else if (this->_car.getAbsoluteAngle() > 30 && this->_car.getAbsoluteAngle() <= 60)
  this->_car.edir = Car::EDirection::UP_RIGHT;
else if (this->_car.getAbsoluteAngle() > 60 && this->_car.getAbsoluteAngle() <= 120)
  this->_car.edir = Car::EDirection::UP;
else if (this->_car.getAbsoluteAngle() > 120 && this->_car.getAbsoluteAngle() <= 150)
  this->_car.edir = Car::EDirection::UP_LEFT;
else if (this->_car.getAbsoluteAngle() > 150 && this->_car.getAbsoluteAngle() <= 210)
  this->_car.edir = Car::EDirection::LEFT;
else if (this->_car.getAbsoluteAngle() > 210 && this->_car.getAbsoluteAngle() <= 240)
  this->_car.edir = Car::EDirection::DOWN_LEFT;
else if (this->_car.getAbsoluteAngle() > 240 && this->_car.getAbsoluteAngle() <= 300)
  this->_car.edir = Car::EDirection::DOWN;
else if (this->_car.getAbsoluteAngle() > 300 && this->_car.getAbsoluteAngle() <= 330)
  this->_car.edir = Car::EDirection::DOWN_RIGHT;

Voglio evitare la ifcatena di s; è davvero brutto. C'è un altro modo, forse più pulito, di scrivere questo?


77
@Oraekia Sembrerebbe molto meno brutto, meno da scrivere e meglio da leggere se fectch this->_car.getAbsoluteAngle()una volta prima dell'intera cascata.
πάντα ῥεῖ

26
Tutto quel dereferenziamento esplicito di this( this->) non è necessario e non fa davvero nulla di buono per la leggibilità ..
Jesper Juhl

2
@ Neil Accoppia come chiave, enum come valore, ricerca personalizzata lambda.
πάντα ῥεῖ

56
Il codice sarebbe molto meno brutto senza tutti quei >test; non sono necessari, poiché ciascuno di essi è già stato testato (nella direzione opposta) ifnell'istruzione precedente .
Pete Becker

10
@PeteBecker Questo è uno dei miei più fastidiosi codici come questo. Troppi programmatori non capiscono else if.
Barmar

Risposte:


176
#include <iostream>

enum Direction { UP, UP_RIGHT, RIGHT, DOWN_RIGHT, DOWN, DOWN_LEFT, LEFT, UP_LEFT };

Direction GetDirectionForAngle(int angle)
{
    const Direction slices[] = { RIGHT, UP_RIGHT, UP, UP, UP_LEFT, LEFT, LEFT, DOWN_LEFT, DOWN, DOWN, DOWN_RIGHT, RIGHT };
    return slices[(((angle % 360) + 360) % 360) / 30];
}

int main()
{
    // This is just a test case that covers all the possible directions
    for (int i = 15; i < 360; i += 30)
        std::cout << GetDirectionForAngle(i) << ' ';

    return 0;
}

Ecco come lo farei. (Come per il mio commento precedente).


92
Ho letteralmente pensato di aver visto il codice Konami al posto dell'enumerazione lì per un secondo.
Zano

21
@CodesInChaos: C99 e C ++ hanno gli stessi requisiti di C #: che if q = a/be r = a%bthen q * b + rdevono essere uguali a. Quindi è legale in C99 che un resto sia negativo. BorgLeader, puoi risolvere il problema con (((angle % 360) + 360) % 360) / 30.
Eric Lippert

7
@ericlippert, tu e la tua conoscenza della matematica computazionale continuate a stupire.
gregsdennis

33
Questo è molto intelligente, ma è completamente illeggibile e non è più probabile che sia mantenibile, quindi non sono d'accordo sul fatto che sia una buona soluzione alla "bruttezza" percepita dell'originale. C'è un elemento di gusto personale, qui, immagino, ma trovo che le versioni di ramificazione ripulite da x4u e motoDrizzt siano di gran lunga preferibili.
IMSoP

4
@cyanbeam il ciclo for in main è solo un "demo", GetDirectionForAngleè quello che sto proponendo in sostituzione della cascata if / else, sono entrambi O (1) ...
Borgleader

71

È possibile utilizzare map::lower_bounde memorizzare il limite superiore di ciascun angolo in una mappa.

Esempio di lavoro di seguito:

#include <cassert>
#include <map>

enum Direction
{
    RIGHT,
    UP_RIGHT,
    UP,
    UP_LEFT,
    LEFT,
    DOWN_LEFT,
    DOWN,
    DOWN_RIGHT
};

using AngleDirMap = std::map<int, Direction>;

AngleDirMap map = {
    { 30, RIGHT },
    { 60, UP_RIGHT },
    { 120, UP },
    { 150, UP_LEFT },
    { 210, LEFT },
    { 240, DOWN_LEFT },
    { 300, DOWN },
    { 330, DOWN_RIGHT },
    { 360, RIGHT }
};

Direction direction(int angle)
{
    assert(angle >= 0 && angle <= 360);

    auto it = map.lower_bound(angle);
    return it->second;
}

int main()
{
    Direction d;

    d = direction(45);
    assert(d == UP_RIGHT);

    d = direction(30);
    assert(d == RIGHT);

    d = direction(360);
    assert(d == RIGHT);

    return 0;
}

Nessuna divisione richiesta. Buona!
O. Jones

17
@ O.Jones: La divisione per una costante del tempo di compilazione è abbastanza economica, solo una moltiplicazione e alcuni spostamenti. Vorrei una delle table[angle%360/30]risposte perché è economico e senza rami. Molto più economico di un ciclo di ricerca ad albero, se viene compilato in asm che è simile al sorgente. (di std::unordered_mapsolito è una tabella hash, ma std::mapè tipicamente un albero binario rosso-nero. La risposta accettata utilizza efficacemente angle%360 / 30come una funzione hash perfetta per gli angoli (dopo aver replicato un paio di voci, e la risposta di Bijay lo evita persino con un offset)).
Peter Cordes

2
Potresti usare lower_boundsu un array ordinato. Sarebbe molto più efficiente di un file map.
wilx

La ricerca della mappa di @PeterCordes è facile da scrivere e da mantenere. Se gli intervalli cambiano, l'aggiornamento del codice di hashing potrebbe introdurre bug e se gli intervalli diventano non uniformi, potrebbero semplicemente cadere a pezzi. A meno che il codice non sia critico per le prestazioni, non mi preoccuperei.
OhJeez,

@ OhJeez: sono già non uniformi, il che viene gestito avendo lo stesso valore in più bucket. Usa un divisore più piccolo per ottenere più bucket, a meno che ciò non significhi utilizzare un divisore molto piccolo e avere troppi bucket. Inoltre, se le prestazioni non contano, anche una catena if / else non è male, se semplificata scomponendo this->_car.getAbsoluteAngle()con una variabile tmp e rimuovendo il confronto ridondante da ciascuna delle if()clausole dell'OP (controllando qualcosa che sarebbe già stato il precedente if ()). Oppure usa il suggerimento di array ordinato di @ wilx.
Peter Cordes

58

Crea un array, ogni elemento del quale è associato a un blocco di 30 gradi:

Car::EDirection dirlist[] = { 
    Car::EDirection::RIGHT, 
    Car::EDirection::UP_RIGHT, 
    Car::EDirection::UP, 
    Car::EDirection::UP, 
    Car::EDirection::UP_LEFT, 
    Car::EDirection::LEFT, 
    Car::EDirection::LEFT, 
    Car::EDirection::DOWN_LEFT,
    Car::EDirection::DOWN, 
    Car::EDirection::DOWN, 
    Car::EDirection::DOWN_RIGHT, 
    Car::EDirection::RIGHT
};

Quindi puoi indicizzare l'array con l'angolo / 30:

this->_car.edir = dirlist[(this->_car.getAbsoluteAngle() % 360) / 30];

Non sono richiesti confronti o ramificazioni.

Il risultato, tuttavia, è leggermente diverso dall'originale. I valori sui bordi, ad esempio 30, 60, 120, ecc. Vengono inseriti nella categoria successiva. Ad esempio, nel codice originale i valori validi per UP_RIGHTsono da 31 a 60. Il codice precedente assegna da 30 a 59 a UP_RIGHT.

Possiamo aggirare questo problema sottraendo 1 dall'angolo:

this->_car.edir = dirlist[((this->_car.getAbsoluteAngle() - 1) % 360) / 30];

Questo ora ci dà RIGHT30, UP_RIGHT60, ecc.

Nel caso di 0, l'espressione diventa (-1 % 360) / 30. Questo è valido perché -1 % 360 == -1e -1 / 30 == 0, quindi otteniamo ancora un indice di 0.

La sezione 5.6 dello standard C ++ conferma questo comportamento:

4 L' /operatore binario restituisce il quoziente e l' %operatore binario restituisce il resto dalla divisione della prima espressione per la seconda. Se il secondo operando di /o %è zero il comportamento non è definito. Per operandi integrali l' /operatore restituisce il quoziente algebrico con qualsiasi parte frazionaria scartata. se il quoziente a/bè rappresentabile nel tipo di risultato, (a/b)*b + a%bè uguale a a.

MODIFICARE:

Sono state sollevate molte domande riguardo alla leggibilità e alla manutenibilità di un costrutto come questo. La risposta data da motoDrizzt è un buon esempio di semplificazione del costrutto originale che è più manutenibile e non è altrettanto "brutto".

Espandendo la sua risposta, ecco un altro esempio che utilizza l'operatore ternario. Poiché ogni caso nel post originale viene assegnato alla stessa variabile, l'utilizzo di questo operatore può aiutare ad aumentare ulteriormente la leggibilità.

int angle = ((this->_car.getAbsoluteAngle() % 360) + 360) % 360;

this->_car.edir = (angle <= 30)  ?  Car::EDirection::RIGHT :
                  (angle <= 60)  ?  Car::EDirection::UP_RIGHT :
                  (angle <= 120) ?  Car::EDirection::UP :
                  (angle <= 150) ?  Car::EDirection::UP_LEFT :
                  (angle <= 210) ?  Car::EDirection::LEFT : 
                  (angle <= 240) ?  Car::EDirection::DOWN_LEFT :
                  (angle <= 300) ?  Car::EDirection::DOWN:  
                  (angle <= 330) ?  Car::EDirection::DOWN_RIGHT :
                                    Car::EDirection::RIGHT;

49

Quel codice non è brutto, è semplice, pratico, leggibile e di facile comprensione. Sarà isolato nel suo metodo, quindi nessuno dovrà affrontarlo nella vita di tutti i giorni. E nel caso in cui qualcuno debba controllarlo, forse perché sta eseguendo il debug della tua applicazione per un problema da qualche altra parte, è così facile che gli ci vorranno due secondi per capire il codice e cosa fa.

Se stessi facendo un debug di questo tipo, sarei felice di non dover passare cinque minuti a cercare di capire cosa fa la tua funzione. A questo proposito, tutte le altre funzioni falliscono completamente, poiché cambiano una semplice routine priva di bug, da dimenticare, in un pasticcio complicato che le persone durante il debug saranno costrette ad analizzare e testare a fondo. Essendo io stesso un project manager, sarei fortemente turbato da uno sviluppatore che intraprende un compito semplice e invece di implementarlo in modo semplice e innocuo, spreca tempo per implementarlo in modo troppo complicato. Pensa solo tutto il tempo che hai sprecato a pensarci, poi sei arrivato a SO chiedendo, e tutto solo per il gusto di peggiorare la manutenzione e la leggibilità della cosa.

Detto questo, c'è un errore comune nel codice che lo rende molto meno leggibile e un paio di miglioramenti che puoi fare abbastanza facilmente:

int angle = this->_car.getAbsoluteAngle();

if (angle <= 30 || angle >= 330)
  return Car::EDirection::RIGHT;
else if (angle <= 60)
  return Car::EDirection::UP_RIGHT;
else if (angle <= 120)
  return Car::EDirection::UP;
else if (angle <= 150)
  return Car::EDirection::UP_LEFT;
else if (angle <= 210)
  return Car::EDirection::LEFT;
else if (angle <= 240)
  return Car::EDirection::DOWN_LEFT;
else if (angle <= 300)
  return Car::EDirection::DOWN;
else if (angle <= 330)
  return Car::EDirection::DOWN_RIGHT;

Inseriscilo in un metodo, assegna il valore restituito all'oggetto, comprimi il metodo e dimenticalo per il resto dell'eternità.

PS c'è un altro bug oltre la soglia 330, ma non so come vuoi trattarlo, quindi non l'ho risolto affatto.


Aggiornamento successivo

Come da commento, puoi persino sbarazzarti dell'altro se non del tutto:

int angle = this->_car.getAbsoluteAngle();

if (angle <= 30 || angle >= 330)
  return Car::EDirection::RIGHT;

if (angle <= 60)
  return Car::EDirection::UP_RIGHT;

if (angle <= 120)
  return Car::EDirection::UP;

if (angle <= 150)
  return Car::EDirection::UP_LEFT;

if (angle <= 210)
  return Car::EDirection::LEFT;

if (angle <= 240)
  return Car::EDirection::DOWN_LEFT;

if (angle <= 300)
  return Car::EDirection::DOWN;

if (angle <= 330)
  return Car::EDirection::DOWN_RIGHT;

Non l'ho fatto perché sento che a un certo punto diventa solo una questione di preferenze personali, e lo scopo della mia risposta era (ed è) quello di dare una prospettiva diversa alla tua preoccupazione sulla "bruttezza del codice". Ad ogni modo, come ho detto, qualcuno lo ha sottolineato nei commenti e penso abbia senso dimostrarlo.


1
Cosa diavolo avrebbe dovuto ottenere?
astrazione è tutto.

8
se vuoi seguire questa strada, dovresti almeno sbarazzarti del superfluo else if, ifè sufficiente.
Ðаn

10
@ Ðаn Sono completamente in disaccordo sul else if. Trovo utile poter dare un'occhiata a un blocco di codice e vedere che è un albero decisionale, piuttosto che un gruppo di dichiarazioni non correlate. Sì, elseo breaknon sono necessari per il compilatore dopo un return, ma sono utili per l'essere umano che sta guardando il codice.
IMSoP

@ Ðаn Non ho mai visto una lingua in cui sarebbe richiesto un annidamento extra. O c'è una parola chiave elseif/ separata elsif, o tecnicamente stai usando un blocco di una sola istruzione che sembra iniziare if, proprio come qui. Rapido esempio di ciò a cui penso tu stia pensando ea cosa sto pensando: gist.github.com/IMSoP/90bc1e9e2c56d8314413d7347e76532a
IMSoP

7
@ Ðаn Sì, sono d'accordo che sarebbe orribile. Ma non è il elsefarti farlo, è una guida di stile scadente che non riconosce else ifcome un'affermazione distinta. Userei sempre le parentesi graffe, ma non scriverei mai codice del genere, come ho mostrato nella mia sintesi.
IMSoP

39

In pseudocodice:

angle = (angle + 30) %360; // Offset by 30. 

Così, abbiamo 0-60, 60-90, 90-150, ... come le categorie. In ogni quadrante con 90 gradi, una parte ne ha 60, una parte ne ha 30. Quindi, ora:

i = angle / 90; // Figure out the quadrant. Could be 0, 1, 2, 3 

j = (angle - 90 * i) >= 60? 1: 0; // In the quardrant is it perfect (eg: RIGHT) or imperfect (eg: UP_RIGHT)?

index = i * 2 + j;

Usa l'indice in una matrice contenente le enumerazioni nell'ordine appropriato.


7
Questo è un bene, probabilmente la migliore risposta qui. È probabile che se l'interrogante originale guardasse al suo uso dell'enumerazione in seguito, scoprirà di avere un caso in cui viene semplicemente riconvertito in un numero! Eliminare completamente l'enumerazione e attenersi a un intero di direzione probabilmente ha senso con altri posti nel suo codice e questa risposta ti porta direttamente lì.
Bill K

18
switch (this->_car.getAbsoluteAngle() / 30) // integer division
{
    case 0:
    case 11: this->_car.edir = Car::EDirection::RIGHT; break;
    case 1: this->_car.edir = Car::EDirection::UP_RIGHT; break;
    ...
    case 10: this->_car.edir = Car::EDirection::DOWN_RIGHT; break;
}

angle = this -> _ car.getAbsoluteAngle (); settore = (angolo% 360) / 30; Il risultato è 12 settori. Quindi indicizza in array o usa switch / case come sopra, il compilatore si trasforma comunque in jump table.
ChuckCottrill

1
Switch non molto meglio delle catene if / else.
Bill K

5
@ BillK: Potrebbe essere, se il compilatore lo trasforma in una tabella di ricerca per te. È più probabile che con una catena if / else. Ma dal momento che è facile e non richiede trucchi specifici per l'architettura, è probabilmente meglio scrivere la tabella di ricerca nel codice sorgente.
Peter Cordes

Generalmente le prestazioni non dovrebbero essere un problema - è leggibilità e manutenibilità - ogni switch e catena if / else di solito significa un mucchio di codice di copia e incolla disordinato che deve essere aggiornato in più punti ogni volta che aggiungi un nuovo elemento. Meglio evitare entrambi e provare a inviare tabelle, calcoli o semplicemente caricare i dati da un file e trattarli come dati, se possibile.
Bill K

PeterCordes è probabile che il compilatore generi codice identico per la LUT come per lo switch. @ BillK puoi estrarre l'interruttore in una funzione 0..12 -> Car :: EDirection, che avrebbe ripetizione equivalente a una LUT
Caleth

16

Ignorando il primo ifche è un caso un po 'speciale, i restanti seguono tutti lo stesso identico schema: un minimo, un massimo e una direzione; pseudo-codice:

if (angle > min && angle <= max)
  _car.edir = direction;

Realizzare questo vero C ++ potrebbe essere simile a:

enum class EDirection {  NONE,
   RIGHT, UP_RIGHT, UP, UP_LEFT, LEFT, DOWN_LEFT, DOWN, DOWN_RIGHT };

struct AngleRange
{
    int min, max;
    EDirection direction;
};

Ora, invece di scrivere un mucchio di ifs, passa in rassegna le tue varie possibilità:

EDirection direction_from_angle(int angle, const std::vector<AngleRange>& angleRanges)
{
    for (auto&& angleRange : angleRanges)
    {
        if ((angle > angleRange.min) && (angle <= angleRange.max))
            return angleRange.direction;
    }

    return EDirection::NONE;
}

( throwing un'eccezione piuttosto che returningNONE è un'altra opzione).

Che poi chiameresti:

_car.edir = direction_from_angle(_car.getAbsoluteAngle(), {
    {30, 60, EDirection::UP_RIGHT},
    {60, 120, EDirection::UP},
    // ... etc.
});

Questa tecnica è nota come programmazione basata sui dati . Oltre a sbarazzarsi di un mucchio di messaggi if, ti permetterebbe di aggiungere facilmente più direzioni (ad esempio, NNW) o ridurre il numero (sinistra, destra, su, giù) senza rielaborare altro codice.


(Gestire il tuo primo caso speciale è lasciato come "un esercizio per il lettore." :-))


1
Tecnicamente potresti eliminare min dato che tutti gli intervalli di angoli corrispondono, il che ridurrebbe la condizione a if(angle <= angleRange.max)ma +1 per l'utilizzo di funzionalità C ++ 11 come enum class.
Pharap

12

Sebbene le varianti proposte basate su una tabella di ricerca per angle / 30siano probabilmente preferibili, ecco un'alternativa che utilizza una ricerca binaria hardcoded per ridurre al minimo il numero di confronti.

static Car::EDirection directionFromAngle( int angle )
{
    if( angle <= 210 )
    {
        if( angle > 120 )
            return angle > 150 ? Car::EDirection::LEFT
                               : Car::EDirection::UP_LEFT;
        if( angle > 30 )
            return angle > 60 ? Car::EDirection::UP
                              : Car::EDirection::UP_RIGHT;
    }
    else // > 210
    {
        if( angle <= 300 )
            return angle > 240 ? Car::EDirection::DOWN
                               : Car::EDirection::DOWN_LEFT;
        if( angle <= 330 )
            return Car::EDirection::DOWN_RIGHT;
    }
    return Car::EDirection::RIGHT; // <= 30 || > 330
}

2

Se vuoi davvero evitare la duplicazione, puoi esprimerla come una formula matematica.

Prima di tutto, supponi di utilizzare Enum di @ Geek

Enum EDirection { RIGHT =0, UP_RIGHT, UP, UP_LEFT, LEFT, DOWN_LEFT,DOWN, DOWN_RIGHT}

Ora possiamo calcolare l'enumerazione usando la matematica dei numeri interi (senza la necessità di array).

EDirection angle2dir(int angle) {
    int d = ( ((angle%360)+360)%360-1)/30;
    d-=d/3; //some directions cover a 60 degree arc
    d%=8;
    //printf ("assert(angle2dir(%3d)==%-10s);\n",angle,dir2str[d]);
    return (EDirection) d;
}

Come sottolinea @motoDrizzt, il codice conciso non è necessariamente un codice leggibile. Ha il piccolo vantaggio che esprimerlo come matematica rende esplicito che alcune direzioni coprono un arco più ampio. Se vuoi andare in questa direzione puoi aggiungere alcune asserzioni per aiutare a capire il codice.

assert(angle2dir(  0)==RIGHT     ); assert(angle2dir( 30)==RIGHT     );
assert(angle2dir( 31)==UP_RIGHT  ); assert(angle2dir( 60)==UP_RIGHT  );
assert(angle2dir( 61)==UP        ); assert(angle2dir(120)==UP        );
assert(angle2dir(121)==UP_LEFT   ); assert(angle2dir(150)==UP_LEFT   );
assert(angle2dir(151)==LEFT      ); assert(angle2dir(210)==LEFT      );
assert(angle2dir(211)==DOWN_LEFT ); assert(angle2dir(240)==DOWN_LEFT );
assert(angle2dir(241)==DOWN      ); assert(angle2dir(300)==DOWN      );
assert(angle2dir(301)==DOWN_RIGHT); assert(angle2dir(330)==DOWN_RIGHT);
assert(angle2dir(331)==RIGHT     ); assert(angle2dir(360)==RIGHT     );

Dopo aver aggiunto le asserzioni hai aggiunto la duplicazione, ma la duplicazione nelle asserzioni non è poi così male. Se hai un'affermazione incoerente, lo scoprirai abbastanza presto. Gli asserti possono essere compilati fuori dalla versione di rilascio in modo da non gonfiare l'eseguibile distribuito. Tuttavia, questo approccio è probabilmente più applicabile se si desidera ottimizzare il codice piuttosto che renderlo meno brutto.


1

Sono in ritardo alla festa, ma potremmo usare flag enum e controlli di portata per fare qualcosa di pulito.

enum EDirection {
    RIGHT =  0x01,
    LEFT  =  0x02,
    UP    =  0x04,
    DOWN  =  0x08,
    DOWN_RIGHT = DOWN | RIGHT,
    DOWN_LEFT = DOWN | LEFT,
    UP_RIGHT = UP | RIGHT,
    UP_LEFT = UP | LEFT,

    // just so we be clear, these won't have much use though
    IMPOSSIBLE_H = RIGHT | LEFT, 
    IMPOSSIBLE_V = UP | DOWN
};

il controllo (pseudo-codice), assumendo che l'angolo sia assoluto (tra 0 e 360):

int up    = (angle >   30 && angle <  150) * EDirection.UP;
int down  = (angle >  210 && angle <  330) * EDirection.DOWN;
int right = (angle <=  60 || angle >= 330) * EDirection.Right;
int left  = (angle >= 120 && angle <= 240) * EDirection.LEFT;

EDirection direction = (Direction)(up | down | right | left);

switch(direction){
    case RIGHT:
         // do right
         break;
    case UP_RIGHT:
         // be honest
         break;
    case UP:
         // whats up
         break;
    case UP_LEFT:
         // do you even left
         break;
    case LEFT:
         // 5 done 3 to go
         break;
    case DOWN_LEFT:
         // your're driving me to a corner here
         break;
    case DOWN:
         // :(
         break;
    case DOWN_RIGHT:
         // completion
         break;

    // hey, we mustn't let these slide
    case IMPOSSIBLE_H:
    case IMPOSSIBLE_V:
        // treat your impossible case here!
        break;
}
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.