19

我最近一直在编写很多涉及与 Win32 API 互操作的代码,并且开始想知道处理由调用 Windows API 函数引起的本机(非托管)错误的最佳方法是什么。

目前,对本机函数的调用如下所示:

// NativeFunction returns true when successful and false when an error
// occurred. When an error occurs, the MSDN docs usually tell you that the
// error code can be discovered by calling GetLastError (as long as the
// SetLastError flag has been set in the DllImport attribute).
// Marshal.GetLastWin32Error is the equivalent managed function, it seems.
if (!WinApi.NativeFunction(param1, param2, param3))
    throw new Win32Exception();

引发异常的行可以等效地重写,我相信:

throw new Win32Exception(Marshal.GetLastWin32Error());

现在,这一切都很好,因为它适当地引发了一个异常,其中包含设置的 Win32 错误代码以及(通常)人类可读的错误描述作为MessageException 对象的属性。但是,我一直认为建议至少修改/包装这些异常中的一些(如果不是全部),以便它们提供稍微更面向上下文的错误消息,即在本机代码的任何情况下更有意义的错误消息是正在使用。为此,我考虑了几种替代方案:

  1. 在构造函数中为Win32Exception.

    throw new Win32Exception(Marshal.GetLastWin32Error(), "My custom error message.");
    
  2. 将 包装Win32Exception在另一个 Exception 对象中,以便保留原始错误代码和消息(Win32Exception现在是InnerException父异常的)。

    throw new Exception("My custom error message.", Win32Exception(Marshal.GetLastWin32Error()));
    
  3. 与 2 相同,除了使用另一个Win32Exception作为包装异常。

  4. 与 2 相同,但使用派生自的自定义类Exception作为包装异常。

  5. 与 2 相同,除了在适当时使用 BCL(基类库)异常作为父异常。不确定在这种情况下是否适合将 设置为InnerExceptionWin32Exception可能是低级包装器,但不是更高级别/抽象的接口,这不会使 Win32 互操作在幕后发生明显?)

基本上我想知道的是:在 .NET 中处理 Win32 错误的推荐做法是什么?我看到它以各种不同的方式在开源代码中完成,但我很好奇是否有任何设计指南。如果没有,我会在这里对您的个人喜好感兴趣。(也许您甚至没有使用上述方法?)

4

3 回答 3

4

这并不是 Win32 异常所特有的。问题是,两种不同的异常派生类型何时应该识别两种不同的错误情况,何时应该抛出存储在其中的不同值的相同类型?

不幸的是,如果不事先知道将调用您的代码的所有情况,这是不可能回答的。:) 这是只能按类型过滤异常的问题。从广义上讲,如果您强烈认为以不同的方式处理两种错误情况会很有用,请抛出不同的类型。

否则,通常情况下 Exception.Message 返回的字符串只需要记录或显示给用户。

如果有其他信息,请使用您自己的更高级别的内容包装 Win32Exception。例如,您正在尝试对文件执行某些操作,而您正在运行的用户没有执行此操作的权限。捕获 Win32Exception,将其包装在您自己的异常类中,其消息给出文件名和正在尝试的操作,然后是内部异常的消息。

于 2009-03-23T16:10:11.103 回答
3

我的观点一直是,处理这个问题的适当方法取决于目标受众以及你的课程将如何被使用。

如果您的类/方法的调用者将意识到他们正在以一种或另一种形式调用 Win32,我将使用您指定的选项 1)。这对我来说似乎是最“清楚”的。(但是,如果是这种情况,我会以清楚表明将直接使用 Win32 API 的方式命名您的类)。话虽如此,BCL 中有一些异常实际上是 Win32Exception 的子类,以便更清楚地了解,而不仅仅是包装它。例如,SocketException 派生自 Win32Exception。我从来没有亲自使用过这种方法,但它似乎确实是一种潜在的干净方式来处理这个问题。

如果您的类的调用者不知道您正在直接调用 Win32 API,我将处理该异常,并使用您定义的自定义、更具描述性的异常。例如,如果我正在使用您的课程,并且没有迹象表明您正在使用 Win32 api(因为您出于某些特定的、非显而易见的原因在内部使用它),我没有理由怀疑我可能需要处理 Win32Exception。您总是可以记录这一点,但对我来说,将其捕获并给出一个在您的特定业务环境中具有更多意义的异常似乎更合理。在这种情况下,我可能会将初始 Win32Exception 包装为内部异常(即:您的情况 4),但取决于导致内部异常的原因,我可能不会。

此外,很多时候会从本机调用中引发 Win32Exception,但 BCL 中还有其他更相关的异常。当您调用未包装的本机 API 时就是这种情况,但也有类似的函数被包装在 BCL 中。在这种情况下,我可能会捕获异常,确保它是我所期望的,然后抛出标准的 BCL 异常。一个很好的例子是使用 SecurityException 而不是抛出 Win32Exception。

不过,总的来说,我会避免您列出的选项 2 和 3。

选项二会抛出一个通用的异常类型——我非常建议完全避免这种情况。将特定异常包装成更通用的异常似乎是不合理的。

选项 3 似乎是多余的 - 与选项 1 相比确实没有优势。

于 2009-03-23T16:18:23.720 回答
2

就我个人而言,我会做#2 或#4 ...最好是#4。将 Win32Exception 包装在上下文相关的异常中。像这样:

void ReadFile()
{
    if (!WinApi.NativeFunction(param1, param2, param3))
        throw MyReadFileException("Couldn't read file", new Win32Exception());
}

这样,如果有人捕捉到异常,他们就会很清楚问题发生在哪里。我不会做#1,因为它需要捕获来解释您的文本错误消息。而#3 并没有真正提供任何额外的信息。

于 2009-03-23T16:18:37.103 回答