Swift 错误处理:do-try-catch 与 throw
程序运行过程中,错误无处不在。网络请求可能失败、文件读写可能出错、用户输入可能不符合预期。Swift 提供了一套完整的错误处理机制,让你能优雅地识别、管理和恢复这些异常情况。
这篇文章将手把手教你掌握 Swift 的错误处理核心:throw、try 和 do-catch。
一、理解 Swift 中的错误
在 Swift 中,错误被表示为遵循 Error 协议的类型。Swift 的枚举类型特别适合定义一组相关的错误状态,因为枚举可以关联额外的信息,帮助你了解错误的具体原因。
// 定义一个错误类型
enum NetworkError: Error {
case invalidURL // URL 无效
case noConnection // 无网络连接
case timeout // 请求超时
case serverError(Int) // 服务器错误,附带状态码
}
Error 协议本身是一个空协议,仅仅用于标记类型为"可错误"。Swift 不关心错误具体长什么样,只要遵循这个协议,就能被错误处理系统识别。
二、用 throw 抛出错误
抛出 错误意味着函数或方法遇到了无法继续执行的情况,需要让调用者知道并处理。throw 关键字用于主动触发一个错误。
func fetchData(from urlString: String) throws -> Data {
// 检查 URL 是否有效
guard let url = URL(string: urlString) else {
// URL 无效,抛出错误
throw NetworkError.invalidURL
}
// 假设这里是网络请求逻辑
// 如果请求失败,可以抛出其他错误
// throw NetworkError.timeout
return Data()
}
注意函数声明后面的 throws 关键字。它告诉调用者:"这个函数可能会抛出错误,你需要准备好处理它"。这种显式声明的好处是,调用者必须直面错误的存在,不能假装它不存在。
对于简单的错误,使用无参数的枚举成员(如 .invalidURL);对于需要携带额外信息的错误,添加关联值(如 .serverError(Int) 保存 HTTP 状态码)。
三、用 try 尝试可能出错的操作
当调用一个 throws 函数时,必须在前面加上 try 关键字。try 有三种形式,每种适用于不同的场景。
1. try + do-catch(最常用)
这是最完整的错误处理方式。执行 可能出错的代码,捕获 并处理 具体错误。
func loadUserProfile() {
do {
// 尝试执行可能出错的代码
let data = try fetchData(from: "https://api.example.com/user")
print("数据加载成功: \(data)")
} catch NetworkError.invalidURL {
// 处理特定错误:URL 无效
print("错误:请检查 URL 是否正确")
} catch NetworkError.timeout {
// 处理特定错误:请求超时
print("错误:网络超时,请稍后重试")
} catch {
// 处理其他所有未知错误
print("发生未知错误: \(error)")
}
}
执行流程是这样的:执行 try 后面的代码,如果一切正常,跳过所有 catch 块继续执行;如果抛出错误,匹配 第一个符合的 catch 块并执行对应逻辑。
catch 块的匹配顺序是从上到下,所以最具体的错误应该放在前面,通用的错误(如 catch { ... })放在最后。
2. try?(转换结果为可选值)
当你只关心"成功还是失败",不关心具体错误原因时,try? 是最简洁的选择。它将结果转换为可选类型:成功返回包装后的值,失败返回 nil。
func exampleOptionalTry() {
// 成功:result 是 Optional(Data)
let result = try? fetchData(from: "https://valid-url.com")
print(result ?? "加载失败") // 输出 Data 的描述或 "加载失败"
// 失败:result 是 nil
let badResult = try? fetchData(from: "invalid-url")
print(badResult ?? "URL 无效") // 输出 "URL 无效"
}
这种写法非常适合链式调用或条件判断场景。如果你想在失败时执行特定逻辑,可以结合 if let 或 nil coalescing operator (??) 使用。
3. try!(强制解包,慎用)
当你百分之百确定某个操作一定会成功时,可以使用 try! 强制执行。如果它失败了,程序会直接崩溃。所以务必谨慎使用,只在有足够信心的情况下才采用。
// 假设这个配置文件在应用中一定存在且格式正确
let configData = try! fetchData(from: "bundle://config.json")
四、用 rethrow 传递错误
如果一个函数的参数是接受 throws 的闭包,它可以使用 rethrows 关键字声明自己只是传递错误,不主动抛出新错误。
func withTask<T>(_ block: () throws -> T) rethrows -> T {
print("开始任务")
// 错误会被 rethrow,传递给调用者处理
let result = try block()
print("任务完成")
return result
}
// 使用
do {
let data = try withTask {
try fetchData(from: "https://example.com")
}
} catch {
print("错误被正确传递: \(error)")
}
rethrows 的主要价值在于明确表达"我只做中间人,不产生新错误",让代码意图更清晰。
五、defer 清理资源
当错误发生时,及时释放资源变得困难。defer 语句确保无论是否发生错误,清理代码都会执行。
func processFile(at path: String) throws {
let file = openFile(at: path)
// 使用 defer 确保文件肯定被关闭
defer {
closeFile(file)
print("文件已关闭")
}
// 如果这里抛出错误,defer 仍会执行
if file.isCorrupted {
throw FileError.corrupted
}
// 处理文件内容...
print("文件处理成功")
}
defer 的执行顺序是后进先出(LIFO)。如果有多个 defer,最后声明的最先执行。
func complexExample() {
defer { print("第一个 defer") }
defer { print("第二个 defer") }
defer { print("第三个 defer") }
}
// 输出顺序:第三个 defer -> 第二个 defer -> 第一个 defer
六、完整实战示例
综合运用上述知识,写一个完整的用户登录函数。
import Foundation
// 1. 定义错误类型
enum AuthError: Error {
case userNotFound
case wrongPassword
case accountLocked
case networkUnavailable
}
// 2. 模拟网络请求
func login(username: String, password: String) throws {
guard !username.isEmpty else {
throw AuthError.userNotFound
}
guard password == "secret123" else {
throw AuthError.wrongPassword
}
// 模拟网络不稳定(随机失败)
let isOnline = Bool.random()
if !isOnline {
throw AuthError.networkUnavailable
}
}
// 3. 处理登录流程
func handleLogin() {
let username = "john_doe"
let password = "wrongpassword"
do {
try login(username: username, password: password)
print("✅ 登录成功!")
} catch AuthError.wrongPassword {
print("❌ 密码错误,请重试")
} catch AuthError.userNotFound {
print("❌ 用户不存在")
} catch AuthError.networkUnavailable {
print("⚠️ 网络连接失败,请检查网络设置")
} catch {
print("🔧 发生未知错误")
}
}
// 执行
handleLogin()
运行这段代码,你会看到错误被正确捕获和处理。如果密码改为 "secret123",则输出登录成功。
七、最佳实践总结
优先使用 do-catch 处理可恢复的错误,让用户有机会修正问题。使用 try? 当你只关心成功与否,不需要具体错误信息。避免使用 try! 除非能 100% 确保不会出错,否则不要让程序有崩溃的风险。
定义清晰的错误类型,让调用者能够针对不同错误做出不同响应。善用关联值携带错误详情,这对调试和问题排查非常有帮助。
使用 defer 释放资源,确保资源不会因为错误而泄漏。这是编写健壮代码的重要习惯。

暂无评论,快来抢沙发吧!