C++说:我家也有 try…finally
快速核查发现:Java、C#、Python、JavaScript都支持这种控制结构,唯独C++例外。
许多支持异常处理的语言¹都具备finally子句,因此你可以这样编写代码:
try {
⟦ 操作内容 ⟧
} finally {
始终执行();
}
其他所有主流语言都支持try…finally。C++宣称“我们家里就有try…finally”。
快速核查发现:Java、C#、Python、JavaScript都支持这种控制结构,唯独C++例外。
C++宣称:“我们家里就有try…finally。”
在C++中,让代码块在控制流离开时执行的方法是将其放入析构函数,因为析构函数会在控制流离开代码块时运行。这是Windows实现库wil::scope_exit函数使用的技巧: 你提供的lambda表达式会被封装在自定义对象中,其析构函数负责执行该lambda。
auto ensure_cleanup = wil::scope_exit([&] { always(); });
⟦ 代码块 ⟧
虽然原理相同,但不同语言在处理finally或析构函数自身抛出异常的情况时存在差异。
若控制流在未抛出异常的情况下离开受保护代码块,则finally代码块或析构函数中发生的任何未捕获异常都会从try代码块抛出。所有语言对此行为一致。
若控制流在抛出异常后离开受保护代码块,且finally代码块或析构函数同时抛出异常,则行为因语言而异:
- 在 Java、Python、JavaScript 和 C# 中,
finally块抛出的异常会覆盖原始异常,原始异常将丢失。更新:Adam Rosenfield 指出 Python 3.2 现将原始异常保存为新异常的上下文,但仍会抛出新异常。 - 在 C++ 中,若析构函数因异常触发执行,其抛出的异常将导致程序自动终止。²
因此 C++ 虽允许在控制流离开作用域时执行代码,但若你明智的话,最好别让异常从析构函数中逃逸。
¹ Microsoft 编译器同样支持 __try 和 __finally 关键字实现结构化异常处理。但这些功能本意是为C代码设计的。请勿在C代码中使用,因为它们与C异常的交互有时会引发混淆 “能否从结构化异常处理中抛出C++异常?”)。
² 正因如此,wil::scope_exit 在文档中明确指出:若 lambda 抛出异常将终止进程。另有替代函数 wil::scope_exit_log 可记录并忽略 lambda 抛出的异常。但不存在提供类似 Java 行为的变体。
本文由 TecHug 分享,英文原文及文中图片来自 All the other cool languages have try…finally. C++ says “We have try…finally at home.”。

提交的标题缺少推动博文的核心关键词*“finally”。Raymond Chen实际撰写的副标题是:“C++说’我们家里有try…finally’。”*
该标题借鉴了网络梗*“妈妈,我们可以买<X>吗?不行,家里有<X>了。”*的雪克隆结构。: https://www.google.com/search?q=%22we+have+x+at+home%22+meme
换言之,Raymond的表达是… “家里C++冰箱里早就备着Java的’finally’功能,它叫’析构函数’”
延续这个梗的比喻,孩子对<X>的理解与妈妈的认知不符,且坚持两者不可等同。例如:“妈妈,能点披萨吗?不行,冰箱里还有剩的砂锅菜。”
因此有些孩子会抱怨C++析构函数的RAII设计理念需要创建整个“class X{public:~X()}”结构,有时很不方便,所以它并不完全等同于“finally”。
HN有套减少夸张表述的启发式算法,偶尔会产生令人捧腹的反效果。
我认为这是个重大错误。我经常看到标题被搞砸,这完全违背了“不得对标题进行编辑性修改”的规则:
我认为,自动大幅改变标题含义直至版主偶然发现并恢复原状的做法,远比偶尔容忍略带夸张的原始标题要糟糕得多。
当前HN标题暗示雷蒙德认为C的’try’关键字是对其他语言’try’的拙劣模仿。实际上,该帖子讨论的是在C中模拟Java’finally’机制的方法,而原始标题已清晰(虽带幽默)地概括了这一核心。雷蒙德的观点在此已被曲解超过4小时,我无法理解这种妥协如何能被接受。
标题遭此处理的投稿应提供独立界面,同时展示两个标题选项,最终选择权归投稿人所有。
我认为这是绝佳的解决方案。
个人更倾向于降低快速删除投稿的门槛——或许只需五到十次标记,同时减少标题的自动化编辑干预。
虽然我不认同你称其为“重大失误”(我认为95%情况下运作良好),但这类语义文本替换恰恰是大型语言模型的绝佳任务。何不直接让廉价的LLM处理那些超过50分左右的帖子,消除其煽动性?
几天前就有人这么干过。
你随时可以联系hn@ycombinator.com指出此类错误,由版主进行修正。
更好的做法是减少对标题的激进修改。
指望用户发现错误→邮件通知版主(存在显著阻力)→再祈祷版主采取行动(此时讨论已偏离主题),这绝非理想方案。
错误标题已存在超过7小时,这几乎占了Hacker News首页展示周期的全部时间。显然,修正错误自动编辑化的系统运作不畅。
得了吧!这不过是些微不足道的小错误。最早发现者本该直接邮件通知版主。我在发表前一条评论前就这么做了,现在看到标题已修正到位。
这绝非小问题,它引发的抵触情绪与那些粗制滥造的AI横幅图片如出一辙。
7. 小时。
估计没人提前告知版主(在我之前),而当时美国时间还很早(假设版主团队在美国)。这就能解释为何处理延迟。
总之今后若再发生类似情况,大家应立即向版主发送邮件。若话题确实值得深入讨论,可通过二次机会池等方式请求版主延长其在首页的展示时间。
这只需花费一两分钟,实在不值得为此大动肝火。
若HN不自动大幅扭曲标题含义,对所有相关方都更便捷,也不必依赖版主是否在线。
重申:这篇帖子歪曲Raymond原意长达7小时,几乎覆盖其在首页展示的全部时间。现行机制显然失效。
如今很少见到标题优化算法能改善标题质量。当时因某类标题党标题泛滥而制定规则,如今原问题已解决,我们却仍受其困扰。
我刻意缩短了标题,因为存在长度限制。或许我处理方式不当,毕竟对提及的梗不太熟悉。对此表示歉意。
即便没有这个梗,这点也很重要。C++有try-catch语句,却没有try-finally结构。
在HN上标题超长很常见。我常没时间斟酌最佳缩短方式。
提交后还有几分钟修改标题,我经常这么做。
现在我对这个梗的真正起源很感兴趣。快速搜索只找到模糊的提及或声称它是近期的产物,但这个梗其实出现在1987年艾迪·墨菲的《Raw》脱口秀中,至少已有这么久的历史。
听起来正是深度研究的绝佳素材。
编辑:Gemini 3.0 Pro的深度研究显示,该梗源自1983-1987年间的脱口秀表演,特别提及埃迪·墨菲在《Delirious》(1983)中提出的社会经济前身“你可没麦当劳的钱”,最终在《Raw》(1987)中形成完整梗图。因此艾迪很可能就是原始创作者。
这就是技术文章标题不该用梗的原因——会大幅降低表达的可理解性。
博客标题本身完全清晰可辨。只有当你随意删减单词时才会变得晦涩。
我作为英语母语者也完全看不懂,文化梗对我来说毫无意义。
要求完全避免文化梗未免太过严苛
即使保留所有单词我依然无法理解。直到看到格式提示“我们家里有
try...finally”才恍然大悟。这些比喻中的孩子被困在思维定式里,试图将${语言A}的惯用法硬塞进${语言B}的代码中。正如文章所言,C自“C with Classes”时代就拥有析构函数。抱怨需要编写类是牵强附会的理由——若存在值得管理的资源,你本就该用RAII来管理它。而RAII正是C最基础的核心特性之一。
归根结底取决于开发者是否真正理解自己在做什么,甚至是否愿意去理解。
好吧,但有时你只需要在finally里写一行代码,而写类实在太麻烦
> 好吧,但有时你只需要在 finally 块里写一行代码,而写个类反而更麻烦
我觉得你没搞懂。
若每次销毁资源时都需要执行清理代码,C++早已设计了专用的成员函数:析构函数。去研究下 RAII 吧。
若你连RAII和基础资源管理都未能理解,仍可使用单行代码。建议学习作用域守护机制。
若你既懒得学习RAII又懒得实现基础作用域守护,可直接采用现成的守护实现方案。Boost库中就有相关实现。
https://www.boost.org/doc/libs/latest/libs/scope/doc/html/sc…
因此,除非你懒得动脑,执意要在${LANGUAGE}中机械地复刻Java写法(无论是否合理),否则在C++中使用finally语句完全没有必要。
更进一步说:若需执行清理代码,被清理的对象应封装为类,并在析构函数中完成清理。
以文件句柄为例:不要使用open()或fopen()后再在finally中关闭,而应使用文件类,让其在作用域结束时自动关闭。
析构函数远优于finally关键字,因为它只需在析构函数中释放资源(仅需记住一次),而finally语句则需在每个代码块中重复。例如文件对象总会在作用域结束时自动关闭,无需显式调用close()。语法也更简洁,缩进更少,尤其在创建多个对象需要嵌套try…finally块时。更不用说分支和条件初始化会使情况复杂化。通常可在代码中将构造函数与析构函数配对,这样资源获取与释放不匹配的情况就非常明显了。
我完全赞同。而在极少数需要内联创建析构函数的情况下,将析构函数与闭包组合到库类型中也并非难事。
举一个例子:我们最近向Rust nightly版本添加了
std::mem::DropGuard[1]。这使得能够轻松地快速创建(并取消)内联析构函数,无需任何额外的关键字或语言支持。[1]: https://doc.rust-lang.org/nightly/std/mem/struct.DropGuard.h…
可写文件在作用域结束时自动关闭通常并非理想方案,因为关闭过程中可能发生错误,尤其在使用网络文件系统时。
https://github.com/isocpp/CppCoreGuidelines/issues/2203
在正常流程中必须关闭文件并检查错误。但值得称赞的是,在异常处理路径(无论是通过提前返回还是抛出异常)中,你可以完全忽略该文件,且永远不会泄漏文件描述符。
错误路径中可能需要显式删除文件,但最佳实践是在封装整个“写入临时文件→重命名至目标位置→错误时删除”流程的类析构函数中处理。
任何不可靠的清理函数都令人困扰,无论采用何种错误处理机制。
Java通过允许异常附加次级异常解决了这个问题,特别是那些在栈展开过程中发生的异常(通过try-with-resources实现)。
结果形成了一棵异常树,它反映了首次异常之后在调用树中发生的失败。
这篇文章的核心观点是:你不能在析构函数中抛出异常。那么如何在析构函数中标记文件关闭/写入失败?
只要栈上没有活跃异常正在展开,析构函数中允许抛出异常。根据我的经验,这在实际场景中完全不是问题。相较于处理活跃异常的情况,从正常路径传播错误更为重要。
例如:因I/O错误无法写入文件,抛出该异常时又发现无法关闭文件。除了在析构函数中记录问题,还能怎么做?等待并反复尝试直到能关闭文件?
若你执意要强行套用Java的异常链语义(尽管没人能优雅处理这种情况),也可以这样做: 获取当前异常,并将新异常的引用封装在原始异常中。但我更倾向于尽可能少用异常。
直接抛出恐慌异常。调用方现实中能用这些信息做什么?
这味道像剩菜炖菜,而非披萨。
> 这篇文章的核心观点就是析构函数不能抛出异常。
你需要重读文章,因为你的断言显然是错误的。析构函数中既能抛出也能处理异常。唯一不能做的是不捕获这些异常——因为根据标准规范,未捕获的异常将导致应用程序立即终止。
因此析构函数内的抛出行为截然不同,导致其无法用于传达非致命错误
> 因此析构函数内的抛出行为截然不同,导致其无法用于传达非致命错误
你竟试图将人类史上最成功编程语言的核心设计特性诠释为“无用”,实在荒谬。
或许问题根源在于你试图将异常机制强行套用到“传达非致命错误”的场景中——更不用说你设想的“资源销毁时处理非致命错误”根本毫无意义。
或许你该退一步思考:将自身思维模型套用到不熟悉的语言是否合理。
我认为析构函数和finally子句各司其职。多数支持finally子句的语言也同时具备析构函数。
> 语法也更简洁,缩进更少——尤其当创建多个对象需要嵌套try…finally块时。
我认为这更像是对try…catch/maybe异常机制整体的质疑,而非针对finally块本身。(尽管我认同这种观点。我厌恶异常机制的这一特性,通常更倾向于类似C++的std::expected或Rust的Result设计。)
> 多数支持finally子句的语言也具备析构函数。
嗯,这是真的吗?我了解Java、JavaScript、C#和Python中的final关键字,但它们都没有真正的析构函数。我的意思是,其中一些语言有对象终结器,可以在垃圾回收器回收对象时清理资源,但这些机制与析构函数完全不同——后者通常在作用域结束时确定性地执行。Python的
with语法虽有类似功能,但与C++和Rust的析构函数差异巨大——你必须通过特殊语法显式要求语言清理资源。请问还有哪些语言同时具备
try..finally和析构函数?在C#中,最接近C++析构函数的对应物可能是
using语句块。虽然需要手动在使用前添加using声明,但静态分析工具可辅助实现。其底层会被转换为try–finally结构,并在finally中调用Dispose方法。或为避免嵌套:
若清理操作为异步,还可使用
await using(await foo.DisposeAsync())Java中类似机制称为try with resources。
Java实现如下:
对于文件操作,我更倾向Java的方法。因为若文件关闭时发生异常,常规
IOException代码块能像处理读写错误那样统一处理该异常。若希望在正常路径返回文件(或包含文件的对象),而在异常路径关闭文件,该如何实现?
应采用如下写法:
话虽如此,我认为这是种不良实践(个人观点)。通常而言,资源的打开与关闭应在同一作用域内完成。
将其设为非局部变量无异于自找麻烦。
*编辑注* 撰写过程中出现疏漏,但保留原文以阐明观点:当异常发生时文件将保持打开状态。
在Java中,我同意资源的打开和关闭应在同一作用域内完成。这是Java中的合理规则,而违反该规则在Java中会导致错误*——因为Java不支持RAII机制*。
但在C++和Rust中,这条规则毫无意义。你不可能犯忘记关闭文件的错误。
正因如此,我才说Java、Python和C#的上下文管理器根本不是一回事。它们只是各自语言中资源管理的实用工具,就像Go语言中的defer语法一样。它们并非“本质上等同于RAII”。
> 你不可能犯忘记关闭文件的错误。
但仍可能出现难以察觉的错误。例如,若将互斥锁绑定到对象上,可能因将锁的生命周期与对象绑定而意外延长锁存活时间。又如数据库连接或文件句柄,若使用后未及时关闭导致句柄泄漏,同样会造成超预期长时间的资源占用。
试图在同一作用域内保持资源的开闭状态本质是所有权问题。即便在C++或Rust中,我也不建议让RAII资源在获取作用域外泄漏。当此类所有权分散在代码各处时,就难以概念化程序在任意位置的状态。
内存是例外情况。
但这种方法无法将文件移入长生命周期对象或在成功路径中返回,对吧?
可将释放责任转移给调用方(返回可释放对象,由调用方置入using语句)。
此外,若调用方本身是长存对象,它可通过委托机制记住该对象并自行实现释放。这样长存对象的使用者就能管理它。
> 可将处置负担转移给调用方(返回可处置对象,由调用方置于using语句中)。
这无济于事。若函数在正常路径需返回可处置对象,同时在异常路径需销毁该对象,此方案仍不可行。
必须编写可处置封装器进行返回。异常情况下同样需返回该封装器。
调用场景示例:
作为从RAII转战C#的开发者,我认为你终将适应这种模式。关键在于转变思维方式:尽可能采用记录类型和不可变对象,无法实现时再借助IDisposable接口(即“using”语法)。这种方案虽非完美,但RAII同样存在局限。虽然仍在学习阶段,但我必须承认在C#中的开发效率远超C++时期。
我赞同这个观点。我并非排斥非RAII语言(尽管确实更偏爱RAII)。我提出那个修辞性问题主要是想强调两者本质截然不同。正如你所说,它并非RAII语言,使用时必须与具备完善析构函数的RAII语言采用不同的思维模式。
思考中——是否存在某种类似C的语言(尽管这个概念涵盖范围极广,但大致可理解为兼具“按需付费”理念与编译特性),既能舍弃原始指针等特性(牺牲C兼容性),又在其他方面高度接近C?
目前我只知道Rust符合这个描述。它在不同人眼中意义各异,但对我这个C开发者而言,它本质上是:拥有更优模板机制、更完善对象生命周期语义(尤其喜爱破坏性移动操作),同时摒弃了C兼容性遗留的冗余特性及四十年演进中累积的赘疣的C。
Rust与C++最本质的差异或许在于借用检查器(个人认为时而便利时而烦人)以及缺乏类继承层次结构。但两者都是编译为原生代码且运行时极简的RAII语言,都通过模板强调泛型编程,都采用带花括号的“C风格语法”——这使得Rust尽管受ML影响,却依然令人倍感亲切。
严格来说CPython具有确定性析构函数——当引用计数归零时__del__总会立即被调用,但这仅是实现细节而非语言规范。
我认为终结器与析构函数并非不同概念。这种区分仅在需要确定性清理行为(而非最终性)或处理线程局部变量等场景时才重要。(历史上C#甚至直接称其为析构函数。)
两者的编程模型存在巨大差异。你可以依赖C++或Rust的析构函数来释放GPU内存、关闭套接字、释放通过FFI获取的不透明指针所占用的内存、实现引用计数等操作。
我曾有过不愉快的经历:修复一个Go代码库时,发现开发者竟主动使用终结器来释放不透明的C内存和GPU内存。Go垃圾回收器显然未将释放这些仅包装指针的8字节对象视为高优先级任务,因为它无法识别这些对象正维持着数十兆字节的C或GPU内存存活。我不得不修改大量代码,在defer块中显式调用Destroy方法以避免内存耗尽。
对于采用垃圾回收的语言,我认为终结器是错误的设计。它们只会增加代码理解难度并掩盖问题,同时对垃圾回收性能产生负面影响。
Java正在积极移除终结器。
有时“最终”意味着“进程结束时”。对许多资源而言这不可接受。
它们本质上是截然不同的概念。
参见Hans Boehm的《析构函数、终结器与同步机制》- https://dl.acm.org/doi/10.1145/604131.604153
我甚至不总认同领域内顶尖专家的观点,他们彼此之间也常有分歧。安德斯·海尔斯伯格在编程语言设计领域绝非新手,但他仍将C#的对应机制称为“析构函数”——尽管如今为与其他语言统一,该机制已被称为终结器。这些机制都在对象生命周期结束时清理资源;垃圾回收语言与RAII语言的区别在于:在垃圾回收运行时中,对象的生命周期具有非确定性。这固然会改变编程模型(正如它在许多其他方面所做的那样),但绝不能因此断言两者存在“根本差异”。它们显然是相关概念…
但它们解决的是不同问题
析构函数固然优秀,但对于无法在析构函数中完成的事项,你仍需要“finally”语句
Python也有类似机制,称为上下文管理器,本质上与C++的RAII相同。
有人认为RAII更优雅,因为它不会强制增加额外的缩进层级。
使用上下文管理器时,如何在正常路径下返回文件?
若无法实现,那根本谈不上“与C++的RAII基本相同”。
两者完全不同——你必须主动使用上下文管理器,而C++中用户无需额外编写代码调用析构函数,它会自动发生。
我常在想,当你投入更多时间后,C++语法是否会变得可读?若真如此——功能性磁共振成像下,我们能观察到多少大脑重塑现象?
确实会…直到你换了雇主。有时甚至只是读同事的代码。或是自己早期的代码。其实不,我认为没人能达到完全可读的顿悟境界。像我这样的人不过是重复相同操作太久后产生的幻觉罢了。
可悲的是,这恰恰是我的切身体会。
然而不知为何,Lisp始终是众人心头好——尽管为每个项目创建字面意义上的新领域特定语言(DSL)正是该语言的特性之一。
Lisp的语法其实相当精简。所有DSL都基于相同的基本结构,自然易读。
C++的语法却极其繁杂:初始化规则、const限定、引用、移动复制、模板、特殊情况等等。它还继承了大部分C语言特性——虽然C本身精简,但其基础设计缺陷之多,甚至催生了《C语言谜题》这样的专著。
语法与概念(常量、移动、复制等)是正交的。你或许能为C++设计Lisp/S表达式语法,但这只会让预处理器中的宏更易用。即使采用项目特有的陌生概念,DSL也不必难以理解。
没错。
我的意思是C++中众多语言特性都通过繁复的语法细节实现,而Lisp的语法结构极为原始,这正是宏机制如此高效的原因。
尽管十年前仅在计算机科学课上接触过Lisp,我至今仍坚信它是完美的语言。仔细想想,或许Lisp恰恰是计算机科学课程(及其他场景)的完美领域特定语言(DSL)…
因为这类DSL能减轻而非增加读者的认知负荷。
精心设计的抽象在任何语言中都能做到这一点。而拙劣的设计则会适得其反,同样适用于所有语言。Lisp在此并无特殊之处。
当然,但是你特意提到了Lisp。DSL的核心价值在于设计专用的形式化语言,使特定问题更易于推理。这与标准C++不断膨胀的词汇量根本不可同日而语。
我认为C++语法相当可读。当然存在难以理解的代码库(尤其是高度抽象化的模板代码库),但这与大多数语言相比并无本质差异。这种情况普遍存在于各类语言中,即便是C语言在宏使用不当的情况下也可能同样糟糕。
目前最糟糕的当属 Scala——每个代码库似乎都采用截然不同的方言,语法结构天差地别。关于语言规范的共识少之又少,远不及 C++。
Scala本质是元语言,实为封装在盒子里的语言构建工具包。
它确实能提升可读性,但随之解锁的深层痛苦在于解读语义:隐式类型转换、谨记3/5法则避免std::move暗中变成复制、因添加的模板特化匹配范围超出预期而无意破坏代码,诸如此类的问题不胜枚举。
“using namespace std;”极大提升了C++的可读性,我并不在意潜在问题。但确实,由于缺乏完善的模块系统,这会迅速引发头文件将所有内容卸载到全局命名空间的问题,比如Windows API。
真希望我们能像JavaScript那样实现“import {vector, string, unordered_map} from std;”。每个元素单独使用声明实在太繁琐。
标准库模块:https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2022/p24…
我完全忘记了 std::ranges::iota 来自哪个头文件。反正我也不在乎。
上次尝试时,模块支持简直一团糟。等它们在CMake、GCC、Clang和Visual Studio中获得铁板钉钉的支持后,我再试一次。
代码可读性极佳,尤其和Rust相比。
我喜欢那些讨厌Rust语法的人大致能分成两类:
(1) 它为什么不像C++?
(2) 它为什么和C++这么像?
这只是个敷衍的评论。
> 投入更多时间后C++语法是否会变得可读
是的,最简单的方法就是按需学习。
我欣赏Swift的解决方案:它提供更通用的
defer { ... }代码块,无论何种情况都会在给定作用域结束时执行,若在函数作用域内则在return语句评估后执行。因此它具有多重用途,不仅限于实现try ... finally。我认为Swift的defer语句(https://docs.swift.org/swift-book/documentation/the-swift-pr…)的灵感源自/借鉴了Go语言的实现(https://go.dev/tour/flowcontrol/12),但该特性可能更早出现在我未曾接触的其他语言中。
相较于try…finally,defer具备两大优势:首先,它不会增加嵌套层级。
其次,当编写如下代码时:
在代码扫描过程中,更容易验证是否遗漏了 revert_foo 部分——相较于在 foo 与调用 revert_foo 的 finally 代码块之间存在大量中间代码的情况。
其缺点在于defer打破了“语句按源代码顺序逻辑执行”的惯例。但我认为这完全值得。
我能找到的最早类似defer的功能是2000年12月《C/C++用户杂志》中这篇文章提及的ON_BLOCK_EXIT宏:
https://jacobfilipp.com/DrDobbs/articles/CUJ/2000/cexp1812/a…
类似的宏后来(2006年)被纳入Boost库,命名为BOOST_SCOPE_EXIT:
https://www.boost.org/doc/libs/latest/libs/scope_exit/doc/ht…
我无法确定Go语言设计者是否受此启发,但即使如此也不足为奇。
我持不同意见。我宁愿采用Python式的上下文管理器——即便它会增加缩进层级——也不愿使用
defer这种扭曲的控制流结构。我理解你的观点,但该方案(https://book.pythontips.com/en/latest/context_managers.html)要求使用对象实现*__enter__和__exit__接口 (或在C#中实现IDisposable接口(https://learn.microsoft.com/en-us/dotnet/csharp/language-ref…),在Java中需实现AutoCloseable*接口(https://docs.oracle.com/javase/tutorial/essential/exceptions…);其他语言可能也提供类似接口)。
Defer更灵活/减少了添加调用点特定处理的冗余代码。示例参见https://news.ycombinator.com/item?id=46410610
确实,这在 UI 代码中尤为实用——既能支持异步操作,又能清晰标记 UI 的开始/结束状态:
我曾思考如何用Rust宏实现类似功能,结果发现有人早已实现。这是对析构函数/RAII模式的语法糖封装。
https://docs.rs/defer-rs/latest/defer_rs/
我不懂Rust,但这个
defer能否像Swift那样在return语句执行后评估?因为Swift可以这样写:在Rust中很容易证明析构函数会在
return语句执行后运行:但在函数返回值被评估后,试图通过
defer块修改该值的做法实在荒谬。请务必避免这种操作,无论在任何语言中。编辑:我认为
defer中实际上无法放置返回语句,可能是记忆有误,毕竟已是数年前的事。请忽略本评论链。在 Swift 中效果更佳,因为你可以将 return 语句放在 defer 中,从而创建一种命名返回值:
这种控制流太离谱了。千万别这么写。
咦,我不知道
defer里能用return,但这真的有用吗?不,其实是我记错了…你不能在defer里返回。
我记错的那个神奇点在于:只要所有代码路径都至少定义过一次,你就能在defer里引用未定义的值:
怪就怪我多年没碰Swift了,记忆模糊。
为什么这比直接使用RAII风格的lambda作用域退出更优?
无所谓啦,反正我本来就要用预处理器处理__LINE__(避免需要变量名),所以干脆改成“老派lambda”。再说scope_exit属于C++23特性,多数情况下仍需显式启用。
我还以为我们正试图彻底淘汰预处理器宏呢。
“家里有语法宏”
从析构函数调用任意回调是个糟糕的主意。迟早会有人违反关于异常的要求,导致程序立即终止。因此我只会在禁用异常的项目中使用这种模式。
同理,在迭代数据结构时调用任意回调也需谨慎——因为回调可能修改正在迭代的数据结构(典型例子是调用时会取消订阅的单次事件处理器),这会破坏简单粗暴的代码逻辑。
本期待 absl::Cleanup 能获得关注。我倾注心血使其兼具易用性与高性能。若您寻求比标准类型更优的解决方案(个人观点),不妨一试!
这确实是“C如何实现”的精辟解释,但更准确的说法应是:C中析构函数实现了类似finally的清理机制,而非它们本身就是finally。finally关注操作作用域内的清理,而析构函数处理所有权转移。C++恰巧用同一工具实现了这两种功能。
> 在Java、Python、JavaScript和C#中,finally代码块抛出的异常会覆盖原始异常,导致原始异常丢失。
我的个人痛点:这些语言都搞错了(而C++更是错得离谱)。
需要记录或向用户报告的错误几乎总是原始异常,而非finally代码块产生的异常。finally 块中的错误很可能是原始异常的副作用。报告 finally 异常会掩盖根本原因信息,增加问题调试难度。
虽然这些语言大多会以某种方式将原始异常附加到新异常上,以便必要时调用,但后续实际捕获并记录异常的机制必须额外确保记录的是根本原因而非愚蠢的副作用。异常层次结构应当颠倒:
finally抛出的异常应作为附件附加到原始异常上,或许可归入“次要”错误列表。或者干脆直接丢弃——说实话,原始异常几乎总是唯一需要关注的对象。(C++在这类场景下表现更差,直接崩溃。我猜这是委员会争论的结果——他们无法决定哪个异常该优先抛出。如今所有人都默认了这个糟糕的决定,嘴上说着“析构函数不该抛异常”,却似乎没意识到这等于说“析构函数不该有漏洞”。当然不该有,但祝你好运吧。)
此处表述有误。虽不便评论其他语言,但在Python中,最初抛出的异常会生成完整的回溯信息。若finally代码块也抛出异常,回溯信息会包含该异常作为附加信息。作者虽补充说明,但关于首个抛出异常的判断仍存在谬误。
我认为此处表述也略有偏差。
实际显示的回溯链基于最后抛出的异常(本例中即finally块抛出的异常),但会包含前置的“链式异常”并优先打印。根据CPython文档[1]:
> 当另一个异常正在被处理时抛出新异常,新异常的__context__属性会自动设置为已处理异常。异常可通过except或finally子句、with语句进行处理。[…]默认跟踪显示代码会在异常自身跟踪信息外,额外呈现这些链式异常。[…] 无论哪种情况,原始异常始终显示在所有链式异常之后,确保回溯的最后一行始终呈现最新抛出的异常。
因此实际中你会看到两种回溯。但若仅使用泛型“except Exception”等方式捕获异常并省略“context”进行日志记录,则会遗漏最初抛出的异常。
[1]: https://docs.python.org/3.14/library/exceptions.html#excepti…
> 更新:Adam Rosenfield指出Python 3.2现已保存…
这篇帖子有多老了?居然把3.2说成“现已”?
作者添加更新时显然没核对Python 3.2的发布时间
换言之:自掘坟墓#17421典型案例A。
博客未提及的是try finally如何破坏控制流。
在Java中以下代码完全有效:
try { throw new IllegalStateException(“Critical error”); } finally { return “Move along, nothing to see here”; }
没错,Java也有自掘坟墓的陷阱。
两种模式各自存在隐患的存在,正是我们无法拥有美好事物的根源。finally不应返回值,仅需void表达式。基于异常驱动的API必须被扼杀。
若方法会抛出异常,请明确标注以强制处理异常,切勿在 finally 中返回无效值。
以 Java 为例恰恰说明我们在这方面已取得多大进步——旧式 Java 异常处理为何糟糕,C++ 借此实现的方案又为何同样不堪。
打破旧思维定式固然艰难,但当编译器对错误行为发出警告时,改变就容易得多。
我确信Java中finally块抛出的异常会保留原始异常,而非被丢弃
既然有goto语句,谁还需要finally呢?