2.3 演出事故救场:Swift错误处理


2.3 演出事故救场:Swift 错误处理

本节摘要:本节处理界面加载最脆弱的一环——失败。用 Error 协议给事故分类,用 throws 声明"这台戏可能出事故",用 do-catch 在界面上接住它;并对比 try、try?、try! 与 Result 四种姿态的取舍。结论先行:错误处理不是补丁,而是界面状态机的第三条分支(加载中 / 成功 / 失败)。

替补演员(2.2 节)能把"稍后的动作"递到现场,但递回来的可能是坏消息——这一节给坏消息写剧本。

事故报告:一次加载失败的解剖

排片界面发起请求后,至少有四种姿势摔倒:断网、服务器抽风、返回的数据缺字段、用户没登录。前三种发生在"取数据"环节,第四种发生在"读凭据"环节。Swift 错误处理的第一步,是把这些事故枚举成类型

// 错误枚举:实现 Error 协议(空协议,签个名即可) enum BoardError: Error { case offline // 断网 case server(Int) // 服务器异常,携带状态码 case decodeFailed(field: String) // 解码失败,携带字段名 case unauthorized // 未登录 }

错误枚举的价值在于让失败原因成为编译器认识的东西。字符串报错("加载失败了")传三层就没人知道原始含义;枚举带关联值,server(503)server(404) 在 catch 里能分开处理。

第二步,给可能出事故的函数挂上 throws

// 模拟取凭据:可能失败 func currentToken() throws -> String { // 假设本地没有存登录凭据 throw BoardError.unauthorized // throw:当场摔台 } // 模拟加载排片:错误会向上抛 func loadBoard() throws -> [String] { _ = try currentToken() // try:明知此处可能摔,先试试 // 模拟网络返回了损坏数据 throw BoardError.decodeFailed(field: "showtimes") }

try 不吞错误,只表示"我知道这里可能炸"。错误沿调用链向上飞,直到有人 do-catch 接住——这正是 2.2 节逃逸闭包之外另一条"控制流拐弯"的路径:

错误的三条去路

救场实操:把失败翻译成界面状态

光有错误类型不够,界面上要有对应的呈现。背景:排片界面要给每种失败配一条用户能看懂的话和一个动作。操作分两步,先在数据层抛错,再在界面层翻译:

// 数据层:假装从服务端拿排片 func fetchBoard(token: String) throws -> [String] { if token.isEmpty { throw BoardError.unauthorized } // 模拟:服务器今天 503 throw BoardError.server(503) } // 界面层:do-catch 是救场主力 func showBoard() { do { let board = try fetchBoard(token: "") print("渲染 \(board.count) 条场次") // 成功分支(本例走不到) } catch BoardError.unauthorized { print("请先登录,正在跳转登录页") // 输出:请先登录,正在跳转登录页 } catch BoardError.server(let code) where code >= 500 { print("影院服务器打盹了,稍后自动重试") // 500 以上算服务端问题 } catch BoardError.decodeFailed(let field) { print("数据缺了 \(field),已上报") } catch { print("未知情况:\(error)") // 兜底分支,error 是隐式常量 } } showBoard()

注意 catch BoardError.server(let code) where code >= 500——catch 也支持模式匹配(第6章会系统讲),错误码可以继续分流。结果解读:这个函数把"失败"从崩溃改写成界面文案,四种事故各有台词。变式:给 showBoard 加一个 retry: () -> Void 参数,在 500 分支里自动调用,就成了带重试的加载器雏形。

三种 try 姿态与 Result 的取舍

同样是"可能失败",四种写法各有适用面:

// 姿势一:try? —— 只要结果,不要原因 let board1 = try? fetchBoard(token: "abc") print(board1 ?? []) // 输出:[](失败被压成 nil) // 姿势二:try! —— 拿职业尊严担保不会失败(本例会真崩,注释掉保命) // let board2 = try! fetchBoard(token: "") // 姿势三:Result —— 把成功失败装箱成一个值,适合跨闭包传递 enum Result<Value> { case success(Value) case failure(Error) } func loadBoardAsResult(token: String, completion: (Result<[String]>) -> Void) { do { let board = try fetchBoard(token: token) completion(.success(board)) } catch { completion(.failure(error)) // 失败也被装箱成值送回去 } } loadBoardAsResult(token: "") { result in switch result { case .success(let board): print("成功:\(board)") case .failure(let error): print("失败:\(error)") // 输出:失败:unauthorized } }
姿态 失败时得到 适用场景 风险
do-catch 错误详情+分支 界面层最终处理
try? nil 不在乎原因的中间步骤 丢失错误信息
try! 直接崩溃 单测里确定成功、字面量转换 上线即事故
Result 装箱的成败值 回调式接口、跨层传递 样板代码多

⚠️ 常见坑:一路 try? 上去,最后界面上只剩"加载失败"四个字,用户和排查的人都不知道为什么。原则:离用户最近的一层必须 do-catch 并区分原因;中间层可以上抛(把错误写进函数签名),但不允许静默吞掉。

💡 关键直觉:throws 改变的是函数的"身份"。看到 throws 就知道这个函数有三分之一的概率不走成功分支——它和可选值的问号一样,是签名里诚实的一部分。第5章会看到 async 与 throws 组合成 async throws,语义完全同源。

谢幕前的清单

  • 错误用枚举建模,实现 Error 协议,关联值携带上下文(状态码、字段名);
  • throws 声明事故可能,try 标记危险调用,错误沿调用链上抛;
  • do-catch 按类型分支,支持 where 条件与兜底分支,界面上翻译成人话;
  • try? 丢原因、try! 赌命、Result 装箱,按"谁需要知道失败原因"来选;
  • 失败是界面状态机的正式成员:加载中、成功、失败三态都要有人演。

第一幕到此收场:函数、闭包、错误处理让界面"能加载、能回调、能摔了再爬起来"。但数据还住在散装字典里——第二幕组建剧团,用类、结构体、枚举与协议给每个角色正式编制。


作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U