Як покращити логіку, щоб перевірити, чи відповідають 4 булеві значення деяким випадкам


118

У мене є чотири boolзначення:

bool bValue1;
bool bValue2;
bool bValue3;
bool bValue4;

Прийнятні значення:

         Scenario 1 | Scenario 2 | Scenario 3
bValue1: true       | true       | true
bValue2: true       | true       | false
bValue3: true       | true       | false
bValue4: true       | false      | false

Наприклад, такий сценарій неприйнятний:

bValue1: false
bValue2: true
bValue3: true
bValue4: true

На даний момент я придумав це ifтвердження про виявлення поганих сценаріїв:

if(((bValue4 && (!bValue3 || !bValue2 || !bValue1)) ||
   ((bValue3 && (!bValue2 || !bValue1)) ||
   (bValue2 && !bValue1) ||
   (!bValue1 && !bValue2 && !bValue3 && !bValue4))
{
    // There is some error
}

Чи можна покращити / спростити цю логіку тверджень?


8
Я б використав таблицю замість складної ifзаяви. Крім того, оскільки це булеві прапори, ви можете моделювати кожен сценарій як постійний і перевіряти його.
Здеслав Войкович

3
if (!((bValue1 && bValue2 && bValue3) || (bValue1 && !bValue2 && !bValue3 && !bValue4)))
мч

14
які сценарії насправді? Часто речі стають набагато простішими, якщо ви просто даєте власним іменам речі, наприкладbool scenario1 = bValue1 && bValue2 && bValue3 && bValue4;
idclev 463035818

6
Використовуючи значущі імена, ви можете витягти кожну складну умову в метод і викликати цей метод в умові if. Це було б набагато легше читати і ремонтувати. наприклад, подивіться на приклад, наведений у посиланні. refactoring.guru/decompose-conditional
Hardik Modha

Відповіді:


195

Я б націлив на читабельність: у вас всього 3 сценарії, вирішуйте їх з 3-ма окремими ifs:

bool valid = false;
if (bValue1 && bValue2 && bValue3 && bValue4)
    valid = true; //scenario 1
else if (bValue1 && bValue2 && bValue3 && !bValue4)
    valid = true; //scenario 2
else if (bValue1 && !bValue2 && !bValue3 && !bValue4)
    valid = true; //scenario 3

Легкий для читання та налагодження, IMHO. Крім того, ви можете призначити змінну whichScenario, продовжуючи роботу з if.

За допомогою всього лише 3 сценаріїв я не хотів би мати щось подібне, "якщо перші 3 значення вірні, я можу уникнути перевірки четвертого значення": це зробить ваш код складнішим для читання та обслуговування.

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

Якщо ваша логіка ускладнюється, відкиньте цей код і подумайте про те, щоб використовувати щось більше для зберігання різних доступних сценаріїв (як пропонує Zladeck).

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

(Майже) поза темою:

Я не пишу тут багато відповідей на StackOverflow. Дійсно смішно, що вищезгадана прийнята відповідь на сьогодні є найбільш оціненою відповіддю в моїй історії (ніколи не було більше 5-10 оновлень, перш ніж я думаю), хоча насправді це не те, що, як правило, я думаю, що це "правильний" спосіб це зробити.

Але простота часто є «правильним способом зробити це», багато людей, здається, думають про це, і я повинен думати це більше, ніж я :)


1
впевнений @hessamhedieh, це нормально лише для невеликої кількості доступних сценаріїв. як я вже говорив, якщо справи ускладняться, краще шукайте щось інше
Джан Паоло

4
Це можна додатково спростити, укладаючи всі умови в ініціалізатор для validта відокремлюючи їх ||, а не мутуючи validв окремих блоках операторів. Я не можу навести приклад у коментарі, але ви можете вертикально вирівняти ||операторів ліворуч, щоб зробити це дуже зрозумілим; індивідуальні умови вже в дужках, стільки, скільки потрібно (для if), тому вам не потрібно додавати жодних символів до виразів понад те, що вже є.
Левшенко

1
@ Левшенко, я думаю, що змішування дужок, && та || умови досить схильні до помилок (хтось в іншій відповіді сказав, що в коді в ОП була помилка в дужках, можливо, вона була виправлена). Правильне вирівнювання може допомогти, звичайно. Але в чому перевага? більш читабельний? простіше в обслуговуванні? Я не думаю, що так. Просто моя думка, звичайно. Безумовно, я дуже ненавиджу, що в коді багато ifs.
Джан Паоло

3
Я б обернув це тим if($bValue1), що завжди має бути правдою, технічно дозволяючи незначне поліпшення продуктивності (хоча ми тут говоримо про незначні суми).
Мартійн

2
FWIW: є лише два сценарії: перші 2 - це той самий сценарій і не залежать відbValue4
Dancrumb

123

Я б прагнув до простоти і читабельності.

bool scenario1 = bValue1 && bValue2 && bValue3 && bValue4;
bool scenario2 = bValue1 && bValue2 && bValue3 && !bValue4;
bool scenario3 = bValue1 && !bValue2 && !bValue3 && !bValue4;

if (scenario1 || scenario2 || scenario3) {
    // Do whatever.
}

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

bool scenario1or2 = bValue1 && bValue2 && bValue3;
bool scenario3 = bValue1 && !bValue2 && !bValue3 && !bValue4;

if (scenario1or2 || scenario3) {
    // Do whatever.
}

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


3
+1 Це я також зробив би. Так само, як вказує @RedFilter, і на відміну від прийнятої відповіді, це самодокументація. Надання сценаріям власних імен окремим кроком набагато легше читати.
Андреас

105

Ми можемо використовувати карту Карно і звести ваші сценарії до логічного рівняння. Я використовував онлайн- вирішувач карти Карно з схемою для 4 змінних.

введіть тут опис зображення

Це дає:

введіть тут опис зображення

Зміна A, B, C, Dв bValue1, bValue2, bValue3, bValue4це не що інше , як:

bValue1 && bValue2 && bValue3 || bValue1 && !bValue2 && !bValue3 && !bValue4

Тож ваша ifзаява стає:

if(!(bValue1 && bValue2 && bValue3 || bValue1 && !bValue2 && !bValue3 && !bValue4))
{
    // There is some error
}
  • Карти Karnaugh особливо корисні, коли у вас є багато змінних та багато умов, які слід оцінити true .
  • Після зменшення trueсценаріїв до логічного рівняння, додавання відповідних коментарів із зазначенням trueсценаріїв є хорошою практикою.

96
Хоча технічно правильний, цей код вимагає багато коментарів, щоб через кілька місяців його змінив інший розробник.
Здеслав Войкович

22
@ZdeslavVojkovic: Я просто додам коментар із рівнянням. //!(ABC + AB'C'D') (By K-Map logic). Це був би хороший час для розробника вивчити K-Maps, якщо він ще не знає їх.
PW

11
Я погоджуюся з цим, але проблема ІМО полягає в тому, що він не чітко відображається в проблемній області, тобто те, як кожна умова співпадає з конкретним сценарієм, що ускладнює зміни / розширення. Що відбувається , коли є Eі Fумови , і 4 нових сценаріїв? Скільки часу потрібно правильно оновити цю ifзаяву? Як перевірка коду перевіряє, чи це нормально чи ні? Проблема не в технічній, а в "діловій" стороні.
Здеслав Войкович

7
Я думаю, що ви можете визначити A: ABC + AB'C'D' = A(BC + B'C'D')(це може бути навіть враховано, A(B ^ C)'(C + D')хоча я з обережністю називаю це "спрощення").
Maciej Piechotka

28
@PW Цей коментар здається настільки ж зрозумілим, як і код, і, таким чином, трохи безглуздо. Кращий коментар пояснив би, як ви насправді придумали це рівняння, тобто, що твердження повинно викликати TTTT, TTTF і TFFF. У цей момент ви можете просто написати ці три умови в коді, і зовсім не потрібно пояснення.
Бернхард Баркер

58

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

Я б запропонував моделювати це як бітові прапори:

const int SCENARIO_1 = 0x0F; // 0b1111 if using c++14
const int SCENARIO_2 = 0x0E; // 0b1110
const int SCENARIO_3 = 0x08; // 0b1000

bool bValue1 = true;
bool bValue2 = false;
bool bValue3 = false;
bool bValue4 = false;

// boolean -> int conversion is covered by standard and produces 0/1
int scenario = bValue1 << 3 | bValue2 << 2 | bValue3 << 1 | bValue4;
bool match = scenario == SCENARIO_1 || scenario == SCENARIO_2 || scenario == SCENARIO_3;
std::cout << (match ? "ok" : "error");

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

int scenarios[3][4] = {
    {true, true, true, true},
    {true, true, true, false},
    {true, false, false, false},
};

int main()
{
  bool bValue1 = true;
  bool bValue2 = false;
  bool bValue3 = true;
  bool bValue4 = true;
  bool match = false;

  // depending on compiler, prefer std::size()/_countof instead of magic value of 4
  for (int i = 0; i < 4 && !match; ++i) {
    auto current = scenarios[i];
    match = bValue1 == current[0] && 
            bValue2 == current[1] && 
            bValue3 == current[2] && 
            bValue4 == current[3];
  }

  std::cout << (match ? "ok" : "error");
}

4
Не найрентабельніший, але, безумовно, спрощує умови if. Тож залишити кілька коментарів навколо побітових операцій буде абсолютною необхідністю тут.
Адам Захран

6
IMO, таблиця - найкращий підхід, оскільки вона краще масштабується за допомогою додаткових сценаріїв та прапорів.
Здеслав Войкович

Мені подобається ваше перше рішення, просте для читання і відкрите для модифікації. Я б зробив 2 вдосконалення: 1: призначити значення дляXXX з явним вказівкою використовуваних булевих значень, наприклад SCENARIO_2 = true << 3 | true << 2 | true << 1 | false;2: уникати змінних SCENARIO_X і зберігати всі доступні сценарії в <std::set<int>. Додавання сценарію буде просто чимось, як, mySet.insert( true << 3 | false << 2 | true << 1 | false;можливо, трохи надмірним для всього 3 сценарію, ОП прийняв швидке, брудне та просте рішення, яке я запропонував у своїй відповіді.
Джан Паоло

4
Якщо ви використовуєте C ++ 14 або вище, я б запропонував замість цього використовувати двійкові літерали для першого рішення - 0b1111, 0b1110 і 0b1000 набагато зрозуміліше. Можливо, ви також можете трохи спростити це за допомогою стандартної бібліотеки ( std::find?).
Бернхард Баркер

2
Я вважаю, що бінарні літерали тут були б мінімальною вимогою, щоб очистити перший код. У своєму теперішньому вигляді він цілком виразний. Описові ідентифікатори можуть допомогти, але я в цьому навіть не впевнений. Насправді, бітові операції по створенню scenarioзначення вражають мене як непотрібне схильність до помилок.
Конрад Рудольф

27

Моя попередня відповідь - це вже прийнята відповідь, я тут додаю щось, що, на мою думку, є читабельним, легким і в цьому випадку відкритим для майбутніх модифікацій:

Починаючи з відповіді @ZdeslavVojkovic (що я вважаю досить непоганим), я придумав таке:

#include <iostream>
#include <set>

//using namespace std;

int GetScenarioInt(bool bValue1, bool bValue2, bool bValue3, bool bValue4)
{
    return bValue1 << 3 | bValue2 << 2 | bValue3 << 1 | bValue4;
}
bool IsValidScenario(bool bValue1, bool bValue2, bool bValue3, bool bValue4)
{
    std::set<int> validScenarios;
    validScenarios.insert(GetScenarioInt(true, true, true, true));
    validScenarios.insert(GetScenarioInt(true, true, true, false));
    validScenarios.insert(GetScenarioInt(true, false, false, false));

    int currentScenario = GetScenarioInt(bValue1, bValue2, bValue3, bValue4);

    return validScenarios.find(currentScenario) != validScenarios.end();
}

int main()
{
    std::cout << IsValidScenario(true, true, true, false) << "\n"; // expected = true;
    std::cout << IsValidScenario(true, true, false, false) << "\n"; // expected = false;

    return 0;
}

Побачити це на роботі можна тут

Ну, це саме "елегантне та ремонтопридатне" (ІМХО) рішення, на яке я звичайно прагну, але, справді, у випадку з ОП, моя попередня відповідь "купа ifs" краще відповідає вимогам ОП, навіть якщо це не елегантно і не доглянуто.


Ви знаєте, що завжди можете редагувати попередню відповідь та вдосконалюватись.
Андреас

20

Я також хотів би подати інший підхід.

Моя ідея - перетворити булі в ціле число, а потім порівняти, використовуючи різні шаблони:

unsigned bitmap_from_bools(bool b) {
    return b;
}
template<typename... args>
unsigned bitmap_from_bools(bool b, args... pack) {
    return (bitmap_from_bools(b) << sizeof...(pack)) | bitmap_from_bools(pack...);
}

int main() {
    bool bValue1;
    bool bValue2;
    bool bValue3;
    bool bValue4;

    unsigned summary = bitmap_from_bools(bValue1, bValue2, bValue3, bValue4);

    if (summary != 0b1111u && summary != 0b1110u && summary != 0b1000u) {
        //bad scenario
    }
}

Зверніть увагу, як ця система може підтримувати до 32 булів як вхід. замінюючи unsignedз unsigned long long(або uint64_t) збільшує підтримку 64 випадків. Якщо вам це не подобається if (summary != 0b1111u && summary != 0b1110u && summary != 0b1000u), ви можете також скористатися ще одним варіантом методу шаблону:

bool equals_any(unsigned target, unsigned compare) {
    return target == compare;
}
template<typename... args>
bool equals_any(unsigned target, unsigned compare, args... compare_pack) {
    return equals_any(target, compare) ? true : equals_any(target, compare_pack...);
}

int main() {
    bool bValue1;
    bool bValue2;
    bool bValue3;
    bool bValue4;

    unsigned summary = bitmap_from_bools(bValue1, bValue2, bValue3, bValue4);

    if (!equals_any(summary, 0b1111u, 0b1110u, 0b1000u)) {
        //bad scenario
    }
}

2
Мені подобається такий підхід, за винятком назви основної функції: "від bool ... до чого ?" - Чому б не явно bitmap_from_bools, або bools_to_bitmap?
Конрад Рудольф

так @KonradRudolph, я не міг придумати кращого імені, хіба що, можливо bools_to_unsigned. Bitmap - це хороше ключове слово; відредаговано.
Стек Денні

Я думаю, ти хочеш summary!= 0b1111u &&.... a != b || a != cзавжди правда, якщоb != c
MooseBoys

17

Ось спрощена версія:

if (bValue1 && (bValue2 == bValue3) && (bValue2 || !bValue4)) {
    // acceptable
} else {
    // not acceptable
}

Зауважте, звичайно, це рішення є більш заплутаним, ніж оригінальне, його значення може бути важче зрозуміти.


Оновлення: MSalters у коментарях знайшов ще простіший вираз:

if (bValue1&&(bValue2==bValue3)&&(bValue2>=bValue4)) ...

1
Так, але важко зрозуміти. Але дякую за пропозицію.
Andrew Andrewle

Я порівняв здатність компіляторів спростити вираз із вашим спрощенням як еталон: компілятор Explorer . gcc не знайшов вашої оптимальної версії, але її рішення все-таки добре. Clang та MSVC, схоже, не здійснюють булевого спрощення експресії.
Олів

1
@AndrewTruckle: зауважте, що якщо вам потрібна версія для читання, то скажіть так. Ви сказали "спрощено", але ви приймаєте ще більш багатослівну версію, ніж оригінальну.
geza

1
simpleнасправді неясний термін. Багато людей розуміють це в цьому контексті як простіший для розуміння розробника, а не для компілятора, щоб генерувати код, тому більш багатослівний може дійсно бути простішим.
Здеслав Войкович

1
@IsmaelMiguel: коли оптимізована логічна формула для кількості термінів, початкове значення зазвичай втрачається. Але можна поставити коментар навколо нього, тож зрозуміло, що це робить. Навіть для прийнятої відповіді коментар не зашкодить.
geza

12

Подумайте про те, щоб якомога прямо перекласти свої таблиці у свою програму. Прогоніть програму на основі таблиці, замість того, щоб наслідувати її логікою.

template<class T0>
auto is_any_of( T0 const& t0, std::initializer_list<T0> il ) {
  for (auto&& x:il)
    if (x==t0) return true;
  return false;
}

зараз

if (is_any_of(
  std::make_tuple(bValue1, bValue2, bValue3, bValue4),
  {
    {true, true, true, true},
    {true, true, true, false},
    {true, false, false, false}
  }
))

це безпосередньо можливо кодує вашу таблицю істинності в компілятор.

Живий приклад .

Ви також можете std::any_ofбезпосередньо використовувати :

using entry = std::array<bool, 4>;
constexpr entry acceptable[] = 
  {
    {true, true, true, true},
    {true, true, true, false},
    {true, false, false, false}
  };
if (std::any_of( begin(acceptable), end(acceptable), [&](auto&&x){
  return entry{bValue1, bValue2, bValue3, bValue4} == x;
}) {
}

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


Перша версія настільки проста для читання і настільки обслуговувана, що мені дуже подобається. Другий - важче для читання, принаймні для мене, і вимагає рівня кваліфікації c ++, можливо, понад середній, безумовно, над моїм. Не те, що кожен вміє писати. Щойно дізнався щось нове, дякую
Джан Паоло

11

Я даю тут лише свою відповідь, як у коментарях, які хтось запропонував показати моє рішення. Хочу подякувати всім за їх розуміння.

Врешті-решт я вирішив додати три нові "сценарійні" booleanметоди:

bool CChristianLifeMinistryValidationDlg::IsFirstWeekStudentItems(CChristianLifeMinistryEntry *pEntry)
{
    return (INCLUDE_ITEM1(pEntry) && 
           !INCLUDE_ITEM2(pEntry) && 
           !INCLUDE_ITEM3(pEntry) && 
           !INCLUDE_ITEM4(pEntry));
}

bool CChristianLifeMinistryValidationDlg::IsSecondWeekStudentItems(CChristianLifeMinistryEntry *pEntry)
{
    return (INCLUDE_ITEM1(pEntry) &&
            INCLUDE_ITEM2(pEntry) &&
            INCLUDE_ITEM3(pEntry) &&
            INCLUDE_ITEM4(pEntry));
}

bool CChristianLifeMinistryValidationDlg::IsOtherWeekStudentItems(CChristianLifeMinistryEntry *pEntry)
{
    return (INCLUDE_ITEM1(pEntry) && 
            INCLUDE_ITEM2(pEntry) && 
            INCLUDE_ITEM3(pEntry) && 
           !INCLUDE_ITEM4(pEntry));
}

Тоді я зміг застосувати такі мої процедури перевірки, як це:

if (!IsFirstWeekStudentItems(pEntry) && !IsSecondWeekStudentItems(pEntry) && !IsOtherWeekStudentItems(pEntry))
{
    ; Error
}

У моєму живому застосуванні 4 значення bool насправді дістаються з коду, DWORDякий має 4 значення, закодовані в нього.

Ще раз дякую всім.


1
Дякуємо, що поділилися рішенням. :) Це насправді краще, ніж складні, якщо умови пекло. Можливо, ви все-таки зможете назвати INCLUDE_ITEM1і т.д. кращим чином, і вам все добре. :)
Hardik Modha

1
@HardikModha Ну, технічно вони "Студентські предмети", і прапор повинен вказувати, чи потрібно їх "включати". Тому я думаю, що ім'я, хоч і звучить загальне, насправді має сенс у цьому контексті. :)
Ендрю Truckle

11

Я не бачу відповідей, які б називали сценарії, хоча рішення ОП робить саме це.

Мені найкраще інкапсулювати коментар до того, що кожен сценарій містить або ім'я змінної, або ім'я функції. Ви швидше ігноруєте коментар, ніж ім'я, і ​​якщо ваша логіка зміниться в майбутньому, ви швидше зміните ім'я, ніж коментар. Ви не можете повторно змінити коментар.

Якщо ви плануєте повторно використовувати ці сценарії поза вашою функцією (або, можливо, захочете), то створіть функцію, яка говорить про те, що вона оцінює ( constexpr/ noexceptнеобов'язково, але рекомендується):

constexpr bool IsScenario1(bool b1, bool b2, bool b3, bool b4) noexcept
{ return b1 && b2 && b3 && b4; }

constexpr bool IsScenario2(bool b1, bool b2, bool b3, bool b4) noexcept
{ return b1 && b2 && b3 && !b4; }

constexpr bool IsScenario3(bool b1, bool b2, bool b3, bool b4) noexcept
{ return b1 && !b2 && !b3 && !b4; }

Зробіть ці методи класу, якщо це можливо (як у рішенні OP). Ви можете використовувати змінні всередині своєї функції, якщо не думаєте, що ви повторно використаєте логіку:

const auto is_scenario_1 = bValue1 && bValue2 && bValue3 && bValue4;
const auto is_scenario_2 = bvalue1 && bvalue2 && bValue3 && !bValue4;
const auto is_scenario_3 = bValue1 && !bValue2 && !bValue3 && !bValue4;

Компілятор, швидше за все, розбереться, що якщо bValue1 невірно, то всі сценарії помилкові. Не турбуйтеся про те, щоб зробити це швидко, просто правильно та легко читати. Якщо ви профілюєте свій код і вважаєте, що це вузьке місце, оскільки компілятор генерував неоптимальний код на рівні -O2 або вище, то спробуйте переписати його.


Мені це подобається трохи більше, ніж (вже приємне) рішення Джиана Паоло: Це дозволяє уникнути контролю над потоком та використання змінної, яка перезаписана - більш функціональний стиль.
Дірк Геррман

9

AC / C ++ спосіб

bool scenario[3][4] = {{true, true, true, true}, 
                        {true, true, true, false}, 
                        {true, false, false, false}};

bool CheckScenario(bool bValue1, bool bValue2, bool bValue3, bool bValue4)
{
    bool temp[] = {bValue1, bValue2, bValue3, bValue4};
    for(int i = 0 ; i < sizeof(scenario) / sizeof(scenario[0]); i++)
    {
        if(memcmp(temp, scenario[i], sizeof(temp)) == 0)
            return true;
    }
    return false;
}

Цей підхід є масштабованим, так як якщо кількість дійсних умов зростає, ви просто просто додаєте їх більше до списку сценаріїв.


Я майже впевнений, що це неправильно. Він передбачає, що компілятор використовує лише одне бінарне представлення для true. Компілятор, який використовує "все, що не має нуля, є істинним", призводить до збою цього коду. Зауважте, що trueнеобхідно перетворити на 1це, воно просто не потрібно зберігати як таке.
MSalters

@MSalters, tnx, я розумію, що, начебто 2 is not equal to true but evaluates to true, мій код не діє int 1 = trueі працює до тих пір, поки всі істинні перетворені на одне і те ж значення int, так ось моє питання: Чому компілятор повинен діяти випадково при перетворенні вірно до базового int. Чи можете ви, будь ласка, розробити більше?
hessam hedieh

Виконання memcmpбулевих умов для тестування не є способом C ++, і я, швидше за все, сумніваюся, що це встановлений спосіб С.
Конрад Рудольф

@hessamhedieh: Проблема у вашій логіці полягає в "перетворенні істини на базовий int". Це не так, як працюють компілятори,
MSalters

Ваш код збільшує складність з O (1) до O (n). Не спосіб перейти на будь-які мови - залиште осторонь C / C ++.
Мейбл

9

Неважко помітити, що перші два сценарії схожі - вони поділяють більшість умов. Якщо ви хочете вибрати, в якому сценарії ви перебуваєте на даний момент, можете написати це так (це модифіковане рішення @ gian-paolo ):

bool valid = false;
if(bValue1 && bValue2 && bValue3)
{
    if (bValue4)
        valid = true; //scenario 1
    else if (!bValue4)
        valid = true; //scenario 2
}
else if (bValue1 && !bValue2 && !bValue3 && !bValue4)
    valid = true; //scenario 3

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

bool valid = false;
if(bValue1)
{
    if(bValue2 && bValue3)
    {
        if (bValue4)
            valid = true; //scenario 1
        else if (!bValue4)
            valid = true; //scenario 2
    }
    else if (!bValue2 && !bValue3 && !bValue4)
        valid = true; //scenario 3
}

Навіть більше, тепер ви можете чітко бачити, що bValue2 і bValue3 дещо пов'язані - ви можете отримати їх стан деяким зовнішнім функціям або змінним з більш підходящою назвою (хоча це не завжди легко і доцільно):

bool valid = false;
if(bValue1)
{
    bool bValue1and2 = bValue1 && bValue2;
    bool notBValue1and2 = !bValue2 && !bValue3;
    if(bValue1and2)
    {
        if (bValue4)
            valid = true; //scenario 1
        else if (!bValue4)
            valid = true; //scenario 2
    }
    else if (notBValue1and2 && !bValue4)
        valid = true; //scenario 3
}

У цього способу є деякі переваги та недоліки:

  • умови менші, тому простіше міркувати про них,
  • простіше зробити приємне перейменування, щоб зробити ці умови зрозумілішими,
  • але вони потребують розуміння сфери,
  • до того ж він більш жорсткий

Якщо ви прогнозуєте, що зміни вищезгаданої логіки відбудуться, вам слід скористатися більш прямим підходом, представленим @ gian-paolo .

В іншому випадку, якщо ці умови добре встановлені і є на зразок "твердих правил", які ніколи не зміниться, розглянемо мій останній фрагмент коду.


7

Як пропонує mch, ви можете зробити:

if(!((bValue1 && bValue2 && bValue3) || 
  (bValue1 && !bValue2 && !bValue3 && !bValue4))
)

де перший рядок охоплює два перші добрі випадки, а другий рядок охоплює останній.

Live Demo, де я грав навколо, і він передає ваші справи.


7

Невелика різниця щодо тонкої відповіді @ GianPaolo, яку деякі можуть прочитати легше:

bool any_of_three_scenarios(bool v1, bool v2, bool v3, bool v4)
{
  return (v1 &&  v2 &&  v3 &&  v4)  // scenario 1
      || (v1 &&  v2 &&  v3 && !v4)  // scenario 2
      || (v1 && !v2 && !v3 && !v4); // scenario 3
}

if (any_of_three_scenarios(bValue1,bValue2,bValue3,bValue4))
{
  // ...
}

7

Кожна відповідь надмірно складна і важко читається. Найкращим рішенням цього є switch()твердження. Він читається і робить додавання / зміну додаткових справ простим. Компілятори також добре оптимізують switch()оператори.

switch( (bValue4 << 3) | (bValue3 << 2) | (bValue2 << 1) | (bValue1) )
{
    case 0b1111:
        // scenario 1
        break;

    case 0b0111:
        // scenario 2
        break;

    case 0b0001:
        // scenario 3
        break;

    default:
        // fault condition
        break;
}

Звичайно, ви можете використовувати константи та АБО їх разом у caseвисловлюваннях для ще більшої читабельності.


Будучи старим програмістом на C, я визначив макрос "PackBools" і використовував би це як для "перемикача (PackBools (a, b, c, d))", так і для випадків, наприклад, безпосередньо "case PackBools (true , true ...) "або визначте їх як локальні константи.eg" const unsigned int script1 = PackBools (true, true ...); "
Саймон F

6

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

bool MAJORLY_TRUE=bValue1 && bValue2 && bValue3
bool MAJORLY_FALSE=!(bValue2 || bValue3 || bValue4)

то ваше вираження перетворюється:

if (MAJORLY_TRUE || (bValue1 && MAJORLY_FALSE))
{
     // do something
}
else
{
    // There is some error
}

Надання змістовних імен змінним MAJORTRUE та MAJORFALSE (а також власне bValue * vars) допоможе читання та обслуговування.


6

Зосередьтеся на читанні проблеми, а не на конкретному висловленні "якщо".

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

Розбивши речі на класи (не соромтесь просто використовувати функції або будь-який інший інструмент, який ви віддаєте перевагу) з зрозумілими іменами - ми можемо набагато легше показати значення кожного сценарію. Що ще важливіше, у системі з безліччю рухомих частин - простіше підтримувати та приєднуватися до існуючих систем (знову ж таки, незважаючи на те, скільки додаткового коду викликається).

#include <iostream>
#include <vector>
using namespace std;

// These values would likely not come from a single struct in real life
// Instead, they may be references to other booleans in other systems
struct Values
{
    bool bValue1; // These would be given better names in reality
    bool bValue2; // e.g. bDidTheCarCatchFire
    bool bValue3; // and bDidTheWindshieldFallOff
    bool bValue4;
};

class Scenario
{
public:
    Scenario(Values& values)
    : mValues(values) {}

    virtual operator bool() = 0;

protected:
    Values& mValues;    
};

// Names as examples of things that describe your "scenarios" more effectively
class Scenario1_TheCarWasNotDamagedAtAll : public Scenario
{
public:
    Scenario1_TheCarWasNotDamagedAtAll(Values& values) : Scenario(values) {}

    virtual operator bool()
    {
        return mValues.bValue1
        && mValues.bValue2
        && mValues.bValue3
        && mValues.bValue4;
    }
};

class Scenario2_TheCarBreaksDownButDidntGoOnFire : public Scenario
{
public:
    Scenario2_TheCarBreaksDownButDidntGoOnFire(Values& values) : Scenario(values) {}

    virtual operator bool()
    {
        return mValues.bValue1
        && mValues.bValue2
        && mValues.bValue3
        && !mValues.bValue4;
    }   
};

class Scenario3_TheCarWasCompletelyWreckedAndFireEverywhere : public Scenario
{
public:
    Scenario3_TheCarWasCompletelyWreckedAndFireEverywhere(Values& values) : Scenario(values) {}

    virtual operator bool()
    {
        return mValues.bValue1
        && !mValues.bValue2
        && !mValues.bValue3
        && !mValues.bValue4;
    }   
};

Scenario* findMatchingScenario(std::vector<Scenario*>& scenarios)
{
    for(std::vector<Scenario*>::iterator it = scenarios.begin(); it != scenarios.end(); it++)
    {
        if (**it)
        {
            return *it;
        }
    }
    return NULL;
}

int main() {
    Values values = {true, true, true, true};
    std::vector<Scenario*> scenarios = {
        new Scenario1_TheCarWasNotDamagedAtAll(values),
        new Scenario2_TheCarBreaksDownButDidntGoOnFire(values),
        new Scenario3_TheCarWasCompletelyWreckedAndFireEverywhere(values)
    };

    Scenario* matchingScenario = findMatchingScenario(scenarios);

    if(matchingScenario)
    {
        std::cout << matchingScenario << " was a match" << std::endl;
    }
    else
    {
        std::cout << "No match" << std::endl;
    }

    // your code goes here
    return 0;
}

5
У якийсь момент багатослівність починає шкодити читабельності. Я думаю, що це надто далеко.
JollyJoker

2
@JollyJoker Я дійсно згоден у цій конкретній ситуації - проте, моє відчуття кишечника від того, як ОП назвала все надзвичайно загально, полягає в тому, що їх "реальний" код, ймовірно, набагато складніший, ніж наведений ними приклад. Дійсно, я просто хотів поставити цю альтернативу там, тому що я буду структурувати її для чогось набагато складнішого / залученого. Але ви маєте рацію - для конкретних прикладів, що стосуються ОП, він надмірно багатослівний і погіршує питання.

5

Це залежить від того, що вони представляють.

Наприклад, якщо 1 є ключовим, а 2 і 3 - двоє людей, які повинні погодитися (за винятком випадків, коли вони згодні, NOTїм потрібна третя особа - 4 - для підтвердження), найбільш читаною може бути:

1 &&
    (
        (2 && 3)   
        || 
        ((!2 && !3) && !4)
    )

за популярним запитом:

Key &&
    (
        (Alice && Bob)   
        || 
        ((!Alice && !Bob) && !Charlie)
    )

2
Можливо, ви маєте рацію, але використання цифр для ілюстрації вашої точки погіршує вашу відповідь. Спробуйте використовувати описові назви.
jxh

1
@jxh Це числа, які використовуються. Я щойно зняв bValue.
ispiro

@jxh Я сподіваюся, що зараз краще.
ispiro

4

Операція по долоту виглядає дуже чисто і зрозуміло.

int bitwise = (bValue4 << 3) | (bValue3 << 2) | (bValue2 << 1) | (bValue1);
if (bitwise == 0b1111 || bitwise == 0b0111 || bitwise == 0b0001)
{
    //satisfying condition
}

1
Розрядне порівняння виглядає для мене читабельним. Композиція, з іншого боку, виглядає штучно.
xtofl

3

Я позначаю a, b, c, d для ясності, а A, B, C, D для доповнень

bValue1 = a (!A)
bValue2 = b (!B)
bValue3 = c (!C)
bValue4 = d (!D)

Рівняння

1 = abcd + abcD + aBCD
  = a (bcd + bcD + BCD)
  = a (bc + BCD)
  = a (bcd + D (b ^C))

Використовуйте будь-які рівняння, які вам підходять.


3
If (!bValue1 || (bValue2 != bValue3) || (!bValue4 && bValue2))
{
// you have a problem
}
  • b1 завжди має бути правдою
  • b2 завжди повинен дорівнювати b3
  • і b4 не може бути помилковим, якщо b2 (і b3) є істинними

просто


3

Просто особиста перевага щодо прийнятої відповіді, але я би написав:

bool valid = false;
// scenario 1
valid = valid || (bValue1 && bValue2 && bValue3 && bValue4);
// scenario 2
valid = valid || (bValue1 && bValue2 && bValue3 && !bValue4);
// scenario 3
valid = valid || (bValue1 && !bValue2 && !bValue3 && !bValue4);

2

По-перше, припускаючи, що ви можете змінити лише перевірку сценарію, я б зосередився на читанні та просто зафіксував би чек у функції, щоб ви могли просто зателефонувати if(ScenarioA()).


Тепер, припускаючи, що ви насправді хочете / потребуєте оптимізувати це, я рекомендував би перетворити тісно пов'язані булеви в постійні цілі числа та використовувати бітові оператори на них

public class Options {
  public const bool A = 2; // 0001
  public const bool B = 4; // 0010
  public const bool C = 16;// 0100
  public const bool D = 32;// 1000
//public const bool N = 2^n; (up to n=32)
}

...

public isScenario3(int options) {
  int s3 = Options.A | Options.B | Options.C;
  // for true if only s3 options are set
  return options == s3;
  // for true if s3 options are set
  // return options & s3 == s3
}

Це робить висловлення сценаріїв настільки ж простим, як перерахування того, що є його частиною, дозволяє використовувати оператор перемикання, щоб перейти до потрібної умови, і заплутати колег-розробників, які цього раніше не бачили. (C # RegexOptions використовує цю схему для встановлення прапорів, я не знаю, чи є приклад бібліотеки c ++)


Насправді я не використовую чотири значення bool, а DWORD з чотирма вбудованими BOOLS. Занадто пізно, щоб зараз її змінити. Але дякую за вашу пропозицію.
Ендрю Truckle

2

Вкладені ifs може бути легше читати для деяких людей. Ось моя версія

bool check(int bValue1, int bValue2, int bValue3, int bValue4)
{
  if (bValue1)
  {
    if (bValue2)
    {
      // scenario 1-2
      return bValue3;
    }
    else
    {
      // scenario 3
      return !bValue3 && !bValue4;
    }
  }

  return false;
}

Особисто я зазвичай уникаю вкладати, якщо це можливо. Незважаючи на те, що цей випадок є приємним і читабельним, коли нові можливості додаються, гніздування може бути дуже важким для читання. Але якщо сценарії ніколи не змінюються, це, безумовно, приємне і читабельне рішення.
Dnomyar96

@ Dnomyar96 я згоден. Я особисто уникаю вкладених ifs теж. Іноді, якщо логіка складна, мені простіше зрозуміти логіку, розбивши її на шматки. Наприклад, як тільки ви введете bValue1блок, ви можете розглядати все, що в ньому, як нову свіжу сторінку у вашому розумовому процесі. Сподіваюся, спосіб підходу до проблеми може бути дуже особистим чи навіть культурним.
сардок

1

На це питання було дано декілька правильних відповідей, але я б по-іншому вважав: якщо код виглядає занадто складно, щось не зовсім правильно . Код буде важко налагодити і, швидше за все, "лише для одноразового використання".

У реальному житті ми виявляємо таку ситуацію:

         Scenario 1 | Scenario 2 | Scenario 3
bValue1: true       | true       | true
bValue2: true       | true       | false
bValue3: true       | true       | false
bValue4: true       | false      | false

Коли чотири стани пов'язані за такою точною схемою, ми маємо справу з конфігурацією деякої "сутності" в нашій моделі .

Крайня метафора - це те, як ми описували б "людину" у моделі, якби ми не усвідомлювали їх існування як унітарних утворень із компонентами, пов'язаними з певними ступенями свободи: нам довелося б описувати незалежні стани "тулубів", "руки", "ноги" та "голова", які ускладнювали б осмислення описаної системи. Безпосереднім результатом стали б неприродно складні булеві вирази.

Очевидно, що спосіб зменшення складності - це абстракція, а інструментом вибору в c ++ є парадигма об'єкта .

Тож питання: чому існує така закономірність? Що це таке і що він представляє?

Оскільки ми не знаємо відповіді, ми можемо повернутися до математичної абстракції: масив : у нас є три сценарії, кожен з яких зараз є масивом.

                0   1   2   3
Scenario 1:     T   T   T   T
Scenario 2:     T   T   T   F
Scenario 3:     T   F   F   F

У цей момент у вас є ваша початкова конфігурація. як масив. Напрstd::array , оператор рівності:

У цей момент ваш синтаксис стає:

if( myarray == scenario1 ) {
  // arrays contents are the same

} 
else if ( myarray == scenario2 ) {
  // arrays contents are the same

} 

else if ( myarray == scenario3 ) {
  // arrays contents are the same

} 
else {
  // not the same

}

Як і відповідь Джана Паоло, вона коротка, зрозуміла і легко піддається контролю / налагодженню. У цьому випадку ми делегували деталі булевих виразів компілятору.


1

Вам не доведеться турбуватися про недійсні комбінації булевих прапорів, якщо ви позбудетесь булевих прапорів.

Прийнятні значення:

         Scenario 1 | Scenario 2 | Scenario 3
bValue1: true       | true       | true
bValue2: true       | true       | false
bValue3: true       | true       | false
bValue4: true       | false      | false

Ви чітко маєте три стани (сценарії). Краще було б моделювати це і виводити булові властивості з цих станів, а не навпаки.

enum State
{
    scenario1,
    scenario2,
    scenario3,
};

inline bool isValue1(State s)
{
    // (Well, this is kind of silly.  Do you really need this flag?)
    return true;
}

inline bool isValue2(State s)
{
    switch (s)
    {
        case scenario1:
        case scenario2:
            return true;
        case scenario3:
            return false;
    }
}

inline bool isValue3(State s)
{
    // (This is silly too.  Do you really need this flag?)
    return isValue2(s);
}

inline bool isValue4(State s)
{
    switch (s)
    {
        case scenario1:
            return true;
        case scenario2:
        case scenario3:
            return false;
    }
}

Це, безумовно, більше коду, ніж у відповіді Джана Паоло , але залежно від вашої ситуації це може бути набагато реконструктивнішим:

  • Існує центральний набір функцій для зміни, якщо додаються додаткові булі властивості або сценарії.
    • Додавання властивостей вимагає додавання лише однієї функції.
    • Якщо додати сценарій, увімкнення попереджень компілятора про незроблені enumвипадки у switchвиписках зачепить власників ресурсів, які не обробляють цей сценарій.
  • Якщо вам потрібно буде динамічно змінювати булеві властивості, вам не потрібно повторно перевіряти їх комбінації скрізь. Замість перемикання окремих булевих прапорів (що може призвести до недійсних комбінацій прапорів), ви натомість матимете стан машини, який переходить з одного сценарію в інший.

Цей підхід також має побічну перевагу - бути дуже ефективним.


0

Мої 2 центи: оголосити змінну суму (ціле число), так що

if(bValue1)
{
  sum=sum+1;
}
if(bValue2)
{
  sum=sum+2;
}
if(bValue3)
{
  sum=sum+4;
}
if(bValue4)
{
  sum=sum+8;
}

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


0

Прийнята відповідь чудова, коли у вас є лише 3 випадки, і де логіка для кожного проста.

Але якщо логіка для кожного випадку була складнішою, або є набагато більше випадків, набагато кращим варіантом є використання схеми проектування ланцюжка відповідальності .

Ви створюєте, BaseValidatorщо містить посилання на a, BaseValidatorі метод на, validateі метод для виклику перевірки на посилальному валідаторі.

class BaseValidator {
    BaseValidator* nextValidator;

    public:
    BaseValidator() {
        nextValidator = 0;
    }

    void link(BaseValidator validator) {
        if (nextValidator) {
            nextValidator->link(validator);
        } else {
            nextValidator = validator;
        }
    }

    bool callLinkedValidator(bool v1, bool v2, bool v3, bool v4) {
        if (nextValidator) {
            return nextValidator->validate(v1, v2, v3, v4);
        }

        return false;
    }

    virtual bool validate(bool v1, bool v2, bool v3, bool v4) {
        return false;
    }
}

Потім ви створюєте ряд підкласів, які успадковують метод BaseValidator, переосмислюючи validateметод з логікою, необхідною для кожного валідатора.

class Validator1: public BaseValidator {
    public:
    bool validate(bool v1, bool v2, bool v3, bool v4) {
        if (v1 && v2 && v3 && v4) {
            return true;
        }

        return nextValidator->callLinkedValidator(v1, v2, v3, v4);
    }
}

Тоді за допомогою нього просто, інстанціюйте кожен свій валідатор і встановіть, щоб кожен з них був коренем інших:

Validator1 firstValidator = new Validator1();
Validator2 secondValidator = new Validator2();
Validator3 thirdValidator = new Validator3();
firstValidator.link(secondValidator);
firstValidator.link(thirdValidator);
if (firstValidator.validate(value1, value2, value3, value4)) { ... }

По суті, у кожному випадку перевірки є свій клас, який відповідає за (a) визначення, чи відповідає валідація цьому випадку, і (b) відправлення перевірки комусь іншому в ланцюжку, якщо це не так.

Зверніть увагу, що я не знайомий із C ++. Я намагався співставити синтаксис з деякими прикладами, які я знайшов в Інтернеті, але якщо це не спрацює, розглядайте його як псевдокод. Я також маю повний приклад робочого Python нижче, який можна використовувати в якості основи, якщо бажано.

class BaseValidator:
    def __init__(self):
        self.nextValidator = 0

    def link(self, validator):
        if (self.nextValidator):
            self.nextValidator.link(validator)
        else:
            self.nextValidator = validator

    def callLinkedValidator(self, v1, v2, v3, v4):
        if (self.nextValidator):
            return self.nextValidator.validate(v1, v2, v3, v4)

        return False

    def validate(self, v1, v2, v3, v4):
        return False

class Validator1(BaseValidator):
    def validate(self, v1, v2, v3, v4):
        if (v1 and v2 and v3 and v4):
            return True
        return self.callLinkedValidator(v1, v2, v3, v4)

class Validator2(BaseValidator):
    def validate(self, v1, v2, v3, v4):
        if (v1 and v2 and v3 and not v4):
            return True
        return self.callLinkedValidator(v1, v2, v3, v4)

class Validator3(BaseValidator):
    def validate(self, v1, v2, v3, v4):
        if (v1 and not v2 and not v3 and not v4):
            return True
        return self.callLinkedValidator(v1, v2, v3, v4)

firstValidator = Validator1()
secondValidator = Validator2()
thirdValidator = Validator3()
firstValidator.link(secondValidator)
firstValidator.link(thirdValidator)
print(firstValidator.validate(False, False, True, False))

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


-2

Простий підхід - пошук відповіді, яку ви вважаєте прийнятною.

Так = (boolean1 && boolean2 && boolean3 && boolean4) + + ...

Тепер по можливості спростіть рівняння за допомогою булевої алгебри.

як у цьому випадку, прийнятні1 та 2 комбіновані до (boolean1 && boolean2 && boolean3).

Отже, остаточна відповідь:

(boolean1 && boolean2 && boolean3) || 
((boolean1 && !boolean2 && !boolean3 && !boolean4)

-3

використовувати бітове поле :

unoin {
  struct {
    bool b1: 1;
    bool b2: 1;
    bool b3: 1;
    bool b4: 1;
  } b;
  int i;
} u;

// set:
u.b.b1=true;
...

// test
if (u.i == 0x0f) {...}
if (u.i == 0x0e) {...}
if (u.i == 0x08) {...}

PS :

Це дуже шкода CPPers. Але, UB не хвилююся, перевірте це на http://coliru.stacked-crooked.com/a/2b556abfc28574a1 .


2
Це викликає UB через доступ до неактивного поля об'єднання.
HolyBlackCat

Формально це UB в C ++, ви не можете встановити одного члена союзу та читати з іншого. Технічно може бути краще реалізувати шаблони getters \ setters для бітів цілісного значення.
Свіфт - П’ятничний пиріг

Я думаю, що поведінка зміститься до визначеної реалізацією, якби перетворити адресу профспілки в unsigned char*, хоча я думаю, що просто використання чогось подібного ((((flag4 <<1) | flag3) << 1) | flag2) << 1) | flag1, ймовірно, було б більш ефективним.
supercat
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.