为了避免我在谷歌上搜索到的所有标准答案,我将提供一个大家都可以随意攻击的例子。
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 结构更清晰,并且不会破坏该方法正在制定的任何合同。
恕我直言,“永远不要将它们用于常规程序流程”的反应似乎每个人都没有那么好,因为这种反应的强度可以证明是合理的。
还是我弄错了?
我读过其他帖子,处理各种特殊情况,但我的观点是,如果你们都是这样的话,这没有什么问题:
清除遵守您的方法的合同
射击我。
if
语句的一个 原因。你会发现这很难。换句话说:您的前提是有缺陷的,因此您从中得出的结论是错误的。
您是否尝试过调试在正常运行过程中每秒引发 5 个异常的程序?
我有。
该程序非常复杂(它是一个分布式计算服务器),在程序的一侧稍作修改就可以很容易地在完全不同的地方破坏某些东西。
我希望我可以启动程序并等待异常发生,但是在正常操作过程中启动期间大约有 200 个异常
我的观点:如果您在正常情况下使用异常,您如何定位异常(即异常)情况?
当然,还有其他强有力的理由不要过多地使用异常,尤其是在性能方面
异常基本上是非本地 goto
语句,具有后者的所有后果。使用流控制异常违反了principle of least astonishment,使程序难以阅读(请记住,程序首先是为程序员编写的)。
此外,这不是编译器供应商所期望的。他们希望很少抛出异常,并且他们通常让 throw
代码效率很低。抛出异常是 .NET 中最昂贵的操作之一。
但是,某些语言(尤其是 Python)使用异常作为流控制结构。例如,如果没有其他项目,迭代器会引发 StopIteration
异常。甚至标准语言结构(例如 for
)也依赖于此。
goto
不同,异常正确地与您的调用堆栈和词法范围进行交互,并且不会让堆栈或范围一团糟。
我的经验法则是:
如果您可以采取任何措施从错误中恢复,请捕获异常
如果错误很常见(例如,用户尝试使用错误的密码登录),请使用返回值
如果您无法从错误中恢复,请不要将其捕获(或者在您的主要捕获器中捕获它以对应用程序进行一些半优雅的关闭)
我看到的异常问题是从纯语法的角度来看的(我很确定性能开销是最小的)。我不喜欢到处都是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,就个人而言,我认为您应该为罕见的错误条件(内存不足等)保留异常,并使用返回值(值类、结构或枚举)来进行错误检查。
希望我理解你的问题是正确的:)
对很多答案的第一反应:
您正在为程序员和最小惊讶原则写作
当然!但是,如果只是不是一直更清楚。
这不应该令人惊讶,例如:divide (1/x) catch (divisionByZero) 对我来说比任何 if 都更清楚(在 Conrad 和其他人)。不期望这种编程的事实纯粹是传统的,实际上仍然是相关的。也许在我的例子中 if 会更清楚。
但是 DivisionByZero 和 FileNotFound 在这方面比 ifs 更清楚。
当然,如果它的性能较低并且每秒需要大量时间,您当然应该避免它,但我仍然没有读到任何避免整体设计的充分理由。
就最小惊讶原则而言:这里存在循环推理的危险:假设整个社区使用了一个糟糕的设计,这个设计将成为预期!因此,该原则不能成为圣杯,应慎重考虑。
正常情况的例外情况,您如何定位异常(即异常)情况?
在许多反应中。像这样闪耀着低谷。抓住他们,不是吗?你的方法应该是清晰的,有据可查的,并且遵守它的合同。我没有得到这个我必须承认的问题。
调试所有异常:相同,有时只是这样做,因为不使用异常的设计很常见。我的问题是:为什么它首先很常见?
1/x
之前,您是否总是检查 x
? 2) 你是否将每个除法操作包装到一个 try-catch 块中以捕获 DivideByZeroException
? 3) 你在 catch 块中放入了什么逻辑来从 DivideByZeroException
中恢复?
openConfigFile();
后面可以是捕获的 FileNotFound,{ createDefaultConfigFile(); setFirstAppRun(); }
FileNotFound 异常处理得当;没有崩溃,让我们让最终用户的体验更好,而不是更糟。你可能会说“但如果这不是真正的第一次运行并且他们每次都得到它怎么办?”至少应用程序每次都运行,而不是每次启动都崩溃!在 1 到 10 的情况下,“这太糟糕了”:“首次运行”每次启动 = 3 或 4,crash 每次启动 = 10。
1/x
之前检查 x
,因为它通常没问题。例外情况是它不好的情况。我们在这里不是在谈论惊天动地的事情,但是例如对于给定随机 x
的基本整数,4294967296 中只有 1 将无法进行除法。这是例外,例外是处理它的好方法。但是,您可以使用异常来实现 switch
语句的等效项,但这很愚蠢。
在异常之前,在 C 中,有 setjmp
和 longjmp
可用于完成类似的堆栈帧展开。
然后给相同的构造一个名称:“异常”。并且大多数答案都依靠这个名字的含义来争论它的用法,声称异常是为了在异常情况下使用。这绝不是原始 longjmp
的意图。在某些情况下,您需要中断许多堆栈帧的控制流。
异常稍微更普遍,因为您也可以在同一个堆栈帧中使用它们。这引发了与 goto
的类比,我认为这是错误的。 Gotos 是紧耦合对(setjmp
和 longjmp
也是如此)。异常遵循松散耦合的发布/订阅,它更干净!因此,在同一个堆栈帧中使用它们与使用 goto
几乎是一回事。
第三个混淆来源与它们是检查异常还是未检查异常有关。当然,未经检查的异常对于控制流和许多其他事情来说似乎特别糟糕。
然而,一旦你克服了所有维多利亚时代的挂断并活了一点,检查异常对于控制流来说非常有用。
我最喜欢的用法是一长段代码中的 throw new Success()
序列,它一个接一个地尝试,直到找到它正在寻找的东西。每件事——每条逻辑——可能有任意嵌套,所以 break
和任何类型的条件测试一样都被排除在外。 if-else
模式很脆弱。如果我编辑出 else
或以其他方式弄乱语法,那么就会出现一个毛茸茸的错误。
使用throw new Success()
线性化 代码流。我使用本地定义的 Success
类——当然检查过——这样如果我忘记捕捉它,代码将无法编译。而且我没有发现其他方法的Success
。
有时我的代码会一个接一个地检查,只有在一切正常的情况下才会成功。在这种情况下,我使用 throw new Failure()
进行了类似的线性化。
使用单独的函数会破坏自然的划分级别。所以 return
解决方案不是最优的。出于认知原因,我更喜欢在一个地方放一两页代码。我不相信超精细划分的代码。
除非有热点,否则 JVM 或编译器所做的与我无关。我无法相信编译器有任何根本原因不检测本地抛出和捕获的异常,而是简单地将它们视为机器代码级别的非常有效的goto
。
至于在控制流的函数中使用它们——即用于常见情况而不是特殊情况——我看不出它们的效率如何低于多次中断、条件测试、返回涉水通过三个堆栈帧而不是仅仅恢复堆栈指针。
我个人不跨堆栈框架使用该模式,我可以看到它需要设计复杂性才能优雅地做到这一点。不过少用应该没问题。
最后,对于令人惊讶的处女程序员,这不是一个令人信服的理由。如果你轻轻地向他们介绍这种做法,他们就会学会喜欢它。我记得 C++ 曾经让 C 程序员大吃一惊。
return
模式替代方案将需要两个函数用于每个此类函数。一个用于准备 servlet 响应或其他此类操作的外部设备,以及一个用于执行计算的内部设备。 PS:一位英语教授可能会建议我在最后一段中使用“惊人”而不是“令人惊讶” :-)
标准答案是异常不是常规的,应该在异常情况下使用。
对我来说很重要的一个原因是,当我在我维护或调试的软件中读取 try-catch
控制结构时,我试图找出为什么原始编码器使用异常处理而不是 if-else
结构。我希望找到一个好的答案。
请记住,您不仅为计算机编写代码,而且还为其他编码人员编写代码。有一个与异常处理程序相关联的语义,你不能仅仅因为机器不介意就丢弃它。
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 变得复杂。
当然,做正确的事是有代价的。代价是每个人都需要了解他们需要阅读和遵循文档。希望无论如何都是这样。
性能怎么样?在对 .NET Web 应用程序进行负载测试时,我们在每台 Web 服务器上模拟了 100 个用户,直到我们修复了一个常见的异常并且这个数字增加到 500 个用户。
我认为您可以使用 Exceptions 进行流量控制。然而,这种技术也有另一面。创建异常是一件代价高昂的事情,因为它们必须创建堆栈跟踪。因此,如果您想更频繁地使用异常而不是仅仅发出异常情况的信号,您必须确保构建堆栈跟踪不会对您的性能产生负面影响。
降低创建异常成本的最佳方法是重写 fillInStackTrace() 方法,如下所示:
public Throwable fillInStackTrace() { return this; }
这样的异常不会填充任何堆栈跟踪。
raise
和 rescue
处理普通异常之外,Ruby 编程语言还有第二个功能,它允许您使用 throw
和 catch
符号。我认为这自然地避免了创建堆栈跟踪的影响,并且只是将一小段文本扔给堆栈上的下一个捕手。
以下是我在 blog post 中描述的最佳做法:
抛出异常以说明软件中的意外情况。
使用返回值进行输入验证。
如果您知道如何处理库抛出的异常,请尽可能在最低级别捕获它们。
如果出现意外异常,请完全放弃当前操作。不要假装你知道如何对付他们。
我真的不明白你是如何在你引用的代码中控制程序流的。除了 ArgumentOutOfRange 异常之外,您永远不会看到另一个异常。 (所以你的第二个 catch 子句永远不会被击中)。您所做的只是使用极其昂贵的 throw 来模仿 if 语句。
此外,您并没有执行更险恶的操作,您只是抛出一个异常,纯粹是为了让它在其他地方被捕获以执行流控制。你实际上是在处理一个例外情况。
除了上述原因之外,不使用异常进行流控制的一个原因是它会使调试过程变得非常复杂。
例如,当我试图追踪 VS 中的错误时,我通常会打开“中断所有异常”。如果您使用异常进行流控制,那么我将定期中断调试器,并且必须继续忽略这些非异常异常,直到遇到真正的问题。这可能会让某人发疯!
因为代码难读,调试时可能会遇到麻烦,时间长了在修复bug时会引入新的bug,在资源和时间上比较昂贵,而且如果你在调试你的代码和它会很烦人调试器在每个异常发生时停止;)
假设您有一种方法可以进行一些计算。它必须验证许多输入参数,然后返回一个大于 0 的数字。
使用返回值来表示验证错误,很简单:如果方法返回的数字小于 0,则发生错误。那么如何判断哪个参数没有验证?
我记得在我的 C 时代,很多函数都返回了这样的错误代码:
-1 - x lesser then MinX
-2 - x greater then MaxX
-3 - y lesser then MinY
等等
它真的比抛出和捕获异常更不可读吗?
您可以使用锤子的爪子转动螺丝,就像您可以使用例外来控制流一样。这并不意味着它是该功能的预期用途。 if
语句表示条件,其预期用途 控制流。
如果您以非预期的方式使用某个功能,而选择不使用为此目的而设计的功能,则会产生相关成本。在这种情况下,清晰度和性能不会受到真正的附加值的影响。与广泛接受的 if
声明相比,使用异常能为您带来什么?
换一种说法:仅仅因为你可以并不意味着你应该。
if
以供正常使用之后不需要例外,还是不打算使用 execptions 因为它不是故意的(循环参数)?
public class ExitHelper{ public static void cleanExit() { cleanup(); System.exit(1); } }
然后只需调用它而不是抛出:ExitHelper.cleanExit();
如果您的论点是合理的,那么这将是首选方法,并且不会有例外。您基本上是在说“异常的唯一原因是以不同的方式崩溃。”
if
单独的错误检查更干净,通常会发生这种情况。
正如其他人多次提到的那样,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)) 否则您的代码基本上没问题。
所以,基本上它归结为任何一种方法可以创建更清晰的代码......如果防止预期异常的操作比处理异常更复杂;比你有我的许可来处理异常而不是到处创建复杂的守卫。
如果您将异常处理程序用于控制流,那么您就太笼统和懒惰了。正如其他人提到的,如果您在处理程序中处理处理,您知道发生了一些事情,但究竟是什么?本质上,如果您将异常用于控制流,则您将异常用于 else 语句。
如果您不知道可能发生的状态,那么您可以使用异常处理程序来处理意外状态,例如当您必须使用第三方库时,或者您必须捕获 UI 中的所有内容以显示一个不错的错误消息并记录异常。
但是,如果您确实知道可能会出现什么问题,并且您没有使用 if 语句或其他东西来检查它,那么您只是懒惰。让异常处理程序成为你知道可能发生的事情的包罗万象是懒惰的,它稍后会回来困扰你,因为你将试图根据一个可能错误的假设来修复你的异常处理程序中的情况。
如果您将逻辑放在异常处理程序中以确定究竟发生了什么,那么您将非常愚蠢,因为您没有将该逻辑放在 try 块中。
异常处理程序是最后的手段,因为当您用尽想法/方法来阻止某些事情出错,或者事情超出您的控制能力时。例如,服务器已关闭并超时,您无法阻止抛出该异常。
最后,预先完成所有检查表明您知道或期望会发生什么,并使其明确。代码的意图应该是明确的。你更愿意读什么?
您可能有兴趣查看 Common Lisp 的条件系统,它是对正确处理异常的一种概括。因为您可以以受控方式展开或不展开堆栈,所以您也可以“重新启动”,这非常方便。
这与其他语言的最佳实践没有太大关系,但它向您展示了(大致)您正在考虑的方向上的一些设计思想可以做什么。
当然,如果您像溜溜球一样在堆栈上上下弹跳,仍然需要考虑性能,但它比大多数捕获/抛出异常系统所体现的“哦,废话,让保释”这种方法更为笼统。
我认为使用异常进行流量控制没有任何问题。异常有点类似于延续,在静态类型的语言中,异常比延续更强大,所以,如果你需要延续但你的语言没有它们,你可以使用异常来实现它们。
好吧,实际上,如果您需要延续而您的语言没有它们,那么您选择了错误的语言,您应该使用不同的语言。但有时你别无选择:客户端 Web 编程就是最好的例子——没有办法绕过 JavaScript。
一个示例:Microsoft Volta 是一个允许在直接 .NET 中编写 Web 应用程序的项目,并让框架负责确定哪些位需要在哪里运行。这样做的一个后果是 Volta 需要能够将 CIL 编译为 JavaScript,以便您可以在客户端上运行代码。但是,有一个问题:.NET 有多线程,而 JavaScript 没有。因此,Volta 使用 JavaScript 异常在 JavaScript 中实现延续,然后使用这些延续实现 .NET 线程。这样,使用线程的 Volta 应用程序可以编译为在未经修改的浏览器中运行——无需 Silverlight。
但是您并不总是知道您调用的 Method/s 中发生了什么。您不会确切知道异常是在哪里引发的。无需更详细地检查异常对象....
我觉得你的例子没有错。相反,忽略被调用函数抛出的异常是一种罪过。
在 JVM 中,抛出异常并没有那么昂贵,只需使用 new xyzException(...) 创建异常,因为后者涉及堆栈遍历。因此,如果您提前创建了一些异常,您可能会多次抛出它们而无需付出任何代价。当然,这种方式不能与异常一起传递数据,但我认为无论如何这是一件坏事。
语言可以通过一些通用机制允许方法退出而不返回值并展开到下一个“catch”块:
让方法检查堆栈帧以确定调用站点,并使用调用站点的元数据来查找有关调用方法中的 try 块的信息,或者调用方法存储其调用者地址的位置;在后一种情况下,检查调用者的调用者的元数据以确定与直接调用者相同的方式,重复直到找到一个尝试块或堆栈为空。这种方法给无异常情况增加了很少的开销(它确实排除了一些优化),但在发生异常时代价高昂。
让该方法返回一个“隐藏”标志,以区分正常返回和异常,并让调用者检查该标志并分支到“异常”例程(如果已设置)。该例程在无异常情况下增加了 1-2 条指令,但发生异常时的开销相对较小。
让调用者将异常处理信息或代码放在相对于堆栈返回地址的固定地址。例如,对于 ARM,不使用指令“BL 子程序”,可以使用以下序列:adr lr,next_instr b subroutine b handle_exception next_instr:
要正常退出,子程序只需执行 bx lr
或 pop {pc}
;在异常退出的情况下,子例程要么在执行返回之前从 LR 中减去 4,要么使用 sub lr,#4,pc
(取决于 ARM 的变体、执行模式等)。如果调用者的设计目的不是为了容纳它。
使用检查异常的语言或框架可能会受益于使用上述 #2 或 #3 之类的机制处理这些异常,而使用 #1 处理未检查异常。尽管 Java 中检查异常的实现相当麻烦,但如果有一种方法可以让调用站点可以说,基本上,“这个方法被声明为抛出 XX,但我不希望它永远这样做;如果确实如此,则作为“未检查”异常重新抛出。在以这种方式处理已检查异常的框架中,它们可能是流控制的有效手段,例如解析在某些情况下可能具有失败的可能性很高,但是失败应该返回与成功完全不同的信息。但是,我不知道有任何框架使用这种模式。相反,更常见的模式是使用上面的第一种方法(最低成本) - 异常情况,但抛出异常时的成本很高)适用于所有异常。
审美原因之一:
try 总是伴随着 catch,而 if 不一定伴随着 else。
if (PerformCheckSucceeded())
DoSomething();
使用 try/catch,它变得更加冗长。
try
{
PerformCheckSucceeded();
DoSomething();
}
catch
{
}
那6行代码太多了。
不定期副业成功案例分享