首页 / Swift 编程语言教程 / 初始化器:指定与便利

Swift 编程语言教程

初始化器:指定与便利

本教程共 93 篇 · 第 64 篇 · 更新于 2026-08-08 · 约 9 分钟阅读

SwiftSwift 编程语言教程初始化器指定初始化器便利初始化器init二段式初始化委托

本节目标:学完你能区分类的 designated 与 convenience 初始化器,理解它们之间的委托规则,并搞懂二段式初始化如何保证每个属性都被正确赋初值。

初始化器(initializer)是创建实例时跑的一段特殊代码,任务是给所有存储属性设好初值,并完成一次性准备。和函数不同,初始化器不返回值,它的唯一使命是确保新实例在第一次使用前处于完全就绪的状态。Swift 给类的初始化器分了两种角色:指定(designated)和便利(convenience)。这一章把它们讲透。

1-1 为什么类需要两种初始化器

结构体和枚举的初始化相对简单,但类有继承链:一个类的所有存储属性——包括从父类继承来的——在初始化结束时都必须有值。为了把这套责任理清楚,Swift 用”指定”和”便利”两种初始化器分工。

指定初始化器是类的主要初始化器,它把本类引入的属性全部初始化,然后往上调用父类的指定初始化器,把流程继续往继承链顶端推。一个类至少要有一个指定初始化器(很多时候继承来的也算)。你可以把它想成”主干道”,所有初始化最终都从它过。

便利初始化器是辅助角色。它不负责把所有属性都填上,而是调用同一个类里的某个指定初始化器,顺手把部分参数填成默认值,给常用场景提供快捷入口。

1-2 语法长啥样

指定初始化器的写法和值类型的普通初始化器一样,就是 init 加参数:

init(name: String, quantity: Int) {
    self.quantity = quantity
    super.init(name: name)
}

便利初始化器只是在 init 前面多一个 convenience 修饰,且它必须去调用同一个类的另一个初始化器:

convenience init(name: String) {
    self.init(name: name, quantity: 1)
}

一眼区分:带 convenience 的是便捷通道,不带的是主干道。

1-3 三条委托规则

Swift 给类初始化器之间的调用定了三条铁律,记住”上”和”横”两个字就够了:

规则一,指定初始化器必须往上委托——调用直接父类的指定初始化器。规则二,便利初始化器必须往横委托——调用同一个类里的另一个初始化器。规则三,便利初始化器最终必须落到某个指定初始化器上。

class Food {
    var name: String
    init(name: String) { self.name = name }
    convenience init() { self.init(name: "[未命名]") }
}

Foodinit(name:) 是指定初始化器,没父类就自己把 name 填上。convenience init() 是便利初始化器,横向委托给 init(name:),并给了个默认名字。这正好满足规则二和三。

这三条规则只约束”你怎么写初始化器的内部调用”,不约束”外部怎么创建实例”。外部随便调哪个初始化器,都能得到完整初始化的实例。

1-4 二段式初始化

类初始化在底层分两个阶段,这是 Swift 保证安全的关键设计。

第一阶段,从最底层的子类开始,逐级往上,每个类把自己引入的属性赋好初值。一路爬到继承链顶端,当最顶层类的属性也都有值了,整块内存才算”完全初始化”,阶段一结束。

第二阶段,再从上往回走,每个类的初始化器都有机会进一步加工实例——改属性、调方法。轮到便利初始化器时,它也能最后再定制一把。

编译器为此做了四道安全检查保驾护航。其一,指定初始化器必须先把自己引入的属性全初始化完,才能往上委托。其二,指定初始化器必须先调父类初始化器,才能去改继承来的属性,否则改了也会被父类覆盖。其三,便利初始化器必须先委托出去,才能给任何属性赋值。其四,在阶段一结束前,不能调用实例方法、不能读实例属性、不能把 self 当值用——因为那时实例还没”活”过来。

这些检查你平时感觉不到,但正是它们让你几乎不会写出”用了没初始化的属性”这种 bug。

1-5 手动调 super.init()

子类初始化器通常在给自己属性赋值后,调一次 super.init() 把父类的活接上。注意顺序:先填自己的,再交给我们。

class Bicycle: Vehicle {
    override init() {
        super.init()
        numberOfWheels = 2
    }
}

这里先 super.init() 让父类把 numberOfWheels 初始化成 0,子类再改成 2。若子类在阶段二不需要任何加工,且父类有个无参同步指定初始化器,那句 super.init() 可以省略,由编译器隐式补上。但若父类的初始化器是异步的,就得显式写 await super.init()

1-6 重写父类初始化器要加 override

如果子类写了一个和父类指定初始化器同签名(参数名、类型一致)的初始化器,你其实是在重写它,必须加 override,哪怕你这个初始化器在子类里是便利初始化器:

class RecipeIngredient: Food {
    var quantity: Int
    init(name: String, quantity: Int) {
        self.quantity = quantity
        super.init(name: name)
    }
    override convenience init(name: String) {
        self.init(name: name, quantity: 1)
    }
}

init(name:)Food 里是指定初始化器,子类用 convenience 实现它,照样得标 override。编译器会核对父类确有匹配的指定初始化器。反之,若你写的和父类便利初始化器同签名,那不算重写,不用写 override——因为便利初始化器本就不能被子类直接调用。

1-7 初始化器的自动继承

子类默认不继承父类初始化器,但有两条宽松规则能让你”白捡”:

规则一,如果子类没定义任何指定初始化器,它就自动继承父类全部的指定初始化器。规则二,如果子类补齐了父类所有指定初始化器(无论是继承来的还是自己写的),它就自动继承父类全部的便利初始化器。

class ShoppingListItem: RecipeIngredient {
    var purchased = false
}

ShoppingListItem 没写任何初始化器,且给新属性 purchased 赋了默认值,于是它自动继承 RecipeIngredient 乃至 Food 的所有初始化器。结果你能直接用 ShoppingListItem()ShoppingListItem(name:)ShoppingListItem(name:quantity:) 三种方式造对象。这条自动继承在日常里能省掉大量样板代码。

1-8 小结

类的初始化器分指定(designated,主干道,必须往上委托)和便利(convenience,辅助,横向委托最终落到指定)。二段式初始化先自底向上填完所有属性、再自顶向下加工,四道安全检查杜绝”用未初始化值”的坑。重写父类指定初始化器要加 override;给新属性都设默认值时常能自动继承父类初始化器。把这套流程吃透,你写任何继承体系的初始化都不会乱。