为了避免所有我可以在谷歌上搜索到的标准答案,我将提供一个你们都可以随意攻击的例子。
c#和Java(以及其他很多语言)有很多类型,有些“溢出”行为我一点也不喜欢(例如type。MaxValue +类型。SmallestValue ==类型。MinValue,例如int。MaxValue + 1 = int.MinValue)。
但是,鉴于我的邪恶本性,我将通过将此行为扩展为重写DateTime类型来对这种伤害进行侮辱。(我知道DateTime在. net中是密封的,但为了这个例子,我使用了一种与c#完全相似的伪语言,除了DateTime没有密封之外)。
被覆盖的Add方法:
/// <summary>
/// Increments this date with a timespan, but loops when
/// the maximum value for datetime is exceeded.
/// </summary>
/// <param name="ts">The timespan to (try to) add</param>
/// <returns>The Date, incremented with the given timespan.
/// If DateTime.MaxValue is exceeded, the sum wil 'overflow' and
/// continue from DateTime.MinValue.
/// </returns>
public DateTime override Add(TimeSpan ts)
{
try
{
return base.Add(ts);
}
catch (ArgumentOutOfRangeException nb)
{
// calculate how much the MaxValue is exceeded
// regular program flow
TimeSpan saldo = ts - (base.MaxValue - this);
return DateTime.MinValue.Add(saldo)
}
catch(Exception anyOther)
{
// 'real' exception handling.
}
}
当然,如果可以很容易地解决这个问题,但事实仍然是,我不明白为什么不能使用异常(从逻辑上讲,我可以看到,当性能是一个问题时,在某些情况下应该避免异常)。
我认为在许多情况下,它们比if结构更清晰,并且不会破坏方法所做的任何契约。
恕我直言,“永远不要在常规程序流程中使用它们”的反应似乎并不是每个人都有,因为这种反应的力量可以证明。
还是我说错了?
我读过其他的帖子,处理各种特殊情况,但我的观点是,如果你们都是:
清晰的
尊重你的方法
拍我。
如果您对控制流使用异常处理程序,那么您就太笼统和懒惰了。正如其他人提到的,如果在处理程序中处理处理,就会发生一些事情,但究竟发生了什么?本质上,如果您将异常用于控制流,则您是在为else语句使用异常。
如果您不知道可能会发生什么状态,那么您可以使用异常处理程序处理意外状态,例如当您必须使用第三方库时,或者当您必须捕获UI中的所有内容以显示良好的错误消息并记录异常时。
但是,如果您知道哪里可能出错,而不使用if语句或其他检查方法,那么您就是懒惰。允许异常处理程序成为您知道可能发生的事情的所有人是懒惰的,它稍后会回来困扰您,因为您将试图根据可能错误的假设来修复异常处理程序中的情况。
如果您在异常处理程序中放入逻辑以确定究竟发生了什么,那么如果您没有将该逻辑放入try块中,那么您将非常愚蠢。
异常处理程序是最后的手段,当您无法阻止某些事情出错时,或者事情超出了您的控制能力。比如,服务器宕机并超时,而您无法阻止抛出异常。
最后,提前完成所有检查,显示您知道或预期会发生什么,并使其显式化。代码的意图应该清楚。你更愿意读什么?
如果您对控制流使用异常处理程序,那么您就太笼统和懒惰了。正如其他人提到的,如果在处理程序中处理处理,就会发生一些事情,但究竟发生了什么?本质上,如果您将异常用于控制流,则您是在为else语句使用异常。
如果您不知道可能会发生什么状态,那么您可以使用异常处理程序处理意外状态,例如当您必须使用第三方库时,或者当您必须捕获UI中的所有内容以显示良好的错误消息并记录异常时。
但是,如果您知道哪里可能出错,而不使用if语句或其他检查方法,那么您就是懒惰。允许异常处理程序成为您知道可能发生的事情的所有人是懒惰的,它稍后会回来困扰您,因为您将试图根据可能错误的假设来修复异常处理程序中的情况。
如果您在异常处理程序中放入逻辑以确定究竟发生了什么,那么如果您没有将该逻辑放入try块中,那么您将非常愚蠢。
异常处理程序是最后的手段,当您无法阻止某些事情出错时,或者事情超出了您的控制能力。比如,服务器宕机并超时,而您无法阻止抛出异常。
最后,提前完成所有检查,显示您知道或预期会发生什么,并使其显式化。代码的意图应该清楚。你更愿意读什么?
在异常之前,C语言中有setjmp和longjmp可以用来完成类似的堆栈帧展开。
然后,同样的构造被赋予了一个名称:“Exception”。大多数答案都依赖于这个名字的含义来争论它的用法,声称例外是为了在特殊情况下使用。这从来不是最初longjmp的意图。只是在某些情况下,您需要在许多堆栈帧之间中断控制流。
异常稍微更通用一些,因为您也可以在同一个堆栈框架中使用它们。我认为这与goto的类比是错误的。Gotos是紧密耦合的一对(setjmp和longjmp也是如此)。异常遵循松散耦合的发布/订阅,这要干净得多!因此,在相同的堆栈框架中使用它们与使用gotos几乎不是一回事。
混淆的第三个来源与它们是被检查的异常还是未检查的异常有关。当然,在控制流和许多其他事情上使用未检查异常似乎特别糟糕。
然而,受控异常对于控制流来说是很好的,一旦你克服了所有维多利亚式的困扰,并生活了一点。
我最喜欢的用法是在一长段代码中使用throw new Success()序列,它不断尝试一件事,直到找到它要找的东西。每一件事——每一段逻辑——都可能有任意的嵌套,因此break也会像任何类型的条件测试一样出现。如果-否则模式是脆弱的。如果我编辑了一个else,或者以其他方式弄乱了语法,那么就有一个毛茸茸的bug。
使用throw new Success()线性化代码流。我使用本地定义的成功类(当然是勾选的),这样如果我忘记捕获它,代码将无法编译。我没有捕捉到另一种方法的成功。
有时我的代码会逐一检查,只有在一切正常的情况下才会成功。在这种情况下,我使用throw new Failure()进行了类似的线性化。
使用单独的函数会破坏划分的自然级别。所以返回解不是最优的。出于认知上的原因,我更喜欢在一个地方放一两页代码。我不相信超精细分割的代码。
除非有一个热点,否则jvm或编译器做什么对我来说不太重要。我不相信编译器有任何根本原因不检测本地抛出和捕获的异常,而只是在机器代码级别上将它们视为非常有效的goto。
至于在控制流的函数之间使用它们——即在常见情况下而不是特殊情况下——我不明白它们为什么会比多次中断、条件测试、返回来遍历三个堆栈帧而不是仅仅恢复堆栈指针的效率更低。
我个人并没有跨堆栈框架使用这种模式,我可以理解它需要复杂的设计才能做到这一点。但适当地使用它应该没问题。
最后,关于令人惊讶的新手程序员,这不是一个令人信服的理由。如果你温和地引导他们练习,他们就会爱上它。我记得c++曾经让C程序员感到惊讶和害怕。
很多答案的第一反应是:
你是在为程序员和最小惊讶原则写东西
当然!但如果只是一直不是更清楚。
这并不令人惊讶,例如:divide (1/x) catch (divisionByZero)对我(Conrad和其他人)来说比任何if都更清楚。事实上,这种编程是不被期待的,纯粹是传统的,实际上,仍然是相关的。也许在我的例子中,if会更清楚。
但是在这个问题上,DivisionByZero和FileNotFound比if更清楚。
当然,如果它的性能较差,需要无数的每秒时间,你当然应该避免它,但我仍然没有读到任何好的理由来避免整体设计。
就最小惊讶原则而言:这里存在循环推理的危险:假设整个社区使用了糟糕的设计,那么这种设计将成为预期!因此,这个原则不能是一个圣杯,应该仔细考虑。
对于正常情况的例外,你如何定位异常情况?
在许多反应中,像这样的东西闪耀着光芒。抓住他们,不是吗?你的方法应该是清晰的,有良好的文档记录,并遵守它的契约。我必须承认,我不明白这个问题。
对所有异常进行调试:都是一样的,只是有时会这样做,因为不使用异常的设计很常见。我的问题是:为什么这么普遍?
我不认为使用异常来进行流控制有什么错。异常有点类似于延续,在静态类型语言中,异常比延续更强大,所以,如果你需要延续,但你的语言没有它们,你可以使用异常来实现它们。
好吧,实际上,如果你需要延续,而你的语言没有,你选择了错误的语言,你应该使用另一种语言。但有时你别无选择:客户端web编程就是最好的例子——没有办法绕过JavaScript。
An example: Microsoft Volta is a project to allow writing web applications in straight-forward .NET, and let the framework take care of figuring out which bits need to run where. One consequence of this is that Volta needs to be able to compile CIL to JavaScript, so that you can run code on the client. However, there is a problem: .NET has multithreading, JavaScript doesn't. So, Volta implements continuations in JavaScript using JavaScript Exceptions, then implements .NET Threads using those continuations. That way, Volta applications that use threads can be compiled to run in an unmodified browser – no Silverlight needed.