为什么Swift中的“throws”不是类型安全的?

kyvafyod  于 2024-01-05  发布在  Swift
关注(0)|答案(3)|浏览(145)

我在Swift中最大的误解是throws关键字。考虑以下代码:

func myUsefulFunction() throws

字符串
我们无法真正理解它会抛出什么样的错误。我们唯一知道的是它可能会抛出一些错误。理解错误可能是什么的唯一方法是查看文档或在运行时检查错误。
Swift拥有强大的泛型和类型系统,使代码具有表达能力,但感觉好像throws正好相反,因为你不能从函数签名中获得任何关于错误的信息。
为什么会这样呢?还是我错过了什么重要的东西,误解了概念?

qlfbtfca

qlfbtfca1#

我是Swift中输入错误的早期支持者,这就是Swift团队如何说服我我错了。
强类型错误是脆弱的,可能会导致API进化不良。如果API承诺只抛出3个错误中的一个,那么当第四个错误条件在以后的版本中出现时,我有一个选择:我以某种方式将它隐藏在现有的3中,或者我迫使每个调用者重写他们的错误处理代码来处理它。这可能不是一个很常见的情况,这给API带来了很大的压力,不要扩大他们的错误列表,特别是当一个框架在很长一段时间内被广泛使用时(想想:Foundation)。
当然,使用开放枚举,我们可以避免这种情况,但是开放枚举不能实现强类型错误的任何目标。它基本上又是一个无类型错误,因为你仍然需要一个“默认值”。
你可能仍然会说“至少我知道了一个开放枚举的错误来自哪里”,但这往往会让事情变得更糟。假设我有一个日志系统,它试图写入并得到一个IO错误。它应该返回什么?Swift没有代数数据类型(我不能说() -> IOError | LoggingError),所以我可能必须将IOError Package 成LoggingError.IO(IOError)(这会强制每一层都显式地重新 Package ;你不能经常使用rethrows)。即使它确实有ADT,你真的想要IOError | MemoryError | LoggingError | UnexpectedError | ...吗?一旦你有了几层,我最终与一些潜在的“根本原因”,必须痛苦地解开处理层层 Package 。
你打算如何处理它?在绝大多数情况下,catch块是什么样子的?

} catch {
    logError(error)
    return
}

字符串
对于可可程序来说,(即“应用程序”)深入挖掘错误的确切根本原因,并根据每个精确的情况执行不同的操作。可能有一两种情况可以恢复,其余的都是你无法做任何事情的事情。(这是Java中的一个常见问题,检查异常不仅仅是Exception;我喜欢Yegor Bugayenko's arguments for checked exceptions in Java,它基本上是他首选的Java实践,而这正是Swift解决方案。)
这并不是说没有强类型错误非常有用的情况。但对此有两个答案:第一,你可以自由地使用枚举来实现强类型错误,并获得相当好的编译器执行。不是完美的(你仍然需要一个默认的catch * 外部 * switch语句,而不是 * 内部 *),但如果你自己遵循一些约定,就相当不错了。
第二,如果这个用例很重要(也可能很重要),那么在不破坏那些需要相当通用的错误处理的常见用例的情况下,为这些用例添加强类型错误并不难。他们只需要添加语法:

func something() throws MyError { }


打电话的人必须把它当作一个强类型。
最后,为了使强类型错误发挥更大的作用,Foundation需要抛出它们,因为它是系统中最大的错误生成者。(与处理Foundation生成的NSError相比,您真正从头开始创建NSError的频率有多高?)这将是对Foundation的一次大规模改革,并且很难与现有代码和ObjC保持兼容。在解决非常常见的可可问题方面绝对出色,值得考虑作为默认行为。它不能再好一点了(更不用说上面描述的问题了)。
所以这并不是说非类型化错误在所有情况下都是100%完美的错误处理解决方案,但这些论点让我相信这是当今Swift的正确之路。

z9smfwbn

z9smfwbn2#

这是一个深思熟虑的设计决定。
他们不希望出现这样的情况,即你不需要像C、C++和C#那样声明异常抛出,因为这使得调用者要么假设所有函数都会抛出异常,要么包含样板文件来处理可能不会发生的异常,或者忽略异常的可能性。这两种都不是理想的,第二种方法使异常不可用,除非你想终止因为你不能保证调用栈中的每个函数在栈展开时都正确地释放了资源。
另一个极端是你所提倡的,每种类型的异常都可以被声明。不幸的是,人们似乎反对这样做的后果,那就是你有大量的catch块,所以你可以处理每种类型的异常。所以,例如,在Java中,他们会抛出Exception,这将情况减少到和我们在Swift中一样,甚至更糟,他们使用未经检查的异常,所以你可以完全忽略这个问题。GSON库是后一种方法的一个例子。
我们选择使用未检查的异常来指示解析失败。这主要是因为客户端通常无法从错误的输入中恢复,因此强制它们捕获已检查的异常会导致catch()块中的代码草率。
https://github.com/google/gson/blob/master/GsonDesignDocument.md
这是一个非常糟糕的决定。“嗨,你不能被信任去做你自己的错误处理,所以你的应用程序应该崩溃”。
我个人认为Swift在这方面做得很好。你必须处理错误,但你不需要写大量的catch语句。如果他们再进一步,人们就会找到破坏机制的方法。
设计决策的全部依据见https://github.com/apple/swift/blob/master/docs/ErrorHandlingRationale.rst

已编辑

似乎有些人对我所说的一些事情有问题。所以这里有一个解释。
程序可能引发异常的原因有两大类。

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

第二种错误通常不应该被捕获,因为它们表明了对环境的错误假设,这可能意味着程序的数据已经损坏。
第一种类型的错误通常可以恢复,但为了安全地恢复,每个堆栈帧必须正确地展开,这意味着对应于每个堆栈帧的函数必须知道它调用的函数可能会抛出异常,并采取措施确保如果抛出异常,则所有内容都得到一致的清理,例如,如果编译器不支持告诉程序员他们忘记了对异常进行计划,程序员就不会总是对异常进行计划,并且会编写泄漏资源或使数据处于不一致状态的代码。
gson的态度如此令人震惊的原因是,他们说您无法从解析错误中恢复(实际上,更糟糕的是,他们告诉你,你缺乏从解析错误中恢复的技能)。这是一个荒谬的Assert,人们总是试图解析无效的JSON文件。如果有人错误地选择了一个XML文件,我的程序崩溃是一件好事吗?否不是。它应该报告问题并要求用户选择不同的文件。
顺便说一句,gson的事情只是一个例子,说明为什么对错误使用未经检查的异常是不好的,如果我想从某人选择的XML文件中恢复,我需要捕获Java运行时异常,但是是哪些异常呢?好吧,我可以看看Gson文档来找出答案,假设它们是正确的和最新的。如果它们带有检查异常,API会告诉我哪些异常会发生,如果我不处理它们,编译器会告诉我。

vql8enpb

vql8enpb3#

Swift Typed Throws-即将推出!

在不久的将来,它将是 * 类型安全的 *。正如你在Swift's GitHub上看到的,它现在合并在main分支上,在实验性功能标志TypedThrows(即将到来的FullTypedThrows)后面。
很快,你就可以定义一个只抛出CatError的函数,比如:

func callCat() throws(CatError) -> Cat { ... }

字符串
然后再把它抓住,就像:

do throws(CatError) {
  try callCat()
} catch let myError {
   // myError is of type CatError
}


🌐这是Swift中的full detailed information about Typed Throws

相关问题