反初始化 deinit
本教程共 93 篇 · 第 66 篇 · 更新于 2026-08-08 · 约 7 分钟阅读
本节目标:学完你知道 deinit 只存在于类、会在实例销毁前自动运行,并会用它做资源释放等清理工作。
有初始化,就有反初始化。当类的实例不再被需要、即将从内存里销毁时,Swift 会自动调用一个叫反初始化器(deinitializer)的东西,给你最后一次机会做清理。它用 deinit 关键字写,只属于类——结构体用不上,因为结构体是值类型,靠值语义自动管理,不需要这层钩子。
1-1 deinit 长什么样
反初始化器的写法很像初始化器,只是把 init 换成 deinit,而且不带任何参数、也不写括号:
class Player {
var coinsInPurse: Int
init(coins: Int) {
coinsInPurse = Bank.distribute(coins: coins)
}
deinit {
Bank.receive(coins: coinsInPurse)
}
}
这里 deinit 里做的事是:把玩家口袋里剩下的金币交还银行。语法上它不能有参数、不能写返回值,就是一个代码块。一个类最多只能有一个 deinit。
1-2 它是自动调用的,你叫不动
最重要的一点:反初始化器由系统在实例销毁前自动调用,你永远不能手动去调它。这点跟初始化器相反——初始化器是你主动 类型名() 调用的,而 deinit 完全是被动触发。
Swift 靠自动引用计数(ARC,第 75 章细讲)决定实例何时该销毁。当没有任何变量、常量再引用这个实例时,内存被回收,销毁前一刻 deinit 自动跑。比如你把一个可选的类实例设为 nil,原本唯一的引用断了,实例就被释放,deinit 随即执行。
1-3 为什么需要 deinit
多数时候你不需要它。Swift 的 ARC 会自动回收实例占用的内存,普通的属性、对象引用都不用你手动管。但当你自己握有”外部资源”时,就需要在销毁前主动归还或关闭:
- 打开了文件要关闭,否则文件句柄一直占着;
- 向某个全局资源池借了东西(比如上面例子的金币银行),要在离开时还回去;
- 注册了某些外部观察者、定时器,要在销毁时注销,避免悬空回调。
这些”非内存但需清理”的事,就是 deinit 的主战场。它让你写的类型在离开作用域时干干净净,不拖泥带水。
1-4 父类 deinit 自动继承
子类的 deinit 执行完后,父类的 deinit 会自动跟着执行——你不用手动调 super.deinit()。哪怕子类根本没写自己的 deinit,父类的 deinit 也照样会被调用。这条规则和初始化相反:初始化要显式 super.init(),反初始化是隐式串联的。
class Bank {
static var coinsInBank = 10_000
static func distribute(coins numberOfCoinsRequested: Int) -> Int {
let numberToVend = min(numberOfCoinsRequested, coinsInBank)
coinsInBank -= numberToVend
return numberToVend
}
static func receive(coins: Int) {
coinsInBank += coins
}
}
上面这个 Bank 用类型属性 coinsInBank 记着银行还剩多少金币,提供 distribute 和 receive 两个类型方法来发币和收币。再配合前面 Player 的 deinit,效果是这样的:
var playerOne: Player? = Player(coins: 100)
print("新玩家带着 \(playerOne!.coinsInPurse) 金币入场")
playerOne = nil
print("银行现在有 \(Bank.coinsInBank) 金币")
玩家入场时从银行领 100 金币,银行剩 9900。当 playerOne = nil 把唯一引用断开,玩家实例被销毁,它的 deinit 把 100 金币还回银行,银行又回到 10000。整个过程不用人催,deinit 自动兜底。
1-5 deinit 里能访问属性
反初始化器在实例真正销毁之前运行,所以此刻实例还”活着”,你能访问它所有的属性、调用方法。比如上面 deinit 里就读了 coinsInPurse,再决定还多少币。这一点很关键:清理逻辑往往依赖实例当前的状态,而 deinit 执行时这些状态都还在。
1-6 和内存管理的关系
deinit 的存在也提醒你关注引用类型的生命周期。如果因为循环强引用(第 76 章)导致实例永远不被释放,那 deinit 就永远不触发——这本身是内存泄漏的信号。所以 deinit 不仅能做清理,还能当”这个实例确实被销毁了”的探针:在调试时打个日志,就能确认引用关系有没有正确断开。
1-7 小结
deinit 是只属于类的反初始化器,无参无返回值,在实例销毁前由系统自动调用,你不能手动触发。它用来归还外部资源、关闭文件、注销观察者。父类 deinit 自动在子类之后执行,无需手写 super.deinit()。执行时实例仍完整可用,可正常读属性、调方法。把初始化(第 64 章)和反初始化合起来看,一个类就做到了”来有始、去有终”。
1-8 deinit 与可选实例的配合
前面例子里 playerOne 是可选类型 Player?,这正是 deinit 常见搭档。因为玩家可能随时离场,用可选变量能表达”此刻有没有玩家”。当把它设为 nil,唯一引用断开,实例销毁,deinit 自动跑。如果你把实例存成非可选常量,它几乎永远不会被释放,除非整个作用域结束,deinit 也就迟迟不触发。所以”需要灵活控制生命周期”的对象,常配合可选变量使用,让 ARC 在该放手时放手。
1-9 deinit 不能做的事
deinit 有几个硬限制要记牢:它不能带参数、不能返回值、不能手动调用、一个类只能有一个。你也不能在 deinit 里调用 super.deinit()——那是自动的。还有,如果实例因为循环强引用始终没被释放,deinit 就永远不跑,这本身就是内存泄漏的红灯。调试时,在 deinit 里打印一句话,是判断”这个对象到底有没有被释放”的常用手段。若日志从没出现,就该去查引用关系。
1-10 deinit 与资源句柄
凡是握有”外部世界资源”的类型,都该认真对待 deinit。比如封装了文件句柄、网络连接、数据库事务、定时器的类,必须在 deinit 里把这些资源交还或关闭,否则它们会一直占着,直到进程结束。值类型(结构体)没有 deinit,所以这类”需要清理的资源”通常要包在类里。把 deinit 当作类型和外部世界之间的”告别仪式”,就能保证程序跑得久也不漏资源。这也是为什么管理资源的类型多用类而非结构体的原因之一:只有类能在销毁时挂上这段清理逻辑。