本节摘要:界面上大量数据"可能不存在":搜索没命中、网络没返回、文件读失败。Swift 用可选类型把"没有值"编码进类型系统,用 throws 把"失败了"编码进函数签名。本节讲 Optional 的盒子模型、if let 与 guard let 两条安全开箱路径、可选项链与 nil 合并,再讲 throws/try/catch 与 Result 两条错误传播路线,最后给出"什么时候用可选、什么时候用错误"的判据。
到目前为止的代码都假设数据在场。真实 App 里"不在场"才是常态——用户搜了一株根本没录入的植物,怎么办?多数老语言用空指针表示"没有",代价是运行时一炸了之。Swift 的做法是把"可能没有"做成类型:String? 读作"一个 String 或 nil",编译器禁止你当普通 String 用,除非先开箱。
var plantByName: [String: Plant] = [ "龟背竹": Plant(name: "龟背竹", kind: .foliage, intervalDays: 3), "多肉": Plant(name: "多肉", kind: .succulent, intervalDays: 10), ] let found = plantByName["绿萝"] // 字典下标天生返回可选 print(type(of: found)) // Optional<Plant> // print(found.name) // 编译错误:Optional 不能直接点出属性

// 三条安全路径的代码验证 if let plant = plantByName["多肉"] { print("找到 \(plant.name),\(plant.remainingDays) 天后浇水") // 找到 多肉,10 天后浇水 } guard let missing = plantByName["绿萝"] else { print("绿萝不在册,引导去添加页") // 走这里 // return:真实函数里此处早退,后面的代码假设一定有值 plantByName["绿萝"] = Plant(name: "绿萝", kind: .fern, intervalDays: 2) print("已自动补录") } print(missing.kind.rawValue) // 蕨类 let label = plantByName["仙人掌"]?.name ?? "未登记" print(label) // 未登记
found?.name 是可选项链:链上任何一环是 nil,整条链短路成 nil,不炸、不响、静默变空——所以链条末端常配 ?? 兜底值。?? 读作"空盒就换这个"。
可选回答"有没有",错误回答"为什么没有"。查无此植物是正常的没找到;但如果浇水时发现花盆传感器失联,调用方需要知道原因,这就要 throws:
enum WateringError: Error { // 错误本身用枚举建模(2.4 的关联值上场) case potMissing(potId: Int) case sensorOffline case overwatering(limit: Int, attempted: Int) } func water(plant: Plant, potId: Int, amountMl: Int) throws -> String { guard potId != 0 else { throw WateringError.potMissing(potId: potId) } guard amountMl <= 500 else { throw WateringError.overwatering(limit: 500, attempted: amountMl) } return "给 \(plant.name) 浇 \(amountMl) 毫升" } do { let msg = try water(plant: plantByName["多肉"]!, potId: 7, amountMl: 200) print(msg) // 给 多肉 浇 200 毫升 let msg2 = try water(plant: plantByName["多肉"]!, potId: 7, amountMl: 800) print(msg2) // 不会执行 } catch WateringError.overwatering(let limit, let attempted) { print("超量:限 \(limit) 毫升,尝试 \(attempted) 毫升") // 超量:限 500 毫升,尝试 800 毫升 } catch { print("其他错误:\(error)") // 兜底分支,error 是隐式常量 }
第二条路线是 Result:把"成功值或错误"装成一个普通值传递,适合异步回调(网络请求回来的时候 do-catch 早散场了):
func waterAsync(plant: Plant, amountMl: Int, completion: (Result<String, WateringError>) -> Void) { if amountMl > 500 { completion(.failure(.overwatering(limit: 500, attempted: amountMl))) } else { completion(.success("浇水完成:\(plant.name) \(amountMl) 毫升")) } } waterAsync(plant: plantByName["多肉"]!, amountMl: 800) { result in switch result { case .success(let text): print(text) case .failure(let err): print("失败:\(err)") // 失败:overwatering(limit: 500, attempted: 800) } }
背景:列表页顶部搜索框,输入名字过滤植物;用户可能输入不存在的名字、空串,甚至直接粘贴一段带空格的文本。操作:把每一步的"可能没有"显式化。
func search(_ raw: String, in dict: [String: Plant]) -> String { let query = raw.trimmingCharacters(in: .whitespaces) // 去首尾空格 guard !query.isEmpty else { return "请输入要找的植物名" } // 空串早退 guard let hit = dict[query] else { let suggestion = dict.keys.first { $0.contains(query) } ?? "无" return "没找到「\(query)」,相近的有:\(suggestion)" } return "找到了:\(hit.badge),\(hit.remainingDays) 天后浇水" } print(search(" 多肉 ", in: plantByName)) // 找到了:多肉·多肉,10 天后浇水 print(search("绿萝", in: plantByName)) // 没找到「绿萝」,相近的有:无 print(search(" ", in: plantByName)) // 请输入要找的植物名
结果:三类异常输入各自得到明确反馈,没有一个分支会崩。解读:注意 guard 的"早退"风格让主路径沉底——成功逻辑永远在函数最底下,读代码不用钻嵌套。变式:模糊建议现在只查前缀包含,可以升级为按拼音或编辑距离匹配;错误若需要跨线程传递,把 throws 版本包成 Result 再进回调,两种机制就这样接在一起。
T? 编译期强制你处理空盒,空指针事故从运行时挪到编译期;?. 链配 ?? 兜底;强解包 ! 是禁路;最后两套自动系统——泛型与内存管理——下一节一次讲完,第 2 章就配齐养分了。