Como evitar a cadeia if / else if ao classificar um título em 8 direções?

111

Eu tenho o seguinte código:

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;

Eu quero evitar a ifcorrente s; é muito feio. Existe outra maneira, possivelmente mais limpa, de escrever isso?

Oraekia
fonte
77
@Oraekia Pareceria muito menos feio, menos para digitar e melhor para ler se você verificasse this->_car.getAbsoluteAngle()uma vez antes de toda a cascata.
πάντα ῥεῖ
26
Toda aquela desreferenciação explícita de this( this->) não é necessária e realmente não faz nada de bom para a legibilidade.
Jesper Juhl
2
@Neil Emparelhar como chave, enum como valor, pesquisa personalizada lambda.
πάντα ῥεῖ
56
O código seria muito menos feio sem todos esses >testes; eles não são necessários, uma vez que cada um deles já foi testado (na direção oposta) na ifinstrução anterior .
Pete Becker
10
@PeteBecker Essa é uma das minhas queixas preferidas sobre códigos como este. Muitos programadores não entendem else if.
Barmar

Respostas:

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;
}

É assim que eu faria. (De acordo com meu comentário anterior).

Borgleader
fonte
92
Eu literalmente pensei ter visto o código Konami no lugar do enum por um segundo.
Zano
21
@CodesInChaos: C99 e C ++ têm o mesmo requisito que C #: se q = a/be r = a%bentão q * b + rdevem ser iguais a. Assim, é legal em C99 que um resto seja negativo. BorgLeader, você pode corrigir o problema com (((angle % 360) + 360) % 360) / 30.
Eric Lippert
7
@ericlippert, você e seu conhecimento de matemática computacional continuam a impressionar.
gregsdennis
33
Isso é muito inteligente, mas é completamente ilegível e não mais provável de ser mantido, então eu discordo que é uma boa solução para a "feiura" percebida do original. Há um elemento de gosto pessoal aqui, eu acho, mas acho as versões de ramificação limpas por x4u e motoDrizzt amplamente preferíveis.
IMSoP
4
@cyanbeam o loop for no principal é apenas uma "demonstração", GetDirectionForAngleé o que estou propondo como uma substituição para a cascata if / else, ambos são O (1) ...
Borgleader
71

Você pode usar map::lower_bounde armazenar o limite superior de cada ângulo em um mapa.

Exemplo de trabalho abaixo:

#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;
}
Steve Lorimer
fonte
Nenhuma divisão necessária. Boa!
O. Jones
17
@ O.Jones: A divisão por uma constante de tempo de compilação é bastante barata, apenas uma multiplicação e algumas mudanças. Eu iria com uma das table[angle%360/30]respostas porque é barato e sem ramos. Muito mais barato do que um loop de busca em árvore, se ele compilar em uma forma semelhante à fonte. ( std::unordered_mapgeralmente é uma tabela hash, mas std::mapé tipicamente uma árvore binária vermelha e preta. A resposta aceita usa efetivamente angle%360 / 30como uma função hash perfeita para ângulos (depois de replicar algumas entradas, e a resposta de Bijay até evita isso com um deslocamento)).
Peter Cordes
2
Você pode usar lower_boundem uma matriz classificada. Isso seria muito mais eficiente do que a map.
Wilx
A pesquisa do mapa @PeterCordes é fácil de escrever e fácil de manter. Se os intervalos mudarem, atualizar o código de hash pode introduzir bugs e se os intervalos se tornarem não uniformes, ele pode simplesmente falhar. A menos que esse código seja crítico para o desempenho, eu não me incomodaria.
OhJeez de
@OhJeez: Eles já são não uniformes, o que é resolvido por ter o mesmo valor em vários baldes. Basta usar um divisor menor para obter mais cubos, a menos que isso signifique usar um divisor muito pequeno e ter muitos cubos. Além disso, se o desempenho não importa, então uma cadeia if / else também não é ruim, se simplificada pela fatoração this->_car.getAbsoluteAngle()com uma var tmp e removendo a comparação redundante de cada uma das if()cláusulas do OP (verificando por algo que já teria correspondido o precedente if ()). Ou use a sugestão de array ordenado de @wilx.
Peter Cordes de
58

Crie uma matriz, cada elemento associado a um bloco de 30 graus:

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
};

Então você pode indexar a matriz com o ângulo / 30:

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

Nenhuma comparação ou ramificação necessária.

O resultado, entretanto, está um pouco diferente do original. Os valores nas bordas, ou seja, 30, 60, 120, etc. são colocados na próxima categoria. Por exemplo, no código original, os valores válidos de UP_RIGHTsão 31 a 60. O código acima atribui 30 a 59 a UP_RIGHT.

Podemos contornar isso subtraindo 1 do ângulo:

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

Isso agora nos dá RIGHT30, UP_RIGHT60 etc.

No caso de 0, a expressão se torna (-1 % 360) / 30. Isso é válido porque -1 % 360 == -1e -1 / 30 == 0, portanto, ainda temos um índice de 0.

A seção 5.6 do padrão C ++ confirma este comportamento:

4 O /operador binário produz o quociente, e o %operador binário produz o resto da divisão da primeira expressão pela segunda. Se o segundo operando de /ou %for zero, o comportamento é indefinido. Para operandos integrais, o /operador produz o quociente algébrico com qualquer parte fracionária descartada. se o quociente a/bé representável no tipo do resultado, (a/b)*b + a%bé igual a a.

EDITAR:

Muitas questões foram levantadas em relação à legibilidade e manutenção de um construto como este. A resposta dada por motoDrizzt é um bom exemplo de simplificação da construção original que é mais sustentável e não é tão "feia".

Expandindo sua resposta, aqui está outro exemplo que faz uso do operador ternário. Como cada caso na postagem original é atribuído à mesma variável, o uso desse operador pode ajudar a aumentar a legibilidade ainda mais.

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;
dbush
fonte
49

Esse código não é feio, é simples, prático, legível e fácil de entender. Ficará isolado em seu próprio método, então ninguém terá que lidar com isso no dia a dia. E apenas no caso de alguém ter que verificar - talvez porque ele está depurando seu aplicativo para um problema em outro lugar - é tão fácil que ele levará dois segundos para entender o código e o que ele faz.

Se eu estivesse fazendo esse tipo de depuração, ficaria feliz em não ter que gastar cinco minutos tentando entender o que sua função faz. A este respeito, todas as outras funções falham completamente, pois mudam uma rotina simples, esquecê-lo, livre de bugs, em uma bagunça complicada que as pessoas ao depurar serão forçadas a analisar e testar profundamente. Como gerente de projeto, ficaria muito chateado se um desenvolvedor pegasse uma tarefa simples e, em vez de implementá-la de uma maneira simples e inofensiva, perdesse tempo para implementá-la de uma forma complicada. Basta pensar o tempo todo que você perdeu pensando sobre isso, então chegando ao SO perguntando, e tudo apenas para piorar a manutenção e a legibilidade da coisa.

Dito isso, há um erro comum em seu código que o torna muito menos legível, e algumas melhorias que você pode fazer 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;

Coloque isso em um método, atribua o valor retornado ao objeto, recolha o método e esqueça-o pelo resto da eternidade.

PS, há outro bug acima do limite de 330, mas não sei como você deseja tratá-lo, então não o corrigi de jeito nenhum.


Atualização posterior

De acordo com o comentário, você pode até mesmo se livrar do else:

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;

Não o fiz porque sinto que a certo ponto se torna apenas uma questão de preferências próprias, e o escopo da minha resposta foi (e é) dar uma perspectiva diferente à sua preocupação sobre a "feiura do código". Enfim, como eu disse, alguém apontou nos comentários e acho que faz sentido mostrar.

motoDrizzt
fonte
1
O que diabos isso deveria realizar?
Abstração é tudo.
8
se você quiser seguir esse caminho, você deve pelo menos se livrar do desnecessário else if, ifé o suficiente.
Ðаn
10
@ Ðаn discordo totalmente sobre o else if. Acho útil poder dar uma olhada em um bloco de código e ver que é uma árvore de decisão, em vez de um grupo de instruções não relacionadas. Sim, elseou breaknão são necessários para o compilador após a return, mas são úteis para o humano que está olhando para o código.
IMSoP
@ Ðаn Nunca vi um idioma em que qualquer aninhamento extra fosse necessário. Ou existe uma palavra-chave elseif/ separada elsif, ou você está tecnicamente usando um bloco de uma instrução que começa if, assim como aqui. Um exemplo rápido do que acho que você está pensando e do que estou pensando: gist.github.com/IMSoP/90bc1e9e2c56d8314413d7347e76532a
IMSoP
7
@ Ðаn Sim, concordo que seria horrível. Mas não é o elseque o faz fazer isso, é um guia de estilo pobre que não reconhece else ifcomo uma afirmação distinta. Eu sempre usaria colchetes, mas nunca escreveria um código assim, como mostrei em minha essência.
IMSoP
39

Em pseudocódigo:

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

Então, nós temos 0-60, 60-90, 90-150, ... como as categorias. Em cada quadrante com 90 graus, uma parte tem 60, uma parte tem 30. Então, agora:

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;

Use o índice em uma matriz contendo os enums na ordem apropriada.

Bijay Gurung
fonte
7
Isso é bom, provavelmente a melhor resposta aqui. As chances são de que se o questionador original olhasse para seu uso do enum mais tarde, ele descobriria que ele tinha um caso em que ele estava apenas sendo convertido de volta em um número! Eliminar o enum completamente e apenas manter um inteiro de direção provavelmente faz sentido com outros lugares em seu código e esta resposta leva você diretamente para 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;
}
Caleth
fonte
ângulo = isto -> _ car.getAbsoluteAngle (); setor = (ângulo% 360) / 30; O resultado são 12 setores. Em seguida, indexe em array ou use switch / case como acima - que o compilador se transforma em tabela de salto de qualquer maneira.
ChuckCottrill
1
Switch não é realmente melhor do que as cadeias if / else.
Bill K
5
@BillK: Pode ser, se o compilador transformar isso em uma consulta de tabela para você. Isso é mais provável do que com uma cadeia if / else. Mas como é fácil e não requer nenhum truque específico de arquitetura, provavelmente é melhor escrever a consulta de tabela no código-fonte.
Peter Cordes
Geralmente o desempenho não deve ser uma preocupação - é legibilidade e manutenção - cada switch e cadeia if / else geralmente significa um monte de código copiado e colado bagunçado que deve ser atualizado em vários lugares sempre que você adiciona um novo item. Melhor evitar ambos e tentar despachar tabelas, cálculos ou apenas carregar dados de um arquivo e tratá-los como dados, se puder.
Bill K de
PeterCordes, o compilador provavelmente gerará um código idêntico para o LUT e para o switch. @BillK você pode extrair o switch em uma função 0..12 -> Car :: EDirection, que teria repetição equivalente a uma LUT
Caleth
16

Ignorando o primeiro, ifque é um caso um tanto especial, os demais seguem exatamente o mesmo padrão: um mínimo, máximo e direção; pseudo-código:

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

Tornar este C ++ real pode ser parecido com:

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

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

Agora, ao invés de escrever um monte de ifs, apenas faça um loop sobre suas várias possibilidades:

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 uma excepção em vez de returning NONEé uma outra opção).

Que você chamaria de:

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

Essa técnica é conhecida como programação baseada em dados . Além de eliminar um monte de ifs, permitiria que você adicionasse facilmente mais direções (por exemplo, NNW) ou reduzisse o número (esquerda, direita, para cima, para baixo) sem retrabalhar outro código.


(Lidar com seu primeiro caso especial é deixado como "um exercício para o leitor." :-))

Ðаn
fonte
1
Tecnicamente, você poderia eliminar o mínimo, visto que todas as faixas de ângulo correspondem, o que reduziria a condição para if(angle <= angleRange.max)mas +1 para usar recursos do C ++ 11 como enum class.
Pharap
12

Embora as variantes propostas com base em uma tabela de pesquisa angle / 30sejam provavelmente preferíveis, aqui está uma alternativa que usa uma pesquisa binária codificada para minimizar o número de comparações.

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
}
x4u
fonte
2

Se você realmente deseja evitar a duplicação, pode expressar isso como uma fórmula matemática.

Em primeiro lugar, suponha que estamos usando @ Geek's Enum

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

Agora podemos calcular o enum usando matemática de inteiros (sem a necessidade de matrizes).

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;
}

Como @motoDrizzt aponta, um código conciso não é necessariamente um código legível. Ele tem a pequena vantagem de expressá-lo como matemática torna explícito que algumas direções cobrem um arco mais amplo. Se você quiser seguir nessa direção, pode adicionar algumas declarações para ajudar a entender o código.

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

Tendo adicionado as declarações, você adicionou duplicação, mas a duplicação em declarações não é tão ruim. Se você tiver uma afirmação inconsistente, logo descobrirá. Asserts podem ser compilados fora da versão de lançamento para não sobrecarregar o executável que você distribui. No entanto, essa abordagem é provavelmente mais aplicável se você quiser otimizar o código em vez de apenas torná-lo menos feio.

gmatht
fonte
1

Estou atrasado para a festa, mas poderíamos usar sinalizadores enum e checagem de alcance para fazer algo legal.

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
};

a verificação (pseudo-código), assumindo que o ângulo é absoluto (entre 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;
}
Leonardo Pina
fonte