1

我正在尝试在不使用异常的情况下在我的 C# 代码中进行错误通信和恢复。举个例子,假设有一个 Func A,它可以被 Func B 或 Func C 或其他函数调用。Func A 的设计必须牢记重用。(此应用程序有一个不断发展的库,其中新功能将在一段时间内不断添加)

如果 Func A 不能做它应该做的事情,它会返回一个 int,其中任何非零值都表示失败。我也想交流失败的原因。调用者函数可以通过多种方式使用此信息:

  1. 它可以向用户显示错误消息,
  2. 它可能会显示与其上下文更相关的自己的错误消息
  3. 它本身可能会返回一个 int 值,指示进一步的祖先调用函数失败。
  4. 它可能会尝试使用一些智能算法从错误中恢复。

假设地,其他函数所依赖的任何函数都可能需要将多个信息传递给其调用函数以采取适当的行动,包括状态代码、错误消息和其他指示数据状态的变量。将所有内容作为分隔字符串返回可能不允许调用函数在不解析字符串的情况下检索信息(这会导致其自身的问题,不推荐)。

唯一的另一种方法是返回一个包含所有必需信息的成员变量的对象。这可能会导致过多的“状态”对象,因为每个函数都需要有它的状态对象。

我想了解如何以最优雅的方式设计此要求。请注意,在编码时,Func A 可能不知道调用者函数是否具有从错误中恢复的智能,因此我不想抛出异常。另外,我想看看在不使用异常的情况下,这样的设计是否可行(同时又很优雅)。

如果唯一的方法是使用每个函数的数据对象进行通信,那么它就是编写专业库的方式。可以有一个通用的数据对象吗?请注意,将来可能会添加新功能,这些功能可能具有不同的状态变量,或有关其错误的支持信息。

另请注意,由于函数的返回值是一个“状态”对象,它应该返回的实际数据可能需要作为 ref 或 out 参数传递。

这有设计模式吗?

在发布此问题之前,我已阅读以下文章:

http://blogs.msdn.com/b/ricom/archive/2003/12/19/44697.aspx

当不抛出异常时,try/catch 块会损害性能吗?

无异常的错误处理

我还阅读了许多其他文章,这些文章建议不要将异常用于代码流控制和可恢复的错误。此外,抛出异常有其自身的成本。此外,如果调用者函数想要从每个被调用函数抛出的异常中恢复,则必须用 try catch 块包围每个函数调用,因为通用 try catch 块不允许从下一行“继续”的错误线。

编辑:

几个具体问题:我需要编写一个应用程序,它将同步 2 个不同的数据库:一个是专有数据库,另一个是 SQL Server 数据库。我想将可重用的函数封装在一个单独的层中。

功能是这样的:专有应用程序可以有许多数据库。需要将来自这些数据库中的每一个的一些信息推送到单个通用 SQL Server 数据库。专有应用程序的数据库只有在应用程序的 GUI 打开时才能读取,并且只能通过 XML 读取。

算法是这样的:

  1. 读取专有应用程序中的打开数据库列表
  2. 对于每个数据库,启动同步进程。
  3. 检查当前登录的用户是否在此数据库中具有同步权限。(注意:每个数据库都可以使用不同的用户 ID 打开)。
  4. 从此数据库中读取数据。
  5. 将数据传输到 SQL Server
  6. 进入下一个数据库。

在开发这个应用程序时,我将编写几个可重用的函数,如 ReadUserPermission、ReadListOfDatabases 等。

在这种情况下,如果 ReadUserPermission 发现该权限不存在,调用者应该记录这个并继续下一个打开的数据库。如果 ReadListOfDatabases 无法与专有应用程序建立连接,调用者应自动启动应用程序等。

那么哪些错误条件应该传达异常,哪些使用返回码呢?

请注意,可重用函数可能会在其他项目中使用,其中调用者可能具有不同的错误恢复要求或功能,因此必须牢记这一点。

编辑:

对于所有提倡异常的人,我问他们:如果 Func A 调用 Func B,C,D,E,F,G 并且 Func B 在某些错误条件下抛出异常,但 Func A 可以从这个错误中恢复并愿意继续执行的其余部分,即调用 Func B、C、D、...,异常处理如何允许“优雅地”执行此操作?唯一的解决方案是将对 B、C、D、... 中的每一个的调用包装在一个 try catch 块中,以便执行剩余的语句。

另请阅读以下两条评论:

https://stackoverflow.com/a/1279137/1113579

https://stackoverflow.com/a/1272547/1113579

注意我不反对使用异常,如果可以优雅地实现错误恢复和剩余代码执行而不影响性能。此外,轻微的性能影响不是问题,但我更喜欢设计应该是可扩展的和优雅的。

编辑:

好的,基于“Zdeslav Vojkovic”评论,我现在正在考虑使用异常。

如果我要使用异常,你能给出一些不使用异常但使用返回码的用例吗?注意:我说的是返回,而不是函数应该返回的数据。有没有使用返回码来指示成功/失败的用例,或者没有用例?这将帮助我更好地理解。

我从“Zdeslav Vojkovic”中了解到的一个异常用例是当被调用函数想要强制通知调用函数某些条件并中断调用者执行时。在没有异常的情况下,调用者可能会或可能不会选择检查返回码。但是在异常的情况下,调用者函数必须处理异常,如果它想继续执行

编辑:

我有另一个有趣的想法。任何想要支持调用者函数从错误中恢复的思想的被调用者函数都可以引发事件,并在事件处理后检查事件数据,然后决定是否抛出异常。根本不会使用错误代码。异常将用于未恢复的错误。基本上,当被调用函数无法执行其合同规定的操作时,它会以任何可用事件处理程序的形式请求“帮助”。如果它不能履行合同,它仍然会抛出一个异常。优点是减少了引发异常的额外开销,并且仅当被调用函数或其任何调用函数无法从错误中恢复时才引发异常。

假设如果调用者函数不想处理错误,而是调用者的调用者函数想要处理错误,自定义事件分派器将确保事件处理程序以事件注册的相反顺序被调用,即最近注册的事件handler 应该在其他注册的事件处理程序之前被调用,如果这个事件处理程序能够解决错误,那么后续的事件处理程序根本不会被调用。另一方面,如果最近的事件处理程序不能解决错误,事件链将传播到下一个处理程序。

请对此方法提供反馈。

4

3 回答 3

1

“是不是专业图书馆的写法?”

不,专业库是通过使用异常处理错误来编写的 - 我不确定是否有使用您建议的方法的模式,但我认为它是一种反模式(在 .NET 中)。毕竟,.NET 本身是一个专业的框架,它使用异常。此外,.NET 开发人员习惯于异常。你认为你的库真的有那么特别,可以迫使用户学习完全不同的错误处理方式吗?

您刚刚所做的是重新发明 COM 错误处理。如果那是您想要做的,那么请检查ISupportErrorInfo接口以获取一些想法。

你为什么要这样做?我敢打赌,这是一种性能“优化”。

担心与异常处理有关的性能问题几乎总是过早的优化。您将创建一个笨拙的 API,其中每个返回值都必须通过 ref/out 参数处理,这将伤害您的 lib 的每个用户,只是为了解决可能根本不存在的问题。

“Func A 可能不知道调用者函数是否具有从错误中恢复的智能,所以我不想抛出异常”

所以你想确保调用者默默地允许 FuncA 搞乱系统不变量,调用者只是愉快地继续?由于这个原因,它只会使调试看似不可能的错误变得更加困难,该错误稍后会在另一个函数中发生。

在某些情况下,避免异常是有意义的,但应该有充分的理由。例外是个好主意。

编辑:我看到您补充说您“还阅读了许多其他文章,建议不要使用异常来控制代码流”。没错,.NET 中的异常不是用于代码流,而是用于错误处理。

你问:

如果 Func A 调用 Func B,C,D,E,F 并且它必须用 try catch 封装每个调用,因为它可以从错误中恢复或者它仍然希望执行剩余的函数调用,那么这么多 try catch 语句就不会尴尬了

不超过替代。您犯了一个错误,您可以以相同的方式简单地处理从函数返回的所有错误,但通常不能。

考虑是否需要单独处理每个函数 - 最坏的情况和代码通常不是这样编写的:

Result x, y;
try {
   x = Function1();
}
catch(SomeException e) {
   // handle error   
}
try {
   y = Function2();
}
catch(SomeOtherException e) {
   // handle error   
}

反对:

int error;
Result x, y;
error = Function1(out x);
if(error != SOME_KNOWN_ISSUE) {
   // handle error    
}
error = Function2(out y);
if(error != SOME_KNOWN_ISSUE) {
   // handle error
}

差别不大。请不要告诉我你不会检查错误代码。但是,如果您决定忽略所有错误(一个可怕的想法),那么异常会更简单:

try {
    var x = Function1();
    var y = Function2();    
    var z = Function3();
}
catch Exception() { you still can see the message here and possibly rethrow }

对比

Result1 r1;
Function1(out r1);
Result2 r2;
Function2(out r2);
Result3 r3;
Function3(out r3);
// and here you still don't know whether there was an error

您能否详细说明“我需要关于时间限制的可预测性”是什么意思?

在某些系统级软件或实时的东西中,您无法承受与异常处理相关的堆栈展开,因为您无法保证持续时间,这可能会违反您的时序要求。但在 .NET 中从来没有这种情况,因为垃圾收集在这方面要糟糕得多。

此外,当您说“在 .NET 中,我总是使用异常处理错误”时,您能否解释一下您如何或将什么定义为错误条件?可恢复的情况是错误条件还是不是错误条件?——</p>

@shambulator 已经在评论中给出了一个很好的例子。在FileStream,丢失的文件是不可恢复的,它会抛出。在它的客户端中,FileStream它可能是可恢复的,也可能是不可恢复的,具体取决于上下文。有些客户端会忽略它,有些会退出应用程序,有些会将其包装在另一个异常中并让上游的人来决定。

什么时候不使用异常?

在那些我也不会返回错误代码的情况下。

于 2013-03-08T16:23:21.197 回答
1

将一个公共FunctionResult对象用作out您不想抛出异常的所有方法的参数怎么样?

public class FuncResultInfo
    {
        public bool ExecutionSuccess { get; set; }
        public string ErrorCode { get; set; }
        public ErrorEnum Error { get; set; }
        public string CustomErrorMessage { get; set; }

        public FuncResultInfo()
        {
            this.ExecutionSuccess = true;
        }

        public enum ErrorEnum
        {
            ErrorFoo,
            ErrorBar,
        }
    }

    public static class Factory
    {
        public static int GetNewestItemId(out FuncResultInfo funcResInfo)
        {
            var i = 0;
            funcResInfo = new FuncResultInfo();

            if (true) // whatever you are doing to decide if the function fails
            {
                funcResInfo.Error = FuncResultInfo.ErrorEnum.ErrorFoo;
                funcResInfo.ErrorCode = "234";
                funcResInfo.CustomErrorMessage = "Er mah gawds, it done blewed up!";
            }
            else
            {
                i = 5; // whatever.
            }

            return i;
        }
    }

确保所有可能无异常失败的函数都具有该out参数FuncResultInfo

于 2013-03-08T15:31:14.057 回答
1

我在 ms-access 中广泛使用了 FunctionResult 方法,它的效果非常好。我认为它比错误处理好得多。首先,每个错误消息都是特定于应用程序的,而不是通常偏离目标的默认错误消息。如果错误向上传播函数的调用列表,则可以将错误消息以菊花链形式链接在一起。这个最终的错误消息看起来像一个调用堆栈,但更清晰,例如[无法从驱动器 F 读取照片:,无法读取文件,驱动器未准备好]。Wacko,我刚刚发现一些驱动器可以安装但尚未准备好。我无法对该错误进行单元测试,因为我不知道可能会发生这样的错误(意味着 SD 卡读卡器为空)。然而,即使事先不知道这个错误,我也可以编写一个优雅地处理它的应用程序。

我的方法是调用类中的方法,该方法被编写为返回布尔值的函数。返回值在函数的最后一行设置为 True,因此如果函数在最后一行之前退出,默认情况下是不成功的。我编码,调用函数看起来像 if getphotos(folderID) then...do something .. Else 报告错误。类模块内部是一个模块级错误变量 (Str meM),它是通过 getter 读取的,因此该类有一个 .em 属性来保存错误消息。我还有一个注释变量,有时用作错误消息,例如,如果文件夹为空,则查找照片的代码有效但未返回任何照片。这不会是一个错误,但它是我可能想要与调用程序通信的东西。如果有错误,用户将收到一条错误消息,调用过程将退出。相反,如果有一个 cmt,例如“没有照片”,那么我可能会尝试阅读照片元数据。Zdeslav Vojkovic 如何处理带有例外的微妙之处?

我正在转向 C#,因此找到了这个线程。我喜欢知道为什么函数调用失败的确定性(我一直与数据库和文件系统交互,所以我很难用单元测试来覆盖我的项目)。我同意 Zdeslav Vojkovic 关于使用标准的异常,但不会在我自己的代码中这样做。我正在寻找一种简洁的设计模式,它允许我验证被调用函数中的参数,并在参数不正确时通知调用者。

于 2015-12-04T06:28:59.537 回答