My Profile Photo

Jesse


Hi, 我是 Jesse,一名 iOS 开发者,热爱编程,平常也喜欢摄影。欢迎大家能多多交流。


初始化与析构:指定|便捷|失败初始化与 deinit 语义全解析

在 Swift 的世界里,创建一个对象不像在某些动态语言里那样随意。Swift 是一种强安全语言,它要求一个实例在被使用之前,其所有的属性都必须被正确地“填满”。这种执着,催生了一套看似复杂、实则严密的初始化规则。

今天我们不谈枯燥的文档定义,而是通过一个“造车”的故事,来搞懂指定初始化器 (Designated)便捷初始化器 (Convenience)失败初始化器 (Failable) 以及对象销毁时的 deinit

🏗️ 核心概念:指定 vs 便捷

想象我们正在设计一个 Vehicle(交通工具)类。

1. 指定初始化器 (Designated Initializer):地基与主梁

这是类的“主入口”。它的职责是确保所有存储属性都有值,并负责调用父类的初始化器(如果有的话)。

特点:

  • 每个类至少有一个(通常只有一个)。
  • 它是“纵向”的,负责向上代理(Up delegation)。
  • 只有它才有资格去调用 super.init
class Vehicle {
    var wheelCount: Int
    var name: String
    
    // 🔴 指定初始化器:必须填满所有属性
    init(name: String, wheelCount: Int) {
        self.name = name
        self.wheelCount = wheelCount
        // 这是一个基类,不需要调用 super.init
    }
}

2. 便捷初始化器 (Convenience Initializer):快捷方式

这是类的“侧门”或“快捷按钮”。它不能独立完成初始化,它必须**“横向”**调用同一个类中的其他初始化器。

特点:

  • 使用 convenience 关键字修饰。
  • 必须调用 self.init(最终必须指向一个指定初始化器)。
  • 它通常用于提供默认值,简化调用者的工作。
extension Vehicle {
    // 🟢 便捷初始化器:提供一个默认的“汽车”配置
    convenience init(carName: String) {
        // 必须调用 self.init (横向代理)
        self.init(name: carName, wheelCount: 4)
    }
    
    // 🟢 另一个便捷初始化器:默认叫 "未知交通工具"
    convenience init() {
        self.init(carName: "Unknown")
    }
}

// 调用
let car = Vehicle(carName: "Tesla") // 使用便捷初始化器
let bike = Vehicle(name: "Giant", wheelCount: 2) // 使用指定初始化器

⛓️ 难点突破:类的继承与“初始化链”

当我们引入子类 Car 时,Swift 的**“两段式构造” (Two-Phase Initialization)** 规则就登场了。这是大多数编译错误的来源。

三大原则(记住这张图):

  1. 指定初始化器必须向上调用 (super.init)。
  2. 便捷初始化器必须横向调用 (self.init)。
  3. 便捷初始化器最终必须调用到指定初始化器
[Image Diagram Description: Initialization Chain]
[
  图解:
  Subclass (子类)           Superclass (父类)
     |                           |
  [Convenience Init] --------> [Convenience Init] (❌ 错误!不能横跨类调用便捷)
     | (调用 self)                |
     v                           v
  [Designated Init] --(调用 super)--> [Designated Init]
]

代码实战:两段式构造

Swift 要求初始化必须分两步走,以确保安全:

  • 阶段 1:确保所有属性都有初始值(从子类到父类)。
  • 阶段 2:给每个类一个机会来通过 self 修改状态(从父类到子类)。
class Car: Vehicle {
    var isElectric: Bool
    
    // 指定初始化器
    init(name: String, isElectric: Bool) {
        // --- 阶段 1 开始 ---
        
        // 1. 先初始化自己的属性
        self.isElectric = isElectric
        
        // ❌ 如果在这里用 self.name = ... 会报错,因为父类还没初始化
        
        // 2. 向上调用父类的指定初始化器
        super.init(name: name, wheelCount: 4)
        
        // --- 阶段 1 结束,self 现在完全可用 ---
        // --- 阶段 2 开始 ---
        
        // 3. 现在可以自定义继承来的属性了
        if isElectric {
            self.name = "Electric " + self.name
        }
    }
    
    convenience init(electricCarName: String) {
        self.init(name: electricCarName, isElectric: true)
    }
}

为什么这么严格? 这防止了你在父类还没准备好(内存还没初始化完)的时候就去访问父类的属性,或者调用依赖父类状态的方法。

❓ 失败初始化器 (Failable Initializer):允许失败

有时候,初始化的参数可能是无效的。比如,用户ID不能为空,年龄不能为负数。Swift 允许我们在 init 后面加个 ?

语义: 这个初始化可能会成功并返回实例,也可能失败并返回 nil

struct User {
    let id: String
    
    // 这是一个可失败初始化器
    init?(id: String) {
        if id.isEmpty {
            // 如果条件不满足,直接返回 nil
            return nil 
        }
        self.id = id
    }
}

// 使用时,得到的是一个 Optional
let user1 = User(id: "123") // User? 类型,值为 Optional(User)
let user2 = User(id: "")    // User? 类型,值为 nil

if let validUser = user1 {
    print("创建成功: \(validUser.id)")
}

注意: 在类的继承中,子类的可失败初始化器可以调用父类的可失败(或不可失败)初始化器。如果链条中任何一环返回了 nil,整个初始化过程立即终止(Fail Early)。

👋 最后的告别:deinit (析构器)

有生就有死。当一个类实例的引用计数降为 0 时,它就会被销毁。在内存释放之前,Swift 会自动调用 deinit

deinit 的几个关键点:

  1. 只有 Class 有 deinit(Struct 和 Enum 不需要,因为它们分配在栈上或由系统自动管理)。
  2. 不能手动调用,由 ARC 自动触发。
  3. 没有参数,没有括号
  4. 子类继承父类的析构逻辑,子类的 deinit 执行完后,会自动调用父类的 deinit

实战场景:清理“非内存”资源

虽然 ARC 帮我们管理了内存,但有些“外部资源”需要我们手动清理,比如移除通知监听 (KVO/NotificationCenter),或者关闭文件句柄。

class FileHandler {
    let fileName: String
    
    init(fileName: String) {
        self.fileName = fileName
        print("📂 打开文件: \(fileName)")
        // 假设这里进行了文件打开操作
    }
    
    deinit {
        // 这是对象临死前最后说话的机会
        print("❌ 关闭文件: \(fileName)")
        // 在这里执行关闭文件的逻辑,或者移除通知
    }
}

var handler: FileHandler? = FileHandler(fileName: "data.json")
// 输出: 📂 打开文件: data.json

// 将引用设为 nil,触发销毁
handler = nil 
// 输出: ❌ 关闭文件: data.json

⚠️ 常见误区: 不要在 deinit 里做耗时太久的操作,也不要试图在 deinit 里把 self 赋值给其他变量来“复活”对象(虽然在某些古老的语言里可以,但在 Swift 里这是未定义行为或不被允许的)。

总结

Swift 的初始化流程虽然看起来繁琐,但它是一套**“防御性编程”**的典范。

  • Designated Init 是基石,保证所有属性不留空白。
  • Convenience Init 是助手,必须横向调用 self.init
  • Inheritance 要求先满足子类,再向上满足父类(两段式)。
  • Failable Init 让我们优雅地处理“创建失败”的情况。
  • Deinit 是最后的守门员,负责清理非内存资源。

掌握了这些,你就不再是跟编译器搏斗,而是在利用编译器帮你写出更稳健的代码。


感谢阅读!你在处理 Swift 初始化时遇到过什么奇怪的 Bug 吗?或者对 required init 还有疑问?欢迎在评论区留言探讨!

目录