ChatGPT解决这个技术问题 Extra ChatGPT

为什么不使用异常作为常规控制流?

为了避免我在谷歌上搜索到的所有标准答案,我将提供一个大家都可以随意攻击的例子。

C# 和 Java(以及太多其他)有很多类型的一些我根本不喜欢的“溢出”行为(例如 type.MaxValue + type.SmallestValue == type.MinValue 例如:int.MaxValue + 1 == int.MinValue)。

但是,看到我的恶毒本性,我将通过将此行为扩展为,假设是 Overridden 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 可以很容易地解决这个问题,但事实仍然是我只是看不到为什么你不能使用异常(从逻辑上讲,我可以看到当性能是一个问题时,在某些情况下应该避免异常)。

我认为在许多情况下,它们比 if 结构更清晰,并且不会破坏该方法正在制定的任何合同。

恕我直言,“永远不要将它们用于常规程序流程”的反应似乎每个人都没有那么好,因为这种反应的强度可以证明是合理的。

还是我弄错了?

我读过其他帖子,处理各种特殊情况,但我的观点是,如果你们都是这样的话,这没有什么问题:

清除遵守您的方法的合同

射击我。

+1 我也有同感。除了性能之外,避免控制流异常的唯一充分理由是调用者代码的返回值将更具可读性。
是:如果发生某些事情则返回-1,如果发生其他事情则返回-2,等等...真的比异常更具可读性吗?
一个人因为说真话而受到负面声誉是可悲的:你的例子不可能用 if 语句编写。 (这并不是说它是正确/完整的。)
我会争辩说,有时抛出异常可能是你唯一的选择。例如,我有一个业务组件,它通过查询数据库在其构造函数中初始化其内部状态。有时,数据库中没有适当的数据可用。在构造函数中抛出异常是有效取消对象构造的唯一方法。这在类的合同(在我的例子中是 Javadoc)中明确说明,所以我没有问题客户端代码可以(并且应该)在创建组件时捕获该异常并从那里继续。
既然你提出了一个假设,那么你也有责任引用确凿的证据/理由。对于初学者,请说出您的代码优于更短、自记录的 if 语句的一个 原因。你会发现这很难。换句话说:您的前提是有缺陷的,因此您从中得出的结论是错误的。

B
Brann

您是否尝试过调试在正常运行过程中每秒引发 5 个异常的程序?

我有。

该程序非常复杂(它是一个分布式计算服务器),在程序的一侧稍作修改就可以很容易地在完全不同的地方破坏某些东西。

我希望我可以启动程序并等待异常发生,但是在正常操作过程中启动期间大约有 200 个异常

我的观点:如果您在正常情况下使用异常,您如何定位异常(即异常)情况?

当然,还有其他强有力的理由不要过多地使用异常,尤其是在性能方面


示例:当我调试 .net 程序时,我从 Visual Studio 启动它,并要求 VS 中断所有异常。如果您将异常作为预期行为,那么我不能再这样做了(因为它会中断 5 次/秒),并且定位代码有问题的部分要复杂得多。
+1 指出您不想创建一个异常干草堆来查找实际的异常针。
根本没有得到这个答案,我认为人们在这里误解了它根本与调试无关,而是与设计有关。恐怕这是纯粹形式的循环推理。你的观点真的是除了前面所说的问题
@Peter:在不中断异常的情况下进行调试很困难,如果设计中有很多异常,那么捕获所有异常是很痛苦的。我认为使调试变得困难的设计几乎被部分破坏(换句话说,设计与调试有关,IMO)
即使忽略我要调试的大多数情况不对应于抛出的异常这一事实,您的问题的答案是:“按类型”,例如,我会告诉我的调试器只捕获 AssertionError 或 StandardError 或其他东西对应坏事的发生。如果您对此有疑问,那么您将如何进行日志记录——您不按级别和类进行日志记录,以便您可以过滤它们吗?你也觉得这是个坏主意吗?
g
glmxndr

异常基本上是非本地 goto 语句,具有后者的所有后果。使用流控制异常违反了principle of least astonishment,使程序难以阅读(请记住,程序首先是为程序员编写的)。

此外,这不是编译器供应商所期望的。他们希望很少抛出异常,并且他们通常让 throw 代码效率很低。抛出异常是 .NET 中最昂贵的操作之一。

但是,某些语言(尤其是 Python)使用异常作为流控制结构。例如,如果没有其他项目,迭代器会引发 StopIteration 异常。甚至标准语言结构(例如 for)也依赖于此。


嘿,例外并不令人惊讶!当你说“这是一个坏主意”然后继续说“但在 python 中这是一个好主意”时,你有点自相矛盾。
我仍然完全不相信:1)效率是除了问题之外,许多非 bacht 程序不在乎(例如用户界面) 2)令人惊讶:就像我说的那样令人惊讶,因为它没有被使用,但是问题仍然存在:为什么不首先使用 id ?但是,既然这是答案
+1 实际上,我很高兴您指出了 Python 和 C# 之间的区别。我不认为这是一个矛盾。 Python 更具动态性,并且以这种方式使用异常的期望已融入到语言中。它也是 Python EAFP 文化的一部分。我不知道哪种方法在概念上更纯粹或更自洽,但我确实喜欢编写其他人期望它做的代码的想法,这意味着不同语言的不同风格。
当然,与 goto 不同,异常正确地与您的调用堆栈和词法范围进行交互,并且不会让堆栈或范围一团糟。
实际上,大多数 VM 供应商都期望异常,并有效地处理它们。正如@LukasEder 所指出的,异常与 goto 完全不同,因为它们是结构化的。
c
cwap

我的经验法则是:

如果您可以采取任何措施从错误中恢复,请捕获异常

如果错误很常见(例如,用户尝试使用错误的密码登录),请使用返回值

如果您无法从错误中恢复,请不要将其捕获(或者在您的主要捕获器中捕获它以对应用程序进行一些半优雅的关闭)

我看到的异常问题是从纯语法的角度来看的(我很确定性能开销是最小的)。我不喜欢到处都是try-blocks。

举个例子:

try
{
   DoSomeMethod();  //Can throw Exception1
   DoSomeOtherMethod();  //Can throw Exception1 and Exception2
}
catch(Exception1)
{
   //Okay something messed up, but is it SomeMethod or SomeOtherMethod?
}

.. 另一个例子可能是当您需要使用工厂将某些内容分配给句柄时,该工厂可能会抛出异常:

Class1 myInstance;
try
{
   myInstance = Class1Factory.Build();
}
catch(SomeException)
{
   // Couldn't instantiate class, do something else..
}
myInstance.BestMethodEver();   // Will throw a compile-time error, saying that myInstance is uninitalized, which it potentially is.. :(

Soo,就个人而言,我认为您应该为罕见的错误条件(内存不足等)保留异常,并使用返回值(值类、结构或枚举)来进行错误检查。

希望我理解你的问题是正确的:)


回复:您的第二个示例-为什么不在构建之后将对 BestMethodEver 的调用放在 try 块中?如果 Build() 抛出异常,它不会被执行,编译器很高兴。
是的,这可能是您最终会得到的结果,但考虑一个更复杂的示例,其中 myInstance-type 本身可以引发异常。方法范围内的其他实例也可以。你最终会得到很多嵌套的 try/catch 块:(
您应该在 catch 块中进行异常转换(到适合抽象级别的异常类型)。仅供参考:“Multi-catch”应该进入 Java 7。
仅供参考:在 C++ 中,您可以在尝试捕获不同的异常之后放置多个捕获。
对于shrinkwrap 软件,您需要捕获所有异常。至少设置一个对话框,解释程序需要关闭,这里有一些难以理解的东西你可以发送一个错误报告。
P
Peter

对很多答案的第一反应:

您正在为程序员和最小惊讶原则写作

当然!但是,如果只是不是一直更清楚。

这不应该令人惊讶,例如:divide (1/x) catch (divisionByZero) 对我来说比任何 if 都更清楚(在 Conrad 和其他人)。不期望这种编程的事实纯粹是传统的,实际上仍然是相关的。也许在我的例子中 if 会更清楚。

但是 DivisionByZero 和 FileNotFound 在这方面比 ifs 更清楚。

当然,如果它的性能较低并且每秒需要大量时间,您当然应该避免它,但我仍然没有读到任何避免整体设计的充分理由。

就最小惊讶原则而言:这里存在循环推理的危险:假设整个社区使用了一个糟糕的设计,这个设计将成为预期!因此,该原则不能成为圣杯,应慎重考虑。

正常情况的例外情况,您如何定位异常(即异常)情况?

在许多反应中。像这样闪耀着低谷。抓住他们,不是吗?你的方法应该是清晰的,有据可查的,并且遵守它的合同。我没有得到这个我必须承认的问题。

调试所有异常:相同,有时只是这样做,因为不使用异常的设计很常见。我的问题是:为什么它首先很常见?


1) 在调用 1/x 之前,您是否总是检查 x? 2) 你是否将每个除法操作包装到一个 try-catch 块中以捕获 DivideByZeroException? 3) 你在 catch 块中放入了什么逻辑来从 DivideByZeroException 中恢复?
除了 DivisionByZero 和 FileNotFound 是不好的例子,因为它们是应该被视为例外的例外情况。
没有什么比这里的人吹捧的“反异常”的方式“未找到文件”“过于异常”了。 openConfigFile(); 后面可以是捕获的 FileNotFound,{ createDefaultConfigFile(); setFirstAppRun(); } FileNotFound 异常处理得当;没有崩溃,让我们让最终用户的体验更好,而不是更糟。你可能会说“但如果这不是真正的第一次运行并且他们每次都得到它怎么办?”至少应用程序每次都运行,而不是每次启动都崩溃!在 1 到 10 的情况下,“这太糟糕了”:“首次运行”每次启动 = 3 或 4,crash 每次启动 = 10。
你的例子是例外。不,您不必总是在调用 1/x 之前检查 x,因为它通常没问题。例外情况是它不好的情况。我们在这里不是在谈论惊天动地的事情,但是例如对于给定随机 x 的基本整数,4294967296 中只有 1 将无法进行除法。这是例外,例外是处理它的好方法。但是,您可以使用异常来实现 switch 语句的等效项,但这很愚蠢。
n
necromancer

在异常之前,在 C 中,有 setjmplongjmp 可用于完成类似的堆栈帧展开。

然后给相同的构造一个名称:“异常”。并且大多数答案都依靠这个名字的含义来争论它的用法,声称异常是为了在异常情况下使用。这绝不是原始 longjmp 的意图。在某些情况下,您需要中断许多堆栈帧的控制流。

异常稍微更普遍,因为您也可以在同一个堆栈帧中使用它们。这引发了与 goto 的类比,我认为这是错误的。 Gotos 是紧耦合对(setjmplongjmp 也是如此)。异常遵循松散耦合的发布/订阅,它更干净!因此,在同一个堆栈帧中使用它们与使用 goto 几乎是一回事。

第三个混淆来源与它们是检查异常还是未检查异常有关。当然,未经检查的异常对于控制流和许多其他事情来说似乎特别糟糕。

然而,一旦你克服了所有维多利亚时代的挂断并活了一点,检查异常对于控制流来说非常有用。

我最喜欢的用法是一长段代码中的 throw new Success() 序列,它一个接一个地尝试,直到找到它正在寻找的东西。每件事——每条逻辑——可能有任意嵌套,所以 break 和任何类型的条件测试一样都被排除在外。 if-else 模式很脆弱。如果我编辑出 else 或以其他方式弄乱语法,那么就会出现一个毛茸茸的错误。

使用throw new Success() 线性化 代码流。我使用本地定义的 Success 类——当然检查过——这样如果我忘记捕捉它,代码将无法编译。而且我没有发现其他方法的Success

有时我的代码会一个接一个地检查,只有在一切正常的情况下才会成功。在这种情况下,我使用 throw new Failure() 进行了类似的线性化。

使用单独的函数会破坏自然的划分级别。所以 return 解决方案不是最优的。出于认知原因,我更喜欢在一个地方放一两页代码。我不相信超精细划分的代码。

除非有热点,否则 JVM 或编译器所做的与我无关。我无法相信编译器有任何根本原因不检测本地抛出和捕获的异常,而是简单地将它们视为机器代码级别的非常有效的goto

至于在控制流的函数中使用它们——即用于常见情况而不是特殊情况——我看不出它们的效率如何低于多次中断、条件测试、返回涉水通过三个堆栈帧而不是仅仅恢复堆栈指针。

我个人不跨堆栈框架使用该模式,我可以看到它需要设计复杂性才能优雅地做到这一点。不过少用应该没问题。

最后,对于令人惊讶的处女程序员,这不是一个令人信服的理由。如果你轻轻地向他们介绍这种做法,他们就会学会喜欢它。我记得 C++ 曾经让 C 程序员大吃一惊。


使用这种模式,我的大多数粗略函数最后都有两个小问题——一个代表成功,一个代表失败,这就是函数包装诸如准备正确的 servlet 响应或准备返回值之类的东西的地方。有一个地方做总结很好。 return 模式替代方案将需要两个函数用于每个此类函数。一个用于准备 servlet 响应或其他此类操作的外部设备,以及一个用于执行计算的内部设备。 PS:一位英语教授可能会建议我在最后一段中使用“惊人”而不是“令人惊讶” :-)
m
mouviciel

标准答案是异常不是常规的,应该在异常情况下使用。

对我来说很重要的一个原因是,当我在我维护或调试的软件中读取 try-catch 控制结构时,我试图找出为什么原始编码器使用异常处理而不是 if-else 结构。我希望找到一个好的答案。

请记住,您不仅为计算机编写代码,而且还为其他编码人员编写代码。有一个与异常处理程序相关联的语义,你不能仅仅因为机器不介意就丢弃它。


我认为一个被低估的答案。当计算机发现异常被吞下时,它可能不会减慢很多,但是当我在处理别人的代码并且遇到它时,它会阻止我在我的轨道上死去,而我是否错过了一些我没有做的重要事情不知道,或者是否实际上没有理由使用这种反模式。
G
Guru Stron

Josh Bloch 在 Effective Java 中广泛讨论了这个主题。他的建议很有启发性,也应该适用于 .NET(细节除外)。

特别是,应在特殊情况下使用例外。其原因主要与可用性有关。为了使给定的方法最大程度地可用,它的输入和输出条件应该受到最大程度的约束。

例如,第二种方法比第一种更容易使用:

/**
 * Adds two positive numbers.
 *
 * @param addend1 greater than zero
 * @param addend2 greater than zero
 * @throws AdditionException if addend1 or addend2 is less than or equal to zero
 */
int addPositiveNumbers(int addend1, int addend2) throws AdditionException{
  if( addend1 <= 0 ){
     throw new AdditionException("addend1 is <= 0");
  }
  else if( addend2 <= 0 ){
     throw new AdditionException("addend2 is <= 0");
  }
  return addend1 + addend2;
}

/**
 * Adds two positive numbers.
 *
 * @param addend1 greater than zero
 * @param addend2 greater than zero
 */
public int addPositiveNumbers(int addend1, int addend2) {
  if( addend1 <= 0 ){
     throw new IllegalArgumentException("addend1 is <= 0");
  }
  else if( addend2 <= 0 ){
     throw new IllegalArgumentException("addend2 is <= 0");
  }
  return addend1 + addend2;
}

无论哪种情况,您都需要检查以确保调用者正确地使用了您的 API。但在第二种情况下,您需要它(隐式)。如果用户没有阅读 javadoc,仍然会抛出软异常,但是:

你不需要记录它。您不需要对其进行测试(取决于您的单元测试策略的激进程度)。您不需要调用者处理三个用例。

最基本的一点是,不应将异常用作返回码,这主要是因为您不仅使您的 API 变得复杂,而且还使调用者的 API 变得复杂。

当然,做正确的事是有代价的。代价是每个人都需要了解他们需要阅读和遵循文档。希望无论如何都是这样。


J
James Koch

性能怎么样?在对 .NET Web 应用程序进行负载测试时,我们在每台 Web 服务器上模拟了 100 个用户,直到我们修复了一个常见的异常并且这个数字增加到 500 个用户。


p
paweloque

我认为您可以使用 Exceptions 进行流量控制。然而,这种技术也有另一面。创建异常是一件代价高昂的事情,因为它们必须创建堆栈跟踪。因此,如果您想更频繁地使用异常而不是仅仅发出异常情况的信号,您必须确保构建堆栈跟踪不会对您的性能产生负面影响。

降低创建异常成本的最佳方法是重写 fillInStackTrace() 方法,如下所示:

public Throwable fillInStackTrace() { return this; }

这样的异常不会填充任何堆栈跟踪。


堆栈跟踪还要求调用者“了解”(即依赖)堆栈中的所有 Throwable。这是一件坏事。抛出适合抽象级别的异常(Services 中的 ServiceExceptions,Dao 方法中的 DaoExceptions 等)。如有必要,只需翻译。
除了使用 raiserescue 处理普通异常之外,Ruby 编程语言还有第二个功能,它允许您使用 throwcatch 符号。我认为这自然地避免了创建堆栈跟踪的影响,并且只是将一小段文本扔给堆栈上的下一个捕手。
V
Vladimir

以下是我在 blog post 中描述的最佳做法:

抛出异常以说明软件中的意外情况。

使用返回值进行输入验证。

如果您知道如何处理库抛出的异常,请尽可能在最低级别捕获它们。

如果出现意外异常,请完全放弃当前操作。不要假装你知道如何对付他们。


J
Jason Punyon

我真的不明白你是如何在你引用的代码中控制程序流的。除了 ArgumentOutOfRange 异常之外,您永远不会看到另一个异常。 (所以你的第二个 catch 子句永远不会被击中)。您所做的只是使用极其昂贵的 throw 来模仿 if 语句。

此外,您并没有执行更险恶的操作,您只是抛出一个异常,纯粹是为了让它在其他地方被捕获以执行流控制。你实际上是在处理一个例外情况。


S
Sean

除了上述原因之外,不使用异常进行流控制的一个原因是它会使调试过程变得非常复杂。

例如,当我试图追踪 VS 中的错误时,我通常会打开“中断所有异常”。如果您使用异常进行流控制,那么我将定期中断调试器,并且必须继续忽略这些非异常异常,直到遇到真正的问题。这可能会让某人发疯!


我已经处理了一个更高的问题:调试所有异常:相同,刚刚完成,因为不使用异常的设计很常见。我的问题是:为什么它首先很常见?
那么你的回答基本上是“这很糟糕,因为 Visual Studio 有这个功能......”?我已经编程了大约 20 年,我什至没有注意到有一个“打破所有异常”选项。不过,“因为这个功能!”听起来是个软弱的理由。只需将异常追溯到其来源即可;希望您使用的语言使这变得容易-否则您的问题在于语言功能,而不是异常本身的一般用法。
G
Gambrinus

因为代码难读,调试时可能会遇到麻烦,时间长了在修复bug时会引入新的bug,在资源和时间上比较昂贵,而且如果你在调试你的代码和它会很烦人调试器在每个异常发生时停止;)


k
kender

假设您有一种方法可以进行一些计算。它必须验证许多输入参数,然后返回一个大于 0 的数字。

使用返回值来表示验证错误,很简单:如果方法返回的数字小于 0,则发生错误。那么如何判断哪个参数没有验证?

我记得在我的 C 时代,很多函数都返回了这样的错误代码:

-1 - x lesser then MinX
-2 - x greater then MaxX
-3 - y lesser then MinY

等等

它真的比抛出和捕获异常更不可读吗?


这就是他们发明枚举的原因 :) 但神奇的数字是一个完全不同的话题.. en.wikipedia.org/wiki/…
很好的例子。我正要写同样的东西。 @IsakSavo:如果期望该方法返回一些有意义的值或对象,则枚举在这种情况下没有帮助。例如 getAccountBalance() 应该返回一个 Money 对象,而不是 AccountBalanceResultEnum 对象。许多 C 程序都有类似的模式,其中一个标记值(0 或 null)表示错误,然后您必须调用另一个函数来获取单独的错误代码,以确定错误发生的原因。 (MySQL C API 是这样的。)
B
Bryan Watts

您可以使用锤子的爪子转动螺丝,就像您可以使用例外来控制流一样。这并不意味着它是该功能的预期用途if 语句表示条件,其预期用途 控制流。

如果您以非预期的方式使用某个功能,而选择不使用为此目的而设计的功能,则会产生相关成本。在这种情况下,清晰度和性能不会受到真正的附加值的影响。与广泛接受的 if 声明相比,使用异常能为您带来什么?

换一种说法:仅仅因为你可以并不意味着你应该。


您是说在我们获得 if 以供正常使用之后不需要例外,还是不打算使用 execptions 因为它不是故意的(循环参数)?
@Val:异常是针对异常情况的——如果我们检测到足以引发异常并处理它,我们就有足够的信息不抛出它并仍然处理它。我们可以直接进入处理逻辑,跳过昂贵的、多余的 try/catch。
按照这种逻辑,您最好没有异常,并且总是执行系统退出而不是抛出异常。如果您想在退出之前做任何事情,请制作一个包装器并调用它。 Java 示例:public class ExitHelper{ public static void cleanExit() { cleanup(); System.exit(1); } } 然后只需调用它而不是抛出:ExitHelper.cleanExit(); 如果您的论点是合理的,那么这将是首选方法,并且不会有例外。您基本上是在说“异常的唯一原因是以不同的方式崩溃。”
@Aaron:如果我同时抛出和捕获异常,我就有足够的信息来避免这样做。这并不意味着所有异常都会突然致命。我无法控制的其他代码可能会捕获它,这很好。我的论点仍然是合理的,即在同一上下文中抛出和捕获异常是多余的。我没有,也不会声明所有异常都应该退出该过程。
@BryanWatts 承认。许多其他人已经说过,您应该只对无法恢复的任何事情使用异常,并且通过扩展应该始终在异常上崩溃。这就是为什么很难讨论这些事情的原因;不仅有两种意见,而且很多。我仍然不同意你的观点,但不是很强烈。有时 throw/catch 是最易读、可维护、最好看的代码;通常,如果您已经在捕获其他异常,因此您已经拥有 try/catch 并且添加 1 或 2 个捕获比 if 单独的错误检查更干净,通常会发生这种情况。
L
Lukas Eder

正如其他人多次提到的那样,the principle of least astonishment 将禁止您将异常过度用于控制流目的。另一方面,没有规则是 100% 正确的,并且总是有例外是“恰到好处的工具”的情况——顺便说一下,就像 goto 本身一样,它以 break 的形式提供,并且continue 在 Java 之类的语言中,这通常是跳出重度嵌套循环的完美方式,而这种循环并不总是可以避免的。

以下博客文章解释了一个相当复杂但也相当有趣的 non-local ControlFlowException 用例:

http://blog.jooq.org/2013/04/28/rare-uses-of-a-controlflowexception

它解释了在 jOOQ (a SQL abstraction library for Java) 内部,当满足某些“罕见”条件时,偶尔会使用此类异常来提前中止 SQL 呈现过程。

这种情况的例子是:

遇到太多绑定值。某些数据库在其 SQL 语句中不支持任意数量的绑定值(SQLite:999、Ingres 10.1.0:1024、Sybase ASE 15.5:2000、SQL Server 2008:2100)。在这些情况下,jOOQ 中止 SQL 呈现阶段并使用内联绑定值重新呈现 SQL 语句。示例: // 附加一个“处理程序”的伪代码,一旦超出绑定值的最大数量 // 就会中止查询呈现: context.attachBindValueCounter();字符串 sql; try { // 在大多数情况下,这会成功: sql = query.render(); } catch (ReRenderWithInlinedVariables e) { sql = query.renderWithInlinedBindValues();如果我们每次都显式地从查询 AST 中提取绑定值来计算它们,那么我们将浪费宝贵的 CPU 周期来处理那些 99.9% 没有遇到此问题的查询。

某些逻辑只能通过我们只想“部分”执行的 API 间接获得。 UpdatableRecord.store() 方法根据 Record 的内部标志生成 INSERT 或 UPDATE 语句。从“外部”来看,我们不知道 store() 中包含什么样的逻辑(例如乐观锁定、事件侦听器处理等),因此当我们将多条记录存储在一个批处理语句,我们希望 store() 只生成 SQL 语句,而不是实际执行它。示例: // 附加“处理程序”的伪代码 // 阻止查询执行并抛出异常 // 相反: context.attachQueryCollector(); // 收集每个存储操作的 SQL for (int i = 0; i < records.length; i++) { try { records[i].store(); } // 附加的处理程序将导致这个 // 异常被抛出,而不是实际 // 将记录存储到数据库中 catch (QueryCollectorException e) { // 在呈现后抛出异常 // SQL 语句可用 queries.add( e.查询());如果我们将 store() 逻辑外部化为“可重用”API,可以定制为选择性地不执行 SQL,我们会考虑创建一个相当难以维护、几乎不可重用的 API。

结论

从本质上讲,我们对这些非本地 goto 的使用与 [Mason Wheeler][5] 在他的回答中所说的一致:

“我刚刚遇到了一个我现在无法正确处理的情况,因为我没有足够的上下文来处理它,但是调用我的例程(或调用堆栈更远的东西)应该知道如何处理它。”

与它们的替代方案相比,ControlFlowExceptions 的两种用法都相当容易实现,这使我们能够重用范围广泛的逻辑,而无需从相关的内部结构中对其进行重构。

但是对于未来的维护者来说,这仍然是一种意外的感觉。代码感觉相当微妙,虽然在这种情况下它是正确的选择,但我们总是不希望对 local 控制流使用异常,因为很容易避免通过 if - else 使用普通分支.


佚名

通常,在低级别处理异常本身并没有错。异常是一条有效消息,它提供了很多关于为什么无法执行操作的详细信息。如果你能处理它,你应该这样做。

一般来说,如果您知道可以检查的失败概率很高...您应该进行检查...即 if(obj != null) obj.method()

在您的情况下,我对 C# 库不够熟悉,无法知道日期时间是否有一种简单的方法来检查时间戳是否超出范围。如果是这样,只需调用 if(.isvalid(ts)) 否则您的代码基本上没问题。

所以,基本上它归结为任何一种方法可以创建更清晰的代码......如果防止预期异常的操作比处理异常更复杂;比你有我的许可来处理异常而不是到处创建复杂的守卫。


补充一点:如果您的 Exception 提供了失败捕获信息(像“Param getWhatParamMessedMeUp()”这样的 getter),它可以帮助您的 API 用户就下一步做什么做出正确的决定。否则,您只是为错误状态命名。
佚名

如果您将异常处理程序用于控制流,那么您就太笼统和懒惰了。正如其他人提到的,如果您在处理程序中处理处理,您知道发生了一些事情,但究竟是什么?本质上,如果您将异常用于控制流,则您将异常用于 else 语句。

如果您不知道可能发生的状态,那么您可以使用异常处理程序来处理意外状态,例如当您必须使用第三方库时,或者您必须捕获 UI 中的所有内容以显示一个不错的错误消息并记录异常。

但是,如果您确实知道可能会出现什么问题,并且您没有使用 if 语句或其他东西来检查它,那么您只是懒惰。让异常处理程序成为你知道可能发生的事情的包罗万象是懒惰的,它稍后会回来困扰你,因为你将试图根据一个可能错误的假设来修复你的异常处理程序中的情况。

如果您将逻辑放在异常处理程序中以确定究竟发生了什么,那么您将非常愚蠢,因为您没有将该逻辑放在 try 块中。

异常处理程序是最后的手段,因为当您用尽想法/方法来阻止某些事情出错,或者事情超出您的控制能力时。例如,服务器已关闭并超时,您无法阻止抛出该异常。

最后,预先完成所有检查表明您知道或期望会发生什么,并使其明确。代码的意图应该是明确的。你更愿意读什么?


根本不正确:“本质上,如果您将异常用于控制流,则您将异常用于 else 语句。”如果将其用于控制流,则您知道准确捕获的内容,并且从不使用一般捕获,但是当然是具体的!
s
simon

您可能有兴趣查看 Common Lisp 的条件系统,它是对正确处理异常的一种概括。因为您可以以受控方式展开或不展开堆栈,所以您也可以“重新启动”,这非常方便。

这与其他语言的最佳实践没有太大关系,但它向您展示了(大致)您正在考虑的方向上的一些设计思想可以做什么。

当然,如果您像溜溜球一样在堆栈上上下弹跳,仍然需要考虑性能,但它比大多数捕获/抛出异常系统所体现的“哦,废话,让保释”这种方法更为笼统。


J
Jörg W Mittag

我认为使用异常进行流量控制没有任何问题。异常有点类似于延续,在静态类型的语言中,异常比延续更强大,所以,如果你需要延续但你的语言没有它们,你可以使用异常来实现它们。

好吧,实际上,如果您需要延续而您的语言没有它们,那么您选择了错误的语言,您应该使用不同的语言。但有时你别无选择:客户端 Web 编程就是最好的例子——没有办法绕过 JavaScript。

一个示例:Microsoft Volta 是一个允许在直接 .NET 中编写 Web 应用程序的项目,并让框架负责确定哪些位需要在哪里运行。这样做的一个后果是 Volta 需要能够将 CIL 编译为 JavaScript,以便您可以在客户端上运行代码。但是,有一个问题:.NET 有多线程,而 JavaScript 没有。因此,Volta 使用 JavaScript 异常在 JavaScript 中实现延续,然后使用这些延续实现 .NET 线程。这样,使用线程的 Volta 应用程序可以编译为在未经修改的浏览器中运行——无需 Silverlight。


M
Mesh

但是您并不总是知道您调用的 Method/s 中发生了什么。您不会确切知道异常是在哪里引发的。无需更详细地检查异常对象....


I
Ingo

我觉得你的例子没有错。相反,忽略被调用函数抛出的异常是一种罪过。

在 JVM 中,抛出异常并没有那么昂贵,只需使用 new xyzException(...) 创建异常,因为后者涉及堆栈遍历。因此,如果您提前创建了一些异常,您可能会多次抛出它们而无需付出任何代价。当然,这种方式不能与异常一起传递数据,但我认为无论如何这是一件坏事。


抱歉,这完全是错误的,布兰。这取决于条件。条件并不总是微不足道的。因此,一个 if 语句可能需要数小时、数天甚至更长的时间。
在 JVM 中,就是这样。不比退货贵。去搞清楚。但问题是,如果不是被调用函数中已经存在的代码来区分异常情况和正常情况,你会在 if 语句中写什么——因此代码重复。
Ingo : 一个特殊的情况是你没有预料到的。即一个你没有想到的程序员。所以我的规则是“编写不抛出异常的代码”:)
我从不编写异常处理程序,我总是解决问题(除非我不能这样做,因为我无法控制错误代码)。而且我从不抛出异常,除非我编写的代码是供其他人使用的(例如库)。不告诉我矛盾吗?
我同意你不要疯狂抛出异常。但可以肯定的是,什么是“例外”是一个定义问题。例如,如果 String.parseDouble 无法提供有用的结果,则它会引发异常。它还应该做什么?返回南?非 IEEE 硬件呢?
s
supercat

语言可以通过一些通用机制允许方法退出而不返回值并展开到下一个“catch”块:

让方法检查堆栈帧以确定调用站点,并使用调用站点的元数据来查找有关调用方法中的 try 块的信息,或者调用方法存储其调用者地址的位置;在后一种情况下,检查调用者的调用者的元数据以确定与直接调用者相同的方式,重复直到找到一个尝试块或堆栈为空。这种方法给无异常情况增加了很少的开销(它确实排除了一些优化),但在发生异常时代价高昂。

让该方法返回一个“隐藏”标志,以区分正常返回和异常,并让调用者检查该标志并分支到“异常”例程(如果已设置)。该例程在无异常情况下增加了 1-2 条指令,但发生异常时的开销相对较小。

让调用者将异常处理信息或代码放在相对于堆栈返回地址的固定地址。例如,对于 ARM,不使用指令“BL 子程序”,可以使用以下序列:adr lr,next_instr b subroutine b handle_exception next_instr:

要正常退出,子程序只需执行 bx lrpop {pc};在异常退出的情况下,子例程要么在执行返回之前从 LR 中减去 4,要么使用 sub lr,#4,pc(取决于 ARM 的变体、执行模式等)。如果调用者的设计目的不是为了容纳它。

使用检查异常的语言或框架可能会受益于使用上述 #2 或 #3 之类的机制处理这些异常,而使用 #1 处理未检查异常。尽管 Java 中检查异常的实现相当麻烦,但如果有一种方法可以让调用站点可以说,基本上,“这个方法被声明为抛出 XX,但我不希望它永远这样做;如果确实如此,则作为“未检查”异常重新抛出。在以这种方式处理已检查异常的框架中,它们可能是流控制的有效手段,例如解析在某些情况下可能具有失败的可能性很高,但是失败应该返回与成功完全不同的信息。但是,我不知道有任何框架使用这种模式。相反,更常见的模式是使用上面的第一种方法(最低成本) - 异常情况,但抛出异常时的成本很高)适用于所有异常。


g
gzak

审美原因之一:

try 总是伴随着 catch,而 if 不一定伴随着 else。

if (PerformCheckSucceeded())
   DoSomething();

使用 try/catch,它变得更加冗长。

try
{
   PerformCheckSucceeded();
   DoSomething();
}
catch
{
}

那6行代码太多了。