自从Swift 1开始就提供了错误处理技术支持;这种技术是受来自于Objective-C的启发而开发出的。Swift 2在这方面的改进使得处理您的应用程序中的意外状态和条件更加简单直接。
就像其他普通的编程语言一样,Swift中的错误处理技术也存在不同类型,具体情况则要取决于遇到的错误类型和您的应用程序的总体结构。
本教程将带您通过具体的例子来介绍如何最有效地处理常见的错误情况。你还会看到如何升级早期版本Swift编写的项目中的错误处理模块。文章最后,将集中介绍未来版本的Swift可能提供的错误处理技术!
接下来,就让我们开始踏上Swift 2错误处理技术的学习征程,你一定会体会到不一样的乐趣的!
简介
本教程提供了两个初学者案例供大家学习使用,它们的下载地址分别是: https://cdn4.raywenderlich.com/wp-content/uploads/2016/04/Avoiding-Errors-with-nil-Starter.playground-1.zip 和 https://cdn4.raywenderlich.com/wp-content/uploads/2016/04/Avoiding-Errors-with-Custom-Handling-Starter.playground-1.zip 。
为了跟踪本文中的实例代码,请下载这两个示例工程。
首先,请使用Xcode打开第一个初学者案例Errors with nil。通读工程中的代码,你会看到有几个类、结构和枚举定义。
请注意代码的以下部分:
protocol MagicalTutorialObject { var avatar: String { get } }
这个协议适用于本教程中使用的所有类和结构,它用于为工程中的每一个对象有关信息提供可视化描述——打印到控制台中。
enum MagicWords: String { case Abracadbra = "abracadabra" case Alakazam = "alakazam" case HocusPocus = "hocus pocus" case PrestoChango = "presto chango" }
此枚举定义了一些可用于创建一个魔法的咒语。
struct Spell: MagicalTutorialObject { var magicWords: MagicWords = .Abracadbra var avatar = "*" }
这是一个魔法的基本构建块。默认情况下,它的初始化咒语值为“Abracadbra”。
现在,您已经熟悉了我们要介绍的例子中的一些基本术语,那么接下来您要开始施行一些“魔法”了。
为什么要关注错误处理
好的错误处理方法有助于增强最终用户和软件维护人员的体验,使其更容易地查明问题、 问题原因及其可能产生的严重后果。代码中错误处理得越具体,问题就越容易诊断。错误处理也可以让系统用适当的方式抛出错误,从而不至于挫败或扰乱用户。
但并不是需要处理一切错误。如果程序员不处理,语言功能本身也可能会使您完全避免某些类别的错误。作为一般规则,如果你能避免错误的可能性,那么请遵循这样的设计思路。如果你不能避免潜在的错误条件,那么显式处理错误是你最佳的选择。
使用nil避免Swift错误
由于Swift提供了优雅的可选项(Optionals)处理能力;所以,在你期望值出现但未提供值的地方您完全可以避免错误条件。作为一个聪明的程序员,你可以操纵此功能:通过一个错误条件判断故意返回nil。这种方法最适合用于当你到达一个错误状态但你不必采取任何措施的情形;也就是说,你选择不采取措施而不是采取紧急措施。
两个典型的使用nil避免Swift错误的例子是可失败构造器和guard语句。
可失败构造器能够防止一个对象的创建——除非已提供了足够的信息。在Swift 2以前(以及在其他语言中),这一功能通常是通过工厂方法模式实现的。
Swift使用这种模式的一个示例在createWithMagicWords方法中就会看到:
static func createWithMagicWords(words: String) -> Spell? { if let incantation = MagicWords(rawValue: words) { var spell = Spell() spell.magicWords = incantation return spell } else { return nil } }
上述初始化器试图使用提供的咒语创建一个魔法(spell);但如果言语不是咒语(magic words),改以返回nil。
你可以在本教程最底部的代码中观察魔法的创建来查看这种方法的使用:
你会注意到: first使用咒语“abracadabra”成功创建一个魔法,而咒语“ascendio”并不会产生这种效果,并返回second的值为nil。
工厂方法是一种老式的编程风格。其实,在Swift中有更好的方式来实现同样的事情。为此,你只需使用一个可失败构造器而不是工厂方法来更新spell扩展即可。
于是,你可以删除createWithMagicWords(_:)方法并把它替换为以下内容:
init?(words: String) { if let incantation = MagicWords(rawValue: words) { self.magicWords = incantation } else { return nil } }
在这里,你简化了代码——并没有显式创建和返回spell对象。
给first和second赋值的语句行现在会抛出编译时错误:
let first = Spell.createWithMagicWords("abracadabra") let second = Spell.createWithMagicWords("ascendio")
你需要更改这些语句——使用新的初始化器。为此,你只需要使用以下内容替换上面的代码行即可:
let first = Spell(words: "abracadabra") let second = Spell(words: "ascendio")
此后,所有错误应修复完毕,再编译示例工程应没有什么错误。这样修改以后,您的代码整洁多了——但是其实你可以做得比这更好!
guard是一种断言某事是否为真实的快速方法。然后,如果检查失败,您可以执行事先设计的代码块。
Guard是Swift 2引入的,通常用于通过调用堆栈以冒泡法处理错误,最终错误将得到处理。Guard语句允许提前退出一个函数或方法;这使得程序员更清楚对于剩下的要运行的处理逻辑需要存在哪些条件。
为了进一步精简魔法的可失败构造器,我们再来使用guard方法修改一下上面代码:
init?(words: String) { guard let incantation = MagicWords(rawValue: words) else { return nil } self.magicWords = incantation }
这样修改后,就没有必要再在一个单独的行上使用一个单独的else子句;而且,失败的情况也更加明显,因为它现在位于初始化程序的顶部。
请注意,第一和第二个魔法常量的值没有改变,但代码却变得更为精简了。
使用定制处理器避免错误
上面通过精简魔法的可失败构造器并通过巧妙地使用nil已经可以避免一些错误。接下来,让我们来处理一些更复杂的错误。
为了学习接下来的错误处理技术,请打开工程Avoiding-Errors-with-Custom-Handling-Starter.playground-1。
请注意下面代码中的特征:
struct Spell: MagicalTutorialObject { var magicWords: MagicWords = .Abracadbra var avatar = "*" init?(words: String) { guard let incantation = MagicWords(rawValue: words) else { return nil } self.magicWords = incantation } init?(magicWords: MagicWords) { self.magicWords = magicWords } }
这里定义的是Spell的构造器,我们对之作了简要修改以匹配您在本教程的第一部分所完成的工作。此外,请注意这里还使用了MagicalTutorialObject协议和另外一个为了方便使用而引入的可失败构造器。
protocol Familiar: MagicalTutorialObject { var noise: String { get } var name: String? { get set } init() init(name: String?) }
这里的Familiar协议将适用于各类宠物(如蝙蝠和蟾蜍,为女巫所豢养和驱使),在本文第二个示例工程中一直这样使用。
接下来看女巫(Witch)的定义:
struct Witch: MagicalBeing { var avatar = "*" var name: String? var familiar: Familiar? var spells: [Spell] = [] var hat: Hat? init(name: String?, familiar: Familiar?) { self.name = name self.familiar = familiar if let s = Spell(magicWords: .PrestoChango) { self.spells = [s] } } init(name: String?, familiar: Familiar?, hat: Hat?) { self.init(name: name, familiar: familiar) self.hat = hat } func turnFamiliarIntoToad() -> Toad { if let hat = hat { if hat.isMagical { // When have you ever seen a Witch perform a spell without her magical hat on ? :] if let familiar = familiar { // Check if witch has a familiar if let toad = familiar as? Toad { // Check if familiar is already a toad - no magic required return toad } else { if hasSpellOfType(.PrestoChango) { if let name = familiar.name { return Toad(name: name) } } } } } } return Toad(name: "New Toad") // This is an entirely new Toad. } func hasSpellOfType(type: MagicWords) -> Bool { // Check if witch currently has appropriate spell in their spellbook return spells.contains { $0.magicWords == type } } }
现在,让我们简单作一下总结:
初始化女巫:使用name和familiar参数,或者再添加一个hat参数。
一个女巫知道有限数量的魔法;这些存储魔法在spells中,spells是一个魔法对象的数组。
每一个巫婆似乎都有一个嗜好,即在turnFamiliarIntoToad()方法中通过使用PrestoChango咒语把她的宠物变成一只癞蛤蟆。
请注意上面方法turnFamiliarIntoToad()中的缩进字符的数量。此外,你还应当注意:该方法中如果有任何差错,将返回一只全新的蟾蜍。这似乎令人费解(而且有些错误!)。在下一节中,通过自定义错误处理技术您会完全明白这段代码的。
在上面的turnFamiliarIntoToad()方法中使用了多级嵌套语句来控制程序流程,而阅读这样的嵌套代码相当费劲。
如你前面看到的,Guard语句和多个可选绑定的使用有助于清除上面金字塔式复杂代码。然而,利用do-catch机制,通过从错误状态处理中解耦控制流能够彻底消除这一问题。
do-catch机制通常出现在以下关键字前后:
throws
do
catch
try
defer
ErrorType
若要查看这些关键字的实际使用,你要抛出多个自定义错误。首先,你要定义你希望处理的语句,你可以通过一个枚举来列出一切可能出错的内容。
然后,将下面的代码添加到你的示例工程(一个与游乐场内容有关的程序)的女巫(Witch)定义的上方:
enum ChangoSpellError: ErrorType { case HatMissingOrNotMagical case NoFamiliar case FamiliarAlreadyAToad case SpellFailed(reason: String) case SpellNotKnownToWitch }
请注意与ChangoSpellError有关的两点:
它符合ErrorType协议,这是在Swift语言中定义错误时的必要条件。
在SpellFailed情形下,可以针对魔法失败通过一个关联值指定一个自定义原因。
接下来,把throws关键字添加到方法签名中,以指示调用此方法时可能会发生错误:
func turnFamiliarIntoToad() throws -> Toad {
然后,在MagicalBeing协议上也作一下更新:
protocol MagicalBeing: MagicalTutorialObject { var name: String? { get set } var spells: [Spell] { get set } func turnFamiliarIntoToad() throws -> Toad }
既然你已经列出所有错误状态,接下来你可以重构turnFamiliarIntoToad()方法。
首先,修改下面的语句以确保女巫戴着她最重要的帽子,把语句:
if let hat = hat {
修改为:
guard let hat = hat else { throw ChangoSpellError.HatMissingOrNotMagical }
下一行包含一个布尔值检查,也是与帽子相关的:
if hat.isMagical {
您可以选择添加一个单独的guard语句来执行这项检查;但是,把一组检查统一放在一行代码中更为清楚。因此,你可以更改第一个的guard语句,像下面这样:
guard let hat = hat where hat.isMagical else { throw ChangoSpellError.HatMissingOrNotMagical }
现在,经这样一修改,也一并消除了if hat.isMagical {检查部分。
在下一节中,你会继续解除那种成金字塔形可怕的条件语句。
接下来,让我们修改检测是否女巫是否含有familiar的语句,即把语句:
if let familiar = familiar {
修改为从另一个guard语句中抛出一个错误:
guard let familiar = familiar else { throw ChangoSpellError.NoFamiliar }
目前,我们先忽略发生的任何错误,因为你接下来的代码更改会使它们消失。
在下一行中,如果巫婆想要对毫无戒心的两栖类施加turnFamiliarIntoToad()咒语的话,你需要返回现有的蟾蜍,但使用一种明确的错误会更好地告知她犯的错误。为此,把以下内容:
if let toad = familiar as? Toad { return toad }
修改成如下语句:
if familiar is Toad { throw ChangoSpellError.FamiliarAlreadyAToad }
注意到,这里把as?修改为is,从而可以更简洁地检查与协议的一致性,而不一定需要使用结果。关键字is还可以用于更普遍形式的类型比较。如果你有兴趣更多地学习is和as,建议你阅读苹果官网中《Swift编程语言》的类型转换部分( https://developer.apple.com/library/ios/documentation/Swift/Conceptual/Swift_Programming_Language/TypeCasting.html )。
使用上面技术,我们就可以把else子句里面的内容放到else子句外面,从而删除掉else。
最后,对hasSpellOfType(type:)方法的调用可以确保女巫在她的魔法书中存在适当的魔法。为此,我们把下面的代码:
if hasSpellOfType(.PrestoChango) { if let toad = f as? Toad { return toad } }
更改为如下代码:
guard hasSpellOfType(.PrestoChango) else { throw ChangoSpellError.SpellNotKnownToWitch } guard let name = familiar.name else { let reason = "Familiar doesn’t have a name." throw ChangoSpellError.SpellFailed(reason: reason) } return Toad(name: name)
现在,您可以删除最后一行代码,这是没有问题的,即删除下面一行:
return Toad(name: "New Toad")
现在,你拥有了以下简练的方法。其中,我提供了几点补充,以进一步解释代码的作用:
func turnFamiliarIntoToad() throws -> Toad { // When have you ever seen a Witch perform a spell without her magical hat on ? :] guard let hat = hat where hat.isMagical else { throw ChangoSpellError.HatMissingOrNotMagical } // Check if witch has a familiar guard let familiar = familiar else { throw ChangoSpellError.NoFamiliar } // Check if familiar is already a toad - if so, why are you casting the spell? if familiar is Toad { throw ChangoSpellError.FamiliarAlreadyAToad } guard hasSpellOfType(.PrestoChango) else { throw ChangoSpellError.SpellNotKnownToWitch } // Check if the familiar has a name guard let name = familiar.name else { let reason = "Familiar doesn’t have a name." throw ChangoSpellError.SpellFailed(reason: reason) } // It all checks out! Return a toad with the same name as the witch's familiar return Toad(name: name) }
以前,从turnFamiliarIntoToad()方法中返回一个可选项仅表明了“施加这个魔法时出现了某种错误”。但,像这样使用自定义的错误,你可以更清楚地表示错误状态,从而对其做出相应的反应。
其他适合定制错误的地方
现在,既然建立了方法可以抛出自定义Swift错误,你就需要进一步处理这些错误。这样做的标准机制称为do-catch语句,这类似于在其他如Java语言中使用的try-catch机制。
现在,请将下面的代码添加到你的工程文件的最后:
func exampleOne() { print("") // Add an empty line in the debug area // 1 let salem = Cat(name: "Salem Saberhagen") salem.speak() // 2 let witchOne = Witch(name: "Sabrina", familiar: salem) do { // 3 try witchOne.turnFamiliarIntoToad() } // 4 catch let error as ChangoSpellError { handleSpellError(error) } // 5 catch { print("Something went wrong, are you feeling OK?") } }
以下是该函数所实现的任务:
1. 创建这个女巫的宠物,它是一只叫Salem的猫。
2. 创建女巫,名字叫Sabrina。
3. 尝试把这只猫变成一只癞蛤蟆。
4. 捕获一个ChangoSpellError错误,并适当地处理错误。
5. 最后,捕捉所有其他错误并打印出一条友好的信息。
添加上述内容后,你会看到一个编译器错误。现在,我们着手解决这个问题。
handleSpellError()方法尚未定义,因此,把下列代码添加到前面exampleOne()函数定义的上面:
func handleSpellError(error: ChangoSpellError) { let prefix = "Spell Failed." switch error { case .HatMissingOrNotMagical: print("/(prefix) Did you forget your hat, or does it need its batteries charged?") case .FamiliarAlreadyAToad: print("/(prefix) Why are you trying to change a Toad into a Toad?") default: print(prefix) } }
最后,把以下内容添加到文件的底部并运行工程代码:
exampleOne()
你会看到调试控制台输出显示有关内容:
下面给出在上面的代码片段中使用的每一个Swift 2错误处理技术的简短概括。
你可以在Swift中使用模式匹配来处理特定错误或把几种错误类型放在一起处理。
上面的代码中向你演示了捕获错误的几种用法:一个是捕获特定的ChangoSpell错误,一个是一起处理剩余错误的情况。
你可以使用try子句并结合do-catch子句来清楚表明哪些行或代码段可能抛出错误。
你可以通过几种不同的方式来使用try命令,上面使用过的是下面之一:
try——清楚和直接的do-catch语句中的标准用法,这是上面代码中使用的方式。
try?——本质上是通过忽视错误的方式来处理错误;如果抛出一个错误,语句的结果将为nil。
try!——该子句强调一种期望结果:理论上,一个语句能够抛出一个错误;但实际上,这种错误条件永远不会发生。try!子句可用于像加载文件这样的编程代码中,这种情况下你有把握确保某些所需的媒体存在。应小心使用这个子句。
现在,让我们具体地了解try?子句的用法。你可以把下列代码剪切并粘贴到你上面文件的底部:
func exampleTwo() { print("") // Add an empty line in the debug area let toad = Toad(name: "Mr. Toad") toad.speak() let hat = Hat() let witchTwo = Witch(name: "Elphaba", familiar: toad, hat: hat) let newToad = try? witchTwo.turnFamiliarIntoToad() if newToad != nil { // Same logic as: if let _ = newToad print("Successfully changed familiar into toad.") } else { print("Spell failed.") } }
请注意上面代码中exampleOne的不同之处。在这里,你不必关心特定错误的输出问题,但仍然要捕捉发生错误的事实。这里并没有创建蟾蜍宠物;所以,newToad的值为nil。
如果一个函数或方法抛出错误,在Swift中需要使用throws关键字。抛出的错误会沿调用堆栈向上自动传播,但人们普遍认为让错误从其发生源地传播太远是一个不好的做法。在整个代码库中增加错误可能性的重大传播可能会躲过恰当的错误处理机会;为此,借助于throws关键字可以确保传播能够在代码中记录在案,并且对编程人员也很容易了解这一点。
目前为止,你所看到的所有示例都使用了throws,但怎么使用rethrows呢?
rethrows告诉编译器仅当函数参数抛出错误时这个函数才抛出错误。下面是一个最直接的例子(无需将它添加到前面的文件中):
func doSomethingMagical(magicalOperation: () throws -> MagicalResult) rethrows -> MagicalResult { return try magicalOperation() }
在这里,doSomethingMagical(_:)方法仅当提供给函数的magicalOperation参数抛出错误时才抛出错误。如果成功了,它返回一个MagicalResult值。
虽然自动传播在大多数情况下工作良好,但也有些情况下,当错误在调用堆栈中向上传播时你可能要进一步控制你的应用程序的行为。
Defer语句提供了一种机制,每当退出当前范围允许程序执行“清理”工作,如方法或函数返回时。它用于管理需要清理的资源——无论动作是否成功。因此,在错误处理上下文中尤为有用。
为了了解defer的使用,请将下面的方法添加到Witch结构的最后:
func speak() { defer { print("*cackles*") } print("Hello my pretties.") }
然后,将下面的代码添加到前面文件的底部:
func exampleThree() { print("") // Add an empty line in the debug area let witchThree = Witch(name: "Hermione", familiar: nil, hat: nil) witchThree.speak() } exampleThree()
在调试控制台中,您应该看到女巫在说完所有话后发出咯咯的笑声。
有趣的是,defer语句是以其编程时顺序的相反的顺序执行的。
现在,我们把另一个defer添加到speak()语句,以便女巫可以咯咯地发笑。然后,女巫在说完所有话后发出咯咯的笑声:
func speak() { defer { print("*cackles*") } defer { print("*screeches*") } print("Hello my pretties.") }
你注意到调试控制台中的输出顺序了吗?这正是defer语句的能力!
与错误有关的更有趣的事情
本文中提供的上述Swift语句使其与很多其他受欢迎的语言保持了一致,从而把Swift从Objective-C基于NSError基础的错误处理方法中分离出来。而大多数情况下的Objective-C错误都是直译式的,编译器中的静态分析器能够很好地帮助你确定你需要哪些错误及何时需要捕获错误。
虽然do-catch相关支持语句在其他语言中也有很大的开销;但是,在Swift语言中,它们基本上像任何其他语句一样处理。这将确保它们的有效性和高效率。
但是,不要因为你可以创建自定义错误并抛出错误并随意地使用。这方面,建议你针对你开发的工程先制定一些准则:何时需要抛出和捕获错误。对此,我提出下列建议:
确保错误类型在您的整个代码库中被清楚地命名。
当单个错误状态时尽量使用可选值(Optionals)。
当存在超过一个错误状态时使用自定义错误处理技术。
不允许错误从其源地传播得太远。
在各种Swift论坛中经常讨论几种高级错误处理想法。其中,谈论最多的概念之一是非类型化传播的问题。
“......我们相信我们可以扩展我们当前的模型以支持非类型化传播的普遍错误。这项工作怎样才能做得很好——特别是在不完全牺牲代码大小和性能的情况下,将引发大量深度研究。可以预测,在Swift 2.0中实现这一方法是没有问题的。”(来自《Swift 2.x Error Handling》)
不论你是否在享用Swift 3中的主流错误处理思想,是否对于今天存在的东西满意,令人高兴的是,随着语言技术的继续发展,整洁的错误处理技术正在各地积极讨论中并不断改进。
小结
您可以下载本教程已完成的游乐场示例工程进一步研究讨论,地址是 https://cdn2.raywenderlich.com/wp-content/uploads/2016/04/Magical-Error-Handling-in-Swift.zip 。
如果你渴望看到有关Swift 3的新进展,我推荐你参阅《Swift Language Proposals》( https://github.com/apple/swift/tree/master/docs/proposals )。
希望到目前为止,你已经真正沉迷于Swift中的错误处理技术。