将标题分为8个方向时,如何避免if / else if连锁?


111

我有以下代码:

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;

我想避免ifs链;这真的很丑。是否有另一种可能更简洁的书写方式?


77
@Oraekia如果您this->_car.getAbsoluteAngle()在整个级联之前完成一次,它将看起来不那么难看打字更少,阅读更好。
πάνταῥεῖ

26
的所有间接引用明确thisthis->也没必要)并没有真正做什么好可读性..
加斯帕居尔

2
@Neil对作为键,枚举作为值,自定义查找lambda。
πάνταῥεῖ

56
如果不进行所有这些>测试,代码将变得不那么丑陋。不需要它们,因为它们每个都已经在前面的if语句中进行了测试(反方向)。
皮特·贝克尔

10
@PeteBecker这是我对这样的代码的不满。太多的程序员不懂else if
巴玛(Barmar)'17

Answers:


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

这就是我要做的。(根据我之前的评论)。


92
我真的以为我看到了Konami代码代替了那里的枚举。
Zano

21
@CodesInChaos:C99和C ++与C#具有相同的要求:if q = a/br = a%bthen q * b + r必须相等a。因此,在C99中,余数为负是合法的。BorgLeader,您可以使用解决问题(((angle % 360) + 360) % 360) / 30
埃里克·利珀特

7
@ericlippert,您和您的计算数学知识继续给您留下深刻印象。
gregsdennis

33
这非常聪明,但是它是完全不可读的,并且不再可能被维护,因此我不同意这是对原件的“丑陋”的良好解决方案。我想这是个人品味的一部分,但是我发现x4u和motoDrizzt的清理分支版本非常可取。
IMSoP '17

4
@cyanbeam main中的for循环只是一个“演示”,GetDirectionForAngle我提议替代if / else级联,它们都是O(1)...
Borgleader

71

您可以map::lower_bound在地图中使用和存储每个角度的上限。

下面的工作示例:

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

无需划分。好!
O. Jones

17
@ O.Jones:用编译时常数进行除法是很便宜的,只需乘以一些即可。我会选择table[angle%360/30]答案之一,因为它便宜又无分支。 远远比树搜索环路便宜,如果这编译为ASM这是类似于源。(std::unordered_map通常是哈希表,但std::map通常是红黑二叉树。可接受的答案有效地angle%360 / 30用作角度的完美哈希函数(在复制了几个条目之后,Bijay的答案甚至避免了偏移)。
彼得·科德斯

2
您可以lower_bound在排序数组上使用。那会比效率更高map
wilx

@PeterCordes地图查找易于编写且易于维护。如果范围更改,则更新哈希码可能会引入错误,并且如果范围变得不均匀,则可能会崩溃。除非该代码对性能至关重要,否则我不会打扰。
OhJeez

@OhJeez:它们已经不一致,可以通过在多个存储桶中使用相同的值来处理。只需使用较小的除数即可获得更多的存储桶,除非这意味着使用很小的除数并占用太多存储桶。另外,如果性能无关紧要,则if / else链也不错,如果通过this->_car.getAbsoluteAngle()使用tmp var进行简化,然后从每个OP if()子句中删除冗余比较(检查是否已经匹配),if / else链也不错。前面的if())。或使用@ wilx的排序数组建议。
Peter Cordes

58

创建一个数组,其每个元素与一个30度的块相关联:

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

然后,您可以使用角度/ 30索引数组:

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

无需比较或分支。

但是结果与原始图像略有出入。边界上的值,即30、60、120等,被放置在下一类别中。例如,在原始代码中,的有效值为UP_RIGHT31到60。以上代码将分配为30到59 UP_RIGHT

我们可以通过从角度减去1来解决这个问题:

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

现在这给了我们RIGHT30个,UP_RIGHT60个等等。

在0的情况下,表达式变为(-1 % 360) / 30。这是有效的,因为-1 % 360 == -1-1 / 30 == 0,所以我们仍然得到0的索引。

C ++标准的 5.6节确认了此行为:

4二元/运算符得出商,二元%运算符得出第一个表达式除以第二个表达式的余数。如果/或的第二个操作数%为零,则行为不确定。对于整数操作数,/运算符得出除掉任何小数部分的代数商。如果商a/b在结果类型中可表示, (a/b)*b + a%b则等于a

编辑:

关于这样的结构的可读性和可维护性,提出了许多问题。motoDrizzt给出的答案是简化原始结构的一个很好的例子,该原始结构更易于维护,并且不太“丑陋”。

扩展他的答案,这是另一个使用三元运算符的示例。由于原始帖子中的每种情况都分配给相同的变量,因此使用此运算符可以帮助进一步提高可读性。

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

该代码并不难看,它简单,实用,易读且易于理解。它会以自己的方法进行隔离,因此在日常生活中没有人需要处理它。万一有人要检查它(也许是因为他在其他地方调试您的应用程序以解决问题),这很容易,他将需要两秒钟来理解代码及其作用。

如果我正在进行这样的调试,那么我很高兴不必花五分钟的时间来了解您的函数的功能。在这方面,所有其他功能都将完全失败,因为它们更改了一个简单的,无所谓的,无bug的例程,陷入了复杂的混乱之中,调试人员将被迫深入分析和测试。作为项目经理,我自己会被开发人员承担的简单任务弄得很沮丧,而不是将其以简单,无害的方式实现,却浪费了时间将其以过于复杂的方式实现。只要想一想,就一直在浪费时间思考问题,然后来问问,所有这些都是为了恶化事物的维护性和可读性。

就是说,您的代码中存在一个常见错误,导致其可读性降低,并且您可以轻松进行一些改进:

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;

将其放入方法中,将返回值分配给对象,折叠该方法,并在剩下的永恒中忘掉它。

PS:还有一个超过330阈值的错误,但是我不知道您要如何对待它,所以我根本没有修复它。


以后更新

根据评论,您甚至可以完全摆脱其他情况:

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;

我之所以没有这样做,是因为我觉得这在一定程度上只是个人喜好问题,我的回答范围是(过去是)对您对“丑陋的代码”的关注给出不同的看法。无论如何,正如我所说,有人在评论中指出了这一点,我认为将其显示是有意义的。


1
那到底该完成什么?
抽象就是一切。

8
如果您想走这条路,那么至少应该摆脱不必要的else ifif就足够了。
Ðаn

10
我完全不同意@ else if。我发现能够看一眼代码块,并且看到它是决策树,而不是一组无关的语句,这很有用。是的,else或者对于之后的编译器来说break不是必需的,但是它们对于浏览代码的人员很有用。return
IMSoP '17

@Ðn我从未见过在那里需要任何额外嵌套的语言。要么有一个单独的elseif/ elsif关键字,要么从技术上讲,您正在使用一个碰巧开始的单语句块,if就像这里一样。我想您正在想以及我在想什么的快速示例:gist.github.com/IMSoP/90bc1e9e2c56d8314413d7347e76532a
IMSoP

7
@Ð是,我同意那将是可怕的。但这并不是else您要做的,这是一个糟糕的样式指南,无法识别else if为明确的陈述。我将始终使用花括号,但绝不会像我在要点中所示那样编写那样的代码。
IMSoP

39

用伪代码:

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

因此,我们有0-6060-9090-150,...作为类别。在每个90度的象限中,一部分具有60,一部分具有30。因此,现在:

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;

以适当的顺序在包含枚举的数组中使用索引。


7
这是很好的,可能是最好的答案。很有可能的是,如果最初的提问者后来看了他对枚举的使用,他会发现他有一个案例,只是将其转换回数字!完全消除枚举,仅坚持一个方向整数可能对他代码中的其他位置有意义,而此答案将使您直接到达那里。
比尔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;
}

角度= this-> _ car.getAbsoluteAngle(); 扇区=(角度%360)/ 30; 结果是12个扇区。然后索引到数组中,或者使用上面的switch / case-无论如何编译器都会将其转换为跳转表。
ChuckCottrill

1
切换并不是真的比if / else链更好。
比尔

5
@BillK:如果编译器为您将其转换为表查找,则可能是这样。这比使用if / else链更有可能。但是,由于它很容易且不需要任何特定于体系结构的技巧,因此最好在源代码中编写表查询。
彼得·科德斯

通常,性能不是问题-它的可读性和可维护性-每个开关和if / else链通常意味着一堆凌乱的复制和粘贴代码,每当添加新项目时都必须在多个位置进行更新。最好同时避免这两种情况,并尝试调度表,计算或仅从文件中加载数据,然后将其视为数据。
比尔K

PeterCordes编译器可能会为LUT生成与开关相同的代码。@BillK,您可以将开关提取到0..12-> Car :: EDirection函数中,该函数与LUT具有相同的重复
Caleth,2016年

16

忽略您的第if一个特殊情况,其余的都遵循完全相同的模式:最小,最大和方向;伪代码:

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

制作真正的C ++可能看起来像:

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

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

现在,不用写一堆ifs,只需遍历各种可能性:

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

throw例外而不是returning NONE是另一种选择)。

然后您将其称为:

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

这种技术被称为数据驱动编程。除了摆脱一堆ifs之外,它还使您可以轻松使用添加更多的方向(例如NNW)或减少数量(向左,向右,向上,向下),而无需重新处理其他代码。


(处理您的第一个特例是“给读者的练习。” :-))


1
从技术上讲,您可以消除所有角度范围都匹配的min,这将使if(angle <= angleRange.max)使用C ++ 11功能(例如)的条件降低到+1 enum class
法拉普

12

尽管基于查找表建议的变体angle / 30可能是更可取的,但这是使用硬编码二进制搜索来最大程度减少比较次数的替代方法。

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

如果您真的想避免重复,可以将其表示为数学公式。

首先,假设我们正在使用@Geek的Enum

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

现在我们可以使用整数数学(无需数组)来计算枚举。

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

正如@motoDrizzt所指出的,简洁的代码不一定是可读的代码。它确实具有一个小的优势,即将其表示为数学形式可以使某些方向覆盖更宽的弧线变得很明确。如果您想朝这个方向发展,可以添加一些断言以帮助理解代码。

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

添加断言后,您已经添加了重复项,但是断言中的重复项还不错。如果您有不一致的断言,您将很快找到答案。可以在发行版本之外编译断言,以免使您分发的可执行文件distribute肿。但是,如果您要优化代码而不是使其变得不那么丑陋,则此方法可能最适用。


1

我来晚了,但是我们可以使用枚举标志和范围检查来完成一些事情。

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

假设角度为绝对值(在0到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;
}
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.