ChatGPT解决这个技术问题 Extra ChatGPT

将标题分类为 8 个方向时如何避免 if / else if 链?

我有以下代码:

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;

我想避开if链;真的很丑。是否有另一种可能更清洁的方式来写这个?

@Oraekia 如果您在整个级联之前输入一次 this->_car.getAbsoluteAngle(),它会看起来不那么难看,打字更少,阅读效果更好。
不需要所有对 this (this->) 的显式取消引用,并且对可读性没有任何好处。
@Neil Pair 作为键,枚举作为值,自定义查找 lambda。
如果没有所有这些 > 测试,代码会变得不那么难看;它们不是必需的,因为它们中的每一个都已经在前面的 if 语句中进行了测试(以相反的方向)。
@PeteBecker 这是我对这样的代码最讨厌的地方之一。太多的程序员不理解else if

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

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


我真的以为我看到了 Konami Code 代替了那里的枚举。
@CodesInChaos:C99 和 C++ 具有与 C# 相同的要求:如果 q = a/br = a%bq * b + r 必须等于 a。因此,在 C99 中,余数为负是合法的。 BorgLeader,你可以用 (((angle % 360) + 360) % 360) / 30 解决问题。
@ericlippert,您和您的计算数学知识继续给人留下深刻印象。
这是非常聪明的,但它完全不可读,并且更不可能被维护,所以我不同意这是一个很好的解决原始“丑陋”的解决方案。我想这里有个人品味的元素,但我发现 x4u 和 motoDrizzt 清理的分支版本非常可取。
@cyanbeam main 中的 for 循环只是一个“演示”,GetDirectionForAngle 是我提议作为 if/else 级联的替代品,它们都是 O(1)...
S
Steve Lorimer

您可以使用 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:除以编译时常数相当便宜,只是乘法和一些移位。我会选择 table[angle%360/30] 答案之一,因为它便宜且无分支。 比树搜索循环便宜,如果它编译为与源代码相似的 asm。 (std::unordered_map 通常是哈希表,但 std::map 通常是红黑二叉树。接受的答案有效地使用 angle%360 / 30 作为角度的完美哈希函数(在复制了几个条目之后,Bijay 的答案甚至避免有一个偏移量))。
您可以在排序数组上使用 lower_bound。这将比 map 高效得多。
@PeterCordes 地图查找易于编写且易于维护。如果范围发生变化,更新散列代码可能会引入错误,如果范围变得不统一,它可能会崩溃。除非该代码对性能至关重要,否则我不会打扰。
@OhJeez:它们已经是不统一的,这是通过在多个存储桶中具有相同的值来处理的。只需使用较小的除数来获得更多的桶,除非这意味着使用非常小的除数并且有太多的桶。此外,如果性能无关紧要,那么 if/else 链也不错,如果通过使用 tmp var 分解 this->_car.getAbsoluteAngle() 并从每个 OP 的 if() 子句中删除冗余比较来简化(检查已经与前面的 if()) 匹配的东西。或者使用@wilx 的排序数组建议。
d
dbush

创建一个数组,其中的每个元素都与一个 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_RIGHT 的有效值为 31 到 60。上面的代码将 30 到 59 分配给 UP_RIGHT

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

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

这现在给了我们 30 的 RIGHT,60 的 UP_RIGHT,等等。

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

C++ standard 的第 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;

R
Rakete1111

该代码并不难看,它简单、实用、可读且易于理解。它将以自己的方式被隔离,因此在日常生活中没有人需要处理它。万一有人必须检查它——也许是因为他正在调试你的应用程序以解决其他地方的问题——这很容易,他需要两秒钟才能理解代码及其作用。

如果我正在做这样的调试,我很高兴不必花五分钟时间来了解你的函数的作用。在这方面,所有其他功能完全失败,因为它们改变了一个简单的、忘记它、没有错误的例程,陷入复杂的混乱状态,人们在调试时将被迫深入分析和测试。作为我自己的项目经理,如果开发人员执行一项简单的任务,而不是以一种简单、无害的方式实现它,而是浪费时间以一种过于复杂的方式实现它,我会感到非常沮丧。想想你一直在浪费时间思考它,然后来问,一切只是为了恶化事物的维护和可读性。

也就是说,您的代码中存在一个常见错误,使其可读性降低,并且您可以很容易地进行一些改进:

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阈值的bug,但是不知道你想怎么处理,所以根本没修。

稍后更新

根据评论,您甚至可以完全摆脱 else if:

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;

我没有这样做是因为我觉得在某一点上它只是个人喜好的问题,而我的回答范围是(并且是)对您对“代码丑陋”的担忧给出不同的看法。无论如何,正如我所说,有人在评论中指出了这一点,我认为展示它是有意义的。


那到底是为了什么?
B
Bijay Gurung

在伪代码中:

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;

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


这很好,可能是这里最好的答案。很有可能,如果最初的提问者稍后查看他对枚举的使用,他会发现他有一个案例,它只是被转换回一个数字!完全消除枚举并仅使用方向整数可能对他的代码中的其他地方有意义,而这个答案可以让你直接到达那里。
C
Caleth
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 - 无论如何编译器都会转换为跳转表。
Switch 并不比 if/else 链好。
@BillK:如果编译器将其转换为您的表查找,则可能是这样。这比 if/else 链更有可能。但是由于它很简单并且不需要任何特定于体系结构的技巧,因此最好在源代码中编写查表。
一般来说,性能不应该是一个问题——它是可读性和可维护性的——每个开关和 if/else 链通常意味着一堆乱七八糟的复制和粘贴代码,每当您添加新项目时,必须在多个位置进行更新。最好避免两者并尝试调度表,计算或只是从文件中加载数据并将其视为数据,如果可以的话。
PeterCordes 编译器可能会为 LUT 生成与开关相同的代码。 @BillK您可以将开关提取到 0..12 -> Car::EDirection 函数中,该函数与LUT具有等效的重复性
Ð
Ðаn

忽略你的第一个 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;
};

现在,与其编写一堆 if,不如遍历您的各种可能性:

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 处理异常而不是 return 处理 NONE 是另一种选择)。

然后你会打电话给:

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

这种技术称为 data-driven programming。除了摆脱一堆 if 之外,它还允许您轻松地使用添加更多方向(例如,NNW)或减少数量(左、右、上、下),而无需重新编写其他代码。

(处理您的第一个特殊情况留作“读者练习”。:-))


从技术上讲,您可以消除 min ,因为所有角度范围都匹配,这会将条件减少到 if(angle <= angleRange.max),但对于使用 enum class 等 C++11 功能时 +1。
x
x4u

尽管基于 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
}

g
gmatht

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

首先,假设我们使用的是@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     );

添加断言后,您添加了重复,但断言中的重复并不是那么糟糕。如果你有一个不一致的断言,你很快就会发现。断言可以从发布版本中编译出来,以免使您分发的可执行文件膨胀。尽管如此,如果您想优化代码而不是让它变得不那么难看,这种方法可能最适用。


L
Leonardo Pina

我迟到了,但我们可以使用枚举标志和范围检查来做一些整洁的事情。

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