Как избежать & quot; if & quot; цепи?

Предполагая, что у меня есть этот псевдокод:

bool conditionA = executeStepA();
if (conditionA){
bool conditionB = executeStepB();
if (conditionB){
bool conditionC = executeStepC();
if (conditionC){
...
}
}
}

executeThisFunctionInAnyCase();

функции executeStepX должен быть выполнен тогда и только тогда, когда предыдущий будет успешным.
В любом случае, executeThisFunctionInAnyCase Функция должна быть вызвана в конце.
Я новичок в программировании, так что извините за очень простой вопрос: есть ли способ (например, в C / C ++), чтобы избежать этого долго if цепочка, производящая подобную «пирамиду кода», за счет разборчивости кода?

Я знаю, что если бы мы могли пропустить executeThisFunctionInAnyCase вызов функции, код может быть упрощен как:

bool conditionA = executeStepA();
if (!conditionA) return;
bool conditionB = executeStepB();
if (!conditionB) return;
bool conditionC = executeStepC();
if (!conditionC) return;

Но ограничение является executeThisFunctionInAnyCase вызов функции.
Может ли break заявление будет использовано каким-то образом?

259

Решение

Вы можете использовать && (логика И):

if (executeStepA() && executeStepB() && executeStepC()){
...
}
executeThisFunctionInAnyCase();

это удовлетворит оба ваших требования:

  • executeStep<X>() следует оценивать, только если предыдущий преуспел (это называется оценка короткого замыкания)
  • executeThisFunctionInAnyCase() будет выполнен в любом случае
482

Другие решения

Просто используйте дополнительную функцию, чтобы ваша вторая версия работала:

void foo()
{
bool conditionA = executeStepA();
if (!conditionA) return;

bool conditionB = executeStepB();
if (!conditionB) return;

bool conditionC = executeStepC();
if (!conditionC) return;
}

void bar()
{
foo();
executeThisFunctionInAnyCase();
}

Использование глубоко вложенных ifs (ваш первый вариант) или желание выйти из «части функции» обычно означает, что вам нужна дополнительная функция.

354

Программисты старой школы C используют goto в этом случае. Это одно использование goto это на самом деле поощряется руководством по стилю Linux, оно называется централизованной функцией выхода:

int foo() {
int result = /*some error code*/;
if(!executeStepA()) goto cleanup;
if(!executeStepB()) goto cleanup;
if(!executeStepC()) goto cleanup;

result = 0;
cleanup:
executeThisFunctionInAnyCase();
return result;
}

Некоторые люди работают вокруг, используя goto заключая тело в петлю и отрываясь от него, но в действительности оба подхода делают одно и то же. goto подход лучше, если вам нужна другая очистка, только если executeStepA() был успешным:

int foo() {
int result = /*some error code*/;
if(!executeStepA()) goto cleanupPart;
if(!executeStepB()) goto cleanup;
if(!executeStepC()) goto cleanup;

result = 0;
cleanup:
innerCleanup();
cleanupPart:
executeThisFunctionInAnyCase();
return result;
}

С циклическим подходом в этом случае вы получите два уровня циклов.

163

Это обычная ситуация, и есть много способов справиться с ней. Вот моя попытка канонического ответа. Пожалуйста, прокомментируйте, если я что-то пропустил, и я буду держать этот пост в актуальном состоянии.

То, что вы обсуждаете, называется стрелка анти-шаблон. Это называется стрелкой, потому что цепочка вложенных if формирует блоки кода, которые расширяются все дальше и дальше вправо, а затем назад влево, образуя визуальную стрелку, которая «указывает» на правую сторону панели редактора кода.

Расправь стрелу с гвардией

Обсуждаются некоторые общие способы избежать Стрелка Вот. Наиболее распространенным методом является использование охрана шаблон, в котором код сначала обрабатывает потоки исключений, а затем обрабатывает основной поток, например вместо

if (ok)
{
DoSomething();
}
else
{
_log.Error("oops");
return;
}

… вы бы использовали ….

if (!ok)
{
_log.Error("oops");
return;
}
DoSomething(); //notice how this is already farther to the left than the example above

Когда имеется длинная серия охранников, это значительно сглаживает код, поскольку все охранники появляются полностью слева, а ваши if не вложены. Кроме того, вы визуально связываете логическое условие с соответствующей ошибкой, что значительно упрощает определение происходящего:

Стрела:

ok = DoSomething1();
if (ok)
{
ok = DoSomething2();
if (ok)
{
ok = DoSomething3();
if (!ok)
{
_log.Error("oops");  //Tip of the Arrow
return;
}
}
else
{
_log.Error("oops");
return;
}
}
else
{
_log.Error("oops");
return;
}

охрана:

ok = DoSomething1();
if (!ok)
{
_log.Error("oops");
return;
}
ok = DoSomething2();
if (!ok)
{
_log.Error("oops");
return;
}
ok = DoSomething3();
if (!ok)
{
_log.Error("oops");
return;
}
ok = DoSomething4();
if (!ok)
{
_log.Error("oops");
return;
}

Это объективно и количественно легче читать, потому что

  1. Символы {и} для данного логического блока расположены ближе друг к другу
  2. Количество ментального контекста, необходимого для понимания конкретной строки, меньше
  3. Вся логика, связанная с условием if, скорее всего, находится на одной странице
  4. Потребность кодера для прокрутки трека страницы / глаза значительно уменьшена

Проблема с паттерном охраны заключается в том, что он опирается на то, что называется «оппортунистическим возвращением» или «оппортунистическим выходом». Другими словами, это нарушает схему, согласно которой каждая функция должна иметь ровно одну точку выхода. Это проблема по двум причинам:

  1. Некоторым людям это не по плечу, например люди, которые научились кодировать на Паскале, узнали, что одна функция = одна точка выхода.
  2. Он не предоставляет раздел кода, который выполняется при выходе независимо от того, что, который является предметом под рукой.

Ниже я предоставил некоторые варианты для обхода этого ограничения, либо используя языковые функции, либо избегая проблемы в целом.

Вариант 1. Вы не можете сделать это: использовать finally

К сожалению, как разработчик C ++, вы не можете сделать это. Но это ответ номер один для языков, которые содержат ключевое слово finally, поскольку именно для этого оно и предназначено.

try
{
if (!ok)
{
_log.Error("oops");
return;
}
DoSomething(); //notice how this is already farther to the left than the example above
}
finally
{
DoSomethingNoMatterWhat();
}

Вариант 2. Избегайте проблемы: реструктурируйте свои функции

Вы можете избежать этой проблемы, разбив код на две функции. Преимущество этого решения — работать на любом языке, и, кроме того, оно может уменьшить цикломатическая сложность, Это проверенный способ снизить уровень дефектов и повысить специфичность любых автоматических модульных тестов.

Вот пример:

void OuterFunction()
{
DoSomethingIfPossible();
DoSomethingNoMatterWhat();
}

void DoSomethingIfPossible()
{
if (!ok)
{
_log.Error("Oops");
return;
}
DoSomething();
}

Вариант 3. Языковой трюк: использовать фальшивый цикл

Другой распространенный трюк, который я вижу, — это использование while (true) и break, как показано в других ответах.

while(true)
{
if (!ok) break;
DoSomething();
break;  //important
}
DoSomethingNoMatterWhat();

Хотя это менее «честно», чем использование gotoон менее подвержен ошибкам при рефакторинге, так как он четко обозначает границы логической области видимости. Наивный кодер, который вырезает и вставляет ваши этикетки или ваши goto заявления могут вызвать серьезные проблемы! (И, откровенно говоря, картина настолько распространена, что теперь я думаю, что она ясно сообщает о намерениях и поэтому вовсе не является «нечестной»).

Есть и другие варианты этой опции. Например, можно использовать switch вместо while, Любая языковая конструкция с break Ключевое слово, вероятно, будет работать.

Вариант 4. Использование жизненного цикла объекта

Еще один подход использует жизненный цикл объекта. Используйте объект контекста для переноса ваших параметров (то, чего подозрительно не хватает нашему наивному примеру) и избавьтесь от него, когда вы закончите.

class MyContext
{
~MyContext()
{
DoSomethingNoMatterWhat();
}
}

void MainMethod()
{
MyContext myContext;
ok = DoSomething(myContext);
if (!ok)
{
_log.Error("Oops");
return;
}
ok = DoSomethingElse(myContext);
if (!ok)
{
_log.Error("Oops");
return;
}
ok = DoSomethingMore(myContext);
if (!ok)
{
_log.Error("Oops");
}

//DoSomethingNoMatterWhat will be called when myContext goes out of scope
}

Примечание. Убедитесь, что вы понимаете жизненный цикл объекта по своему выбору. Чтобы это работало, вам нужна какая-то детерминированная сборка мусора, то есть вы должны знать, когда будет вызван деструктор. На некоторых языках вам нужно будет использовать Dispose вместо деструктора.

Вариант 4.1. Использовать жизненный цикл объекта (шаблон оболочки)

Если вы собираетесь использовать объектно-ориентированный подход, можете сделать это правильно. Эта опция использует класс, чтобы «обернуть» ресурсы, которые требуют очистки, а также другие его операции.

class MyWrapper
{
bool DoSomething() {...};
bool DoSomethingElse() {...}void ~MyWapper()
{
DoSomethingNoMatterWhat();
}
}

void MainMethod()
{
bool ok = myWrapper.DoSomething();
if (!ok)
_log.Error("Oops");
return;
}
ok = myWrapper.DoSomethingElse();
if (!ok)
_log.Error("Oops");
return;
}
}
//DoSomethingNoMatterWhat will be called when myWrapper is destroyed

Опять же, убедитесь, что вы понимаете жизненный цикл вашего объекта.

Вариант 5. Языковой трюк: использовать оценку короткого замыкания

Другая техника заключается в том, чтобы воспользоваться оценка короткого замыкания.

if (DoSomething1() && DoSomething2() && DoSomething3())
{
DoSomething4();
}
DoSomethingNoMatterWhat();

Это решение использует преимущества способа && оператор работает. Когда левая сторона && оценивается как ложное, правая часть никогда не оценивается.

Этот трюк наиболее полезен, когда требуется компактный код, и когда код вряд ли нуждается в большом обслуживании, например, вы реализуете хорошо известный алгоритм. Для более общего кодирования структура этого кода слишком хрупкая; даже незначительное изменение в логике может вызвать полное переписывание.

127

Просто делать

if( executeStepA() && executeStepB() && executeStepC() )
{
// ...
}
executeThisFunctionInAnyCase();

Это так просто.


Из-за трех правок, которые у каждого есть в корне изменил вопрос (четыре, если считать версию до версии 1), я включил пример кода, на который я отвечаю:

bool conditionA = executeStepA();
if (conditionA){
bool conditionB = executeStepB();
if (conditionB){
bool conditionC = executeStepC();
if (conditionC){
...
}
}
}

executeThisFunctionInAnyCase();
60

На самом деле в C ++ есть способ отложить действия: использовать деструктор объекта.

Предполагая, что у вас есть доступ к C ++ 11:

class Defer {
public:
Defer(std::function<void()> f): f_(std::move(f)) {}
~Defer() { if (f_) { f_(); } }

void cancel() { f_ = std::function<void()>(); }

private:
Defer(Defer const&) = delete;
Defer& operator=(Defer const&) = delete;

std::function<void()> f_;
}; // class Defer

А затем с помощью этой утилиты:

int foo() {
Defer const defer{&executeThisFunctionInAnyCase}; // or a lambda

// ...

if (!executeA()) { return 1; }

// ...

if (!executeB()) { return 2; }

// ...

if (!executeC()) { return 3; }

// ...

return 4;
} // foo
35

Есть хороший метод, который не нуждается в дополнительной функции-обертке с операторами return (метод, предписанный Itjax). Это делает делать while(0) псевдо-петля. while (0) гарантирует, что это на самом деле не цикл, а выполняется только один раз. Тем не менее, синтаксис цикла позволяет использовать оператор break.

void foo()
{
// ...
do {
if (!executeStepA())
break;
if (!executeStepB())
break;
if (!executeStepC())
break;
}
while (0);
// ...
}
34

Вы также можете сделать это:

bool isOk = true;
std::vector<bool (*)(void)> funcs; //vector of function ptr

funcs.push_back(&executeStepA);
funcs.push_back(&executeStepB);
funcs.push_back(&executeStepC);
//...

//this will stop at the first false return
for (auto it = funcs.begin(); it != funcs.end() && isOk; ++it)
isOk = (*it)();
if (isOk)
//doSomeStuff
executeThisFunctionInAnyCase();

Таким образом, у вас будет минимальный линейный размер роста, +1 линия на вызов, и его легко обслуживать.


РЕДАКТИРОВАТЬ: (Спасибо @Unda) Не большой поклонник, потому что вы теряете видимость ИМО:

bool isOk = true;
auto funcs { //using c++11 initializer_list
&executeStepA,
&executeStepB,
&executeStepC
};

for (auto it = funcs.begin(); it != funcs.end() && isOk; ++it)
isOk = (*it)();
if (isOk)
//doSomeStuff
executeThisFunctionInAnyCase();
19
По вопросам рекламы [email protected]