文章目录

Swift 错误处理:do-try-catch 与 throw

发布于 2026-04-05 15:16:34 · 浏览 13 次 · 评论 0 条

Swift 错误处理:do-try-catch 与 throw

程序运行过程中,错误无处不在。网络请求可能失败、文件读写可能出错、用户输入可能不符合预期。Swift 提供了一套完整的错误处理机制,让你能优雅地识别、管理和恢复这些异常情况。

这篇文章将手把手教你掌握 Swift 的错误处理核心:throwtrydo-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 letnil 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 释放资源,确保资源不会因为错误而泄漏。这是编写健壮代码的重要习惯。

评论 (0)

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

扫一扫,手机查看

扫描上方二维码,在手机上查看本文