Як уникнути, якщо / ще, якщо ланцюг, коли класифікує заголовок на 8 напрямків?


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;

Я хочу уникати ifланцюга s; це справді некрасиво. Чи є інший, можливо, чистіший спосіб написання цього?


77
@Oraekia Це виглядало б набагато менш потворно, менше набирати текст і краще читати, якщо ви промацуєте this->_car.getAbsoluteAngle()один раз перед усім каскадом.
πάντα ῥεῖ

26
Все, що явна перенаправлення this( this->), не потрібне і насправді не робить нічого доброго для читабельності ..
Jesper Juhl

2
@Neil Пара як ключ, перерахунок як значення, користувацька лямбда пошуку.
πάντα ῥεῖ

56
Код був би набагато менш потворним без усіх цих >тестів; вони не потрібні, оскільки кожен з них вже був протестований (у зворотному напрямку) у попередньому ifтвердженні.
Піт Бекер

10
@PeteBecker Це один з моїх вихованців, який визирає з таким кодом. Занадто багато програмістів не розуміють else if.
Бармар

Відповіді:


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
Я буквально подумав, що на секунду побачив Кодекс Конамі замість перерахунку.
Зано

21
@CodesInChaos: C99 і C ++ мають ту саму вимогу, що і C #: що, якщо q = a/bі r = a%bтоді, q * b + rповинна дорівнювати a. Таким чином, законним у С99 залишається негативним. BorgLeader, ви можете вирішити проблему (((angle % 360) + 360) % 360) / 30.
Ерік Ліпперт

7
@ericlippert, ви та ваші знання з обчислювальної математики продовжуєте вражати.
gregsdennis

33
Це дуже розумно, але цілком нечитабельно і більше не є ретельним, тому я не погоджуюся з тим, що це гарне рішення сприйнятої "потворності" оригіналу. Тут, мабуть, є елемент особистого смаку, але я вважаю, що очищені розгалужені версії на x4u та motoDrizzt є переважнішими.
IMSoP

4
@cyanbeam the 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;
}

Не потрібно ділення. Добре!
О. Джонс

17
@ O.Jones: Ділення на константу часу компіляції є досить дешевим, просто множенням і деякими зрушеннями. Я б поїхав з однією з table[angle%360/30]відповідей, тому що це дешево і без галузей. Набагато дешевше, ніж цикл пошуку дерев, якщо це компілюється в asm, подібний до джерела. ( std::unordered_mapзазвичай це хеш-таблиця, але, std::mapяк правило, бінарне червоно-чорне дерево. Прийнята відповідь ефективно використовує angle%360 / 30як ідеальну хеш-функцію для кутів (після реплікації декількох записів, а відповідь Біджая навіть уникає цього зі зміщенням)).
Пітер Кордес

2
Ви можете використовувати lower_boundна відсортованому масиві. Це було б набагато ефективніше, ніж map.
вілкс

@PeterCordes пошук карт легко писати та легко обслуговувати. Якщо діапазони змінюються, оновлення хеш-коду може ввести помилки, і якщо діапазони стануть нерівномірними, вони можуть просто розпастися. Якщо цей код не є критичним для продуктивності, я б не переймався.
OhJeez

@OhJeez: Вони вже неоднорідні, що обробляється тим, що має однакове значення у кількох відрах. Просто скористайтеся меншим дільником, щоб отримати більше відра, якщо це не означає використання дуже маленького дільника і занадто багато відра. Крім того, якщо продуктивність не має значення, то ланцюг if / else також не поганий, якщо його спростити шляхом розбиття коду за this->_car.getAbsoluteAngle()допомогою tmp var та видалення зайвого порівняння з кожного із if()пунктів ОП (перевірка на те, що вже відповідатиме попередній if ()). Або скористайтеся пропозицією відсортованого масиву @ wilx.
Пітер Кордес

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

Це тепер дає нам RIGHTза 30, UP_RIGHTза 60 і т.д.

У випадку 0 вираз стає (-1 % 360) / 30. Це дійсно, тому що -1 % 360 == -1і -1 / 30 == 0, тому ми все одно отримуємо індекс 0.

Розділ 5.6 стандарту C ++ підтверджує таку поведінку:

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

Цей код не потворний, він простий, практичний, читабельний і легко зрозумілий. Він буде ізольований у своєму власному методі, тому нікому не доведеться з цим боротися у повсякденному житті. І на всякий випадок, коли комусь доведеться перевірити це - можливо, тому що він налагоджує вашу заявку на проблему десь в іншому місці - це так просто, що йому знадобиться дві секунди, щоб зрозуміти код і що він робить.

Якщо я робив таку налагодження, я був би радий не витрачати п'ять хвилин, намагаючись зрозуміти, що робить ваша функція. У цьому відношенні всі інші функції відмовляються повністю, оскільки вони змінюють просту, незабуднуючу, безкоштовну рутину, в складний безлад, який люди при налагодженні змушені будуть глибоко проаналізувати та перевірити. Як сам менеджер проекту, я сильно засмучувався тим, що розробник приймає просту задачу і замість того, щоб реалізувати її простим, нешкідливим способом, витрачає час на реалізацію цього надмірно складного способу. Просто подумайте весь час, коли ви даремно думаєте про це, потім приїжджайте до ТА просити, і все заради погіршення технічного обслуговування та читання речі.

Однак, у вашому коді є поширена помилка, яка робить його менш читабельним, і пару покращень ви можете зробити досить легко:

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 if, ifдостатньо.
23

10
@ Ðаn Я повністю не згоден з цим питанням else if. Мені здається корисним переглядати блок коду та бачити, що це дерево рішень, а не група непов'язаних висловлювань. Так, elseабо breakне потрібні компілятору після return, але вони корисні для людини, яка дивиться на код.
IMSoP

@ Ðаn Я ніколи не бачив мови, де потрібне було б додаткове вкладення. Або є окремий elseif/ elsifключове слово, або ви технічно використовуєте блок одного твердження, який, мабуть, починається if, як і тут. Короткий приклад того, що я думаю, що ти думаєш, і про що я думаю: gist.github.com/IMSoP/90bc1e9e2c56d8314413d7347e76532a
IMSoP

7
@ Ран Так, я згоден, це було б жахливо. Але elseзмусити вас це не зробити, це погане керівництво стилем, яке не визнається else ifвиразним твердженням. Я завжди використовував дужки, але ніколи не писав би такий код, як показав у своїй суті.
IMSoP

39

У псевдокоді:

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

Отже, у нас є 0-60, 60-90, 90-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
Це гарна, мабуть, найкраща відповідь тут. Цілком ймовірно, що якби первісний запитувач переглянув його використання перерахунку пізніше, він виявить, що у нього є випадок, коли він просто перетворюється назад у число! Повністю усунути перерахунок і просто дотримуватися цілого напряму, ймовірно, має сенс і в інших місцях його коду, і ця відповідь потрапляє прямо туди.
Білл К

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 (); сектор = (кут% 360) / 30; Результат - 12 секторів. Потім індексуйте в масив або використовуйте перемикач / випадок, як зазначено вище, який компілятор в будь-якому випадку перетворюється на таблицю стрибків.
ChuckCottrill

1
Переходити не дуже краще, ніж ланцюги if / else.
Білл К

5
@BillK: Можливо, якщо компілятор перетворить його на пошук таблиці. Це швидше, ніж із ланцюгом if / else. Але оскільки це легко і не вимагає ніяких хитрощів, що стосуються архітектури, можливо, найкраще написати пошук таблиці у вихідному коді.
Пітер Кордес

Зазвичай продуктивність не повинна викликати занепокоєння - це читабельність та ремонтопридатність - кожен перемикач & if / else ланцюг зазвичай означає купу безладного копію та вставлення коду, який доводиться оновлювати в декількох місцях щоразу, коли ви додаєте новий елемент. Найкраще уникати обох і спробувати відправити таблиці, обчислення або просто завантажити дані з файлу та обробляти їх як дані, якщо можете.
Білл К

PeterCordes компілятор, ймовірно, генерує ідентичний код для LUT, як і для комутатора. @BillK Ви можете витягнути перемикач на функцію 0..12 -> Автомобіль :: Направлення, яка б повторювалась як LUT
Caleth

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
Технічно ви можете усунути хв, враховуючи, що всі кутові діапазони збігаються, що зменшить умову до if(angle <= angleRange.max)+1, але для використання функцій C ++ 11, як-от 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's 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     );

Додавши твердження, ви додали дублювання, але дублювання в твердженнях не так вже й погано. Якщо у вас є суперечливе твердження, ви дізнаєтесь досить скоро. Твердження можуть бути складені з версії версії, щоб не роздути виконуваний вами файл. Тим не менш, такий підхід, мабуть, найбільш застосовний, якщо ви хочете оптимізувати код, а не просто зробити його менш потворним.


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;
}
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.