Swift: Swift 错误处理教程:do-catch、try 和 throws 详解
错误处理就像汽车的安全气囊——平时用不到,但关键时刻能救命。本课带你掌握 Swift 中从抛出错误到优雅捕获的完整错误处理体系。
1. 你将学到
- 定义符合 Error 协议的自定义错误类型
- 使用 throws 声明可能抛出错误的函数
- 使用 do-catch 捕获和处理不同类型的错误
- 使用 try? 将错误转换为可选值和 try! 绕过错误
- 使用 defer 在函数退出前执行清理操作
2. 一个 iOS 开发者的真实故事
(1) 痛点:配置文件不存在,App 直接闪退
Alice 开发了一个天气 App,启动时需要读取 config.json 配置文件。如果文件不存在或格式错误,App 直接崩溃: `swift let configPath = "/app/config.json" let configData = try! String(contentsOfFile: configPath) let configLines = configData.split(separator: "\n") 用户 Charlie 更新 App 后发现一直闪退——因为更新时配置文件被误删了。Alice 收到了 50 多个 1 星差评:"打开就闪退,垃圾 App!" 没有错误处理的应用,一个异常就能毁掉整个用户体验。
(2) do-catch 的解法
`swift enum ConfigError: Error { case fileNotFound case invalidFormat } func loadConfig(path: String) throws -> [String: String] { guard FileManager.default.fileExists(atPath: path) else { throw ConfigError.fileNotFound } return ["theme": "dark", "units": "metric"] } do { let config = try loadConfig(path: configPath) print("Config loaded: (config)") } catch ConfigError.fileNotFound { print("Config file not found, using defaults") }
(3) 收益:崩溃率从 12% 降到 0.1%
| 维度 | try! 直接崩溃 | do-catch 处理 |
|---|---|---|
| 配置缺失时 | App 闪退 | 使用默认配置 |
| 1 星差评 | 50+ 个 | 0 |
| 用户流失率 | 8% | 0.5% |
| 修复紧急度 | 必须立即发版 | 下次更新修复 |
3. Error 协议与抛出错误
Swift 用 Error 协议表示错误类型。任何遵守 Error 协议的枚举、结构体或类都可以作为错误抛出。 `mermaid graph TB A[Function] --> B{Error?} B -->|No| C[Return value] B -->|Yes| D[throw Error] D --> E[Caller catches with do-catch] E --> F[Handle] E --> G[Propagate] E --> H[Convert to Optional]
| 关键字 | 用途 | 示例 |
|---|---|---|
| Error | 错误协议 | enum MyError: Error { } |
| hrow | 抛出错误 | hrow MyError.someCase |
| hrows | 函数声明 | unc foo() throws { } |
| ethrows | 参数是闭包且闭包会抛 | unc foo(fn: () throws -> Void) rethrows { } |
(1) 定义错误类型
`swift enum NetworkError: Error { case badURL case timeout(seconds: Int) case serverError(code: Int) case noConnection } enum ValidationError: Error { case emptyField(fieldName: String) case tooShort(minLength: Int) case invalidFormat(pattern: String) }
(2) 声明抛出函数
`swift enum DivisionError: Error { case divisionByZero } func divide(_ a: Int, by b: Int) throws -> Int { guard b != 0 else { throw DivisionError.divisionByZero } return a / b } do { let result = try divide(10, by: 0) print(result) } catch { print("Error: (error)") }
▶ 示例:用户输入验证器
`swift // ============================================ // 用自定义错误验证用户输入 // ============================================ enum ValidationError: Error { case emptyField(String) case tooShort(field: String, min: Int) case invalidEmail } func validateRegistration(username: String, email: String, password: String) throws { guard !username.isEmpty else { throw ValidationError.emptyField("Username") } guard username.count >= 3 else { throw ValidationError.tooShort(field: "Username", min: 3) } guard !email.isEmpty else { throw ValidationError.emptyField("Email") } guard email.contains("@") else { throw ValidationError.invalidEmail } guard password.count >= 6 else { throw ValidationError.tooShort(field: "Password", min: 6) } print("Validation passed!") } do { try validateRegistration(username: "Al", email: "alice@test.com", password: "123") } catch ValidationError.emptyField(let field) { print("(field) cannot be empty") } catch ValidationError.tooShort(let field, let min) { print("(field) must be at least (min) characters") } catch ValidationError.invalidEmail { print("Please enter a valid email") } catch { print("Unknown error: (error)") }
输出:
ext Username must be at least 3 characters
4. do-catch 与错误捕获
do-catch 是捕获和处理错误的主要方式。你可以捕获特定错误、使用模式匹配、或将错误传递出去。
| 捕获方式 | 语法 | 说明 |
|---|---|---|
| 全部捕获 | catch { } | 捕获所有错误 |
| 特定捕获 | catch Error.specific { } | 只处理特定错误 |
| 模式匹配 | catch let error as MyError { } | 类型转换后处理 |
| 带条件 | catch where condition { } | 满足条件才处理 |
| 传递 | ry canThrow() | 不处理,继续抛出 |
(1) do-catch 基本用法
`swift enum FileError: Error { case notFound, permissionDenied, corrupted } func readFile(_ name: String) throws -> String { if name == "secret.txt" { throw FileError.permissionDenied } return "File content: (name)" } do { let content = try readFile("secret.txt") print(content) } catch FileError.notFound { print("File not found") } catch FileError.permissionDenied { print("Access denied") } catch { print("Other error: (error)") }
(2) 错误的传递(不捕获)
`swift func processFile() throws { let content = try readFile("data.txt") print("Processing: (content)") } do { try processFile() } catch { print("Failed to process: (error)") }
▶ 示例:文件读取与解析
`swift // ============================================ // 模拟文件读取和解析的错误处理 // ============================================ enum FileParseError: Error { case fileNotFound(String) case emptyFile case invalidFormat(line: Int) } func parseConfigFile(path: String) throws -> [String: String] { guard path.hasSuffix(".json") else { throw FileParseError.fileNotFound(path) } let content = "name:Alice\nage:25\ninvalid_line" let lines = content.split(separator: "\n") guard !lines.isEmpty else { throw FileParseError.emptyFile } var config: [String: String] = [:] for (index, line) in lines.enumerated() { let parts = line.split(separator: ":") guard parts.count == 2 else { throw FileParseError.invalidFormat(line: index + 1) } config[String(parts[0])] = String(parts[1]) } return config } do { let config = try parseConfigFile(path: "app.config") print("Config: (config)") } catch FileParseError.fileNotFound(let path) { print("File not found at: (path)") } catch FileParseError.emptyFile { print("File is empty") } catch FileParseError.invalidFormat(let line) { print("Invalid format at line (line)") } catch { print("Unknown error: (error)") }
输出:
ext File not found at: app.config
5. try?、try! 与 defer
try? 将错误转换为可选值,try! 断言不会出错(出错时崩溃),defer 在函数退出前执行清理。
| 方式 | 行为 | 适用场景 |
|---|---|---|
| ry | 需在 do-catch 中用 | 标准错误处理 |
| ry? | 错误时返回 nil | 不关心错误类型,只需判断成功/失败 |
| ry! | 出错时崩溃 | 确信不会出错(如测试/硬编码) |
| defer | 函数退出前执行 | 资源清理、关闭文件 |
(1) try? 和 try!
`swift enum ParseError: Error { case invalidNumber } func parseInt(_ text: String) throws -> Int { guard let num = Int(text) else { throw ParseError.invalidNumber } return num } let result1 = try? parseInt("42") let result2 = try? parseInt("abc") print("Result 1: (result1 ?? 0)") print("Result 2: (result2 ?? 0)") let result3 = try! parseInt("100") print("Result 3: (result3)")
(2) defer 清理
`swift func processResource() { print("Step 1: Opening resource") defer { print("Step 3: Closing resource (always runs)") } print("Step 2: Using resource") } processResource()
▶ 示例:安全的网络请求处理
`swift // ============================================ // 综合使用 try/try?/try!/defer // ============================================ enum NetworkError: Error { case badURL, noData, timeout } func fetchData(from urlString: String) throws -> String { defer { print(" [cleanup] Closing connection") } guard !urlString.isEmpty else { throw NetworkError.badURL } guard urlString.contains(".") else { throw NetworkError.badURL } return "{"name": "Alice", "age": 30}" } print("=== try? ===") if let data = try? fetchData(from: "api.example.com") { print("Data: (data)") } else { print("Failed to fetch") } print() print("=== do-catch ===") do { let data = try fetchData(from: "") print("Data: (data)") } catch NetworkError.badURL { print("Error: Invalid URL") } catch { print("Error: (error)") } print() print("=== try! ===") let safeData = try! fetchData(from: "api.example.com") print("Data: (safeData)")
输出: ` ext === try? === [cleanup] Closing connection Data: {"name": "Alice", "age": 30}
=== do-catch === [cleanup] Closing connection Error: Invalid URL
=== try! === [cleanup] Closing connection Data: {"name": "Alice", "age": 30} `
6. 完整示例:配置文件加载器
`swift // ============================================ // 配置文件加载器 // 综合运用 Error/throws/do-catch/try?/defer // ============================================ import Foundation enum ConfigError: Error { case fileNotFound(path: String) case emptyFile case invalidKeyValue(line: Int, content: String) case unsupportedKey(String) } struct AppConfig { var theme: String = "light" var fontSize: Int = 14 var language: String = "en" var notifications: Bool = true } func loadConfig(from path: String) throws -> AppConfig { defer { print("[cleanup] Config loader finished") } guard FileManager.default.fileExists(atPath: path) else { throw ConfigError.fileNotFound(path: path) } let content = try String(contentsOfFile: path) let lines = content.split(separator: "\n") guard !lines.isEmpty else { throw ConfigError.emptyFile } var config = AppConfig() for (index, line) in lines.enumerated() { let trimmed = line.trimmingCharacters(in: .whitespaces) guard !trimmed.isEmpty, !trimmed.hasPrefix("#") else { continue } let parts = trimmed.split(separator: "=", maxSplits: 1).map(String.init) guard parts.count == 2 else { throw ConfigError.invalidKeyValue(line: index + 1, content: String(line)) } let key = parts[0].trimmingCharacters(in: .whitespaces) let value = parts[1].trimmingCharacters(in: .whitespaces) switch key { case "theme": config.theme = value case "fontSize": config.fontSize = Int(value) ?? config.fontSize case "language": config.language = value case "notifications": config.notifications = (value == "true") default: throw ConfigError.unsupportedKey(key) } } return config } let tempDir = NSTemporaryDirectory() let testConfigPath = tempDir + "app.config" let configContent = """ theme=dark fontSize=16 language=en notifications=true """ try? configContent.write(toFile: testConfigPath, atomically: true, encoding: .utf8) do { let config = try loadConfig(from: testConfigPath) print("=== App Configuration ===") print("Theme: (config.theme)") print("Font Size: (config.fontSize)") print("Language: (config.language)") print("Notifications: (config.notifications)") } catch ConfigError.fileNotFound(let path) { print("Fatal: Config file not found at (path)") print("Using default configuration") } catch ConfigError.invalidKeyValue(let line, let content) { print("Error: Invalid format at line (line): (content)") } catch ConfigError.unsupportedKey(let key) { print("Warning: Unsupported key '(key)', using default") } catch { print("Unexpected error: (error)") } if let fallbackConfig = try? loadConfig(from: tempDir + "nonexistent.config") { print("\nLoaded fallback config") } else { print("\nFallback not found, will use defaults") }
输出: ` ext [cleanup] Config loader finished === App Configuration === Theme: dark Font Size: 16 Language: en Notifications: true [cleanup] Config loader finished
Fallback not found, will use defaults `
❓ 常见问题
📖 小节
- Error 协议是 Swift 错误处理的基础,通常用 enum 定义错误类型
- throws 标记可能抛出错误的函数,throw 实际抛出错误
- do-catch 捕获和处理错误,支持按类型分别处理
- try? 将错误转为可选值,失败时返回 nil
- try! 跳过错误处理,出错时直接崩溃——谨慎使用
- defer 在函数退出时执行清理代码,确保资源释放
- 良好的错误处理能将崩溃率从 10%+ 降到接近 0
📝 作业
- 基础题: 定义一个 BankError 枚举(包含 insufficientFunds、invalidAccount、rozenAccount),写一个模拟取款的函数 withdraw(amount: Double),用 throws 抛出相应错误,然后用 do-catch 处理。
- 进阶题: 写一个 JSON 解析函数 parsePerson(json: String) throws -> (name: String, age: Int),处理三种错误:空字符串、缺少 name 字段、年龄不是数字。用 try? 和 do-catch 分别调用一次。
- 挑战题: 创建一个"命令行计算器"。输入字符串如 "3 + 4"、"10 / 0"、"abc",解析并计算结果。自定义 CalculationError(包含 invalidExpression、divisionByZero、unknownOperator),实现完整的错误处理流程。用 do-catch 给出友好的错误提示。