[英]Is there ever a need for a "do {...} while ( )" loop?
Bjarne Stroustrup(C++ 的创造者)曾经说过他避免使用“do/while”循环,而是更喜欢用“while”循环来编写代码。 [请参阅下面的报价。]
自从听到这个,我发现这是真的。 你怎么看? 有没有一个例子,其中“do/while”比使用“while”更清晰、更容易理解?
回应一些答案:是的,我理解“do/while”和“while”之间的技术区别。 这是一个关于可读性和结构化涉及循环的代码的更深层次的问题。
让我问另一种方式:假设你被禁止使用“do/while”——是否有一个现实的例子,这会让你别无选择,只能使用“while”编写不干净的代码?
来自“C++ 编程语言”,6.3.3:
根据我的经验,do-statement 是错误和混乱的根源。 原因是它的主体总是在评估条件之前执行一次。 然而,为了让身体正常工作,即使第一次通过,也必须保持非常类似的情况。 比我预想的更多的是,我发现无论是在程序第一次编写和测试时,还是在其前面的代码被修改之后,这种情况都没有按预期保持。 我也更喜欢“在我可以看到的地方”的条件。 因此,我倾向于避免使用 do 语句。 -Bjarne
避免 do/while 循环是包含在C++ 核心指南中的建议,如ES.75,避免 do-statements 。
是的,我同意 do while 循环可以重写为 while 循环,但是我不同意总是使用 while 循环更好。 do while 总是至少运行一次,这是一个非常有用的属性(最典型的例子是输入检查(从键盘))
#include <stdio.h>
int main() {
char c;
do {
printf("enter a number");
scanf("%c", &c);
} while (c < '0' || c > '9');
}
这当然可以重写为 while 循环,但这通常被视为更优雅的解决方案。
do-while 是一个带有后置条件的循环。 在循环体至少要执行一次的情况下,您需要它。 这对于在合理评估循环条件之前需要一些操作的代码来说是必要的。 使用 while 循环,您必须从两个站点调用初始化代码,而使用 do-while 则只能从一个站点调用它。
另一个例子是当你在第一次迭代开始时已经有一个有效的对象,所以你不想在第一次迭代开始之前执行任何东西(包括循环条件评估)。 一个例子是 FindFirstFile/FindNextFile Win32 函数:您调用 FindFirstFile,它返回一个错误或第一个文件的搜索句柄,然后调用 FindNextFile 直到它返回一个错误。
伪代码:
Handle handle;
Params params;
if( ( handle = FindFirstFile( params ) ) != Error ) {
do {
process( params ); //process found file
} while( ( handle = FindNextFile( params ) ) != Error ) );
}
do { ... } while (0)
是使宏表现良好的重要结构。
即使它在实际代码中并不重要(我不一定同意),但对于修复预处理器的一些缺陷很重要。
编辑:我遇到了今天在我自己的代码中 do/while 更干净的情况。 我正在对成对的LL/SC指令进行跨平台抽象。 这些需要在循环中使用,如下所示:
do
{
oldvalue = LL (address);
newvalue = oldvalue + 1;
} while (!SC (address, newvalue, oldvalue));
(专家可能会意识到在 SC 实现中没有使用 oldvalue,但它被包含在内,以便可以用 CAS 模拟这种抽象。)
LL 和 SC 是一个很好的例子,说明 do/while 比等效的 while 形式要干净得多:
oldvalue = LL (address);
newvalue = oldvalue + 1;
while (!SC (address, newvalue, oldvalue))
{
oldvalue = LL (address);
newvalue = oldvalue + 1;
}
出于这个原因,我对 Google Go选择删除 do-while 结构这一事实感到非常失望。
当您想“做”某事“直到”满足条件时,它很有用。
它可以像这样在一个 while 循环中被篡改:
while(true)
{
// .... code .....
if(condition_satisfied)
break;
}
以下常用习语对我来说似乎很简单:
do {
preliminary_work();
value = get_value();
} while (not_valid(value));
避免do
的重写似乎是:
value = make_invalid_value();
while (not_valid(value)) {
preliminary_work();
value = get_value();
}
第一行用于确保测试在第一次总是评估为真。 换句话说,第一次测试总是多余的。 如果没有这个多余的测试,也可以省略初始分配。 这段代码给人的印象是它在自我战斗。
在这种情况下, do
构造是一个非常有用的选项。
(假设您知道两者之间的区别)
Do/While 有利于在检查条件和运行 while 循环之前引导/预初始化代码。
在我们的编码约定中
所以我们几乎从来没有do {} while(xx)
因为:
int main() {
char c;
do {
printf("enter a number");
scanf("%c", &c);
} while (c < '0' || c > '9');
}
改写为:
int main() {
char c(0);
while (c < '0' || c > '9'); {
printf("enter a number");
scanf("%c", &c);
}
}
和
Handle handle;
Params params;
if( ( handle = FindFirstFile( params ) ) != Error ) {
do {
process( params ); //process found file
} while( ( handle = FindNextFile( params ) ) != Error ) );
}
改写为:
Params params(xxx);
Handle handle = FindFirstFile( params );
while( handle!=Error ) {
process( params ); //process found file
handle = FindNextFile( params );
}
这是我见过的最干净的 do-while 替代方法。 这是推荐用于没有 do-while 循环的 Python 的习惯用法。
一个警告是,您不能在<setup code>
使用continue
,因为它会跳过中断条件,但是显示 do-while 好处的示例中没有一个需要在条件之前执行 continue。
while (true) {
<setup code>
if (!condition) break;
<loop body>
}
在这里,它应用于上述 do-while 循环的一些最佳示例。
while (true) {
printf("enter a number");
scanf("%c", &c);
if (!(c < '0' || c > '9')) break;
}
下一个示例是结构比 do-while 更具可读性的情况,因为条件保持在顶部附近,因为//get data
通常很短,而//process data
部分可能很长。
while (true) {
// get data
if (data == null) break;
// process data
// process it some more
// have a lot of cases etc.
// wow, we're almost done.
// oops, just one more thing.
}
这都是关于可读性的。
更易读的代码可以减少代码维护的麻烦,并更好地协作。
到目前为止,其他考虑因素(例如优化)在大多数情况下并不重要。
我会详细说明,因为我在这里得到了评论:
如果您有一个使用do { ... } while()
的代码片段A ,并且它比它的while() {...}
等效B更具可读性,那么我会投票给A 。 如果您更喜欢B ,因为您在“前面”看到循环条件,并且您认为它更具可读性(因此可维护等) - 然后继续使用B 。
我的观点是:使用对你的眼睛(和你的同事)来说更具可读性的代码。 当然,选择是主观的。
首先,我同意do-while
的可读性不如while
。
但令我惊讶的是,在这么多答案之后,没有人考虑为什么do-while
甚至存在于语言中。 原因是效率。
假设我们有一个带有N
条件检查的do-while
循环,其中条件的结果取决于循环体。 然后,如果我们用while
循环替换它,我们会得到N+1
条件检查,而额外的检查毫无意义。 如果循环条件只包含对整数值的检查,那没什么大不了的,但是可以说我们有
something_t* x = NULL;
while( very_slowly_check_if_something_is_done(x) )
{
set_something(x);
}
然后循环第一圈中的函数调用是多余的:我们已经知道x
尚未设置为任何内容。 那么为什么要执行一些毫无意义的开销代码呢?
在编写实时嵌入式系统时,我经常为此目的使用 do-while,其中条件内部的代码相对较慢(检查来自某些慢速硬件外围设备的响应)。
在我看来,这只是个人选择。
大多数时候,你可以找到一种方法将 do ... while 循环重写为 while 循环; 但不一定总是。 此外,有时编写 do while 循环以适应您所处的上下文可能更合乎逻辑。
如果你看上面,来自 TimW 的回复,它不言自明。 带有 Handle 的第二个,尤其是在我看来更凌乱。
阅读结构化程序定理。 do{} while() 总是可以重写为 while() do{}。 序列、选择和迭代都是需要的。
由于循环体中包含的任何内容始终可以封装到例程中,因此必须使用 while() do{} 的肮脏性永远不会比
LoopBody()
while(cond) {
LoopBody()
}
do-while 循环总是可以重写为 while 循环。
是只使用 while 循环,还是使用 while、do-while 和 for 循环(或它们的任意组合)在很大程度上取决于您对美学的品味以及您正在处理的项目的约定。
就我个人而言,我更喜欢 while 循环,因为它简化了关于循环不变量恕我直言的推理。
至于是否存在确实需要 do-while 循环的情况:而不是
do
{
loopBody();
} while (condition());
你总是可以
loopBody();
while(condition())
{
loopBody();
}
所以,不,如果由于某种原因不能使用,则永远不需要使用 do-while。 (当然,这个例子违反了 DRY,但这只是一个概念验证。根据我的经验,通常有一种方法可以将 do-while 循环转换为 while 循环,并且在任何具体用例中都不会违反 DRY。)
“在罗马时,做罗马人。”
顺便说一句:您正在寻找的报价可能是这个([1],第 6.3.3 节的最后一段):
根据我的经验,do-statement 是错误和混乱的根源。 原因是它的主体总是在测试条件之前执行一次。 然而,为了身体的正常运作,在第一次运行中必须保持与最终条件类似的条件。 我发现这些条件不正确的次数比我预期的要多。 当我从头开始编写有问题的程序然后对其进行测试时以及在更改代码之后都是这种情况。 此外,我更喜欢“预先,我可以看到它”的条件。 因此,我倾向于避免使用 do 语句。
(注意:这是我对德文版的翻译。如果您碰巧拥有英文版,请随时编辑引用以匹配他的原始措辞。不幸的是,Addison-Wesley 讨厌 Google。)
[1] B. Stroustrup: C++ 编程语言。 第 3 版。 艾迪生-韦斯利,雷丁,1997 年。
我几乎从不使用它们只是因为以下几点:
即使循环检查后置条件,您仍然需要在循环中检查此后置条件,以便不处理后置条件。
以示例伪代码为例:
do {
// get data
// process data
} while (data != null);
理论上听起来很简单,但在现实世界中,它可能看起来像这样:
do {
// get data
if (data != null) {
// process data
}
} while (data != null);
额外的“如果”检查不值得IMO。 我发现执行 do-while 而不是 while 循环更简洁的例子很少。 天啊。
回应未知(谷歌)对丹奥尔森的回答的问题/评论:
“do { ... } while (0) 是使宏表现良好的重要结构。”
#define M do { doOneThing(); doAnother(); } while (0)
...
if (query) M;
...
你看到没有do { ... } while(0)
什么吗? 它将始终执行 doAnother()。
好吧,也许这可以追溯到几步,但在这种情况下
do
{
output("enter a number");
int x = getInput();
//do stuff with input
}while(x != 0);
有可能,但不一定可读
int x;
while(x = getInput())
{
//do stuff with input
}
现在,如果您想使用 0 以外的数字退出循环
while((x = getInput()) != 4)
{
//do stuff with input
}
但是同样,可读性有所损失,更不用说在条件中使用赋值语句被认为是不好的做法,我只是想指出,与分配“保留”值相比,有更紧凑的方法来表示到它是初始运行的循环。
我喜欢 David Božjak 的例子。 然而,我认为你总是可以将你想要至少运行一次的代码分解到一个单独的函数中,从而实现可能是最易读的解决方案。 例如:
int main() {
char c;
do {
printf("enter a number");
scanf("%c", &c);
} while (c < '0' || c > '9');
}
可能变成这样:
int main() {
char c = askForCharacter();
while (c < '0' || c > '9') {
c = askForCharacter();
}
}
char askForCharacter() {
char c;
printf("enter a number");
scanf("%c", &c);
return c;
}
(请原谅任何不正确的语法;我不是 C 程序员)
考虑这样的事情:
int SumOfString(char* s)
{
int res = 0;
do
{
res += *s;
++s;
} while (*s != '\0');
}
碰巧 '\\0' 是 0,但我希望你明白这一点。
我的do / while问题严格在于它在 C 中的实现。由于while关键字的重用,它经常使人们绊倒,因为它看起来像一个错误。
如果while仅用于while循环并且do / while已更改为do / until或repeat / until ,我认为循环(这当然很方便,并且是编写某些循环的“正确”方式)不会导致这么多麻烦。
我之前就 JavaScript 对此进行了咆哮,它也从 C 继承了这个遗憾的选择。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.