为什么'抛出'在Swift中不安全?

Noo*_*ass 46 try-catch throw swift

throws关键字是对Swift最大的误解.考虑以下代码:

func myUsefulFunction() throws
Run Code Online (Sandbox Code Playgroud)

我们无法真正理解它会抛出什么样的错误.我们唯一知道的是它可能会引发一些错误.了解错误可能的唯一方法是查看文档或在运行时检查错误.

但这不是针对斯威夫特的本性吗?Swift具有强大的泛型和类型系统,可以使代码具有表现力,但感觉就好像throws完全相反,因为您无法从查看函数签名中获得有关错误的任何信息.

为什么会这样?或者我错过了重要的事情并误解了这个概念?

Rob*_*ier 30

我是Swift中类型错误的早期支持者.这就是斯威夫特团队让我相信我错了.

强类型错误是脆弱的,可能会导致API演变不佳.如果API承诺只抛出3个错误中的一个,那么当在后来的版本中出现第四个错误情况时,我有一个选择:我以某种方式将它埋在现有的3中,或者我强制每个调用者重写他们的错误处理代码处理它.由于它不是原来的3,它可能不是一个非常常见的条件,这给API施加了很大的压力,不会扩展它们的错误列表,特别是一旦框架长期广泛使用(想想:基金会) ).

当然,对于开放的枚举,我们可以避免这种情况,但是开放的枚举不能实现强类型错误的目标.它基本上是一个无类型错误,因为你仍然需要一个"默认".

您可能仍然会说"至少我知道错误来自开放枚举的地方",但这往往会使事情变得更糟.假设我有一个日志记录系统,它尝试编写并获得IO错误.应该归还什么?斯威夫特没有代数数据类型(我不能说() -> IOError | LoggingError),所以我可能不得不换IOErrorLoggingError.IO(IOError)(这迫使每一层都明确地重新包装,你不能有rethrows很频繁).即使它确实有ADT,你真的想要IOError | MemoryError | LoggingError | UnexpectedError | ...吗?一旦你有几层,我就会一层一层地包裹着一些潜在的"根本原因",这些根本原因必须被痛苦地解开才能解决.

你打算怎么处理它?在绝大多数情况下,catch块看起来像什么?

} catch {
    logError(error)
    return
}
Run Code Online (Sandbox Code Playgroud)

Cocoa程序(即"应用程序")深入挖掘错误的确切根本原因并根据每个精确的情况执行不同的操作是非常罕见的.可能有一两个案例有恢复,其余的是你无论如何都做不了的事情.(这是Java中的一个常见问题,带有检查异常,不仅仅是这样Exception;它不像之前没有人走过这条道路.我喜欢Yegor Bugayenko关于Java检查异常的论据,它基本上认为他的首选Java实践恰好是Swift解.)

这并不是说没有强类型错误非常有用的情况.但是有两个答案:第一,你可以自由地使用枚举实现强类型错误,并获得相当好的编译器实施.不完美(你仍然需要在switch语句之外的默认catch ,但不在内部),但如果你自己遵循一些约定,那就相当不错了.

其次,如果这个用例变得很重要(并且可能),那么在不破坏需要相当通用错误处理的常见情况的情况下,为这些情况添加强类型错误并不困难.他们只会添加语法:

func something() throws MyError { }
Run Code Online (Sandbox Code Playgroud)

呼叫者必须将其视为强类型.

最后,对于非常有用的强类型错误,Foundation需要抛出它们,因为它是系统中最大的错误生成器.(NSError与处理基金会生成的相比,你有多少经常创建一个从头开始?)这将是对Foundation的大规模改革,并且很难与现有代码和ObjC保持兼容.因此,在解决非常常见的Cocoa问题时,类型错误需要绝对精彩,值得考虑作为默认行为.它不可能只是更好一点(更不用说有上述问题).

所以,这并不是说无类型错误是所有情况下错误处理的100%完美解决方案.但这些论点使我确信这是今天在斯威夫特进行的正确方式.

  • 我不相信他们的意见。虽然 SO 不是一个很好的讨论平台... IMO,(1) 键入的错误可能对 API 不利,但对应用程序来说非常好,特别是如果我使用错误抛出作为流程控制,因为我需要可预测的分支。(2)“重新抛出”是不好的。较高级别的代码通常无法正确理解或处理较低级别的错误。由于缺乏上下文。所有较低级别的错误都需要在中级进行处理,并且中级应该为较高级别的代码产生适当的中级错误。仅仅重新抛出意味着对错误不做任何事情。 (2认同)

Jer*_*myP 22

选择是一个刻意的设计决定.

他们不希望你不需要在Objective-C,C++和C#中声明异常抛出的情况,因为这使得调用者必须假定所有函数抛出异常并包含样板来处理可能不会发生的异常,或者只是忽略异常的可能性.这些都不是理想的,第二个使得异常不可用,除了你想要终止程序的情况,因为你无法保证在堆栈被展开时调用堆栈中的每个函数都正确地释放了资源.

另一个极端是你提倡的想法,并且可以声明抛出的每种类型的异常.不幸的是,人们似乎反对这样做的后果,即你有大量的catch块,所以你可以处理每种类型的异常.因此,例如,在Java中,他们会将Exception情况减少到与我们在Swift中相同的情况,或者更糟糕的是,他们使用未经检查的异常,因此您可以完全忽略该问题.GSON库是后一种方法的一个例子.

我们选择使用未经检查的异常来指示解析失败.这主要是因为通常客户端无法从错误的输入中恢复,因此强制它们捕获已检查的异常会导致catch()块中出现草率的代码.

https://github.com/google/gson/blob/master/GsonDesignDocument.md

这是一个非常糟糕的决定."嗨,你不能信任你做自己的错误处理,所以你的应用程序应该崩溃".

就个人而言,我认为斯威夫特在权利方面取得了平衡.您必须处理错误,但您不必编写大量的catch语句来执行此操作.如果他们继续前进,人们就会找到颠覆机制的方法.

设计决策的完整理由是https://github.com/apple/swift/blob/master/docs/ErrorHandlingRationale.rst

编辑

似乎有些人对我所说的一些事情有疑问.所以这是一个解释.

程序可能抛出异常有两大类原因.

  • 程序外部环境中的意外情况,例如文件上的IO错误或格式错误的数据.这些是应用程序通常可以处理的错误,例如通过向用户报告错误并允许他们选择不同的操作过程.
  • 编程中的错误,例如空指针或数组绑定错误.解决这些问题的正确方法是让程序员进行代码更改.

第二种类型的错误通常不会被捕获,因为它们表明对环境的错误假设可能意味着程序的数据已损坏.我无法安全地继续,所以你必须中止.

第一种类型的错误通常可以恢复,但为了安全恢复,每个堆栈帧必须正确展开,这意味着对应于每个堆栈帧的函数必须知道它调用的函数可能抛出异常并采取步骤确保在抛出异常时一致地清理所有内容,例如,使用finally块或等效内容.如果编译器不支持告诉程序员他们忘记计划异常,程序员就不会总是计划异常,而是编写泄漏资源或使数据处于不一致状态的代码.

gson态度如此令人震惊的原因是因为他们说你无法从解析错误中恢复(实际上,更糟糕的是,他们告诉你你缺乏从解析错误中恢复的技能).断言这是一个荒谬的事情,人们总是试图解析无效的JSON文件.如果有人错误地选择XML文件,我的程序会崩溃是一件好事吗?不,不是.它应该报告问题并要求他们选择不同的文件.

顺便说一下,gson的一个例子就是为什么使用未经检查的异常来解决你可以从中恢复的错误.如果我想从某人选择XML文件中恢复,我需要捕获Java运行时异常,但是哪些异常呢?好吧,我可以查看Gson文档,了解它们是否正确并且是最新的.如果他们已经检查了异常,那么API会告诉我期待哪些异常,编译器会告诉我是否处理它们.

  • `...因为这使得调用者必须假设所有函数抛出异常并包含样板来处理可能不会发生的异常,或者只是忽略异常的可能性 - 这些断言都不是真的.每个正确编写的C#程序在调用堆栈顶部附近都有一个支持`try/catch`处理程序,如果在框架中的任何地方发生未处理的异常并记录异常,则会阻止程序崩溃.然后,开发人员可以决定如何处理*在实践中实际发生的那些未处理的异常.* (6认同)
  • 引用段落:<<(... gson ...)"这是一个非常糟糕的决定."嗨,你不能信任你自己的错误处理,所以你的应用程序应该崩溃".>>它实际上这是唯一明智的决定,并且显示出你的一些误解,恕我直言.更重要的是,它似乎很有争议,而且也没有添加任何其他非常好的答案.我会删除它.YMMV. (6认同)
  • 问题是,Swift中一个非常流行的模式是使用枚举作为错误类型.这允许我们定义一个函数可以抛出的单一类型的错误,但是有多个案例可以准确描述出错的地方,并在必要时提供每个案例的附加信息.这个事实几乎打击了"你将被迫写出大量的catch块"的论点,因为强制一个函数抛出一个枚举类型仍然允许你只写一个catch块.就个人而言,我认为允许`throws`进行类型注释的附加安全性远远超过任何缺点. (5认同)
  • *这是一个非常糟糕的决定*.这完全是你的意见,这是最不受欢迎的.您可能不同意设计选择,但很明显,其他人认为这样做是个好主意.称他们的作品"非常糟糕"在我的书中并不合适,而且非常不尊重. (5认同)
  • @JeremyP:总是*总是*抛出异常的可能性,除非你使用类似`TryParse()`的东西,甚至可能.我没有看到提供一个止回异常处理程序如何被归类为*忽略*这种可能性. (4认同)
  • @Hamish我并不是说输入类型错误是错误的,但经验表明人们会对他们引入的开销感到厌倦并破坏机制,就像他们使用Java一样. (2认同)
  • 我认为这个答案支持了对如何在实践中使用异常的深刻误解。“两类”思维方式是错误的二分法。对于“第二种”,Web 应用程序是一个常见的反例。它*应该*处理所有异常,因为一个请求的失败不应该拒绝对所有其他请求的服务。对于“第一种类型”,编译器给你一个错误,你没有捕捉到异常 * 没有办法* 强制你关闭你的文件,所以它只是噪音。使用*始终清理资源*的约定*远*更安全,甚至更有效。 (2认同)
  • *"并非所有资源都是内存"* - 当然,这就是我写"使用`finally`块"的原因.+++*"需要知道异常"* - 不,只是*总是*使用`finally`进行清理.+++我无法想象任何非平凡的非投掷方法.+++*"程序员错误需要与数据错误相同的处理"* - 实际上,它们都需要显示消息,记录和回滚.既不需要*特殊*清理,因为无论如何都必须进行清理,即使范围是范围正常.+++实际上,导致异常的编程错误通常是良性的...... (2认同)
  • @JeremyP:你会感到惊讶。对于许多类型的应用程序,业务逻辑中的编程错误只需要中止单个业务执行,而不是整个流程,前提是有一个单独的抽象层可以正确隔离业务执行。因此,例如,如果您有一个网页,您可以从一个请求中吞下一个 `NullPointerException` 并显示 500 错误,同时仍然处理其他请求,前提是内部状态仅限于请求。 (2认同)
  • 回复:“所以你希望人们总是向调用另一个函数的每个函数添加一个 `try ... finally ...` 块”:绝大多数 Java 方法不需要 `try ... finally ... `,因为无论如何 `finally` 块都是空的。当您获取资源(文件、数据库连接等)并需要保证释放它时,您需要`try ... finally ...`;但相对较少的方法获得这样的资源。 (2认同)