以下是我编写代码的两种方式,受我使用两种高度宏化的伪汇编语言编写经验的影响,其中一种不是 C。我移动了全名的 deref,不是因为它是错误的在您的代码中,但是因为我想演示您如何在两种方案中处理寿命较长的资源。所以想象一下,稍后在例程中将再次需要“全名”:
箭头代码
result = NULL;
py_fullname = PyObject_CallMethod(os, "path.join", "ss", folder, filename);
if (py_fullname) {
image = PyObject_CallMethodObjArgs(pygame, "image.load", py_fullname, NULL);
if (image) {
image = PyObject_CallMethodObjArgs(image, "convert", NULL);
result = // something to do with image, presumably.
}
Py_DECREF(py_fullname);
}
Py_DECREF(pygame);
Py_DECREF(os);
return result;
玩这个游戏的方式是,每当你调用一个返回资源的函数时,你会立即检查返回值(或者可能在释放一些不再需要的资源之后,如你的示例代码),并且与成功调用对应的块必须在块退出之前释放资源,或者将其分配给返回值,或者实际返回它。这通常是在块的第二行,在第一行使用之后,或者在块的最后一行。
之所以称为“箭头代码”,是因为如果您在一个函数中进行 5 或 6 次这样的调用,您最终会得到 5 或 6 级缩进,并且您的函数看起来像一个“右转”标志。当这种情况发生时,你要么重构,要么违背你的每一个 Python 直觉,使用制表符进行缩进,并减少制表位;-)
转到
result = NULL;
py_fullname = PyObject_CallMethod(os, "path.join", "ss", folder, filename);
if (!py_fullname) goto cleanup_pygame
image = PyObject_CallMethodObjArgs(pygame, "image.load", py_fullname, NULL);
if (!image) goto cleanup_fullname
image = PyObject_CallMethodObjArgs(image, "convert", NULL);
result = // something to do with image, presumably.
cleanup_fullname:
Py_DECREF(py_fullname);
cleanup_pygame:
Py_DECREF(pygame);
Py_DECREF(os);
return result;
这个 goto 代码在结构上与箭头代码相同,只是缩进更少,更容易弄乱和跳转到错误的标签。在某些情况下,您将清理成功与失败时清理的不同资源(例如,如果您正在构建和返回某些东西,那么在失败时您需要清理到目前为止所做的任何事情,但在成功时您只清理你不返回的东西)。在这些情况下,goto 代码明显胜过箭头代码,因为您可以为这两种情况使用单独的清理路径,但它们看起来仍然相同,在例程结束时一起出现,甚至可能共享代码.所以你最终可能会得到这样的结果:
result = NULL;
helper = allocate_something;
if (!helper) goto return_result;
result = allocate_something_else;
if (!result) goto error_return; // OK, result is already NULL, but it makes the point
result->contents = allocate_another_thing;
if (!result->contents) goto error_cleanup_result;
result->othercontents = allocate_last_thing;
if (!result->othercontents) goto error_cleanup_contents;
free_helper:
free(helper);
return_result:
return result;
error_cleanup_contents:
free(result->contents);
error_cleanup_result:
free(result);
error_return;
result = NULL;
goto free_helper;
是的,这太可怕了,Python 或 C++ 程序员看到它会身体不适。如果我再也不用写这样的代码,我就不会那么失望了。但是只要你有一个如何清理资源的系统方案,你应该总是知道当出现问题时要跳转到哪个错误标签,并且那个错误标签应该“知道”清理所有已分配的资源,所以远的。以相反的顺序执行它允许通过共享代码。一旦你习惯了它,做两件事就相当容易了:首先沿着从任何给定错误标签到出口的路径,并确认所有应该释放的东西都被释放了。其次,查看两个错误案例之间的差异,并确认这是所需的错误处理之间的正确差异,因为差异正是将在跳转之间分配的东西释放到那些标签。
也就是说,一个半体面的优化编译器将为您的示例中的错误情况提供代码。当你在这样的地方复制和粘贴代码时,更容易出错,尤其是当你以后修改它时。