Swift中对我最大的误解是throws
关键字。请考虑以下代码:
func myUsefulFunction() throws
我们无法真正理解它会抛出什么样的错误。我们唯一知道的是它可能会引发一些错误。了解错误可能的唯一方法是查看文档或在运行时检查错误。
但这不是针对斯威夫特的本性吗? Swift具有强大的泛型和类型系统来使代码具有表现力,但感觉好像throws
正好相反,因为你无法从函数签名中得到任何有关错误的信息。
为什么会这样?或者我错过了一些重要的事情并误解了这个概念?
答案 0 :(得分:28)
我是Swift中类型错误的早期支持者。这就是斯威夫特团队让我确信自己错了。
强类型错误很脆弱,导致API演变不佳。如果API承诺只抛出3个错误中的一个,那么当在后来的版本中出现第四个错误情况时,我有一个选择:我以某种方式将它埋在现有的3中,或者我强制每个调用者重写他们的错误处理代码处理它。由于它不是原来的3,它可能不是非常常见的条件,这给API带来了很大的压力,不会扩展它们的错误列表,特别是一旦框架长期大量使用(想:基金会)。
当然,对于开放式枚举,我们可以避免这种情况,但开放式枚举不会实现强类型错误的目标。它基本上是一个无类型错误,因为你仍然需要一个"默认值。"
你可能仍然会说"至少我知道错误来自于开放的枚举,"但这往往会让事情变得更糟。假设我有一个日志系统,它会尝试编写并获得IO错误。应该归还什么? Swift没有代数数据类型(我不能说() -> IOError | LoggingError
),所以我可能不得不将IOError
包裹到LoggingError.IO(IOError)
(强制每一个)图层要明确地重新包装;你不能经常rethrows
。即使它确实有ADT,你真的想要IOError | MemoryError | LoggingError | UnexpectedError | ...
吗?一旦你有几层,我就会层层叠加一些潜在的"根本原因"必须痛苦地解开才能处理。
你打算怎么处理它?在绝大多数情况下,catch块看起来像什么?
} catch {
logError(error)
return
}
Cocoa程序(即" apps")深入挖掘错误的确切根本原因并根据每个精确的情况执行不同的操作是非常罕见的。可能有一两个案例有恢复,其余的事情是你无论如何也无法做任何事情。 (这是Java中的一个常见问题,检查异常不仅仅是Exception
;它不像之前没有人走过这条道路。我喜欢Yegor Bugayenko's arguments for checked exceptions in Java基本上认为作为他首选的Java实践,完全是Swift解决方案。)
这并不是说在强类型错误非常有用的情况下并非如此。但是有两个答案:首先,您可以自由地使用枚举实现强类型错误,并获得相当好的编译器实施。不完美(你仍需要一个默认的catch 在之外的开关语句,但不需要在中),但如果你自己遵循一些约定,那就非常好。
其次,如果这个用例变得很重要(并且可能),那么在不破坏需要相当一般错误处理的常见情况的情况下,为这些情况添加强类型错误并不困难。他们只会添加语法:
func something() throws MyError { }
呼叫者必须将其视为强类型。
最后,对于非常有用的强类型错误,Foundation需要抛出它们,因为它是系统中最大的错误生成器。 (与处理基金会生成的问题相比,你经常从头开始创建一个NSError
吗?)这将是对基金会的大规模改革,很难与现有代码和ObjC保持兼容。因此,在解决非常常见的Cocoa问题时,类型错误需要绝对精彩,值得考虑作为默认行为。它不可能更好一些(更不用说上面提到的问题了)。
所以,这并不是说无类型错误是所有情况下错误处理的100%完美解决方案。但是这些论点使我确信今天在斯威夫特这是正确的方式。
答案 1 :(得分: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
修改强>
似乎有些人对我所说的一些事情有疑问。所以这是一个解释。
程序可能会抛出异常有两大类原因。
第二种类型的错误通常不会被捕获,因为它们表明对环境的错误假设可能意味着程序的数据已损坏。在那里我无法安全地继续,所以你必须中止。
第一种类型的错误通常可以恢复,但为了安全恢复,每个堆栈帧必须正确解开,这意味着每个堆栈帧对应的函数必须知道它调用的函数可能抛出异常并且采取措施确保在抛出异常时一致地清理所有内容,例如,使用finally块或等效内容。如果编译器不支持告诉程序员他们忘记计划异常,程序员就不会总是计划异常,而是编写泄漏资源或使数据处于不一致状态的代码。
gson态度如此令人震惊的原因是因为他们说你无法从解析错误中恢复(实际上,更糟糕的是,他们告诉你你缺乏从解析错误中恢复的技能)。断言这是一个荒谬的事情,人们总是试图解析无效的JSON文件。如果有人错误地选择XML文件,我的程序会崩溃是一件好事吗?不,不是。它应该报告问题并要求他们选择不同的文件。
顺便说一下,gson的一个例子就是为什么使用未经检查的异常来解决你可以从中恢复的错误。如果我想从某人选择XML文件中恢复,我需要捕获Java运行时异常,但是哪些异常呢?好吧,我可以查看Gson文档,了解它们是否正确并且是最新的。如果他们已经检查了异常,那么API会告诉我期待哪些异常,编译器会告诉我是否处理它们。