GetLastError()是一种设计模式吗?这是好的机制吗

Is GetLastError() kind of design pattern? Is it good mechanism?

本文关键字:机制 一种 GetLastError 设计模式      更新时间:2023-10-16

Windows API使用GetLastError()机制来检索有关错误或失败的信息。我正在考虑与为专有模块编写API相同的机制来处理错误。我的问题是API直接返回错误代码更好吗?GetLastError()有什么特别的优势吗?考虑下面简单的Win32 API示例:

HANDLE hFile = CreateFile(sFile,
    GENERIC_WRITE, FILE_SHARE_READ,
    NULL, CREATE_NEW, FILE_ATTRIBUTE_NORMAL, NULL);
if (hFile == INVALID_HANDLE_VALUE)
{
    DWORD lrc = GetLastError();
    if (lrc == ERROR_FILE_EXISTS)
    {
          // msg box and so on
    }
}

在编写自己的API时,我意识到GetLastError()机制意味着CreateFile()必须在所有出口点设置最后一个错误代码。如果有很多出口点,其中一个可能会错过,这可能会有点容易出错。愚蠢的问题,但这是怎么做的,还是有某种设计模式?

另一种选择是为函数提供一个额外的参数,该参数可以直接填写错误代码,因此不需要单独调用GetLastError()。另一种方法如下。我将坚持使用上面的Win32 API,这是分析这一点的好例子。在这里,我将格式更改为这个(假设)。

result =  CreateFile(hFile, sFile,
    GENERIC_WRITE, FILE_SHARE_READ,
    NULL, CREATE_NEW, FILE_ATTRIBUTE_NORMAL, NULL);
if (result == SUCCESS)
{
   // hFile has correct value, process it
}
else if (result == FILE_ALREADY_EXIT )
{
   // display message accordingly
  return;
}
else if ( result == INVALID_PATH )
{
   // display message accordingly.
  return;
}

我的终极问题是,从API甚至函数返回错误代码的首选方式是什么,因为它们都是相同的?

总的来说,这是一个糟糕的设计。这并不是Windows的GetLastError函数所特有的,Unix系统与全局errno变量具有相同的概念。它是一个,因为它是函数的输出,是隐式的。这有一些恶劣的后果:

  1. 同时执行的两个函数(在不同的线程中)可能会覆盖全局错误代码。因此,您可能需要一个每个线程的错误代码。正如对这个答案的各种评论所指出的,这正是GetLastErrorerrno所做的——如果你考虑为你的API使用全局错误代码,那么你也需要这样做,以防你的API应该可以从多个线程使用。

  2. 如果外部函数覆盖内部函数设置的错误代码,那么两个嵌套函数调用可能会丢弃错误代码。

  3. 忽略错误代码非常容易。事实上,很难真正记住它在那里,因为不是每个函数都使用它

  4. 当你自己实现一个函数时,很容易忘记设置它。可能有许多不同的代码路径,如果你不注意,其中一个可能会允许控制流在没有正确设置全局错误代码的情况下逃逸。

通常,错误情况是例外的。这种情况不常发生,但可以。您需要的配置文件可能不可读,但大多数情况下是可读的。对于此类异常错误,您应该考虑使用C++异常。任何一本值得一读的C++书都会列出任何语言(不仅仅是C++)中的异常都很好的原因,但在兴奋之前有一件重要的事情需要考虑:

异常将展开堆栈。

这意味着,当您有一个产生异常的函数时,它会被传播到所有调用方(直到它被某人捕获,可能是C运行时系统)。这反过来又产生了一些后果:

  1. 所有调用方代码都需要意识到异常的存在,因此所有获取资源的代码都必须能够释放它们,即使面对异常(在C++中,通常使用"RAII"技术来处理它们)。

  2. 事件循环系统通常不允许异常对事件处理程序进行转义。在这种情况下,对付他们没有什么好主意。

  3. 处理回调的程序(例如,普通函数指针,甚至Qt库使用的"signal&slot"系统)通常不希望被调用的函数(slot)会产生异常,所以他们不必试图捕捉它

底线是:如果你知道他们在做什么,就使用异常。由于您似乎对这个主题还很陌生,所以现在还是坚持返回函数的代码,但请记住,这通常不是一个好的技术。无论哪种情况,都不要使用全局错误变量/函数。

GetLastError模式是迄今为止最容易出错且最不受欢迎的模式。

到目前为止,返回状态代码enum是更好的选择。

另一个您没有提到但非常流行的选项是为失败情况抛出异常。如果你想做得好(而不是泄露资源或让对象处于半设置状态),这需要非常小心的编码,但这会导致代码看起来非常优雅,所有核心逻辑都在一个地方,错误处理被巧妙地分离出来。

我认为GetLastError是多线程之前的遗留物。我认为这种模式不应该再使用了,除非在错误非常罕见的情况下。问题是错误代码必须是每个线程的。

GetLastError的另一个刺激是它需要两个级别的测试。您首先必须检查返回代码,看看它是否指示错误,然后您必须调用GetLastError来获取错误。这意味着你必须做两件事中的一件,都不是特别优雅:

1) 您可以返回一个指示成功或失败的布尔值。但是,为什么不直接返回错误代码为零表示成功呢?

2) 您可以根据作为主要返回值的非法值对每个函数进行不同的返回值测试。但是,如果任何返回值都是合法的,函数又是什么呢?这是一个非常容易出错的设计模式。(零是某些函数的唯一非法值,因此在这种情况下,错误返回零。但如果零是合法的,则可能需要使用-1或类似的值。很容易出错。)

我不得不说,当无法使用异常处理时,我认为全局错误处理程序样式(具有适当的线程本地存储)是最实际适用的。这当然不是一个最佳的解决方案,但我认为,如果你生活在我的世界里(一个懒惰的开发人员的世界,他们不经常检查错误状态),这是最实用的。

理由:开发人员往往不会经常检查错误返回值。在现实世界的项目中,函数返回了一些错误状态,但调用方却忽略了这些错误状态,我们可以举出多少例子?或者,我们见过多少次函数甚至没有正确返回错误状态,即使它在分配内存(可能会失败)?我见过太多这样的例子,回过头来修复它们有时甚至需要通过代码库进行大规模的设计或重构更改。

全局错误处理程序在这方面要宽容得多:

  • 如果函数未能返回布尔值或某些ErrorStatus类型来指示失败,我们不必修改其签名或返回类型来指示故障,也不必在整个应用程序中更改客户端代码。我们可以修改它的实现来设置全局错误状态。诚然,我们仍然需要在客户端添加检查,但如果我们在呼叫站点立即错过错误,那么稍后仍有机会发现。

  • 如果客户端未能检查错误状态,我们稍后仍然可以捕捉到错误。诚然,错误可能会被随后的错误覆盖,但我们仍然有机会看到在某个时候发生了错误,而在调用站点上简单忽略错误返回值的调用代码将永远不会让错误在以后被注意到。

虽然这是一个次优解决方案,但如果不能使用异常处理,并且我们正在与一组代码猴子合作,他们有忽略错误返回值的可怕习惯,那么就我所见,这是最实用的解决方案。

当然,具有适当异常安全性的异常处理(RAII)是迄今为止最好的方法,但有时不能使用异常处理(例如:我们不应该抛出模块边界)。虽然从严格的工程角度来看,像Win API的GetLastError或OpenGL的glGetError

然而,如果应用此模式,则必须仔细注意,以确保它可以与多个线程正常工作,并且不会对性能造成重大影响。实际上,我必须设计自己的线程本地存储系统才能做到这一点,但我们的系统主要使用异常处理,只有这个全局错误处理程序才能将跨模块边界的错误转换为异常。

总而言之,异常处理是可行的,但如果由于某种原因无法做到这一点,我不得不不同意这里的大多数答案,并建议对规模较大、纪律性较差的团队使用GetLastError之类的方法(我认为对规模较小、纪律性较强的团队使用通过调用堆栈返回的错误),因为如果返回的错误状态被忽略,这使我们以后至少可以注意到错误,并且允许我们通过简单地修改其实现而不修改接口,将错误处理修改为一个未正确设计为返回错误的函数。

如果API在DLL中,并且您希望支持使用不同编译器的客户端,则不能使用异常。没有针对异常的二进制接口标准。

所以你几乎必须使用错误代码。但不要以GetLastError为例对系统进行建模。如果你想要一个如何返回错误代码的好例子,看看COM。每个函数都返回一个HRESULT。这允许调用方编写简洁的代码,将COM错误代码转换为本机异常。像这样:

Check(pIntf->DoSomething());

其中,Check()是一个由您编写的函数,它接收HRESULT作为其单个参数,并在HRESULT指示失败时引发异常。正是函数的返回值指示状态,才允许进行更简洁的编码。想象一下通过参数返回状态的替代方案:

pIntf->DoSomething(&status);
Check(status);

或者,更糟糕的是,它在Win32中的操作方式:

if (!pIntf->DoSomething())
    Check(GetLastError());

另一方面,如果您准备指定所有客户端都使用与您相同的编译器,或者您将库作为源代码交付,那么请使用异常。

不建议在非托管代码中进行异常处理。毫无例外地处理内存泄漏是一个大问题,有了例外,它就成了噩梦。

错误代码的线程局部变量并不是一个坏主意,但正如其他一些人所说,它有点容易出错。

我个人更喜欢每种方法都返回错误代码。这给功能方法带来了不便,因为它不是:

int a = foo();

你需要写:

int a;
HANDLE_ERROR(foo(a));

这里HANDLE_ERROR可以是一个宏,用于检查从foo返回的代码,如果是错误,则将其传播(返回)。

如果你准备了一组好的宏来处理不同的情况,那么在没有异常处理的情况下,用良好的错误处理扭曲代码是可能的。

现在,当您的项目开始增长时,您会注意到错误的调用堆栈信息非常重要。您可以扩展宏,将调用堆栈信息存储在线程本地存储变量中。这是非常有用的。

然后您会注意到,即使是调用堆栈也是不够的。在许多情况下,在fopen(路径,…)行出现"找不到文件"的错误代码;没有给你足够的信息来找出问题所在。是找不到的文件。在这一点上,你可以扩展你的宏,以便能够存储按摩。然后您可以提供未找到的文件的实际路径。

问题是,为什么要麻烦所有这些,你可以做的例外。几个原因:

  1. 同样,非托管代码中的异常处理很难做到正确
  2. 基于宏的代码(如果完成写入)恰好比异常处理所需的代码更小、更快
  3. 它要灵活得多。您可以启用禁用功能

在我目前正在工作的项目中,我实现了这样的错误处理。我花了2天的时间来设置一个级别以准备开始使用它。在大约一年的时间里,我可能总共花了大约2周的时间来维护和添加它的功能。

你还应该考虑一个基于对象/结构的错误代码变量。就像stdioC库为FILE流所做的那样。

例如,在我的一些io对象上,当设置了错误状态时,我会跳过所有进一步的操作,这样用户在一系列操作后只检查一次错误时就可以了。

此模式允许您更好地微调错误处理方案。

例如,当将C/C++与谷歌GO语言进行比较时,它的一个糟糕设计在这里得到了充分的体现。函数只返回一个值。GO不使用异常,而是始终返回两个值,即结果和错误代码。

有一小部分人认为,例外在大多数情况下都是糟糕的和被滥用的,因为错误不是例外,而是你必须期待的。但它并没有证明软件变得更可靠、更容易。尤其是在C++中,现在唯一的编程方法是RIIA技术。

相关文章: