一些编程语言使用“延迟”模式进行资源清理。这是一个结构,例如defer
关键字,它安排清理代码在封闭块(或函数)的末尾运行。这在 Zig、Go 甚至 GCC C 中都可用。
fn printFile ( path : str ) ! void { var f = try open ( path ) defer f . close () for line in f { print ( try line ) } // File is closed here. }
这提供了一些关键的好处。
- 清理就在分配旁边。很容易发现并保持同步。
- 即使对于早期返回和错误情况,也会自动进行清理。
- 清理以正确的顺序自动进行(关键字的相反顺序)。
这是对手动编写和运行清理代码的巨大改进。以上每一点都是常见的错误来源,现在很容易避免。
然而, defer
有一个巨大的缺点。你需要记住它。
与 C++ 或 Rust 之类的东西相比:
fn printFile ( path : & str ) -> std :: io :: Result < () > { let f = std :: fs :: File :: open ( path ) ? ; for line in std :: io :: BufReader :: new ( f ) .lines () { println! ( "{}" , line ? ); } Ok (()) // File is closed here. }
当然,添加一行代码并不难。但你可以忘记。你怎么知道需要close
、 unlock
或以其他方式处理的类型?自动处置(称为RAII )让程序员摆脱了这种想法并避免了错误。所有需要清理的东西都在不再使用的时候清理干净。
Java也有同样的问题。许多程序员喜欢垃圾收集器,因为它们会在你之后清理。但是大多数垃圾收集器的问题是它们只清理内存!还有许多其他类型的资源,例如锁、文件、线程和套接字,您需要自己处理。其中一些可以由终结器处理,但由于它们运行之前的时间可能很长,它们不适合某些资源,例如锁。
一个好的资源管理解决方案(例如 RAII)可以处理您的程序使用的所有资源。在很长一段时间内,java 的最佳解决方案是finally
块。
FileInputStream inputStream = new FileInputStream ( "story.txt" ); try { // Read file here... } finally { inputStream . close (); }
虽然这显然比 defer 更多样板并导致令人不快的右移,但实际上是相同的。最重要的是,您仍然需要记住哪些类型需要关闭。
这在 Java 7 中通过try-with-resources 语句变得稍微好一些。
try ( FileInputStream inputStream = new FileInputStream ( "foo.txt" )) { // Read file here... }
但即便如此,正确性仍需要手动操作。然而,这个特性最好的部分可能是java.lang.AutoCloseable
接口。最后有一种方法可以知道需要清理哪些类型。这种机器可见的信息可以允许构建有效的 lint。如果你曾经得到一个AutoClosable
类型,你最好在 try-with-resources 语句中使用它,或者快速将它传递给其他人(并将负担转移给他们)。但是复杂的情况很难可靠地检测到。例如,如果您将类型存储在数据结构中并在删除后对其进行清理。即使只是简单地将引用传递给多个地方,也会对谁负责清理它产生歧义。
这个故事的寓意是,避免错误的最好方法是让它们难以编写。理想情况下,使错误比正确的代码更难编写。 defer
和 try-with-resources “默认情况下是错误的”,需要代表程序员进行明确的工作才能正确。 RAII 是正确的,无需程序员做任何额外的事情。
这篇文章的灵感来自defer
关键字,但这是一个比这更普遍的问题。我记得 Google 上有一篇关于--dont-be-stupid
标志的文档。这些通常会在发现错误时添加,但修复包含某些客户端所依赖的行为更改,或者需要仔细推出更改。几年后,大多数服务将有几十个这样的标志,所有这些标志都硬编码在服务配置中。当然,每隔一段时间就会设置一个新的服务实例,这个错误会再次出现。向后兼容性和逐步推出是好的,但在某些时候,您需要更新代码以使其默认正确。