不确定何时抛出异常

7

我正在开发一个库,旨在通过RS232串行通信与外部设备通信。我正在考虑错误处理策略,异常似乎是报告错误的正确和行业标准方式。

因此,我阅读了一些关于异常的指南。其中一篇非常明确地指出我不应该担心性能损失:

不要使用错误代码,因为担心异常会对性能产生负面影响。

其他一篇告诉我在正常情况下不要抛出异常:

不要将异常用于正常或预期的错误,或用于正常的控制流程。

我无法清楚地划分正常/预期和其他情况之间的界限。例如,在我的库中,操作可能失败,因为:

  1. 设备无响应。(未连接电缆,设备未开启,波特率错误)
  2. 操作请求被设备拒绝,因为它无法验证该请求。
  3. 通信失败。(有人绊倒了电缆,设备突然断电)

我认为以上所有问题都可以视为预期问题,因为它们在实践中经常发生(事实上,许多营销白痴打电话给我解决他们的软件“问题”,结果发现他们没有将电缆连接到笔记本电脑)。因此,可能不应抛出异常,否则应用程序员将不得不在很多地方捕获这些异常(我认为太多的catch块也不是好事)。

另一方面,我也倾向于认为这些都是错误,我必须以某种方式向应用程序员报告,而异常似乎是这样做的方法。如果我不使用异常,我将需要使用某些错误代码或错误枚举来报告这些问题。(很丑陋,我知道)。

你认为我应该采取哪种方法?

9个回答

5
您正在开发一个库,这是其他应用程序将使用的组件。
因此,在您提到的预期情况下,我肯定会使用异常来向调用应用程序传达问题。您应该为每种情况定义自定义异常,然后清楚地记录它们可能发生的时间。
这样,应用程序客户端代码就可以决定如何最好地继续。只有客户端应用程序可以做出此决定,明确记录的异常有助于这一点。
自定义异常的最好之处在于,可以提供与问题/异常相关的多个有意义/有用的数据。此数据也被封装在一个对象中。与错误代码和返回值相比较,这样做更加便捷。
性能可能是个问题,但只有当异常在紧密循环或某些高活动情况中抛出时才会发生。为避免这种情况,您还可以应用.NET Framework使用的模式,即提供Try...()方法(例如TryParse()),以指示操作是否成功或失败。
无论哪种方式,我都会首先使用自定义异常,然后进行性能测试,以实际查看库中可能需要优化的某些部分。

关于性能,我只见过一次性能成为问题的情况,那时有数百个异常被连续抛出和捕获。我们有一个可以用两种方式完成的操作,一种有时可以工作但在失败时会抛出异常,另一种需要使用反射。我们决定如果不能不使用反射就应该使用反射作为后备方案。结果发现异常被频繁抛出,总是使用反射方法要快得多(差别是用户等待10秒还是几分之一秒)。 - Davy8

3
我会使用异常来实现以下方法(受设计合同的启发):
  • 在可能的情况下,提供布尔检查函数,告诉您是否可以安全地应用操作
  • 对于给定的操作,请将此类检查函数视为前提条件:如果成立,则可以安全地执行操作;否则,您会抛出异常。
这样,如果API用户可以使用if-then-else结构编写其关键逻辑。
如果出现意外情况(例如由于微妙的时间问题),则会抛出异常:开发人员可以捕获此异常并处理它。但请注意:这不一定是调用该方法的地方:它可以更高/更早地在调用堆栈中的一个集中处理所有奇怪异常的地方处理。
我已经在分析多百万行C程序的错误处理代码方面做了一些工作。这些基于编码标准,要求手动检查和传播错误代码。事实证明,开发人员不喜欢编写这种代码,他们会忘记它,并且很容易在其中犯错误。实际上,我们发现每1000行C代码有2个编码标准偏差(可以说是2个故障)。
  • 参见M. Bruntink,A. van Deursen和T. Tourwé的文章。《在基于成语的异常处理中发现错误》。发表于第28届国际软件工程大会(ICSE'06)论文集,ACM出版社,2006年,页码242-251。

总之:(1)我会使用布尔检查器(2)异常可以在调用栈更高的位置被捕获;(3)在实践中依赖错误代码是不安全的。


1
设计契约赢得胜利! - akuhn

2
异常情况下使用异常处理,这是它的设计初衷。可以这样理解:如果你假设你所依赖的所有内容大部分时间都是正常运行的,但某个方法X失败了,那么这就是一个异常情况,因为它通常不会发生,你应该定义异常来捕获这种情况。
在你的情况下,你假定设备处于开启状态。因此,在这种情况下,异常情况包括设备不接受连接、拒绝连接、不接受你发送的数据或者你没有收到它应该发送的数据等等。如果你的设备每天多次被关闭,那么你就预计它们会被关闭,因此在连接之前使用返回代码或“bool IsDeviceOn();”方法进行检查。
如果它是你在正常情况下期望发生的事情,比如查询设备的功能,但你想要的那个功能不可用,那么使用返回代码或布尔方法——例如“bool DoesDeviceHaveThisCapability();”,不要在这种情况下使用异常处理。
另一个例子(适用于GUI应用程序)是用户输入。不要使用异常处理,因为你确实希望用户输入不正确——我们并不完美。
我曾经因为在非异常情况下使用异常处理而遇到过严重的性能问题。一个例子是当我每天2-3次处理一个2GB的数据文件时。一些行中有格式为0.00的有效价格,而另一些则没有。我使用了FormatException来捕获那些没有价格的行。
两年后,当我有机会对其进行性能分析时,发现捕获异常的那行代码占用了80%的时间。我改用int的TryParse()方法,并获得了巨大的速度提升。
针对你的例子,我可能会这样使用:
1. 设备无响应——如果设备应该24/7开启,则使用异常;如果经常关闭,则使用返回代码 2. 操作未授权——异常 3. 通信失败——异常

我最终做的基本上就是你建议的。谢谢。 - Hemant

1

使用RS232,除非启用了硬件握手(大多数情况下人们不会这样做),否则您将无法从该线路中看到更多的数据。您无法确定设备是否连接,除非没有任何数据发送到PC。

我认为将1和3分类为RS232TimeoutError,将2分类为RS232AuthenticationError可能更合适。

一般来说,TimeoutError表示远程设备已锁定或未连接。身份验证错误有点像协议错误,但在通信正常的情况下,远程设备“拒绝”与PC建立连接,两者略有不同。

我认为将它们设置为异常是有充分理由的:在正常操作期间,您永远不会预期超时错误,也不会预期身份验证错误。


该库旨在用于与多个设备(一次最多32个设备)通信,并且必须尽可能快,因为它将在生产环境中使用。您认为经常引发异常会影响性能吗? - Hemant
1
如果速度必须快到抛出异常成为问题的地步,那么你需要用 C 语言编写库。如果你引发了超时异常,你不会立即返回并再次与硬件通信以看到另一个异常——你会忽略该设备,直到明确告知你要返回它。 - Mark Rushakoff
@Hemant:把问题反过来想。如果错误一直发生,你认为这会影响性能吗?你预计会经常出现错误吗?与在下一个抽象级别恢复错误所涉及的性能损失相比,使用异常带来的性能损失是否显著? - Stephen C

1

是否抛出异常取决于函数的类型。

  • 如果函数返回 X,但你无法确定有效的 X,则抛出异常。
  • 如果函数是一个操作(例如连接),但未能完成该操作,则抛出异常。
  • 如果函数是 TryX 类型,则不要抛出异常。

因此,我的意思是,你应该将问题从“我应该抛出异常吗?”推到“调用我的库的人需要哪些方法?”但要注意,你抛出的异常应该基于你提供的方法是明显的


1
不要将异常用于正常或预期的错误,也不要用于正常控制流程。
在方法实现中,避免故意引发异常以改变执行流程、处理特殊逻辑、特殊情况或处理正常或预期的错误。例如,以下函数应该删除异常处理(它正在处理正常或预期的错误,但是如注释所说,Convert.ToString 实际上不会失败)。由于需要在方法内部“设置”异常处理,因此会有轻微的性能损失。这不是一个重大的问题,但如果您在循环中调用此函数,则可能会变得显著。如果此方法在库中,则让任何异常冒泡到库的用户。(自定义异常是不同的,请参见 Ash 的答案。)
Public Function Nz(ByVal value As String, ByVal valueIfNothing As String) As String
    Try
        Dim sValue As String = System.Convert.ToString(value) 'using Convert.ToString on purpose
        If IsNothing(sValue) Then
            Return valueIfNothing 
        Else
            Return sValue
        End If
    Catch ex As Exception
        'Convert.ToString handles exceptions, but just in case...
        Debug.Fail("Nz() failed. Convert.ToString threw exception.")
        Return String.Empty
    End Try
End Function

这里是一个“更好”的方法实现:

Public Function Nz(ByVal value As String, ByVal valueIfNothing As String) As String
    Dim sResult As String = String.Empty
    Dim sValue As String = System.Convert.ToString(value) 'using Convert.ToString on purpose
    If IsNothing(sValue) Then
        sResult = valueIfNothing 
    Else
        sResult = sValue
    End If
    Return sResult
End Function

1
不要使用错误代码,因为担心异常会对性能产生负面影响。
避免将所有设计都作为返回带有“out”参数的true/false函数,仅仅是为了避免使用异常所带来的“想象中”的性能问题。

0
如果一个方法无法以调用者能够处理的方式执行其主要功能,那么该函数通常应通过返回值(或者很少通过写入由引用传递的变量)来指示此类失败。如果函数以调用者可能无法准备好处理的方式失败,则最好抛出异常。如果某些调用者可以准备好处理失败,而其他调用者则不能,则最好使用try/do模式。基本上,提供两种方法:DoSomething和TryDoSomething。DoSomething承诺它将成功或抛出异常。TryDoSomething承诺除非发生真正意外的情况,否则不会抛出异常;它将通过返回值指示“预期”的失败。

0
选择不必在抛出异常或返回错误代码之间做出。尝试在异常情况下返回异常对象。性能损失不在于创建异常,而在于抛出异常。调用者可以检查“null”返回值。返回的异常可以是函数返回值,也可以是多个“out”参数之一。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接