本节摘要:本章最后两块养分都是"自动系统":泛型让一套代码适配所有类型(按需加约束),ARC 用引用计数自动回收无人使用的对象。本节实现阳台花园的泛型水箱与泛型瓶颈检查,再直击 iOS 面试与真实泄漏的头号话题——循环引用:它怎么形成、怎么用 weak 拆、闭包捕获列表怎么写。学完本节,第 2 章的根系全部扎完,界面篇开种。
前七节的所有代码都在"手动接线"。还有两件事 Swift 替你自动做了,但出问题时你要能看懂现场:一是泛型——同一份函数与容器逻辑,编译器按类型自动实例化;二是 ARC——引用计数归零的对象自动回收,不用手动释放。
先看泛型。2.3 写过 totalLeaves(counts: Int...),如果还要统计 Double 型水量、String 型标签,难道写三份?泛型的答案是不用:
// 泛型函数:T 是占位类型,调用时才确定 func firstThirsty<T: CareRule>(among plants: [Plant], rule: T) -> Plant? { for p in plants where rule.daysUntilWater(for: p) == 0 { return p } return nil } let anyRule = SimpleRule() if let p = firstThirsty(among: [testPlant, monstera], rule: anyRule) { print("最急的是 \(p.name)") // 最急的是 龟背竹(按测试数据的 passed 值而定) } // 泛型容器:装什么类型由使用处决定,约束保证可排序 struct Tank<Element: Comparable> { var items: [Element] = [] mutating func fill(_ newItems: [Element]) { items.append(contentsOf: newItems) } var lowest: Element? { items.min() } // Comparable 约束让 min 可用 } var waterTank = Tank<Int>() waterTank.fill([300, 150, 450]) print(waterTank.lowest ?? 0) // 150(最小水量, Comparable 的功劳) var nameTank = Tank<String>() nameTank.fill(["绿萝", "多肉"]) print(nameTank.lowest ?? "") // 多肉(按字符串排序) // 泛型 + 扩展连招:给所有"序列里装 Plant"的类型加统计 extension Array where Element == Plant { var thirstyNames: [String] { filter { $0.isThirsty }.map { $0.name } } } print([monstera, testPlant].thirstyNames) // [] 或按当前数据输出缺水名单
约束(<T: CareRule>、Element: Comparable)是泛型的闸门:声明"只有满足契约的类型才许进来",换来的是函数体内可以用契约里的能力。SwiftUI 的 View 协议、some View 语法、ForEach 的泛型容器,全部建在这套机关上。
引用计数(Automatic Reference Counting)的规则一句话:有人强引用,计数加一;引用消失,计数减一;归零即回收。看一段带尸检报告的生命周期:
class WateringSession { let plantName: String init(plantName: String) { self.plantName = plantName print("开档:\(plantName) 的浇水记录") // 出生 } deinit { print("结档:\(plantName) 记录已归档") } // 回收时自动调用——观察 ARC 的探针 } var session: WateringSession? = WateringSession(plantName: "龟背竹") // 输出:开档:龟背竹 的浇水记录 var shared = session // 强引用计数:1 → 2 session = nil // 2 → 1,还活着 shared = nil // 1 → 0,此刻输出:结档:龟背竹 记录已归档
deinit 是观察回收的探针:它不响,说明对象一直没死——多半是泄漏;它响两次,说明出了重复生命。第 6 章排错时会用它做内存取证的起手式。

背景:详情页给"浇水"按钮挂了一个延迟提示闭包(浇水后一秒弹出成功文案)。上线后用内存工具发现每进一次详情页,内存涨一块、不回落。操作:先复现,再拆结。
class DetailViewController { var plantName = "龟背竹" var onTap: (() -> Void)? func bind() { // 事故版:闭包捕获了 self(强引用),而闭包本身又被 self 持有 onTap = { print("已给 \(self.plantName) 浇水") // 这里的 self 让计数 +1 } } deinit { print("详情页已释放") } } var vc: DetailViewController? = DetailViewController() vc?.bind() vc = nil // 没有任何输出!闭包与控制器互掐着,谁也死不了 class FixedDetailViewController { var plantName = "绿萝" var onTap: (() -> Void)? func bind() { // 修复版:捕获列表声明 weak,闭包不再抬计数 onTap = { [weak self] in guard let self else { return } // 复活检查:控制器若已释放就静默退出 print("已给 \(self.plantName) 浇水") } } deinit { print("修复版详情页已释放") } } var vc2: FixedDetailViewController? = FixedDetailViewController() vc2?.bind() vc2?.onTap?() // 已给 绿萝 浇水 vc2 = nil // 输出:修复版详情页已释放
结果:事故版 vc 置 nil 后 deinit 无声——泄漏实锤;修复版正常释放。解读:泄漏链条是"控制器持有闭包属性 + 闭包捕获控制器"这条环;weak 把其中一条边变成不计数,环就断了。闭包里的 self 因此变成可选,用 guard let 复活是标准姿势。变式:两个类互相持有同理(A 持 B、B 持 A),解法同一条——确定生命周期的长短,长者持强、短者持弱;另一条规则是委托属性一律 weak(第 4 章会反复见到)。第 6 章手册里"内存只涨不跌"一症,第一处方就是查闭包与委托的强弱引用。
💡 关键直觉:ARC 只回收"没人要的",不回收"互相指认为主人"的。泄漏从来不是忘了释放,而是引用图里有环。
where 扩展可精确到某元素类型;[weak self],闭包内 guard let 复活;委托属性声明 weak;养分配齐。下一章回到界面本身,把阳台花园种进 UIKit 的土壤——先看那张叫 MVC 的骨架图纸。