假设我有这个伪代码:
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
语句吗?
false
的函数视为类似于异常情况吗?
false
返回可能很正常。
您可以使用 &&
(逻辑与):
if (executeStepA() && executeStepB() && executeStepC()){
...
}
executeThisFunctionInAnyCase();
这将满足您的两个要求:
executeStep
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(您的第一个变体)或打破“函数的一部分”的愿望通常意味着您确实需要一个额外的函数。
foo
在一系列相关条件和操作中发挥作用。函数 bar
与决策完全分离。如果我们查看条件和操作的详细信息,可能会发现 foo
仍然做得太多,但目前这是一个很好的解决方案。
bar
的变量,您将不得不手动将它们作为参数传递给 foo
。如果是这种情况,并且如果 foo
只被调用一次,我会错误地使用 goto 版本以避免定义两个紧密耦合的函数,这些函数最终不会非常可重用。
if (!executeStepA() || !executeStepB() || !executeStepC()) return
在这种情况下,老派 C 程序员使用 goto
。 Linux 样式指南实际上鼓励使用 goto
的一种用法,它被称为集中式函数出口:
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
的问题,但实际上这两种方法都做同样的事情。如果只有在 executeStepA()
成功时才需要进行其他清理,goto
方法会更好:
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;
}
在这种情况下,使用循环方法最终会得到两个级别的循环。
goto
并立即认为“这是糟糕的代码”,但它确实有其有效用途。
goto
,我什至不用想就知道这是个糟糕的代码。我曾经不得不保持这样,这非常讨厌。 OP 在问题的末尾提出了一个合理的 C 语言替代方案,我将其包含在我的答案中。
throw
在很多方面都比 goto
更糟糕,因为使用 throw
甚至不清楚从本地上下文您将在哪里结束!对 goto 样式的控制流使用与异常相同的设计注意事项。
这是一种常见的情况,有很多常用的方法来处理它。这是我对规范答案的尝试。如果我遗漏了什么,请发表评论,我会及时更新这篇文章。
这是一个箭头
您所讨论的内容称为 arrow anti-pattern。之所以称为箭头,是因为嵌套的 ifs 链形成了代码块,这些代码块向右扩展,然后再向左扩展,形成一个“指向”代码编辑器窗格右侧的可视箭头。
用卫兵压平箭
讨论了一些避免箭头的常用方法here。最常见的方法是使用 guard 模式,其中代码首先处理异常流,然后处理基本流,例如,而不是
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;
}
这在客观上和量化上更容易阅读,因为
给定逻辑块的 { 和 } 字符更接近 理解特定行所需的心理上下文量更少 与 if 条件相关的整个逻辑更有可能在一页上 编码器需要滚动页面/眼动轨迹大大减少
如何在最后添加通用代码
守卫模式的问题在于它依赖于所谓的“机会回归”或“机会退出”。换句话说,它打破了每个函数都应该只有一个退出点的模式。这是一个问题,原因有两个:
它以错误的方式惹恼了一些人,例如,在 Pascal 上学习编码的人已经了解到一个功能 = 一个退出点。无论如何,它都没有提供在退出时执行的一段代码,这是手头的主题。
下面我提供了一些解决此限制的选项,方法是使用语言功能或完全避免该问题。
选项 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. 避免问题:重组你的功能
您可以通过将代码分成两个函数来避免该问题。此解决方案的优点是适用于任何语言,此外它还可以减少 cyclomatic complexity,这是一种降低缺陷率的行之有效的方法,并提高了任何自动化单元测试的特异性。
这是一个例子:
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. 语言技巧:使用短路评估
另一种技术是利用 short-circuit evaluation。
if (DoSomething1() && DoSomething2() && DoSomething3())
{
DoSomething4();
}
DoSomethingNoMatterWhat();
该解决方案利用了 && 运算符的工作方式。当 && 的左侧评估为 false 时,永远不会评估右侧。
当需要紧凑的代码并且代码不太可能进行太多维护时,这个技巧最有用,例如,您正在实现一个众所周知的算法。对于更一般的编码,此代码的结构太脆弱;即使是对逻辑的微小更改也可能触发完全重写。
做就是了
if( executeStepA() && executeStepB() && executeStepC() )
{
// ...
}
executeThisFunctionInAnyCase();
就是这么简单。
由于三个编辑都从根本上改变了问题(如果有四个将修订计算回版本#1),我将包含我正在回答的代码示例:
bool conditionA = executeStepA();
if (conditionA){
bool conditionB = executeStepB();
if (conditionB){
bool conditionC = executeStepC();
if (conditionC){
...
}
}
}
executeThisFunctionInAnyCase();
&&
列表更清晰。我通常将条件分解为单独的行,并在每行后面添加一个 // explanation
.... 最后,它的代码要少得多,一旦您了解 &&
的工作原理,就不需要持续的脑力劳动。我的印象是,大多数专业的 C++ 程序员都会熟悉这一点,但正如你所说,在不同的行业/项目中,重点和经验是不同的。
实际上有一种方法可以在 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
foo();
抛出异常,executeThisFunctionInAnyCase();
也会执行。在编写异常安全代码时,最好将所有此类清理函数放在析构函数中。
foo()
中抛出任何异常。如果你这样做了,抓住它。问题解决了。通过修复它们来修复错误,而不是通过编写解决方法。
Defer
类是一个可重用 小段代码,可让您以异常安全的方式进行任何块结束清理。它通常被称为 范围保护。是的,范围保护的任何使用都可以用其他更手动的方式表示,就像任何 for
循环都可以表示为一个块和一个 while
循环,而后者又可以用 if
和 goto
表示,如果你愿意,可以用汇编语言表达,或者对于那些真正的大师,通过特殊短咕噜声和圣歌的蝴蝶效应引导的宇宙射线改变记忆中的位。但是,为什么要这样做。
有一种很好的技术,它不需要带有返回语句的附加包装函数(Itjax 规定的方法)。它使用 do while(0)
伪循环。 while (0)
确保它实际上不是一个循环,而是只执行一次。但是,循环语法允许使用 break 语句。
void foo()
{
// ...
do {
if (!executeStepA())
break;
if (!executeStepB())
break;
if (!executeStepC())
break;
}
while (0);
// ...
}
inline
。无论如何,这是一个很好的技术,因为它不仅能解决这个问题。
你也可以这样做:
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)不是一个大粉丝,因为你失去了能见度 IMO:
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();
这行得通吗?我认为这与您的代码等效。
bool condition = true; // using only one boolean variable
if (condition) condition = executeStepA();
if (condition) condition = executeStepB();
if (condition) condition = executeStepC();
...
executeThisFunctionInAnyCase();
ok
。
假设所需的代码是我目前看到的:
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();
这避免了任何对 goto
、异常、虚拟 while
循环或其他困难构造的需求,并且只需继续手头的简单工作即可。
return
和 break
跳出循环,而无需引入额外的“标志”变量。在这种情况下,使用 goto 将同样无害 - 请记住,您正在用额外的 goto 复杂性换取额外的可变变量复杂性。
newbie
的人使用,它提供了一种更清洁的解决方案,没有任何缺点。我注意到它也不依赖于 steps
具有相同的签名甚至是函数而不是块。即使在更复杂的方法有效的情况下,我也可以看到这被用作第一遍重构。
可以以某种方式使用break语句吗?
也许不是最好的解决方案,但您可以将语句放在 do .. while (0)
循环中并使用 break
语句而不是 return
。
do .. while (0)
进行宏定义也是滥用循环,但它被认为是可以的。
您可以将所有 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 lambda 可用(OP 中没有 C++11 标记,但它们可能仍然是一个选项),那么我们可以放弃单独的函数并将其包装成 lambda。
// 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();
如果您不喜欢 goto
并且不喜欢 do { } while (0);
循环并且喜欢使用 C++,您也可以使用临时 lambda 来获得相同的效果。
[&]() { // create a capture all lambda
if (!executeStepA()) { return; }
if (!executeStepB()) { return; }
if (!executeStepC()) { return; }
}(); // and immediately call it
executeThisFunctionInAnyCase();
代码中的 IF/ELSE 链不是语言问题,而是程序设计问题。如果您能够重构或重新编写程序,我建议您查看设计模式 (http://sourcemaking.com/design_patterns) 以找到更好的解决方案。
通常,当你看到很多 IF's & else 在您的代码中,这是实现策略设计模式 (http://sourcemaking.com/design_patterns/strategy/c-sharp-dot-net) 或其他模式组合的机会。
我敢肯定有其他方法可以编写一个长长的 if/else 列表,但我怀疑它们会改变任何东西,除了链对你来说看起来很漂亮(但是,情人眼中的美仍然适用于代码也:-) ) 。您应该关心的事情是(在 6 个月内,当我有一个新情况并且我不记得有关此代码的任何内容时,我是否能够轻松添加它?或者如果链发生变化,如何快速和无错误我会实施吗)
你就这样做..
coverConditions();
executeThisFunctionInAnyCase();
function coverConditions()
{
bool conditionA = executeStepA();
if (!conditionA) return;
bool conditionB = executeStepB();
if (!conditionB) return;
bool conditionC = executeStepC();
if (!conditionC) return;
}
100的99次,这是唯一的方法。
永远,永远,永远尝试在计算机代码中做一些“棘手”的事情。
顺便说一句,我很确定以下是您想到的实际解决方案...
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");
}
(首先请注意:像该示例这样的裸块是编写漂亮代码的关键和重要部分,尤其是在您处理“算法”编程时。)
再说一次,这正是你的想法,对吧?这就是写它的美妙方式,所以你有很好的直觉。
然而,可悲的是,在当前版本的 objective-c 中(除此之外 - 我不了解 Swift,抱歉)有一个可笑的功能,它检查封闭块是否是一个循环。
https://i.stack.imgur.com/glq81.png
这就是你如何解决这个问题......
-(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);
和简单地写 {}
之间完全没有区别。
现在可以按照您的要求完美运行...这是输出...
https://i.stack.imgur.com/trGXZ.png
因此,这可能就是您在脑海中看到算法的方式。您应该始终尝试写下您的想法。 (特别是如果你不清醒,因为那是漂亮出来的时候!:))
在经常发生这种情况的“算法”项目中,在objective-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");
}
有两点:
a,即使objective-c检查continue语句所在的块类型很愚蠢,但“与之抗争”还是很麻烦。所以这是一个艰难的决定。
b,在这个例子中,你应该缩进那个块吗?我会因为这样的问题而失眠,所以我无法提供建议。
希望能帮助到你。
if
中,您还可以使用更具描述性的函数名称并将注释放在函数中。
让您的执行函数在失败时抛出异常,而不是返回 false。然后您的调用代码可能如下所示:
try {
executeStepA();
executeStepB();
executeStepC();
}
catch (...)
当然,我假设在您的原始示例中,执行步骤只会在步骤内部发生错误的情况下返回 false?
已经有很多很好的答案,但它们中的大多数似乎都牺牲了一些(诚然很少)灵活性。不需要这种权衡的一种常见方法是添加状态/持续变量。当然,价格是要跟踪的一个额外价值:
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;
ok &= conditionX;
而不仅仅是 ok = conditionX;
?
在 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();
如果您的代码与您的示例一样简单,并且您的语言支持短路评估,您可以试试这个:
StepA() && StepB() && StepC() && StepD();
DoAlways();
如果您将参数传递给您的函数并返回其他结果,以便您的代码无法以以前的方式编写,那么许多其他答案将更适合该问题。
对于 C++11 及更高版本,一个不错的方法可能是实现类似于 D's scope(exit) 机制的 范围退出 系统。
实现它的一种可能方法是使用 C++11 lambda 和一些辅助宏:
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()
可以直接使用。
[=]
通常是错误的。
[&]
:它是安全的,并且不会令人惊讶。仅当 lambda(或副本)的生存时间超过声明点的范围时才按值捕获...
为什么没有人给出最简单的解决方案? :D
如果您的所有函数都具有相同的签名,那么您可以这样做(对于 C 语言):
bool (*step[])() = {
&executeStepA,
&executeStepB,
&executeStepC,
...
};
for (int i = 0; i < numberOfSteps; i++) {
bool condition = step[i]();
if (!condition) {
break;
}
}
executeThisFunctionInAnyCase();
对于一个干净的 C++ 解决方案,您应该创建一个接口类,其中包含一个执行方法并将您的步骤包装在对象中。然后,上面的解决方案将如下所示:
Step *steps[] = {
stepA,
stepB,
stepC,
...
};
for (int i = 0; i < numberOfSteps; i++) {
Step *step = steps[i];
if (!step->execute()) {
break;
}
}
executeThisFunctionInAnyCase();
假设您不需要单独的条件变量,反转测试并使用 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();
这看起来像一个状态机,很方便,因为您可以使用 state-pattern 轻松实现它。
在 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();
正如 Rommik 提到的,您可以为此应用设计模式,但我会使用装饰器模式而不是策略,因为您想要链接调用。如果代码很简单,那么我会使用结构良好的答案之一来防止嵌套。但是,如果它很复杂或需要动态链接,那么装饰器模式是一个不错的选择。这是一个 yUML class diagram:
https://i.stack.imgur.com/lkall.png
这是一个示例 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。
几个答案暗示了我多次看到和使用过的模式,尤其是在网络编程中。在网络堆栈中,通常有很长的请求序列,其中任何一个都可能失败并停止进程。
常见的模式是使用 do { } while (false);
我为 while(false)
使用了一个宏来使其成为 do { } once;
常见的模式是:
do
{
bool conditionA = executeStepA();
if (! conditionA) break;
bool conditionB = executeStepB();
if (! conditionB) break;
// etc.
} while (false);
这种模式相对容易阅读,并且允许使用可以正确破坏的对象,并且还避免了多次返回,从而使步进和调试更容易一些。
为了改进 Mathieu 的 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;
正如 Mathieu 的回答一样,这个解决方案是完全异常安全的,并且在所有情况下都会调用 executeThisFunctionInAnyCase
。如果 executeThisFunctionInAnyCase
本身抛出,析构函数被隐式标记为 noexcept
,因此将发出对 std::terminate
的调用,而不是在堆栈展开期间引发异常。
deferred
的构造函数中完美转发 functor
,无需强制使用 move
。
似乎您想从一个块中完成所有呼叫。正如其他人所建议的那样,您应该使用 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();
因为您在执行之间也有[...代码块...],我猜您有内存分配或对象初始化。这样,您必须关心在退出时清理所有已初始化的内容,如果遇到问题并且任何函数都将返回 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 都初始化一些东西,分配内存等等。最好确保一切都会被清理。
这是我在 C-whatever 和 Java 中多次使用的技巧:
do {
if (!condition1) break;
doSomething();
if (!condition2) break;
doSomethingElse()
if (!condition3) break;
doSomethingAgain();
if (!condition4) break;
doYetAnotherThing();
} while(FALSE); // Or until(TRUE) or whatever your language likes
为了清晰起见,我更喜欢它而不是嵌套 ifs,尤其是在正确格式化并为每个条件提供清晰注释的情况下。
finally
块来解决这个问题。
一个有趣的方法是处理异常。
try
{
executeStepA();//function throws an exception on error
......
}
catch(...)
{
//some error handling
}
finally
{
executeThisFunctionInAnyCase();
}
如果你编写这样的代码,你就会以某种方式走错方向。我不会认为拥有这样的代码是“问题”,而是拥有如此凌乱的“架构”。
提示:与您信任的经验丰富的开发人员讨论这些案例 ;-)
不定期副业成功案例分享
&&
和||
的组合),因此在不影响可读性的情况下,您不可能将它们加入单个if
语句。将这些条件转移到外部函数并不总是那么容易,因为它们可能依赖于许多先前计算的局部变量,如果您尝试将每个变量作为单独的参数传递,这将造成可怕的混乱。