Logo

理解 Swift 中的错误处理

了解如何在 Swift 代码中触发和优雅地处理错误。此外,学习如何设计代码以抛出和捕获错误,从而采取纠正措施,或向用户说明所遇到问题的性质。

检查点

理解 Swift 中的错误处理

了解如何在 Swift 代码中触发和优雅地处理错误。此外,学习如何设计代码以抛出和捕获错误,从而采取纠正措施,或向用户说明所遇到问题的性质。

在理想世界中,运行代码永远不会遇到错误。然而现实是,无法保证应用程序在执行过程中不会出现任何形式的错误。因此,确保应用程序的代码能够优雅地处理可能出现的任何错误至关重要。自 Swift 2 引入以来,Swift 开发者处理错误的任务变得容易得多。

本课程将介绍使用 Swift 处理错误的方法,并涵盖以下主题:错误类型、抛出方法和函数、guard、defer 和 do-catch 语句。

理解错误处理

无论 Swift 代码设计和实现得多么仔细,总会存在超出应用程序控制的情况。例如,依赖活跃网络连接的代码无法控制信号丢失,也无法阻止用户开启“飞行模式”。但代码可以实现稳健的错误处理机制(例如,向用户显示提示信息,说明应用需要有效的互联网连接才能继续)。

Swift 中的错误处理包含两个方面。第一方面是在方法中未能达到预期结果时触发(或抛出)错误;第二方面是在方法抛出错误后进行捕获和处理。

当抛出错误时,该错误具有特定的错误类型,可用于识别错误的具体性质,并决定最合适的应对措施。错误类型值可以是任何符合 Error 协议的值。

声明错误类型

举个例子,考虑一个需要将文件传输到远程服务器的方法。该方法可能因多种原因失败,例如没有网络连接、连接速度过慢,或找不到要传输的文件。这些可能的错误都可以通过一个符合 Error 协议的枚举来表示,如下所示:

1enum FileTransferError: Error { 2 case noConnection 3 case lowBandwidth 4 case fileNotFound 5}

一旦声明了错误类型,就可以在方法中使用它来抛出错误。

抛出错误

方法或函数通过使用 throws 关键字声明其可能抛出错误。例如:

1func transferFile() throws { 2}

如果函数或方法返回结果,则 throws 关键字应放在返回类型之前,如下所示:

1func transferFile() throws -> Bool { 2}

一旦方法被声明为可能抛出错误,就可以添加代码,在遇到错误时使用 throw 语句进行抛出。这通常与 guard 语句结合使用。以下代码声明了一些常量作为状态值,并实现了 guard 和 throw 行为:

1enum FileTransferError: Error { 2 case noConnection 3 case lowBandwidth 4 case fileNotFound 5} 6 7func fileTransfer() throws { 8 9 guard connectionOK else { 10 throw FileTransferError.noConnection 11 } 12 13 guard connectionSpeed > 30 else { 14 throw FileTransferError.lowBandwidth 15 } 16 17 guard fileFound else { 18 throw FileTransferError.fileNotFound 19 } 20}

在方法体中,每个 guard 语句检查某个条件是否为真或假。如果结果为假,则执行 else 块中的代码。在这种情况下,使用 throw 语句抛出 FileTransferError 枚举中定义的某个错误值。

调用抛出错误的方法和函数

一旦方法或函数被声明为可能抛出错误,就不能再以常规方式调用。必须使用 try 语句来调用此类方法,如下所示:

1try fileTransfer()

以下代码示例结合了 FileTransferError 的声明和 fileTransfer() 方法。还声明了一些状态常量以模拟真实场景。最后,使用 try 语句调用了 fileTransfer() 方法:

1let connectionOK = true 2let connectionSpeed = 30.00 3let fileFound = false 4 5enum FileTransferError: Error { 6 case noConnection 7 case lowBandwidth 8 case fileNotFound 9} 10 11func fileTransfer() throws { 12 13 guard connectionOK else { 14 throw FileTransferError.noConnection 15 } 16 17 guard connectionSpeed > 30 else { 18 throw FileTransferError.lowBandwidth 19 } 20 21 guard fileFound else { 22 throw FileTransferError.fileNotFound 23 } 24} 25 26try fileTransfer()

如我们所见,当代码执行时,会抛出 lowBandwidth 错误,导致应用程序崩溃。由于错误处理的目标是避免崩溃,显然还需要进一步完善。

事实上,除了使用 try 语句外,调用还必须位于 do-catch 语句中,以捕获并处理可能抛出的任何错误。例如,假设 fileTransfer 方法需要在名为 sendFile 的方法中被调用,该方法的实现可能如下:

1let connectionOK = true 2let connectionSpeed = 30.00 3let fileFound = false 4 5func sendFile() -> String { 6 7 do { 8 try fileTransfer() 9 } catch FileTransferError.noConnection { 10 return("No Network Connection") 11 } catch FileTransferError.lowBandwidth { 12 return("File Transfer Speed Too Low") 13 } catch FileTransferError.fileNotFound { 14 return("File not Found") 15 } catch { 16 return("Unknown error") 17 } 18 19 return("Successful transfer") 20} 21 22let result = sendFile() 23print("Result from sendFile() call = \(result)")

该方法在 do-catch 语句中调用 fileTransfer 方法,并为三种可能的错误情况分别设置了 catch 条件。每种情况下,方法都简单地返回一个包含错误描述的字符串。如果没有抛出错误,则返回表示文件传输成功的字符串。注意,还包含了一个不带模式匹配的第四种 catch 条件,这是一个“兜底”语句,确保所有未被前面 catch 语句匹配的错误也能被处理。这是必需的,因为 do-catch 语句必须是全面的(即,必须覆盖所有可能的错误情况)。

现在,当代码执行时,sendFile() 方法能够输出更有意义的消息,处理错误,并防止程序崩溃。

Swift 还允许在一个 catch 语句中声明多个匹配项,多个匹配项之间用逗号分隔。例如,可以使用单个 catch 声明同时处理 noConnection 和 lowBandwidth 错误,如下所示:

1let connectionOK = false 2let connectionSpeed = 35.00 3let fileFound = true 4 5func sendFile() -> String { 6 7 do { 8 try fileTransfer() 9 } catch FileTransferError.noConnection, FileTransferError.lowBandwidth { 10 return("Connection problem") 11 } catch FileTransferError.fileNotFound { 12 return("File not Found") 13 } catch { 14 return("Unknown error") 15 } 16 17 return("Successful transfer") 18} 19 20let result = sendFile() 21print("Result from sendFile() call = \(result)")

访问错误对象

当方法调用失败时,通常会返回一个标识失败性质的 Error 对象。在 catch 语句中,一个常见需求是获取该对象,以便在应用代码中采取适当的纠正措施。以下代码演示了如何在尝试创建新文件系统目录时从 catch 语句中访问错误对象:

1do { 2 try filemgr.createDirectory(atPath: newDir, 3 withIntermediateDirectories: true, 4 attributes: nil) 5} catch let error { 6 print("Error: \(error.localizedDescription)") 7}

在上述代码中,通过访问 error 对象的 localizedDescription 属性,获取了以用户选择语言显示的错误消息文本,并将其打印到控制台。

禁用错误捕获

可以通过使用 try! 语句强制让一个抛出错误的方法运行,而无需将其包裹在 do-catch 语句中,如下所示:

1try! fileTransfer()

注意:使用此方法时,我们告知编译器,我们绝对确定该方法调用不会引发错误。如果使用此技术时确实抛出了错误,代码将在运行时失败。因此,应谨慎使用此方法。

使用 defer 语句

之前实现的 sendFile 方法展示了处理错误的常见场景。do-catch 语句中的每个 catch 子句都包含一个 return 语句,将控制权返回给调用方。但在这种情况下,可能希望在返回控制权之前执行某些其他任务,无论遇到何种错误。例如,sendFile 方法可能需要在返回前删除临时文件。这种行为可以通过 defer 语句实现。

defer 语句允许声明一组代码,在方法返回时立即执行。在以下代码中,sendFile 方法已修改并加入了 defer 语句:

1let connectionOK = false 2let connectionSpeed = 35.00 3let fileFound = true 4 5func sendFile() -> String { 6 7 defer { 8 removeTmpFiles() 9 closeConnection() 10 } 11 12 do { 13 try fileTransfer() 14 } catch FileTransferError.noConnection { 15 return("No Network Connection") 16 } catch FileTransferError.lowBandwidth { 17 return("File Transfer Speed too Low") 18 } catch FileTransferError.fileNotFound { 19 return("File not Found") 20 } catch { 21 return("Unknown error") 22 } 23 24 return("Successful transfer") 25} 26 27func removeTmpFiles() { 28 print("Removing temp files") 29} 30 31func closeConnection() { 32 print("Closing connection") 33} 34 35let result = sendFile() 36print("Result from sendFile() call = \(result)")

加入 defer 语句后,无论哪个 return 语句被触发,都会在方法返回前始终调用 removeTmpFiles 和 closeConnection 方法。

总结

错误处理是编写健壮可靠代码的重要组成部分。自 Swift 2 引入以来,触发和处理错误变得更加容易。错误类型通常通过符合 Error 协议的值来创建,最常见的实现方式是使用枚举。可能抛出错误的方法和函数通过 throws 关键字声明。在这些方法或函数的主体中,使用 guard 和 throw 语句根据错误类型抛出错误。

可抛出错误的方法或函数必须使用 try 语句调用,且必须封装在 do-catch 语句中。do-catch 语句由一系列详尽的 catch 模式构成,每个模式包含在特定错误被抛出时要执行的代码。通过 defer 语句,可以在方法返回时定义清理任务,无论是否因错误中断执行。

课程回顾

  • Swift 错误处理涉及抛出、捕获和处理错误事件。

  • 错误类型通常声明为符合 Swift Error 协议的枚举。

  • 任何可能抛出错误的函数或方法都必须使用 throws 关键字声明。

  • 使用 guard 和 throw 语句抛出错误。

  • 可能抛出错误的函数或方法必须使用 try 语句调用。

  • 使用 try 语句的调用必须通过 do-catch 语句处理潜在错误。

  • 可以使用 try! 关键字禁用调用的错误捕获。

  • defer 语句允许在函数或方法返回前执行特定任务,无论是否因错误中断执行。

测验

花些时间测试你对 Swift 错误处理知识的掌握程度。

练习

使用以下练习编写自己的 Swift 错误处理代码。

练习 1

修改以下代码中的 moveFile 方法,以检测并抛出 StorageError 枚举中的两种错误状态:

1let storageAvailable = false 2let diskOK = true 3 4enum StorageError: Error { 5 case diskError 6 case diskFull 7} 8 9func moveFile() { 10 print("File moved successfully") 11} 12 13try moveFile()

解答

1let storageAvailable = false 2let diskOK = true 3 4enum StorageError: Error { 5 case diskError 6 case diskFull 7} 8 9func moveFile() throws { 10 11 guard storageAvailable else { 12 throw StorageError.diskFull 13 } 14 15 guard diskOK else { 16 throw StorageError.diskError 17 } 18 19 print("File moved successfully") 20} 21 22try moveFile()

练习 2

修改 backupFile 方法,使其捕获 moveFile 方法可能抛出的两种错误,并输出合适的诊断信息。

1let storageAvailable = false 2let diskOK = true 3 4enum StorageError: Error { 5 case diskError 6 case diskFull 7} 8 9func moveFile() throws { 10 11 guard storageAvailable else { 12 throw StorageError.diskFull 13 } 14 15 guard diskOK else { 16 throw StorageError.diskError 17 } 18 19 print("File moved successfully") 20} 21 22func backupFile() { 23 try moveFile() 24}

解答

1let storageAvailable = false 2let diskOK = true 3 4enum StorageError: Error { 5 case diskError 6 case diskFull 7} 8 9func moveFile() throws { 10 11 guard storageAvailable else { 12 throw StorageError.diskFull 13 } 14 15 guard diskOK else { 16 throw StorageError.diskError 17 } 18 19 print("File moved successfully") 20} 21 22func backupFile() { 23 24 do { 25 try moveFile() 26 } catch StorageError.diskError { 27 print("Disk failure detected.") 28 } catch StorageError.diskFull { 29 print("The destination disk is full.") 30 } catch { 31 print("Unknown failure.") 32 } 33} 34 35backupFile()

练习 3

修改以下示例中的 backupFile 方法,使得在捕获到错误时调用名为 cleanUp 的方法:

1let storageAvailable = false 2let diskOK = true 3 4enum StorageError: Error { 5 case diskError 6 case diskFull 7} 8 9func moveFile() throws { 10 11 guard storageAvailable else { 12 throw StorageError.diskFull 13 } 14 15 guard diskOK else { 16 throw StorageError.diskError 17 } 18 19 print("File moved successfully") 20} 21 22func backupFile() { 23 24 do { 25 try moveFile() 26 } catch StorageError.diskError { 27 print("Disk failure detected.") 28 } catch StorageError.diskFull { 29 print("The destination disk is full.") 30 } catch { 31 print("Unknown failure.") 32 } 33} 34 35func cleanUp() { 36 print("Cleaning up file.") 37} 38 39backupFile()

解答

1let storageAvailable = false 2let diskOK = true 3 4enum StorageError: Error { 5 case diskError 6 case diskFull 7} 8 9func moveFile() throws { 10 11 guard storageAvailable else { 12 throw StorageError.diskFull 13 } 14 15 guard diskOK else { 16 throw StorageError.diskError 17 } 18 19 print("File moved successfully") 20} 21 22func backupFile() { 23 24 defer { 25 cleanUp() 26 } 27 28 do { 29 try moveFile() 30 } catch StorageError.diskError { 31 print("Disk failure detected.") 32 } catch StorageError.diskFull { 33 print("The destination disk is full.") 34 } catch { 35 print("Unknown failure.") 36 } 37} 38 39func cleanUp() { 40 print("Cleaning up file.") 41} 42 43backupFile()

Quiz

单选题

可能引发错误的方法或函数必须使用哪个关键字进行声明?

1 / 4

Swift 数组和字典集合

在 Swift 中处理文件和目录