问题

传统的try-catch异常处理是否是编程语言发展中的弯路?

回答
当我们深入探讨传统 `trycatch` 异常处理机制在编程语言发展中的角色时,会发现它并非一条简单的“弯路”,而是更像一条充满探索与进化的蜿蜒小径。最初,当软件复杂度开始攀升,程序员们迫切需要一种方式来应对那些意料之外的状况,比如文件不存在、网络中断、无效输入等等。在这种背景下,`trycatch` 应运而生,它提供了一种结构化的方法,允许开发者将可能出错的代码块包裹起来,并在发生错误时执行特定的处理逻辑。

这种机制的出现,无疑是当时的一大进步。它使得代码更加清晰,避免了充斥着大量错误检查的冗余代码,将“正常流程”和“异常流程”有效地分离。开发者可以将主要的精力放在实现业务逻辑上,而将对潜在错误的应对策略放在 `catch` 块中。这在一定程度上提升了代码的可读性和可维护性,至少,它比那些到处是 `if (error_code != SUCCESS)` 检查的代码要优雅得多。

然而,正如任何技术都不是完美无缺一样,`trycatch` 也在实践中暴露出了它的局限性。一个显著的问题是,`catch` 块往往会捕获过于宽泛的异常类型,或者仅仅是为了“忽略”一个潜在的问题而写成空的 `catch {}` 块。这导致了“隐藏”的错误,使得问题在运行时悄无声息地发生, debug 时如同大海捞针。更糟糕的是,有时候 `catch` 块中的处理逻辑本身就存在问题,或者只是简单地将异常重新抛出,却没有真正解决根本原因。这种情况下,`trycatch` 并没有解决问题,反而增加了一层抽象,让问题变得更加扑朔迷离。

此外,一些语言的设计者开始反思,过度依赖 `trycatch` 可能会阻碍开发者对程序控制流的深入理解。例如,当一个函数可能因为多种原因失败时,如果所有的失败路径都通过异常来表示,那么调用者就需要捕获所有这些可能的异常。这使得函数的契约变得不那么明确,开发者需要仔细阅读文档或者源码才能了解所有潜在的异常类型。

随着编程语言的不断演进,一些新的范式和机制开始出现,试图提供更精细、更明确的错误处理方式。例如,一些函数式编程语言倾向于使用返回值来表示成功或失败,通常会返回一个包含结果或错误信息的元组(如 `Result` 类型)。这种方式将错误信息作为程序正常返回值的“一部分”,迫使开发者在每次使用函数时都显式地处理成功和失败两种情况,从而避免了遗漏。还有些语言引入了更具表现力的模式匹配,允许开发者根据不同的错误类型或状态执行不同的处理逻辑,这种方式比简单的 `catch` 块更加灵活和安全。

那么,说 `trycatch` 是“弯路”是否过于绝对?我认为不是。它更像是早期探索阶段的一个重要里程碑。它为我们提供了一种基本的、在当时是相当有效的错误处理方式,帮助我们跨过了处理复杂性的最初门槛。从这个意义上讲,它不是一条“死胡同”或“弯路”,而是通往更成熟、更健壮错误处理机制的一段必经之路。

许多现代编程语言也并没有完全抛弃 `trycatch`。相反,它们在 `trycatch` 的基础上进行了改进,或者与其他错误处理机制并存。例如,一些语言允许更细粒度的异常捕获,或者引入了“作用域”的概念,使得异常的处理更加局部化。开发者也通过实践,逐渐认识到 `trycatch` 的正确使用方式——只捕获你知道如何处理的异常,并且在 `catch` 块中进行有效的错误恢复或记录。

所以,与其说是“弯路”,不如说 `trycatch` 是编程语言在应对复杂性和错误处理方面的一次早期尝试,它奠定了基础,也暴露了问题,从而驱动了后续更精妙、更具表达力的错误处理机制的发展。它是一个学习的过程,一个不断优化的过程,而不是一个可以被轻易否定的“错误选择”。

网友意见

user avatar

我更喜欢用“如何处理好错误”来分析这个问题。至于是不是“异常”,这个“异常”是特制java里的异常还是泛指程序发生了“异常”都是很值得讨论的。

怎样才算是处理错误的正确姿势?

但在讨论所有细节之前,先要确定目标,到底怎么才算是“正确的处理错误”?据我所知,至少有两种不同目标:

  • 第一种是:要编写正确的代码。这里“正确”是指对任何设计上要处理的错误都必须设计处理流程。这类设计在操作系统、驱动、自动控制、存储、基础中间件等领域被使用;
  • 另外一种是:要尽量快速的开发质量可以接受的程序。此时可以忽略一大部分的错误的处理,就是放任。这种开发模式适用于RAD,如互联网领域里大量的上层应用和服务,网站,工具类的脚本等都隶属于这一类。

现实中的项目往往处于这两种模式中间的某个点,靠某一边近一点。

这两种目标存在矛盾。对于第一种开发模式就意味着程序员必须认真的设计正确/错误的代码路径。而我们都知道错误的路径会多很多。但也得必须认真处理。在C写的底层程序里经常会看到每调用一次函数都得跟着一句if判断出不出错。很多第二种的开发人员往往会觉得这种代码很不“优雅”。大量的错误处理路径会夹杂在代码中,显得啰嗦,写起来很不爽,也不便于阅读。但从第一种目标看来,这么做是天经地义,无可厚非的。

而第二种目标是把处理正确逻辑的路径重点完成。只处理少数的必要的错误(比如判断用户授权是否通过)。其余的一般会选择粗略的“集中处理”。比如一个web业务后端代码往往如此。一旦出现任何非预期的情况,会通过异常的方式一路向上抛到顶层,然后被一段集中处理的代码转换成某种4xx/5xx状态码给到调用者。这类程序往往会接入非常全面的监控,来实时抓取运行时发生的各种问题。大部分抓到的问题处理的价值很低,所以程序员还是会继续选择不处理;只有少量真正会影响用户体验,系统安全和稳定的问题会被收集下来处理。

第一种开发模式的程序员往往会对第二种的处理方式嗤之以鼻。但如果按照第一种方式处理,把程序的绝对正确性摆在非常高的位置,反倒会伤害用户的体验。交付给用户的新功能会延期,开发成本因为开发周期延长会急剧提高。而花费很高代价开发的程序可能因为业务的快速变化而被大改甚至丢弃。从商业上这明显不划算,用户也不买账。

这两种开发模式不同会造成任何单一的错误模型很难同时兼顾二者。如果对错误处理要求的很严格,就意味着无法“凑合处理”;反过来如果错误模型很利于快速/忽略处理,就意味着程序员很容易忽略某段代码可能产生的错误。编译器无法有效的引导程序员提高程序的正确性。这种问题可能通过其他手段变相的改进(比如使用静态代码扫描),但总是没有语言built-in用起来自然和舒适。就算是Java这种同时提供了Checked Exception和Unchecked Exception两种机制的语言也总会引发各种争吵。

当然,后面我们会看到go,rust和swift等语言在尝试弥合这两种不同的目标,提供一套统一的错误处理,而不需要程序员根据自己业务的需要二次开发一套错误处理机制。

错误模型如何引导编写正确的代码?

很多同学都喜欢编写“优雅”的代码。这种代码看上去十分“干净”,线性的完成一件任务。阅读起来非常顺畅。但现实情况是,几乎任何一句代码都有可能出错。并不是说你不处理一个错误,那个错误自然就不存在了。

如果想编写绝对正确的代码,就必须每次调用都得处理这个调用可能发生的错误。而且通常一个操作正常的结果就一种情况,但错误的情况却有N种,于是就会出现:

       result, error := doSomething() if error != nil {   // 处理error1   // 处理error2   // …… }  // 使用result      

这种看起来很啰嗦的代码。但从正确性角度,这就是最好的代码了。

所以我们暂时把“优雅”放在一边。先谈正确性的问题。

人是非常容易犯错的。编程语言最好可以提醒程序员不要遗漏任何一个错误。早期C语言编程时,语言并没有内建错误处理,而是靠程序员自己约定和检查错误码。

       int errno = foo(); if (errno) { // errno不是0会被认为是“true”   // handle error } // 正常逻辑     

这很容易出错,程序员很容易就忘记了errno,直接写成:

                foo         ();            

错误就丢了。一旦发生,foo后面的代码的结果就可能是无法预料的。假如这是一段交易所交易的代码,或者武器系统的控制代码,后果就会相当严重。

因此保证正确性的第一个原则是:编程语言应该尽力提醒程序员不要忽略错误。程序员要不处理错误,或者明确告诉编译器这个错误我觉得可以不处理。比如在go中,

       result, error := doSomething()      

如果写成了

       result := doSomething()      

会发生编译错误。

如果程序员想明确地忽略错误,需要

       result, _ := doSomething()      

但可惜的是,go中如果一个函数只会返回一个error,是允许这么写的:

       // 理想中的写法 error := doSomething()  // 这么写可以通过编译器,但IDE的辅助检查可以标记这个问题 doSomething()      

这个设计从“避免人为忘记处理错误”的角度是有瑕疵的,但还好可以通过IDE辅助检查来强制避免忽略检查错误。

在Java中,这个功能是Checked Exception(CE)提供的。

Java Checked Exception

如果一个函数抛出了CE,上层调用者要不得处理,要不得重新上抛。

       public void foo() throw SomeCheckedException {   // do something   throw new SomeCheckedException(); }  public void bar() {   foo(); // 编译不通过,没有处理 SomeCheckedException }  // 明确的处理可以通过编译 public void bar() {   try {     foo();   } catch(SomeCheckedException e) {     // handle the exception   } finally {     // clean up the resource   } }  // 声明自己会"re-throw"foo抛出的Checked Exception也可以通过编译 public void bar() throws SomeCheckedException {    foo();  }     

Java的CE设计的本意是改进C++的exception specification机制。C++ exception specification的设计也是希望帮助程序员避免遗漏错误处理。但其设计的问题更巨大,连基本的静态检查也做不到。已经在2010年C++ 11标准出来是被废弃了,这里就不展开了。

Java 的CE比C++ exception specification的设计好得多,但还是架不住其设计上的缺陷,造成落地时很多人都讨厌他。落地困难。比如很多时候,函数调用者也不知道怎么处理一个Checked Exception。

       try {   byte[] bytes = Files.readAllBytes(aPath);   return new String(bytes); } catch (IOException e) {   // 我知道有这么个exception,但咋处理呢??? }     

一种常见的方式是把下层的Checked Exception直接向上抛。但这又带来一系列的问题。举个例子,比如当你编写一个对象池“object-pool“的接口时,本来borrow函数的签名表示只会在“没有对象可以借”的时候报错:

       interface ObjectPool<T> {   T borrow() throws NoObjectException;   // ... }     

初看起来很正常,但是当你尝试利用这个接口实现数据库连接池时,你会发现JDBC要返回另外一个Checked Exception SQLException

       class DBConnectionPool implements ObjectPool<DBConnection> {   // ...   @Override   public DBConnection borrow() throws /* 这里写啥呢? */ {     doSomethingWithSQLException(); // 怎么处理这里抛出的SQLException呢?   } }     

此时,你有3种选择:

1) 将SQLException包装进一个RuntimeException里。改用RuntimeException。这样一切都能解决。但这就相当于干掉CE。

2)直接向上抛SQLException,但这样需要同时修改DBConnectionPool#borrowObjectPool#borrow的函数签名,否则无法通过编译(实现类的函数不能声明抛出比接口函数声明更多的exception)。如果改了接口,就引起接口兼容性问题。此外,这也要求borrow的使用者增加import SQLExcepton。为了做到这一点,mvn里也许需要增加一个新的dependency。而且

       T borrow() throws NoObjectException, SQLException;     

看起来会非常奇怪,因为直觉上这似乎暴露了borrow的实现细节,同时因为这个接口是通用的,因此用来实现其他什么别的对象池时,因为无法抛出SQLException反而通不过编译。这就影响了接口的通用性。

3)把SQLException包在NoObjectException里面抛出:

       T borrow() throws NoObjectException {   try {     // ...   } catch (SQLException e) {     throw new NoObjectException(e)   } }     

但这样语义上不太对。毕竟“数据库连接失败”和“没有对象可以用”是两件不同的事情。并且这么包完后,如果上层的代码想重试连接数据库呢?看起来又太过于奇怪了:

       try {   DBConnection c = pool.borrow(); } catch(NoObjectException e) {   try {     throw e.cause;   } catch (SQLException) {     // 尝试重新连接数据库   } }     

这样看来,似乎问题是之前那个NoObjectException太过于具体了。如果能稍微抽象一点,让其表意含糊一点似乎就能说得过去了,比如改成PoolException。但如果万一实现还需要抛出其他什么exception,让PoolException都显得不合适呢?能否有个比较通用的办法呢?

James Gosling在一个访谈中表示希望抛出异常能尽量具体,这样有利于调用者搞清楚如何处理问题。

But in general, when you are writing a declaration in a throws clause, it is good to try to be as specific as possible.

但现实中这样做根本就不现实。因为一旦Exception变得具体,就会因为兼容性问题让接口的维护工作变得几乎不可能;即便是可以改变接口签名,也会带来一个函数随着调用层级升高而必须声明大量的Checked Exception问题。前者被称为Versioning问题,后者被称为Scalability问题。这些问题在C#的作者Anders Hejlsberg的一个访谈中提及。

但请不要误解我完全支持Anders的想法和C#的现状。后边讲到Unchecked Exception的地方再说。

除此之外CE在JDK8上遇到了另外一个问题。引入了stream后,因为stream的处理函数都不声明抛异常,因此没法通过这些函数调用抛出CE的函数。于是你看到了Java里的语法在打架。

       void foo(Integer n) throws SomeCheckedException {   // ... }  // 编译不通过,forEach没声明throws SomeCheckedException numbers.stream().forEach(it -> foo(it));     

现在很多Java程序员都完全拒绝使用Checked Exception,也只用RuntimeException来构造自己的错误处理。后果就是会产生很多只有到运行时才能观察到的错误,编译器无法起到帮助程序员写好代码的作用。然而,在Java中这是个很无奈但又很现实的选择。

Checked Exception已经可以被认为是一个失败的设计,正如C++的exception specification一样。但我要再次强调下,我这里提到的CE设计失败并不是指这个设计的本意——避免开发者遗漏错误——是不对的。恰恰相反,我觉得正确性是非常关键和必要的。这里说的失败仅仅是指CE实际效果并没有实现其初步的设计动机。为此,要设计一个更好的模型。

效率优先的“凑合”错误处理

这是大多数互联网公司,或者写了就丢那种程序对错误处理的方式。现实中一般就是用Unchecked Exception。这包括Java中继承于RuntimeException的exceptions,C#,kotlin,javascript,python等中所有的exceptions等。

顾名思义,Unchecked就是“不用必须检查的exception”,但一般会配合一个“兜底处理“。对于这种exception,程序员可以在任何地方throw,也可以在任何地方catch。所以一个Unchecked Exception被抛出后可以跨越很多个调用层次才有可能被catch。如果彻底忘记被catch了(Uncaught Exception),通常就会让程序立刻crash。兜底的错误处理因为距离错误发生的上下文太远,只能做非常粗略的处理。如Web程序会报一个5xx错误;而GUI也许会选择弹出一个信息除了程序员之外谁也看不懂的对话框。

对于脚本语言的场景,或者是逻辑简单,错误处理一般都是兜底就足够的场景,这似乎并不是什么太大的问题。但是如果是系统开发场景(比如写一个中间件或一个存储系统)这就不能接受了,编程语言能提供的帮助实在是太有限了,太不鼓励程序员正经的处理错误了。系统程序一旦出现错误,就不是报个错误信息的问题了,而可能会损毁数据,造成不可恢复的后果。我们都知道人是注定会犯错的,无论这个人的本心如何。

因为用Unchecked Exception写程序太容易漏过错误处理,因此为了保证程序质量,必须更注意编写准确的文档(尤其是会抛出什么exception),必须附加更多的测试,包括程序员自己写的UT,以及专门的测试人员编写的各种集成测试等。当然,如果业务需求来的太快,活催的太紧,这些改善软件质量的工作可能都会被简化和忽略。所以经常可以遇到完全无文档,无UT,以及测试通过手工草草测完了事的程序。于是写出来的程序质量……

然而令人惊讶的是,我们的市场环境在鼓励这样的开发模式。如果是一个初创互联网项目,需要快速启动,快速迭代的服务,如果用正确性优先的做法的团队,一定会被采用效率优先的团队打死。尽管我们都知道长期看,代码质量可以让一个团队走得更远,技术债越少后期负担越轻越灵活,但如果熬不过初期,一切都是无意义的。

回到错误处理,上面提到了Checked Exception因为太过于严格而无法落地,而Unchecked Exception又过于松散,完全放弃编译器的支持,而人总是不可靠的,总是会犯错误。有没有两全的办法呢?Swfit给出了一个还不错的解决方案。

Swift的启示

Swfit的主要是思路是,先承认一个函数是否可能抛出错误,然后再考虑怎么处理具体的错误。这个思路和Go、Rust等语言是非常相似的。下面是一个简单的例子

       // swift通常使用enum表示error enum SomeError: Error {   case reason1   case reason2 }   // 定义时 func foo() throws -> int {  // 正常处理,设置failed   if failed {     throw SomeError.reason1   }   return 42 }  // 调用时 func bar() {   do {     let value = try foo()     // 使用value   } catch SomeError.reason1 {     // handle reason1   } catch SomeError.reason2 {     // handle reason2   } catch {     // handle everything else possible   } }  bar()     

在swfit中,所有错误处理都是"checked"。对于所有错误程序员必须显示的处理,不然编译过不去。而一个函数如果可能抛错,需要在声明时标记throws关键字,但无需声明可能抛什么错误(敲黑板,这是重点)。调用这种throws函数时,swift要求开发者必须使用try关键字调用函数,并且增加必要的do……catch。这就避免了程序员因为马虎大意而忽略了错误处理。

throws无须声明抛的具体错误是相比CE的一个非常大的优势。它解决了CE的scaling和versioning问题。只有当一个函数从不会抛错变为throws时才会有一次签名的变更,之后就不会再有了。而一般设计函数时函数可不可能抛错可以做得比较准确(如排序就不可能抛错,但读配置文件就可以抛错),让改变签名的机会很少。 同时上游也可以只处理自己关心的错误,不用catch自己不理解的error。

并且通常,调用者只会在意调用是否出错,而不在意具体出什么错,就可以这样写:

       do {   let value = try foo()   // 使用value } catch {   // 错误处理 }      

但即便如此,还是比较烦,但swift提供了语法糖进一步简化了程序员的工作。

       // try? 可以让出错时直接返回nil let value = try? foo() //foo抛错后value会得到nil  // 配合??操作符提供一个默认值 let value = (try? foo()) ?? 10 // foo抛错后value会得到10  // try!是一种强检查模式,如果遇到了错误,会让程序立刻crash。 // 也就是说如果程序员认为他的上下文肯定不会遇到错误,他可以选择主动忽略编译器的帮助。 let value = try! foo() // foo抛错后程序会crash     

swift还提供了rethrows关键字解决上面Java CE在lambda表达式遇到的问题。

       // 给Sequence做一个“myMap”高阶函数 extension Sequence {    // rethrows表示忠实的重新抛出其内部抛错函数的错误     public func myMap<T>(_ process: (Element) throws -> T) rethrows -> [T] {       var result = [T]()       for item in self {         result.append(try process(item)) // process是抛错函数所以得用try,但不需要catch       }       return result     } }  func tripleIt(n: Int) throws -> Int {     if n == 2 {       throw SomeError     }     return n * 3 }  // 这样就可以愉快地使用给高阶函数传入抛错函数了 let res = try [1, 2, 3, 4, 5].myMap(tripleIt)     

个人感觉是,Swift以优雅的语法和务实的态度近乎完美的解决了错误处理的问题。

到底何谓错误,何谓异常?

如果留意的话会发现整个文章我都避免用“异常”这个词,而更多的用“错误处理”。因为一个问题是不是“异常”,并不全部取决于问题本身,而与程序员在不在意相关。如果一个错误出了程序员不在意,或者没法在意,那么就是一个异常。异常是程序员不处理的错误。异常意味着程序进入了一个未知的,不确定的状态。

比如OOM,对null解引用这类问题。一旦出现,程序员做什么都没啥意义,无法把程序能从出问题的状态确定性的拉回到可控的状态。

另外一类异常是开发者是主动的不在意。比如一个服务启动时要在一个路径下读取配置文件。但这个文件不存在或者没读取权限。作为服务本身,是可以设计为“一旦找不到配置文件就直接挂掉“的。因为设计一个”没配置文件也可以正常运行“的系统的代价会高的多,但没啥收益。

在编写代码时,如果可以判断出一个问题上层无论如何都处理不了,就建议直接抛出异常。在go中就是panic,rust是panic!,在java中是throw一个Error(比如OutOfMemoryError)。如果上层有可能处理,就还是要以错误的形式向上返回,让上层决定自己到底是不管直接挂掉还是把程序从有问题的状态转变回来。

非常有趣的是,在Java的术语里,“错误”和“异常”和现代语言的术语是相反的。java说的exception往往表达一个“错误”;而java里说的“error”才是几乎无法恢复的问题。这点讨论各种错误模型时要特别留意。

挂掉和稳定性的关系是什么?

很多人误以为一旦遇到异常挂掉就是稳定性降低。但事实刚好相反,因为异常意味着程序运行的状态处于一个非预期的状态,在没有事先设计过错误处理路径的情况下,挂掉是最好的选择。这通常被称为fail-fast。

但只挂掉是远远不够的,不然真的就变成“不稳定”的程序了。配合fail-fast一般配合做两件事。

第一件是灰度开发和发布流程,即可以让不稳定的代码在一个风险可控的范围内跑,尽情的暴露各种问题,尽快的crash,然后开发者尽快去修。所以很多互联网公司都有很多套上线前的环境,从最小的联调环境,到预上线环境,再到部分灰度的线上环境一圈圈扩大范围。苹果iOS每年发布时也是类似,会先在内部alpha1,alpha2……然后经过WWDC后向开发者发布beta1,beta2……直到最后GA。到真正的面向公众时,程序的可靠性已经大大增加了。

另外一件是隔离。当程序crash时是,只会影响到整个系统的优先的范围。这就像是,当一个程序挂了,不会影响操作系统;当Chrome里某几个网页挂了,不会影响到其他Chrome的进程;当一个非核心业务的微服务挂了,上游可以实现降级,服务大体上还是能继续。

在这种隔离体系下,一个挂了的子系统可以被迅速重启,恢复到初始状态。Erlang把这套机制吸收到了自己的语言里,实现了supervisor模式。

当然,给定同样的输入还是可能会继续挂,继续重启——但这种情况要通过灰度开发和发布流程来控制,让在线上跑的系统已经具有极高的稳定性才可行。

而在Web后端经常看到的用"Unchecked Exception报错+兜底处理"的方式也可以看作是一种隔离。因为通常每个请求都是相互独立的,每个请求都与其他请求的上下文是隔离的。某个请求出问题,其实是让这个请求的上下文全部挂掉,而不是让整个web server挂掉。web server只要继续处理下一个请求即可。这是一种非常高效的错误处理。

但这个模式很容易让程序员产生一种误解,即——写代码可以不处理错误,反正有地方兜底。但事实恰好相反,这种模式只在并发请求处理的场景中才成立。web后端这个领域已经被web server,web框架,数据库事务等照顾的很体贴了。换一个领域,比如mysql,假如不好好处理错误,共享的B+树,各种buffer和锁信息可能就会搞乱套,一个错误会影响其他数据的正确性。

小结

错误处理的方式是让我们达成开发目标的工具。而在开发中,根据领域不同,要求的正确性也不同。因此选择一个适合自己领域的错误处理方式是很重要的。如果恰好用到的编程语言能够与自己的领域相match,那就再理想不过。

但通常我们无法选择团队的语言,或者自己就只会一种语言,这时就要想办法去搭建自己错误处理。就像是C并不提供内建的错误处理,但程序员还是凭错误码这么简单的机制搞定了复杂的系统开发。在Java中,如果确认Unchecked Exception + 兜底已经可以足够满足场景的需要,大可以放心用。但在一些局部关键的子系统,需要更高正确性,还是可以采用CE或者增加更多的静态代码检查,并且在代码设计时就要设计各种错误的处理流程。

至于说try……catch是不是“弯路”。我认为不是,从C++、Java等经典语言的设计和落地后的效果可以学到很多。在CE设计之初,Gosling是绝对想不到后面大家使用CE时的各种问题的。因此后面的go、rust、swift等语言才能改进自己的错误处理模型。在这个方面,过程比结果可能更重要。

但,无论如何,“编写正确的程序“的大原则不会改变,关键点在于如何表达错误,如何避免开发者遗漏错误,如何组合多个可能发生错误的代码,如何提供语法糖简化一些不得不写的,反复重复的代码。语言设计的关键是不能劝退一个希望写好代码的人

user avatar

1、我看不出来异常和资源泄漏之间的关系……你这明显是该用finally的地方没用导致的……

2、仅仅从代价去分析而不考虑异常是怎么来的使得这个分析没啥意义。


异常是怎么来的?

很简单,发明异常的目的就是为了避免接口污染

异常本质上就是一种返回值,一个确定的函数(不考虑虚函数和lambda)会抛出什么异常是完全可以通过静态分析来解决以及可以在语法层面上进行约束的(例如Java的throws)。

你扯那么多所谓的东西,你不从根本上解决掉发明异常的意义,其实都是白扯……

就像你要论证GC的问题,不是去论证GC性能差,卡顿等等乱七八糟的。你应该从根本上讨论不需要GC也能很好的释放内存资源(例如引用计数什么的)。


否则你说这么多有什么意义呢?


其实很多时候争议都在一些细枝末节的细节上,譬如说要不要显示throws,要不要自动rethrow,要不要自动捕获堆栈,要不要强类型等等。本质上来说,异常这玩意儿是客观存在的……

类似的话题

  • 回答
    当我们深入探讨传统 `trycatch` 异常处理机制在编程语言发展中的角色时,会发现它并非一条简单的“弯路”,而是更像一条充满探索与进化的蜿蜒小径。最初,当软件复杂度开始攀升,程序员们迫切需要一种方式来应对那些意料之外的状况,比如文件不存在、网络中断、无效输入等等。在这种背景下,`trycatch.............
  • 回答
    中国传统美学,宛如一条绵延千年的长河,孕育了无数璀璨的艺术瑰宝和深刻的哲学思想。它并非一个单一固定的范式,而是随着时代变迁、文人思想的演进,不断丰富和发展,呈现出一种包罗万象、兼容并蓄的特质。要将其尽数道来,实非易事,但我愿倾尽所能,描摹出其大致的轮廓与神韵,如同品一杯醇厚的老酒,细细咀嚼其中的滋味.............
  • 回答
    我一直对地图 pretty 那个行业抱有浓厚的兴趣。在我看来,地图出版社不仅仅是印制纸张,它承载着对一个地方的认知、对历史的梳理,甚至是对未来规划的指导。所以,当我接触到一些传统的地图出版社从业者,听他们聊起工作状态和行业变化时,总会觉得里面有很多值得深挖的东西。工作内容:从“纸上谈兵”到“数字触达.............
  • 回答
    老话常说,“人无完人,金无足赤”,这人生路上,磕磕绊绊是常有的事。有时候,真话扎人,急了眼,话赶话,就容易冒出些“不入耳”的词儿来。这些词儿,说起来,就像是陈年的老酒,虽然味道重了点,但仔细咂摸咂摸,里面也藏着不少咱们祖祖辈辈的智慧和情绪。咱们先从最常见的,也是最“基础”的来说。说到“傻”,这词儿可.............
  • 回答
    说起英国菜,很多人脑海里会浮现出炸鱼薯条、炸肉排、布丁之类的画面,或许还会带点“粗糙”、“乏味”的标签。但其实,英格兰的饮食文化远比这些刻板印象要丰富和有趣得多,它就像英国的历史一样,融合了各种文化的影响,形成了自己独特的风味。早餐:开启一日能量的“大满贯”英国的早餐,尤其是传统的“Full Eng.............
  • 回答
    关于传统的师徒制度能否适应当今的经纪人模式,这确实是一个值得深入探讨的问题。在我看来,这并非一个简单的“能”或“不能”的二元判断,而是一个如何在保留核心价值的同时,进行创新与融合的复杂课题。首先,我们得回顾一下传统的师徒制度。它强调的是“言传身教”和“耳濡目染”。师傅不仅传授技艺、经验,更重要的是传.............
  • 回答
    关于用传统土肥(动物粪便)种植作物是否会造成污染,以及其污染程度与化肥相比如何,这是一个非常值得探讨的问题,也牵涉到农业的持续发展和环境保护。我将尽量详细地阐述,并尽量避免 AI 写作的痕迹,以一种更贴近真实思考和经验的方式来讲述。传统土肥的“污染”:并非全然负面,但有潜在风险首先,我们要明确一点:.............
  • 回答
    不少人可能会觉得,在现代社会,像那种古老、用石头一圈一圈垒起来的拱桥,早就被钢筋混凝土、更加现代化的桥梁技术给替代得干干净净了。毕竟,提起桥梁,我们脑海里浮现的往往是那些庞大、线条流畅的现代工程,比如横跨大海的跨海大桥,或是穿梭在城市高楼之间的立交桥。它们似乎代表着科技进步的极致,而传统的石拱桥则像.............
  • 回答
    与传统的海运相比,中欧班列展现出了其独有的、在特定情境下更为突出的优势。这些优势并非否定海运的地位,而是从效率、时效性、成本控制、地理覆盖、环保以及安全性等方面进行了差异化的补充和优化。下面将详细阐述这些独有优势:1. 时效性优势: 显著缩短运输时间: 这是中欧班列最核心的优势。海运通常需要40.............
  • 回答
    助记词(Mnemonic Phrase),也称为种子短语(Seed Phrase),在加密货币、区块链钱包等领域扮演着至关重要的角色。与传统的密码相比,助记词具有显著的优势,这些优势使其成为管理数字资产的更安全、更便捷的选择。下面我将详细阐述这些优势:1. 更高的安全性: 随机性与不可预测性: .............
  • 回答
    随着生活水平的提高,我们身边确实有不少曾经熟悉、如今却渐行渐远的传统食物。它们或因制作繁琐,或因食材难寻,或因口味不再迎合大众,悄悄地从我们的餐桌上退居幕后,只留下一些模糊的记忆,偶尔在老一辈人的叙述中被提及。还记得小时候,逢年过节,村子里总会有几家专门做这类食物的。其中,“年糕团子”是我记忆中最深.............
  • 回答
    ARHUD系统和传统的HUD系统在显示信息的方式、交互性、应用场景和技术复杂度上都存在显著的差异。下面我将详细阐述这些区别:1. 显示信息的“实境感”和“叠加方式” 传统HUD (HeadUp Display): 信息叠加: 传统HUD是将数字信息(如速度、导航箭头、警告图标等)以二.............
  • 回答
    空中突击师和传统的空降师,听起来都挺酷炫的,都是从天上掉下来打仗的,但仔细一掰扯,区别可就大了去了。咱们这就来聊聊,把这俩哥们儿的家底儿都揭个底儿朝天。首先,核心区别得搞清楚: 空降师: 顾名思义,核心就是“空降”。他们得坐着飞机飞到敌方后方,然后像跳伞运动员一样,带着自己的装备跳下去。他们的主.............
  • 回答
    很多我们习以为常、觉得“自古以来”就有的东西,追根溯源,其实历史一点也不长。这种“短历史”带来的错位感,往往会让人觉得有些意外,甚至有点颠覆。今天就来聊聊这些看似“传统”,实则“新晋”的玩意儿。1. 圣诞老人:一个相对年轻的“古人”提起圣诞节,脑海里立刻浮现出那个胖乎乎、红帽子、白胡子的圣诞老人。可.............
  • 回答
    当我们谈论世界各地的传统戏剧时,我们会发现,尽管文化背景、表演形式和审美追求各不相同,但它们都承载着人类的情感、历史和价值观念。就像我们中国的传统戏曲一样,它们都是一种集文学、音乐、舞蹈、美术、表演于一体的综合艺术。在西方,古希腊戏剧无疑是最早、也最具影响力的传统戏剧形式之一。它起源于祭祀酒神狄俄尼.............
  • 回答
    咱们中国自古以来就是个讲究“仁义礼智信”的国家,儒家思想更是咱们的立身之本。按理说,咱们骨子里都应该沉稳、内敛,懂得“中庸之道”,遇事三思而后行。可如今放眼望去,怎么回事儿?感觉许多人都变得心浮气躁,一点小事就上头,不是东风压倒西风,就是西风压倒东风,很少能看到好好说话,心平气和地把事儿说明白。这事.............
  • 回答
    要回答“陇西李氏是否属于传统的山东士族”,我们需要先理解几个关键概念,并结合历史文献进行梳理。一、 “山东士族”的界定首先,我们得弄清楚“山东士族”这个概念本身。在历史上,尤其是在魏晋南北朝时期,士族门阀的概念非常盛行。这些士族并非是地理区域的简单划分,而是指那些世代簪缨、家世显赫、具有政治经济文化.............
  • 回答
    Netflix 如今在内容产业占据着举足轻重的地位,这并非偶然。与好莱坞那些根深蒂固的传统电影、电视制片厂相比,Netflix 在多个方面展现出了独特的优势,这些优势共同塑造了它如今的成功格局。首先,Netflix 在数据驱动的决策上表现出了前所未有的敏锐度。 传统制片厂在内容开发和发行方面,很大程.............
  • 回答
    问这个问题,其实更像是在问,未来我们的上网方式会变成什么样子。5G,这个名词大家都不陌生了,它带来的超高速、超低延迟和海量连接,听起来就像是从科幻电影里走出来的。很多人就想知道,它能不能直接把我们现在用的光纤宽带给“淘汰”了。要回答这个问题,咱们得先明白,5G 和有线宽带,它们各自的优势和特点是什么.............
  • 回答
    面向对象程序设计(OOP)之所以成为现代软件开发的主流,绝非偶然。相比之下,传统的面向过程程序设计(POP)虽然在很多场景下依然有效,但在处理复杂、大型、以及需要长期维护和演进的软件系统时,往往显得力不从心。OOP的核心优势在于它提供了一种更贴近现实世界、更符合人类思维模式的组织和管理代码的方式。想.............

本站所有内容均为互联网搜索引擎提供的公开搜索信息,本站不存储任何数据与内容,任何内容与数据均与本站无关,如有需要请联系相关搜索引擎包括但不限于百度google,bing,sogou

© 2025 tinynews.org All Rights Reserved. 百科问答小站 版权所有