Как избежать цепочек «если»?

266

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

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то использовать это утверждение?

ABCplus
источник
254
@ FrédéricHamidi неправильно неправильно неправильно! Никогда не говорите, что управление потоком программ с исключениями - это хорошо! Исключения определенно НЕ подходят для этой цели по слишком многим причинам.
Петр Циргофер
26
@Piotr, я был избалован Python (который на самом деле поощряет это). Я знаю, что исключения не должны использоваться для управления потоком в C ++, но действительно ли это здесь управление потоком? Не может ли функция, возвращающая функцию, falseрассматриваться как исключительная ситуация?
Фредерик Хамиди
13
Это зависит от семантики программы. falseВозвращение может быть вполне нормальным.
Дорнхедж
28
Я откатил ваш вопрос до его первой ревизии. Вы не должны радикально менять свой вопрос после того, как вы получили определенное количество вопросов (> 0), потому что это сделает недействительными все ответы, которые были даны до этого момента, и создаст путаницу. Вместо этого откройте новый вопрос.
Чистка
13
Я бы хотел, чтобы все «новички-программисты» задавали такие вопросы о дизайне.
Джезен Томас

Ответы:

486

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

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

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

  • executeStep<X>()следует оценивать, только если предыдущий преуспел (это называется оценкой короткого замыкания )
  • executeThisFunctionInAnyCase() будет выполнен в любом случае
башмак
источник
29
Это и семантически правильно (действительно, мы хотим, чтобы ВСЕ условия выполнялись), а также очень хороший метод программирования (оценка короткого замыкания). Кроме того, это может быть использовано в любой сложной ситуации, когда создание функций может испортить код.
Sanchises
58
@RobAu: Тогда начинающим программистам будет полезно, наконец, увидеть код, который опирается на кратковременную оценку, и может побудить их перейти к этой теме, что, в свою очередь, поможет им стать старшими программистами. Ясно, что это беспроигрышный вариант: достойный код, и кто-то чему-то научился, прочитав его.
x4u
24
Это должен быть главный ответ
Отображаемое имя
61
@RobAu: Это не хак принимая преимущество какой - то непонятной синтаксической трюк, это очень идиоматических почти на всех языках программирования, вплоть до того , чтобы быть undebatably стандартной практикой.
BlueRaja - Дэнни Пфлюгофт
38
Это решение применяется только в том случае, если условия на самом деле являются простыми вызовами функций. В реальном коде эти условия могут быть длиной от 2 до 5 строк (и сами по себе могут быть комбинациями многих других &&и ||), поэтому вы не захотите объединить их в одно ifутверждение без потери читабельности. И не всегда легко перенести эти условия во внешние функции, потому что они могут зависеть от множества предварительно вычисленных локальных переменных, что может создать ужасный беспорядок, если вы попытаетесь передать каждое из них в качестве отдельного аргумента.
hamstergene
358

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

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 (ваш первый вариант) или желание выйти из «части функции» обычно означает, что вам нужна дополнительная функция.

ltjax
источник
51
+1 наконец кто-то выложил вменяемый ответ. Это самый правильный, безопасный и читаемый способ, на мой взгляд.
Лундин
31
+1 Это хорошая иллюстрация «Принципа единой ответственности». Функция fooработает через последовательность связанных условий и действий. Функция barчетко отделена от решений. Если бы мы увидели детали условий и действий, может оказаться, что fooвсе еще делается слишком много, но пока это хорошее решение.
GraniteRobert
13
Недостатком является то, что в C нет вложенных функций, поэтому, если эти 3 шага необходимы для использования переменных из barвас, вам придется вручную передавать их в fooкачестве параметров. Если это так и если fooвызывается только один раз, я бы ошибся в использовании версии goto, чтобы избежать определения двух тесно связанных функций, которые в конечном итоге не будут использоваться повторно.
hugomg
7
Не уверен в синтаксисе короткого замыкания для C, но в C # foo () можно записать какif (!executeStepA() || !executeStepB() || !executeStepC()) return
Travis
6
@ user1598390 Goto используются постоянно, особенно в системном программировании, когда вам нужно развернуть много кода настройки.
Скотти Бауэр
166

Программисты старой школы 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;
}

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

cmaster - восстановить монику
источник
108
+1. Многие люди видят gotoи сразу думают: «Это ужасный код», но он действительно имеет свое применение.
Грэм Перроу
46
За исключением того, что это на самом деле довольно грязный код с точки зрения обслуживания. Особенно, когда есть несколько меток, где код также становится сложнее для чтения. Для этого есть более элегантные способы: использовать функции.
Лундин
29
-1 Я вижу, gotoи мне даже не нужно думать, чтобы увидеть, что это ужасный код. Мне когда-то приходилось поддерживать такое, это очень противно. ОП предлагает разумную альтернативу языка Си в конце вопроса, и я включил это в свой ответ.
ура и hth. - Альф
56
Нет ничего плохого в этом ограниченном, автономном использовании goto. Но будьте осторожны, goto - это наркотик, и если вы не будете осторожны, вы однажды поймете, что никто больше не ест спагетти ногами, и вы делаете это в течение 15 лет, начиная с того, что подумали: «Я могу это просто исправить». иначе кошмарный баг с меткой ... »
Тим Пост
66
Я написал, вероятно, сто тысяч строк чрезвычайно ясного, простого в обслуживании кода в этом стиле. Две ключевые вещи, которые нужно понять, это (1) дисциплина! установить четкое руководство для компоновки каждой функции и нарушать ее только при крайней необходимости, и (2) понимать, что мы делаем здесь, имитируя исключения в языке, в котором их нет . throwво многих отношениях хуже, чем gotoпотому, throwчто даже из местного контекста неясно, где вы собираетесь оказаться! Используйте те же принципы проектирования для потока управления в стиле Goto, что и для исключений.
Эрик Липперт
131

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

Это стрелка

То, что вы обсуждаете, называется анти-паттерном стрелки . Это называется стрелкой, потому что цепочка вложенных 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;
}

Guard:

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

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

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

Джон Ву
источник
12
В заключение? C ++ не имеет предложения finally. Объект с деструктором используется в ситуациях, когда вы думаете, что вам нужно предложение finally.
Билл Дверь
1
Отредактировано с учетом двух комментариев выше.
Джон Ву
2
С тривиальным кодом (например, в моем примере) вложенные шаблоны, вероятно, более понятны. При использовании реального кода (который может занимать несколько страниц) шаблон защиты объективно легче прочитать, поскольку он потребует меньше прокрутки и меньше отслеживания глаз, например, среднее расстояние от {до} короче.
Джон Ву
1
Я видел вложенный шаблон, где код больше не был виден на экране 1920 x 1080 ... Попробуйте выяснить, какой код обработки ошибок будет выполнен, если третье действие не выполнено ... Я использовал do {...} в то время как (0), поэтому вам не нужен последний перерыв (с другой стороны, в то время как (true) {...} позволяет «продолжить» начинать все заново.
gnasher729
2
Ваш вариант 4 на самом деле является утечкой памяти в C ++ (игнорируя незначительную синтаксическую ошибку). В этом случае нельзя использовать «новый», просто скажите «MyContext myContext;».
Сумуду Фернандо
60

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

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

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


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

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

executeThisFunctionInAnyCase();
Ура и hth. Альф
источник
14
Я ответил на это (более кратко) в ответ на первую версию вопроса, и он получил 20 голосов, прежде чем был удален Биллом Ящерицей, после некоторого комментария и редактирования Расами Тьмы на Орбите.
ура и hth. - Альф
1
@ Cheersandhth-Alf: я не могу поверить, что он был удален модом. Это очень плохо. (+1)
Парамагнитный круассан
2
+1 к твоей смелости! (3 раза имеет значение: D).
Хак
Совершенно важно, чтобы начинающие программисты узнали о таких вещах, как выполнение заказов с несколькими логическими «и», как это различно в разных языках и т. Д. И этот ответ является отличным указателем на это. Но это просто «не стартер» в обычном коммерческом программировании. Это не просто, это не ремонтопригодно, не модифицируемо. Конечно, если вы просто пишете какой-нибудь быстрый «ковбойский код» для себя, сделайте это. Но он просто не имеет никакого отношения к повседневной разработке программного обеспечения, как это практикуется сегодня. Кстати, извините за нелепую «правку-путаницу», которую вам пришлось пережить здесь, @cheers :)
Толстяк
1
@JoeBlow: я с Альфом на этом - &&список мне кажется намного понятнее. Я обычно разбиваю условия на отдельные строки и добавляю трейлинг // explanationк каждой .... В конце концов, это значительно меньше кода, и когда вы понимаете, как &&работает, нет никаких постоянных умственных усилий. У меня сложилось впечатление, что большинство профессиональных программистов на C ++ знакомы с этим, но, как вы говорите, в разных отраслях / проектах фокус и опыт отличаются.
Тони Делрой
35

На самом деле в 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
Матье М.
источник
67
Это просто полная путаница для меня. Я действительно не понимаю, почему так много программистов на C ++ любят решать проблему, бросая в нее как можно больше языковых возможностей, до тех пор, пока все не забудут о том, какую проблему вы на самом деле решали: им уже все равно, потому что они сейчас так интересно использовать все эти экзотические языковые возможности. И с этого момента вы можете занять себя днями и неделями, писать метакод, затем поддерживать метакод, а затем писать метакод для обработки метакода.
Лундин
24
@Lundin: Ну, я не понимаю, как можно быть удовлетворенным хрупким кодом, который сломается, как только будет введено раннее продолжение / разрыв / возврат или будет сгенерировано исключение. С другой стороны, это решение является гибким перед лицом будущего обслуживания и опирается только на тот факт, что деструкторы выполняются во время разматывания, что является одной из наиболее важных функций C ++. Относиться к этому экзотично, хотя в основе всех стандартных контейнеров лежит основной принцип, забавно, если не сказать больше.
Матье М.
7
@Lundin: Преимущество кода Матье в том, что он executeThisFunctionInAnyCase();будет выполняться, даже если foo();выдает исключение. При написании кода, исключающего исключение, рекомендуется помещать все такие функции очистки в деструктор.
Брайан
6
@Brian Тогда не бросай никаких исключений foo(). И если вы это сделаете, поймайте это. Задача решена. Исправлять ошибки, исправляя их, а не писать обходные пути.
Лундин
18
@Lundin: Deferкласс представляет собой небольшой фрагмент кода, который можно многократно использовать, который позволяет выполнять любую очистку конца блока безопасным способом. это более известно как охрана прицела . да, любое использование защиты области видимости может быть выражено другими, более ручными способами, точно так же, как любой forцикл может быть выражен как блок и whileцикл, который, в свою очередь, может быть выражен с помощью ifи goto, который может быть выражен на языке ассемблера, если вы хотите, или для тех, кто является настоящими мастерами, изменяя биты в памяти с помощью космических лучей, управляемых эффектом бабочки специальных коротких ворчаний и пений но зачем это делать
ура и hth. - Альф
34

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

void foo()
{
  // ...
  do {
      if (!executeStepA())
          break;
      if (!executeStepB())
          break;
      if (!executeStepC())
          break;
  }
  while (0);
  // ...
}
Debasis
источник
11
Использование функции с несколькими возвратами, на мой взгляд, похоже, но более читабельно.
Лундин
4
Да, определенно это более читабельно ... однако с точки зрения эффективности можно избежать дополнительных затрат на вызов функции (передача и возврат параметров) с помощью конструкции do {} while (0).
Debasis
5
Вы все еще можете сделать функцию inline. В любом случае, это хорошая техника, которую нужно знать, потому что она помогает не только в решении этой проблемы.
Руслан
2
@Lundin Вы должны принять во внимание локальность кода, распространение кода на слишком много мест имеет свои проблемы.
API-Beast,
3
По моему опыту, это очень необычная идиома. Мне понадобится некоторое время, чтобы понять, что, черт возьми, происходит, и это плохой знак, когда я рассматриваю код. Учитывая, что он, кажется, не имеет преимуществ перед другими, более распространенными и, следовательно, более читаемыми подходами, я не смог подписать его.
Коди Грей
19

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

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

источник
1
Речь шла о случайных вызовах функций внутри push_back (), но вы все равно это исправили :)
Квентин,
4
Я хотел бы знать, почему это получило отрицательный голос. Если предположить, что шаги выполнения действительно симметричны, как они кажутся, тогда они чистые и поддерживаемые.
ClickRick
1
Хотя, возможно, это выглядит чище. Это может быть труднее понять людям, и определенно сложнее понять компилятору!
Рой Т.
1
Лучше всего рассматривать ваши функции как данные - как только вы это сделаете, вы также заметите последующие рефакторинги. Еще лучше, когда каждая часть, которую вы обрабатываете, является ссылкой на объект, а не просто ссылкой на функцию - это даст вам еще больше возможностей для улучшения вашего кода.
Билл К
3
Немного перегружен для тривиального случая, но эта техника определенно имеет приятную особенность, которой другие не делают: вы можете изменить порядок выполнения и [количество] функций, вызываемых во время выполнения, и это хорошо :)
Xocoatzin
18

Будет ли это работать? Я думаю, что это эквивалентно вашему коду.

bool condition = true; // using only one boolean variable
if (condition) condition = executeStepA();
if (condition) condition = executeStepB();
if (condition) condition = executeStepC();
...
executeThisFunctionInAnyCase();
sampathsris
источник
3
Я обычно вызываю переменную, okкогда использую такую ​​же переменную, как эта.
Маке
1
Мне было бы очень интересно узнать, почему отрицательные голоса. Что здесь не так?
ABCplus
1
сравните ваш ответ с подходом короткого замыкания для цикломатической сложности.
AlphaGoku
14

Предполагая, что нужный код такой, какой я его сейчас вижу:

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

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

// Pre-declare the variables for the conditions
bool conditionA = false;
bool conditionB = false;
bool conditionC = false;

// Execute each step only if the pre-conditions are met
conditionA = executeStepA();
if (conditionA)
    conditionB = executeStepB();
if (conditionB)
    conditionC = executeStepC();
if (conditionC) {
    ...
}

// Unconditionally execute the 'cleanup' part.
executeThisFunctionInAnyCase();

Это исключает любую необходимость в gotos, исключениях, фиктивных whileциклах или других сложных конструкциях и просто выполняет простую работу под рукой.

ClickRick
источник
При использовании циклов обычно допустимо использовать returnи breakвыпрыгивать из цикла без необходимости вводить дополнительные «флаговые» переменные. В этом случае использование goto будет также безобидным - помните, что вы торгуете дополнительной сложностью goto для дополнительной изменчивой переменной сложности.
hugomg
2
@hugomg Переменные были в оригинальном вопросе. Здесь нет никакой дополнительной сложности. По этому вопросу были сделаны предположения (например, что переменные нужны в отредактированном коде), поэтому они были сохранены. Если они не нужны, тогда код может быть упрощен, но, учитывая неполный характер вопроса, нет другого предположения, которое можно было бы сделать обоснованно.
ClickRick
Очень полезный подход, особенно для использования самопровозглашенным newbie, это обеспечивает более чистое решение без недостатков. Я отмечаю, что это также не зависит от stepsналичия одной и той же сигнатуры или от того, что они являются функциями, а не блоками. Я вижу, что это используется в качестве первого рефакторинга даже в тех случаях, когда более сложный подход оправдан.
Кит
12

Может ли оператор break быть использован каким-либо образом?

Возможно, не лучшее решение, но вы можете поместить свои операторы в do .. while (0)цикл и использовать breakвместо них return.

ouah
источник
2
Это был не я, кто проголосовал против него, но это было бы злоупотреблением циклической конструкцией для чего-то, где эффект - это то, что требуется в настоящее время, но неизбежно приведет к боли. Вероятно, для следующего разработчика, который должен поддерживать его через 2 года после того, как вы перешли к другому проекту.
ClickRick
3
Использование @ClickRick do .. while (0)для определений макросов также неправильно использует циклы, но считается, что все в порядке.
ouah
1
Возможно, но есть более чистые способы достичь этого.
ClickRick
4
@ClickRick единственная цель моего ответа - ответить. Можно ли каким-то образом использовать оператор break, и ответ - да, первые слова в моем ответе предполагают, что это может быть не решение для использования.
ouah
2
Этот ответ должен быть просто комментарий
msmucker0527
12

Вы можете поместить все ifусловия, отформатированные так, как вы хотите, в их собственную функцию, при возврате выполнить executeThisFunctionInAnyCase()функцию.

Из базового примера в OP тестирование и выполнение условий можно разделить как таковые;

void InitialSteps()
{
  bool conditionA = executeStepA();
  if (!conditionA)
    return;
  bool conditionB = executeStepB();
  if (!conditionB)
    return;
  bool conditionC = executeStepC();
  if (!conditionC)
    return;
}

И затем называется таковым;

InitialSteps();
executeThisFunctionInAnyCase();

Если доступны лямбды C ++ 11 (в OP не было тега C ++ 11, но они все еще могут быть опцией), тогда мы можем отказаться от отдельной функции и обернуть ее в лямбду.

// Capture by reference (variable access may be required)
auto initialSteps = [&]() {
  // any additional code
  bool conditionA = executeStepA();
  if (!conditionA)
    return;
  // any additional code
  bool conditionB = executeStepB();
  if (!conditionB)
    return;
  // any additional code
  bool conditionC = executeStepC();
  if (!conditionC)
    return;
};

initialSteps();
executeThisFunctionInAnyCase();
Найл
источник
10

Если вам не нравятся gotoи не нравятся do { } while (0);циклы и вам нравится использовать C ++, вы также можете использовать временную лямбду, чтобы получить тот же эффект.

[&]() { // create a capture all lambda
  if (!executeStepA()) { return; }
  if (!executeStepB()) { return; }
  if (!executeStepC()) { return; }
}(); // and immediately call it

executeThisFunctionInAnyCase();
Alex
источник
1
if вам не нравится goto && вы не любите do {}, в то время как (0) && вам нравится C ++ ... Извините, не смог устоять, но последнее условие не выполняется, потому что вопрос помечен как c, так и c ++
ClickRick
@ClickRick всегда сложно угодить всем. На мой взгляд, нет такой вещи, как C / C ++, которую вы обычно кодируете в одном из этих вариантов, а использование другого недовольно.
Алекс
9

Цепочки IF / ELSE в вашем коде - это не проблема языка, а дизайн вашей программы. Если вы можете переформулировать или переписать свою программу, я бы посоветовал вам посмотреть в Design Patterns ( http://sourcemaking.com/design_patterns ), чтобы найти лучшее решение.

Обычно, когда вы видите много IF и других в вашем коде, это возможность реализовать шаблон проектирования стратегии ( http://sourcemaking.com/design_patterns/strategy/c-sharp-dot-net ) или, возможно, комбинацию других моделей.

Я уверен, что есть альтернативы, чтобы написать длинный список if / else, но я сомневаюсь, что они изменят что-либо, кроме того, что цепочка будет выглядеть симпатично для вас (однако, красота в глазах смотрящего по-прежнему относится к коду слишком:-) ) . Вас должны беспокоить такие вещи, как (через 6 месяцев, когда у меня появилось новое условие, и я ничего не помню об этом коде, смогу ли я добавить его легко? Или что если цепь изменится, как быстро и безошибочно буду ли я это реализовывать)

Роман Мик
источник
9

Вы просто делаете это ..

coverConditions();
executeThisFunctionInAnyCase();

function coverConditions()
 {
 bool conditionA = executeStepA();
 if (!conditionA) return;
 bool conditionB = executeStepB();
 if (!conditionB) return;
 bool conditionC = executeStepC();
 if (!conditionC) return;
 }

99 раз из 100, это единственный способ сделать это.

Никогда, никогда не пытайтесь сделать что-то «хитрое» в компьютерном коде.


Кстати, я почти уверен, что именно то, что вы задумали ...

Оператор continue важен в алгоритмическом программировании. (Так же, как и оператор goto имеет решающее значение в алгоритмическом программировании.)

На многих языках программирования вы можете сделать это:

-(void)_testKode
    {
    NSLog(@"code a");
    NSLog(@"code b");
    NSLog(@"code c\n");
    
    int x = 69;
    
    {
    
    if ( x == 13 )
        {
        NSLog(@"code d---\n");
        continue;
        }
    
    if ( x == 69 )
        {
        NSLog(@"code e---\n");
        continue;
        }
    
    if ( x == 13 )
        {
        NSLog(@"code f---\n");
        continue;
        }
    
    }
    
    NSLog(@"code g");
    }

(Прежде всего обратите внимание: голые блоки, подобные этому примеру, являются критической и важной частью написания красивого кода, особенно если вы имеете дело с «алгоритмическим» программированием.)

Опять же, это именно то, что у вас было в голове, верно? И это прекрасный способ написать это, чтобы у вас были хорошие инстинкты.

Однако, к сожалению, в текущей версии target-c (кроме - я не знаю о Swift, извините) есть уязвимая функция, которая проверяет, является ли заключающий блок циклом.

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

Вот как вы можете обойти это ...

-(void)_testKode
    {
    NSLog(@"code a");
    NSLog(@"code b");
    NSLog(@"code c\n");
    
    int x = 69;
    
    do{
    
    if ( x == 13 )
        {
        NSLog(@"code d---\n");
        continue;
        }
    
    if ( x == 69 )
        {
        NSLog(@"code e---\n");
        continue;
        }
    
    if ( x == 13 )
        {
        NSLog(@"code f---\n");
        continue;
        }
    
    }while(false);
    
    NSLog(@"code g");
    }

Так что не забывайте это ..

do {} while (false);

просто означает «сделать этот блок один раз».

то есть, нет абсолютно никакой разницы между письмом do{}while(false);и простым письмом {}.

Теперь это работает отлично, как вы хотели ... вот вывод ...

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

Так что, возможно, именно так вы видите алгоритм в своей голове. Вы всегда должны пытаться написать, что у вас в голове. (Особенно, если вы не трезвые, потому что именно тогда красавица выходит! :))

В «алгоритмических» проектах, где это часто случается, в target-c у нас всегда есть макрос вроде ...

#define RUNONCE while(false)

... так что вы можете сделать это ...

-(void)_testKode
    {
    NSLog(@"code a");
    int x = 69;
    
    do{
    if ( x == 13 )
        {
        NSLog(@"code d---\n");
        continue;
        }
    if ( x == 69 )
        {
        NSLog(@"code e---\n");
        continue;
        }
    if ( x == 13 )
        {
        NSLog(@"code f---\n");
        continue;
        }
    }RUNONCE
    
    NSLog(@"code g");
    }

Есть два момента:

а, даже если глупо, что target-c проверяет тип блока, в котором находится оператор continue, трудно "бороться с этим". Так что это сложное решение.

б, есть вопрос, стоит ли в этом примере делать отступ? Я теряю сон из-за таких вопросов, поэтому не могу советовать.

Надеюсь, поможет.

Fattie
источник
Вы пропустили второй лучший ответ .
Палек
Вы положили награду, чтобы получить больше представителей? :)
TMS
Вместо того, чтобы помещать все эти комментарии в if, вы также можете использовать более описательные имена функций и помещать комментарии в функции.
Томас Ахле
Тьфу. Я возьму 1-строчное решение, которое кратко читается с оценкой короткого замыкания (которая существует на языках более 20 лет и хорошо известна) над этим беспорядком в любой день. Я думаю, что мы оба можем согласиться, что мы рады, что не работаем друг с другом.
M2tM
8

Сделайте так, чтобы ваши исполняющие функции выдавали исключение, если они терпят неудачу, вместо возврата false. Тогда ваш код вызова может выглядеть так:

try {
    executeStepA();
    executeStepB();
    executeStepC();
}
catch (...)

Конечно, я предполагаю, что в вашем исходном примере шаг выполнения вернул бы false только в случае ошибки внутри шага?

Rik
источник
3
использование исключения для управления потоком часто считается плохой практикой и вонючим кодом
user902383
8

Много хороших ответов уже есть, но большинство из них, похоже, компромисс с некоторой (по общему признанию, очень мало) гибкостью. Обычный подход, который не требует этого компромисса, это добавление переменной status / keep-идя . Цена, конечно, одно дополнительное значение для отслеживания:

bool ok = true;
bool conditionA = executeStepA();
// ... possibly edit conditionA, or just ok &= executeStepA();
ok &= conditionA;

if (ok) {
    bool conditionB = executeStepB();
    // ... possibly do more stuff
    ok &= conditionB;
}
if (ok) {
    bool conditionC = executeStepC();
    ok &= conditionC;
}
if (ok && additionalCondition) {
    // ...
}

executeThisFunctionInAnyCase();
// can now also:
return ok;
blgt
источник
Почему ok &= conditionX;и не просто ok = conditionX;?
ABCplus
@ user3253359 Во многих случаях, да, вы можете сделать это. Это концептуальная демонстрация; в рабочем коде мы постараемся максимально упростить его
blgt
+1 Один из немногих чистых и понятных ответов, который работает в c , как указано в вопросе.
ClickRick
6

В C ++ (вопрос помечен как C, так и C ++), если вы не можете изменить функции для использования исключений, вы все равно можете использовать механизм исключений, если вы напишете небольшую вспомогательную функцию, такую ​​как

struct function_failed {};
void attempt(bool retval)
{
  if (!retval)
    throw function_failed(); // or a more specific exception class
}

Тогда ваш код может выглядеть следующим образом:

try
{
  attempt(executeStepA());
  attempt(executeStepB());
  attempt(executeStepC());
}
catch (function_failed)
{
  // -- this block intentionally left empty --
}

executeThisFunctionInAnyCase();

Если вам нравится необычный синтаксис, вы можете вместо этого заставить его работать через явное приведение:

struct function_failed {};
struct attempt
{
  attempt(bool retval)
  {
    if (!retval)
      throw function_failed();
  }
};

Тогда вы можете написать свой код как

try
{
  (attempt) executeStepA();
  (attempt) executeStepB();
  (attempt) executeStepC();
}
catch (function_failed)
{
  // -- this block intentionally left empty --
}

executeThisFunctionInAnyCase();
celtschk
источник
Преобразование проверок значений в исключения не обязательно является хорошим способом, поскольку существуют значительные накладные расходы на раскручивание исключений.
Джон Ву
4
-1 Использование исключений для нормального потока в C ++, как это - плохая практика программирования В C ++ исключения должны быть зарезервированы для исключительных обстоятельств.
Джек Эйдли
1
Из текста вопроса (выделено мной): «Функции executeStepX должны выполняться тогда и только тогда, когда предыдущий успешен. » Другими словами, возвращаемое значение используется для обозначения ошибки. То есть это обработка ошибок (и можно надеяться, что сбои являются исключительными). Обработка ошибок - это именно то , для чего были придуманы исключения.
celtschk
1
Нет. Во-первых, исключения были созданы для распространения ошибок , а не для обработки ошибок ; во-вторых, «функции executeStepX должны выполняться тогда и только тогда, когда предыдущий завершается успешно». не означает, что логическое false, возвращаемое предыдущей функцией, указывает на явно исключительный / ошибочный случай. Ваше утверждение, таким образом, не является следствием . Обработка ошибок и обеззараживание потока может быть реализована многими различными способами, исключения являются инструментом , позволяющим распространение ошибок и вне места обработки ошибок и преуспеть в этом.
6

Если ваш код так же прост, как ваш пример, и ваш язык поддерживает оценку короткого замыкания, вы можете попробовать это:

StepA() && StepB() && StepC() && StepD();
DoAlways();

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

Noctis Skytower
источник
Фактически я отредактировал свой вопрос, чтобы лучше объяснить тему, но он был отклонен, чтобы не аннулировать большинство ответов. : \
ABCplus
Я новый пользователь в SO и начинающий программист. 2 вопроса: есть ли риск, что другой вопрос, такой как тот, который вы сказали, будет помечен как дублированный из-за ЭТОГО вопроса? Другой вопрос: как мог пользователь / программист новичка SO выбрать лучший ответ среди всех (почти, наверное, хорошо)?
ABCplus
6

Для C ++ 11 и выше, хорошим подходом может быть реализация системы выхода из области действия, аналогичной механизму D (выхода) из области видимости .

Один из возможных способов его реализации - использование лямбда-кода C ++ 11 и некоторых вспомогательных макросов:

template<typename F> struct ScopeExit 
{
    ScopeExit(F f) : fn(f) { }
    ~ScopeExit() 
    { 
         fn();
    }

    F fn;
};

template<typename F> ScopeExit<F> MakeScopeExit(F f) { return ScopeExit<F>(f); };

#define STR_APPEND2_HELPER(x, y) x##y
#define STR_APPEND2(x, y) STR_APPEND2_HELPER(x, y)

#define SCOPE_EXIT(code)\
    auto STR_APPEND2(scope_exit_, __LINE__) = MakeScopeExit([&](){ code })

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

SCOPE_EXIT(
    delete pointerA;
    delete pointerB;
    close(fileC); );

if (!executeStepA())
    return;

if (!executeStepB())
    return;

if (!executeStepC())
    return;

Макросы действительно просто украшение. MakeScopeExit()можно использовать напрямую.

glampert
источник
Для этой работы нет необходимости в макросах. И [=]это обычно не так для лямбды с ограниченным диапазоном.
Якк - Адам Невраумонт
Да, макросы просто для украшения и могут быть выброшены. Но разве вы не сказали бы, что захват по стоимости - это самый безопасный «общий» подход?
Гламперт
1
нет: если ваша лямбда не сохранится за пределами текущей области, в которой создана лямбда, используйте [&]: она безопасна и минимально удивительна. Захват по значению только тогда, когда лямбда (или копии) может выжить дольше, чем прицел на момент объявления ...
Якк - Адам Невраумонт
Да, это имеет смысл. Я изменю это. Спасибо!
Гламперт
6

Почему никто не дает самое простое решение? : D

Если все ваши функции имеют одинаковую подпись, то вы можете сделать это следующим образом (для языка Си):

bool (*step[])() = {
    &executeStepA,
    &executeStepB,
    &executeStepC,
    ... 
};

for (int i = 0; i < numberOfSteps; i++) {
    bool condition = step[i]();

    if (!condition) {
        break;
    }
}

executeThisFunctionInAnyCase();

Для чистого решения C ++ вы должны создать интерфейсный класс, который содержит метод execute и упаковывает ваши шаги в объекты.
Тогда приведенное выше решение будет выглядеть так:

Step *steps[] = {
    stepA,
    stepB,
    stepC,
    ... 
};

for (int i = 0; i < numberOfSteps; i++) {
    Step *step = steps[i];

    if (!step->execute()) {
        break;
    }
}

executeThisFunctionInAnyCase();
Gaskoin
источник
5

Предполагая, что вам не нужны отдельные переменные условия, инвертирование тестов и использование else-falthrough в качестве пути «ok» позволит вам получить более вертикальный набор операторов if / else:

bool failed = false;

// keep going if we don't fail
if (failed = !executeStepA())      {}
else if (failed = !executeStepB()) {}
else if (failed = !executeStepC()) {}
else if (failed = !executeStepD()) {}

runThisFunctionInAnyCase();

Пропуск неудачной переменной делает код немного неясным IMO.

Объявление переменных внутри - это хорошо, не беспокойтесь о = vs ==.

// keep going if we don't fail
if (bool failA = !executeStepA())      {}
else if (bool failB = !executeStepB()) {}
else if (bool failC = !executeStepC()) {}
else if (bool failD = !executeStepD()) {}
else {
     // success !
}

runThisFunctionInAnyCase();

Это неясно, но компактно:

// keep going if we don't fail
if (!executeStepA())      {}
else if (!executeStepB()) {}
else if (!executeStepC()) {}
else if (!executeStepD()) {}
else { /* success */ }

runThisFunctionInAnyCase();
Маке
источник
5

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

В Java это будет выглядеть примерно так:

interface StepState{
public StepState performStep();
}

Реализация будет работать следующим образом:

class StepA implements StepState{ 
    public StepState performStep()
     {
         performAction();
         if(condition) return new StepB()
         else return null;
     }
}

И так далее. Тогда вы можете заменить большое условие if на:

Step toDo = new StepA();
while(toDo != null)
      toDo = toDo.performStep();
executeThisFunctionInAnyCase();
CarrKnight
источник
5

Как упоминал Роммик, вы можете применить шаблон проектирования для этого, но я бы использовал шаблон Декоратор, а не Стратегию, так как вы хотите связывать вызовы. Если код прост, то я бы пошел с одним из хорошо структурированных ответов, чтобы предотвратить вложение. Однако, если он сложный или требует динамического связывания, то шаблон Decorator является хорошим выбором. Вот диаграмма класса yUML :

диаграмма классов yUML

Вот пример программы LinqPad C #:

void Main()
{
    IOperation step = new StepC();
    step = new StepB(step);
    step = new StepA(step);
    step.Next();
}

public interface IOperation 
{
    bool Next();
}

public class StepA : IOperation
{
    private IOperation _chain;
    public StepA(IOperation chain=null)
    {
        _chain = chain;
    }

    public bool Next() 
    {
        bool localResult = false;
        //do work
        //...
        // set localResult to success of this work
        // just for this example, hard coding to true
        localResult = true;
        Console.WriteLine("Step A success={0}", localResult);

        //then call next in chain and return
        return (localResult && _chain != null) 
            ? _chain.Next() 
            : true;
    }
}

public class StepB : IOperation
{
    private IOperation _chain;
    public StepB(IOperation chain=null)
    {
        _chain = chain;
    }

    public bool Next() 
    {   
        bool localResult = false;

        //do work
        //...
        // set localResult to success of this work
        // just for this example, hard coding to false, 
            // to show breaking out of the chain
        localResult = false;
        Console.WriteLine("Step B success={0}", localResult);

        //then call next in chain and return
        return (localResult && _chain != null) 
            ? _chain.Next() 
            : true;
    }
}

public class StepC : IOperation
{
    private IOperation _chain;
    public StepC(IOperation chain=null)
    {
        _chain = chain;
    }

    public bool Next() 
    {
        bool localResult = false;
        //do work
        //...
        // set localResult to success of this work
        // just for this example, hard coding to true
        localResult = true;
        Console.WriteLine("Step C success={0}", localResult);
        //then call next in chain and return
        return (localResult && _chain != null) 
            ? _chain.Next() 
            : true;
    }
}

ИМХО, лучшая книга для чтения по шаблонам проектирования - « Head First Design Patterns» .

Что бы круто
источник
Какая польза от этого по сравнению с ответом Джеффри?
Дейсон
гораздо более устойчивы к изменениям, когда требования меняются, таким подходом проще управлять без особых знаний предметной области. Особенно, если учесть, насколько глубокими и длинными могут быть некоторые разделы вложенных if. Все это может стать очень хрупким и, следовательно, с большим риском работать. Не поймите меня неправильно, некоторые сценарии оптимизации могут привести к тому, что вы перестанете это делать и вернетесь к ifs, но в 99% случаев это нормально. Но дело в том, что когда вы переходите на этот уровень, вас не волнует ремонтопригодность, вам нужна производительность.
Джон Николас
4

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

Общий шаблон должен был использовать do { } while (false);

Я использовал макрос для того, while(false)чтобы сделать это do { } once;. Общий шаблон был:

do
{
    bool conditionA = executeStepA();
    if (! conditionA) break;
    bool conditionB = executeStepB();
    if (! conditionB) break;
    // etc.
} while (false);

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

Билл Дверь
источник
4

Чтобы улучшить ответ Матье на C ++ 11 и избежать затрат времени выполнения, понесенных при использовании std::function, я бы предложил использовать следующее

template<typename functor>
class deferred final
{
public:
    template<typename functor2>
    explicit deferred(functor2&& f) : f(std::forward<functor2>(f)) {}
    ~deferred() { this->f(); }

private:
    functor f;
};

template<typename functor>
auto defer(functor&& f) -> deferred<typename std::decay<functor>::type>
{
    return deferred<typename std::decay<functor>::type>(std::forward<functor>(f));
}

Этот простой шаблонный класс будет принимать любой функтор, который может быть вызван без каких-либо параметров, и делает это без каких-либо динамических выделений памяти и, следовательно, лучше соответствует цели абстракции C ++ без ненужных накладных расходов. Дополнительный шаблон функции предназначен для упрощения использования путем вывода параметров шаблона (который недоступен для параметров шаблона класса).

Пример использования:

auto guard = defer(executeThisFunctionInAnyCase);
bool conditionA = executeStepA();
if (!conditionA) return;
bool conditionB = executeStepB();
if (!conditionB) return;
bool conditionC = executeStepC();
if (!conditionC) return;

Так же, как и ответ Матье, это решение полностью безопасно и executeThisFunctionInAnyCaseбудет вызываться во всех случаях. Если executeThisFunctionInAnyCaseсам по себе выбрасывать, деструкторы помечаются неявно, noexceptи поэтому будет вызван вызов std::terminateвместо того, чтобы вызывать исключение во время разматывания стека.

Джо
источник
+1 Я искал этот ответ, чтобы мне не пришлось его публиковать. Вы должны совершить перемотку вперед functorв deferredконструкторе d, не нужно форсировать a move.
Якк - Адам Невраумонт
@Yakk изменил конструктор на конструктор пересылки
Джо
3

Кажется, что вы хотите сделать все ваши звонки из одного блока. Как и предлагали другие, вы должны использовать либо whileцикл, и уходить с помощью, breakили новую функцию, с которой вы можете выйти return(может быть, чище).

Я лично прогоняю gotoдаже за функцией выхода. Их сложнее обнаружить при отладке.

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

const int STEP_ARRAY_COUNT = 3;
bool (*stepsArray[])() = {
   executeStepA, executeStepB, executeStepC
};

for (int i=0; i<STEP_ARRAY_COUNT; ++i) {
    if (!stepsArray[i]()) {
        break;
    }
}

executeThisFunctionInAnyCase();
AxFab
источник
К счастью, отладчик замечает их для вас. Если вы отлаживаете код, а не пошагово просматриваете код, вы делаете это неправильно.
Коди Грей
Я не понимаю, что вы имеете в виду, и почему я не могу использовать одноступенчатый?
AxFab
3

Поскольку у вас также есть [... блок кода ...] между выполнениями, я думаю, у вас есть выделение памяти или инициализация объектов. Таким образом, вы должны заботиться об очистке всего, что вы уже инициализировали при выходе, а также очистить его, если вы столкнетесь с проблемой, и любая из функций вернет false.

В этом случае лучшее, что у меня было в моем опыте (когда я работал с CryptoAPI), было создание небольших классов, в конструкторе вы инициализировали свои данные, в деструкторе вы неинициализировали их. Каждый следующий класс функции должен быть дочерним по отношению к предыдущему классу функции. Если что-то пошло не так - киньте исключение.

class CondA
{
public:
    CondA() { 
        if (!executeStepA()) 
            throw int(1);
        [Initialize data]
    }
    ~CondA() {        
        [Clean data]
    }
    A* _a;
};

class CondB : public CondA
{
public:
    CondB() { 
        if (!executeStepB()) 
            throw int(2);
        [Initialize data]
    }
    ~CondB() {        
        [Clean data]
    }
    B* _b;
};

class CondC : public CondB
{
public:
    CondC() { 
        if (!executeStepC()) 
            throw int(3);
        [Initialize data]
    }
    ~CondC() {        
        [Clean data]
    }
    C* _c;
};

И тогда в вашем коде вам просто нужно позвонить:

shared_ptr<CondC> C(nullptr);
try{
    C = make_shared<CondC>();
}
catch(int& e)
{
    //do something
}
if (C != nullptr)
{
   C->a;//work with
   C->b;//work with
   C->c;//work with
}
executeThisFunctionInAnyCase();

Я думаю, это лучшее решение, если каждый вызов ConditionX что-то инициализирует, выделяет память и т. Д. Лучше быть уверенным, что все будет очищено.

Аркадий
источник
3

интересный способ - работать с исключениями.

try
{
    executeStepA();//function throws an exception on error
    ......
}
catch(...)
{
    //some error handling
}
finally
{
    executeThisFunctionInAnyCase();
}

Если вы пишете такой код, вы идете как-то не в том направлении. Я не считаю «проблемой» иметь такой код, но иметь такую ​​грязную «архитектуру».

Совет: обсудите эти случаи с опытным разработчиком, которому вы доверяете ;-)

Карстен
источник
Я думаю, что эта идея не может заменить каждую цепочку if. Во всяком случае, во многих случаях это очень хороший подход!
Во
3

Другой подход - do - whileцикл, хотя он был упомянут ранее, и не было ни одного примера, который бы показывал, как он выглядит:

do
{
    if (!executeStepA()) break;
    if (!executeStepB()) break;
    if (!executeStepC()) break;
    ...

    break; // skip the do-while condition :)
}
while (0);

executeThisFunctionInAnyCase();

(Ну, уже есть ответ с whileциклом, но do - whileцикл не избыточно проверяет истину (в начале), а вместо этого в конце xD (хотя это можно пропустить)).

Zaffy
источник
Привет, Заффи - в этом ответе подробно объясняется подход do {} while (false). stackoverflow.com/a/24588605/294884 Два других ответа также упоминали об этом.
Толстяк
Это увлекательный вопрос, будет ли элегантнее использовать CONTINUE или BREAK в этой ситуации!
Толстяк
Привет @JoeBlow Я видел все ответы ... просто хотел показать вместо того, чтобы говорить об этом :)
Zaffy
Мой первый ответ здесь: «Никто не упомянул об ЭТОМ ...», и сразу же кто-то любезно отметил, что это был второй топ-ответ :)
Толстяк
@JoeBlow Э, ты прав. Я постараюсь это исправить. Я чувствую, что ... xD В любом случае, спасибо, в следующий раз я уделю немного больше
внимания