Предполагая, что у меня есть этот псевдокод:
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
заявление будет использовано каким-то образом?
Вы можете использовать &&
(логика И):
if (executeStepA() && executeStepB() && executeStepC()){
...
}
executeThisFunctionInAnyCase();
это удовлетворит оба ваших требования:
executeStep<X>()
следует оценивать, только если предыдущий преуспел (это называется оценка короткого замыкания)executeThisFunctionInAnyCase()
будет выполнен в любом случаеПросто используйте дополнительную функцию, чтобы ваша вторая версия работала:
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 (ваш первый вариант) или желание выйти из «части функции» обычно означает, что вам нужна дополнительная функция.
Программисты старой школы 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;
}
С циклическим подходом в этом случае вы получите два уровня циклов.
Это обычная ситуация, и есть много способов справиться с ней. Вот моя попытка канонического ответа. Пожалуйста, прокомментируйте, если я что-то пропустил, и я буду держать этот пост в актуальном состоянии.
То, что вы обсуждаете, называется стрелка анти-шаблон. Это называется стрелкой, потому что цепочка вложенных 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;
}
Это объективно и количественно легче читать, потому что
Проблема с паттерном охраны заключается в том, что он опирается на то, что называется «оппортунистическим возвращением» или «оппортунистическим выходом». Другими словами, это нарушает схему, согласно которой каждая функция должна иметь ровно одну точку выхода. Это проблема по двум причинам:
Ниже я предоставил некоторые варианты для обхода этого ограничения, либо используя языковые функции, либо избегая проблемы в целом.
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();
}
Вы можете избежать этой проблемы, разбив код на две функции. Преимущество этого решения — работать на любом языке, и, кроме того, оно может уменьшить цикломатическая сложность, Это проверенный способ снизить уровень дефектов и повысить специфичность любых автоматических модульных тестов.
Вот пример:
void OuterFunction()
{
DoSomethingIfPossible();
DoSomethingNoMatterWhat();
}
void DoSomethingIfPossible()
{
if (!ok)
{
_log.Error("Oops");
return;
}
DoSomething();
}
Другой распространенный трюк, который я вижу, — это использование while (true) и break, как показано в других ответах.
while(true)
{
if (!ok) break;
DoSomething();
break; //important
}
DoSomethingNoMatterWhat();
Хотя это менее «честно», чем использование goto
он менее подвержен ошибкам при рефакторинге, так как он четко обозначает границы логической области видимости. Наивный кодер, который вырезает и вставляет ваши этикетки или ваши goto
заявления могут вызвать серьезные проблемы! (И, откровенно говоря, картина настолько распространена, что теперь я думаю, что она ясно сообщает о намерениях и поэтому вовсе не является «нечестной»).
Есть и другие варианты этой опции. Например, можно использовать switch
вместо while
, Любая языковая конструкция с break
Ключевое слово, вероятно, будет работать.
Еще один подход использует жизненный цикл объекта. Используйте объект контекста для переноса ваших параметров (то, чего подозрительно не хватает нашему наивному примеру) и избавьтесь от него, когда вы закончите.
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
вместо деструктора.
Если вы собираетесь использовать объектно-ориентированный подход, можете сделать это правильно. Эта опция использует класс, чтобы «обернуть» ресурсы, которые требуют очистки, а также другие его операции.
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
Опять же, убедитесь, что вы понимаете жизненный цикл вашего объекта.
Другая техника заключается в том, чтобы воспользоваться оценка короткого замыкания.
if (DoSomething1() && DoSomething2() && DoSomething3())
{
DoSomething4();
}
DoSomethingNoMatterWhat();
Это решение использует преимущества способа && оператор работает. Когда левая сторона && оценивается как ложное, правая часть никогда не оценивается.
Этот трюк наиболее полезен, когда требуется компактный код, и когда код вряд ли нуждается в большом обслуживании, например, вы реализуете хорошо известный алгоритм. Для более общего кодирования структура этого кода слишком хрупкая; даже незначительное изменение в логике может вызвать полное переписывание.
Просто делать
if( executeStepA() && executeStepB() && executeStepC() )
{
// ...
}
executeThisFunctionInAnyCase();
Это так просто.
Из-за трех правок, которые у каждого есть в корне изменил вопрос (четыре, если считать версию до версии 1), я включил пример кода, на который я отвечаю:
bool conditionA = executeStepA();
if (conditionA){
bool conditionB = executeStepB();
if (conditionB){
bool conditionC = executeStepC();
if (conditionC){
...
}
}
}
executeThisFunctionInAnyCase();
На самом деле в 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
Есть хороший метод, который не нуждается в дополнительной функции-обертке с операторами return (метод, предписанный Itjax). Это делает делать while(0)
псевдо-петля. while (0)
гарантирует, что это на самом деле не цикл, а выполняется только один раз. Тем не менее, синтаксис цикла позволяет использовать оператор break.
void foo()
{
// ...
do {
if (!executeStepA())
break;
if (!executeStepB())
break;
if (!executeStepC())
break;
}
while (0);
// ...
}
Вы также можете сделать это:
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();