Swift: Swift 错误处理教程:do-catch、try 和 throws 详解

错误处理就像汽车的安全气囊——平时用不到,但关键时刻能救命。本课带你掌握 Swift 中从抛出错误到优雅捕获的完整错误处理体系。

1. 你将学到


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 `


❓ 常见问题

Q do-catch 和 try? 应该怎么选?
A 需要针对不同错误做不同处理时用 do-catch。只关心成功或失败、不需要错误详情时用 try?。try? 返回 nil 表示失败,简洁高效。
Q try! 安全吗?
A 不安全。try! 在出错时直接崩溃,相当于对编译器说"我保证这不会错"。只在确信 100% 不会出错时使用(如硬编码的测试数据)。
Q 一个函数可以抛出多种错误,catch 时怎么区分?
A 在 catch 后面指定具体错误类型,如 catch FileError.notFound。也可以用多个 catch 子句分别处理不同类型。最后一个 catch { } 兜底。
Q defer 的执行时机是什么?
A defer 在当前作用域退出时执行,无论正常退出还是抛出错误。如果有多个 defer,按注册顺序的逆序执行(栈式)。
Q throws 和 rethrows 有什么区别?
A throws 是函数自身可能抛出错误。rethrows 是函数本身不抛错,但它的闭包参数可能抛出错误。rethrows 常用于高阶函数如 map。
Q 能抛出的错误类型必须是 enum 吗?
A 不必须。任何遵守 Error 协议的类型都可以——enum、struct、class 都行。但 enum 最常用,因为可以清晰地列举所有错误情况。

📖 小节


📝 作业

  1. 基础题: 定义一个 BankError 枚举(包含 insufficientFunds、invalidAccount、 rozenAccount),写一个模拟取款的函数 withdraw(amount: Double),用 throws 抛出相应错误,然后用 do-catch 处理。
  2. 进阶题: 写一个 JSON 解析函数 parsePerson(json: String) throws -> (name: String, age: Int),处理三种错误:空字符串、缺少 name 字段、年龄不是数字。用 try? 和 do-catch 分别调用一次。
  3. 挑战题: 创建一个"命令行计算器"。输入字符串如 "3 + 4"、"10 / 0"、"abc",解析并计算结果。自定义 CalculationError(包含 invalidExpression、divisionByZero、unknownOperator),实现完整的错误处理流程。用 do-catch 给出友好的错误提示。
Web-Tutorial.com

Web-Tutorial 技术团队

由多位开发者共同维护的编程教程平台。每篇教程由对应领域的开发者编写和审核,确保内容准确可靠。如发现任何问题,欢迎向我们反馈。

100%

🙏 帮我们做得更好

我们是刚上线的编程教程站,几个人的小团队,精力有限。页面虽经检查,难免还有疏漏——链接失效、排版错乱、内容有误、语言生硬……

如果您发现了,麻烦告诉我们,我们会在收到反馈后第一时间进行修复,再次感谢您的光临 🙏