我读过这GOTO
很糟糕,但我该如何避免呢?我不知道如何在没有GOTO
. 在 BASIC 中,我GOTO
什么都用。我应该在 C 和 C++ 中改用什么?
我GOTO
在 BASIC 中这样使用:
MainLoop:
INPUT string$
IF string$ = "game" THEN
GOTO game
ENDIF
考虑以下一段 C++ 代码:
void broken()
{
int i = rand() % 10;
if (i == 0) // 1 in 10 chance.
goto iHaveABadFeelingAboutThis;
std::string cake = "a lie";
// ...
// lots of code that prepares the cake
// ...
iHaveABadFeelingAboutThis:
// 1 time out of ten, the cake really is a lie.
eat(cake);
// maybe this is where "iHaveABadFeelingAboutThis" was supposed to be?
std::cout << "Thank you for calling" << std::endl;
}
归根结底,“goto”与 C++ 的其他流控制关键字没有太大区别:“break”、“continue”、“throw”等;从功能上讲,它引入了一些与范围相关的问题,如上所示。
依赖goto会教你产生难读、难调试、难维护代码的坏习惯,而且一般容易导致bug。为什么?因为 goto 在最糟糕的情况下是自由形式的,它可以让你绕过语言中内置的结构控制,例如范围规则等。
很少有替代方案特别直观,其中一些可以说与“goto”一样模棱两可,但至少您是在语言结构内操作 - 回到上面的示例,做我们所做的事情要困难得多上面的示例除了 goto 之外的任何内容(当然,在使用指针时,您仍然可以使用 for/while/throw 射击自己)。
您可以选择避免它并使用语言的自然流控制结构来保持代码的可读性和可维护性:
不要害怕小的、离散的、有名的函数,只要你不总是带着大量的参数(如果你是,那么你可能想看看用一个类封装)。
许多新手使用“goto”是因为他们编写了非常长的函数,然后发现他们想从一个 3000 行函数的第 2 行到第 2998 行。在上面的代码中,如果拆分,则 goto 创建的 bug 更难创建函数分为两个有效载荷,逻辑和函数。
void haveCake() {
std::string cake = "a lie";
// ...
// lots of code that prepares the cake
// ...
eat(cake);
}
void foo() {
int i = rand() % 10;
if (i != 0) // 9 times out of 10
haveCake();
std::cout << "Thanks for calling" << std::endl;
}
有些人将此称为“提升”(我将所有需要用“蛋糕”限定范围的东西提升到 haveCake 函数中)。
这些对于刚开始的程序员来说并不总是显而易见的,它说这是一个 for/while/do 循环,但实际上它只打算运行一次。
for ( ; ; ) { // 1-shot for loop.
int i = rand() % 10;
if (i == 0) // 1 time in 10
break;
std::string cake = "a lie";
// << all the cakey goodness.
// And here's the weakness of this approach.
// If you don't "break" you may create an infinite loop.
break;
}
std::cout << "Thanks for calling" << std::endl;
这些可能非常强大,但它们也可能需要大量样板。另外,您可以抛出异常以进一步备份调用堆栈或根本不捕获(并退出程序)。
struct OutOfLuck {};
try {
int i = rand() % 10;
if (i == 0)
throw OutOfLuck();
std::string cake = "a lie";
// << did you know: cake contains no fat, sugar, salt, calories or chemicals?
if (cake.size() < MIN_CAKE)
throw CakeError("WTF is this? I asked for cake, not muffin");
}
catch (OutOfLuck&) {} // we don't catch CakeError, that's Someone Else's Problem(TM).
std::cout << "Thanks for calling" << std::endl;
形式上,您应该尝试从std::exception派生您的异常,但我有时偏向于抛出 const char* 字符串、枚举和偶尔的 struct Rock。
try {
if (creamyGoodness.index() < 11)
throw "Well, heck, we ran out of cream.";
} catch (const char* wkoft /*what kind of fail today*/) {
std::cout << "CAKE FAIL: " << wkoft << std::endl;
throw std::runtime_error(wkoft);
}
这里最大的问题是异常用于处理错误,就像上面两个示例中的第二个一样。
使用 有几个原因goto
,主要是:条件执行、循环和“退出”例程。
条件执行一般由if
/管理else
,应该够用了
循环由for
,while
和do while
;管理 并且进一步加强了continue
和break
最困难的是“退出”例程,但在 C++ 中,它被析构函数的确定性执行所取代。因此,为了让您在退出函数时调用例程,您只需创建一个对象,该对象将在其析构函数中执行您需要的操作:直接的好处是您不会忘记在添加一个操作时执行该操作,return
并且它甚至可以在异常的存在。
Edsger Dijkstra 发表了一封著名的信,题为Go To Statement Considered Harmful。你应该阅读它,他提倡结构化编程。那篇维基百科文章描述了您需要了解的有关结构化编程的知识。您可以使用 goto 编写结构化程序,但现在这不是一种流行的观点,因为这种观点请阅读 Donald Knuth 的Structured Programming with goto Statements。
通常像for
,while
和do while
和 函数这样的循环或多或少地处理了使用 GOTO 的需要。了解如何使用这些,经过一些示例后,您将不再考虑 goto。:)
也许代替
if(something happens)
goto err;
err:
print_log()
一个可以使用:
do {
if (something happens)
{
seterrbool = true;
break; // You can avoid using using go to I believe
}
} while (false) //loop will work only one anyways
if (seterrbool)
printlog();
它可能看起来不友好,因为在上面的示例中只有一个 goto,但如果有很多 "goto" 会更易读。
goto
本质上并不坏,它有它的用途,就像任何其他语言特性一样。您可以完全避免使用goto
, 通过使用exceptions
,try/catch
和 循环以及适当的if/else
结构。
但是,如果您意识到自己非常偏心,只是为了避免它,则可能暗示使用它会更好。
我个人使用 goto 来实现具有单一入口和出口点的函数,这使得代码更具可读性。这是我仍然觉得 goto 有用并且实际上改进了代码的结构和可读性的唯一事情。
举个例子:
int foo()
{
int fd1 = -1;
int fd2 = -1;
int fd3 = -1;
fd1 = open();
if(fd1 == -1)
goto Quit:
fd2 = open();
if(fd2 == -1)
goto Quit:
fd3 = open();
if(fd3 == -1)
goto Quit:
... do your stuff here ...
Quit:
if(fd1 != -1)
closefile();
if(fd2 != -1)
closefile();
if(fd3 != -1)
closefile();
}
在 C++ 中,您会发现,如果您正确实现封装对资源的访问的类,那么对此类结构的需求可能会大大减少。例如使用智能指针就是这样一个例子。
在上面的示例中,您将在 C++ 中实现/使用文件类,这样,当它被破坏时,文件句柄也会关闭。使用类还有一个优点,即它会在抛出异常时工作,因为编译器会确保所有对象都被正确破坏。因此,在 C++ 中,您绝对应该使用带有析构函数的类来实现这一点。
当你想用 C 编写代码时,你应该考虑额外的块也会给代码增加额外的复杂性,这反过来会使代码更难理解和控制。为了避免这种情况,我更喜欢在任何时候都使用一个放置良好的 goto,而不是一系列人为的 if/else 子句。如果您以后必须重新访问代码,您仍然可以理解它,而无需遵循所有额外的块。
goto
现在被其他更易于阅读的编程结构(如for
,while
等)取代。do-while
但goto
仍然有它的用途。我在函数中的不同代码块(例如,涉及不同条件检查)具有单个退出点的情况下使用它。除了这个用途之外,您还应该使用适当的编程结构。
上述函数的这种实现避免了使用 goto。请注意,这不包含循环。编译器将对此进行优化。我更喜欢这种实现。
使用 'break' 和 'continue',可以(几乎?)避免 goto 语句。
int foo()
{
int fd1 = -1;
int fd2 = -1;
int fd3 = -1;
do
{
fd1 = open();
if(fd1 == -1)
break;
fd2 = open();
if(fd2 == -1)
break:
fd3 = open();
if(fd3 == -1)
break;
... do your stuff here ...
}
while (false);
if(fd1 != -1)
closefile();
if(fd2 != -1)
closefile();
if(fd3 != -1)
closefile();
}
BASIC 最初是一种解释型语言。它没有结构,因此它依赖GOTO
s 来跳转特定的行,就像你在汇编中跳转的方式一样。这样,程序流程很难跟踪,使调试更加复杂。
Pascal、C 和包括Visual Basic(基于 BASIC)在内的所有现代高级编程语言都是由组合成块的“命令”组成的强结构。例如 VB 有Do... Loop
, While... End While
, For...Next
. 甚至一些BASIC 的旧衍生产品也支持Microsoft QuickBASIC 等结构:
DECLARE SUB PrintSomeStars (StarCount!)
REM QuickBASIC example
INPUT "What is your name: ", UserName$
PRINT "Hello "; UserName$
DO
INPUT "How many stars do you want: ", NumStars
CALL PrintSomeStars(NumStars)
DO
INPUT "Do you want more stars? ", Answer$
LOOP UNTIL Answer$ <> ""
Answer$ = LEFT$(Answer$, 1)
LOOP WHILE UCASE$(Answer$) = "Y"
PRINT "Goodbye "; UserName$
END
SUB PrintSomeStars (StarCount)
REM This procedure uses a local variable called Stars$
Stars$ = STRING$(StarCount, "*")
PRINT Stars$
END SUB
Visual Basic .NET 中的另一个示例
Public Module StarsProgram
Private Function Ask(prompt As String) As String
Console.Write(prompt)
Return Console.ReadLine()
End Function
Public Sub Main()
Dim userName = Ask("What is your name: ")
Console.WriteLine("Hello {0}", userName)
Dim answer As String
Do
Dim numStars = CInt(Ask("How many stars do you want: "))
Dim stars As New String("*"c, numStars)
Console.WriteLine(stars)
Do
answer = Ask("Do you want more stars? ")
Loop Until answer <> ""
Loop While answer.StartsWith("Y", StringComparison.OrdinalIgnoreCase)
Console.WriteLine("Goodbye {0}", userName)
End Sub
End Module
类似的东西将在 C++ 中使用,例如if
, then
, for
, do
, while
... 它们共同定义了程序流程。您不需要使用goto
跳转到下一条语句。在特定情况下,如果它使控制流更清晰,您仍然可以使用goto
,但通常不需要它