Swift 构造过程
构造过程是创建类、结构体或枚举实例时,为每个存储属性设置初始值并完成必要准备工作的过程。
这个过程由构造器(initializer)完成,用关键字 init 声明。构造器不返回任何值,它的职责是保证实例在第一次使用前已经处于有效状态。
存储属性初始赋值
实例创建时,所有存储属性都必须有初始值,有两种给法:在声明时直接给默认值,或者在构造器里赋值。
在构造器中给属性赋值是直接设置,不会触发属性观察器。
实例
class Rectangle {
var length = 6.0 // 声明时直接给默认值
var breadth: Double // 没有默认值,必须在构造器里赋值
init(breadth: Double) {
self.breadth = breadth
}
}
let rect = Rectangle(breadth: 12.0)
print("面积:\(rect.length * rect.breadth)")
运行结果:
面积:72.0
如果属性有默认值,就可以省略它的类型标注,Swift 会从默认值推断出类型,例如上面的 length 被推断为 Double。
构造器、默认构造器与成员逐一构造器
构造器的最简形式是一个不带参数的 init(),用来给属性设置固定的初始值。
如果类型的所有存储属性都有默认值,并且自己没有写任何构造器,Swift 会自动提供两种构造器。
| 构造器 | 适用类型 | 生成条件 | 调用形式 |
|---|---|---|---|
| 默认构造器 | 类、结构体 | 所有存储属性都有默认值,且没有自定义构造器 | TypeName() |
| 成员逐一构造器 | 结构体 | 所有存储属性都有默认值,且没有自定义构造器 | TypeName(a: 1, b: 2) |
实例
// 所有存储属性都有默认值,类自动获得默认构造器 init()
class ShoppingListItem {
var name: String?
var quantity = 1
var purchased = false
}
let item = ShoppingListItem()
print("名字:\(item.name ?? "无"),数量:\(item.quantity),已购买:\(item.purchased)")
// 结构体没有自定义构造器时,自动获得成员逐一构造器
struct Size {
var width = 0.0
var height = 0.0
}
let size = Size(width: 3.0, height: 4.0)
print("宽:\(size.width),高:\(size.height)")
运行结果:
名字:无,数量:1,已购买:false 宽:3.0,高:4.0
一旦你为结构体写了任意一个自定义构造器,成员逐一构造器就不再自动生成。
构造参数与外部参数名
构造器可以带参数,用来在创建实例时传入不同的初始值。
和普通函数不同,构造器的参数默认同时是局部名和外部名,调用时每个参数都要写标签;用下划线 _ 可以显式省略某个参数的外部名。
实例
struct Color {
let red, green, blue: Double
// 构造器的参数默认同时作为局部名和外部名
init(red: Double, green: Double, blue: Double) {
self.red = red
self.green = green
self.blue = blue
}
init(white: Double) {
red = white
green = white
blue = white
}
// 用 _ 省略外部名
init(_ gray: Double) {
red = gray
green = gray
blue = gray
}
}
let magenta = Color(red: 1.0, green: 0.0, blue: 1.0)
let halfGray = Color(white: 0.5)
let gray = Color(0.25)
print(magenta.red, halfGray.green, gray.blue)
运行结果:
1.0 0.5 0.25
注意:构造器的参数默认都带外部标签,这一点与方法一致(Swift 3 起 SE-0046 统一了规则)。带标签反而更清楚:
Color(red:green:blue:)与Color(white:)一眼就能区分。
可选属性类型
如果某个存储属性在逻辑上允许为空,就把它声明成可选类型。
可选属性会自动初始化为 nil,因此不需要在构造器里手动赋值。
实例
class SurveyQuestion {
let text: String // 常量属性,只能在构造过程中赋值一次
var response: String? // 可选属性自动初始化为 nil
init(text: String) {
self.text = text
}
func ask() {
print(text)
}
}
let question = SurveyQuestion(text: "你最喜欢 Swift 的哪个特性?")
question.ask()
print("回答:\(question.response ?? "尚未回答")")
运行结果:
你最喜欢 Swift 的哪个特性? 回答:尚未回答
如果可选属性在构造时就能确定值,也可以照常赋值;不确定的场合留空即可。
构造过程中修改常量属性
用 let 声明的常量属性并不是完全不能赋值,只要在构造过程结束前确定值,就可以在构造器里给它赋值一次。
对类来说,常量属性只能在定义它的那个类的构造器中修改,不能在子类中修改。
实例
class Rectangle {
let width: Double
let height: Double
let area: Double // 常量,但可以在构造过程结束前算出来再赋值
init(width: Double, height: Double) {
self.width = width
self.height = height
self.area = width * height
}
}
let rect = Rectangle(width: 6.0, height: 12.0)
print("面积:\(rect.area)")
运行结果:
面积:72.0
area 依赖 width 和 height 计算得到,放在构造器末尾赋值既满足了 let 的规则,也保证了实例创建完成后它一定是正确的。
两段式构造过程与安全检查
Swift 的类构造过程分成两个阶段,目的是防止属性在还没初始化完时就被访问。
第一阶段自下而上:子类先初始化自己的存储属性,然后向上调用父类构造器,逐层完成整条继承链的属性初始化。
第二阶段自上而下:从最顶层父类开始,每一层都有机会在新实例准备就绪后进一步定制属性。
实例
class Animal {
var name: String
var description: String {
return "动物:\(name)"
}
init(name: String) {
print("阶段一:Animal.init 开始")
self.name = name
print("阶段一:Animal.init 结束")
}
}
class Dog: Animal {
var breed: String
init(name: String, breed: String) {
print("阶段一:Dog.init 开始")
self.breed = breed
super.init(name: name) // 调用父类构造器,完成第一阶段
print("阶段二:Dog.init 可以访问 self.description -> \(self.description)")
self.name = name + "(已登记)" // 第二阶段才能修改父类的属性
print("阶段二:Dog.init 结束")
}
}
let dog = Dog(name: "旺财", breed: "柴犬")
print(dog.description)
运行结果:
阶段一:Dog.init 开始 阶段一:Animal.init 开始 阶段一:Animal.init 结束 阶段二:Dog.init 可以访问 self.description -> 动物:旺财 阶段二:Dog.init 结束 动物:旺财(已登记)
从输出顺序可以看到,父类构造器先完成,子类才能进入第二阶段修改继承来的属性。
Swift 编译器会用四条安全检查约束这个过程:
| 编号 | 安全检查 |
|---|---|
| 1 | 指定构造器必须保证它所在类引入的所有存储属性都初始化完毕,才能向上代理给父类构造器。 |
| 2 | 指定构造器必须先调用父类构造器,然后才能为继承来的属性设置新值。 |
| 3 | 便利构造器必须先调用同一个类中的其他构造器,然后才能为任意属性赋新值。 |
| 4 | 第一阶段完成之前,构造器不能调用实例方法、不能读取属性值,也不能引用 self。 |
值类型的构造器代理
构造器可以调用其他构造器来分担部分初始化工作,这个过程叫构造器代理,能减少重复代码。
结构体和枚举是值类型,不支持继承,所以只能代理给自身提供的其他构造器,用 self.init 调用。
实例
struct Size {
var width = 0.0, height = 0.0
}
struct Point {
var x = 0.0, y = 0.0
}
struct Rect {
var origin = Point()
var size = Size()
init() {}
init(origin: Point, size: Size) {
self.origin = origin
self.size = size
}
// 值类型的构造器代理:用 self.init 调用同类型的其他构造器
init(center: Point, size: Size) {
let originX = center.x - (size.width / 2)
let originY = center.y - (size.height / 2)
self.init(origin: Point(x: originX, y: originY), size: size)
}
}
let basic = Rect()
let centered = Rect(center: Point(x: 4.0, y: 4.0), size: Size(width: 3.0, height: 3.0))
print("basic 原点:\(basic.origin.x), \(basic.origin.y)")
print("centered 原点:\(centered.origin.x), \(centered.origin.y)")
运行结果:
basic 原点:0.0, 0.0 centered 原点:2.5, 2.5
类的构造器代理:指定构造器与便利构造器
类有继承关系,构造器分成两种角色,职责不同。
| 类型 | 职责 | 声明方式 | 调用规则 |
|---|---|---|---|
| 指定构造器 | 初始化本类引入的全部属性,并向上调用父类指定构造器 | init(...) | 每个类至少要有一个 |
| 便利构造器 | 为某个指定构造器提供更便捷的调用入口 | convenience init(...) | 必须用 self.init 调用同类中的其他构造器 |
下面这个例子中,Recipe 通过覆盖父类的全部指定构造器,继承了父类的便利构造器。
实例
class Food {
var name: String
// 指定构造器:负责初始化本类的全部存储属性
init(name: String) {
self.name = name
}
// 便利构造器:必须用 self.init 调用同一个类中的其他构造器
convenience init() {
self.init(name: "[未命名]")
}
}
class Recipe: Food {
var quantity: Int
// 指定构造器:先初始化自己的属性,再调用父类的指定构造器
init(name: String, quantity: Int) {
self.quantity = quantity
super.init(name: name)
}
// 覆盖父类的指定构造器,这是继承父类便利构造器的前提
override init(name: String) {
self.quantity = 1
super.init(name: name)
}
}
let food = Food()
print(food.name)
let recipe = Recipe(name: "RUNOOB 炖菜", quantity: 3)
print("\(recipe.name):\(recipe.quantity) 份")
// Recipe 覆盖了父类全部指定构造器,因此自动继承了 Food 的 convenience init()
let inherited = Recipe()
print(inherited.name)
运行结果:
[未命名] RUNOOB 炖菜:3 份 [未命名]
构造器继承规则
Swift 的子类默认不继承父类的构造器,只有在满足特定条件时才会自动继承。
规则一:如果子类没有定义任何指定构造器,它会自动继承父类的全部指定构造器。
规则二:如果子类实现了父类所有的指定构造器(无论是通过规则一继承的,还是自己重写的),它就会自动继承父类所有的便利构造器。
上面的 Recipe 就属于规则二:它重写了 Food 唯一的指定构造器 init(name:),于是连便利构造器 init() 也一起继承了下来。
注意:重写父类的指定构造器时,即使子类的实现没有额外参数,也必须写
override修饰符。
required 构造器
在构造器前加 required,表示所有子类都必须实现这个构造器。
子类实现时也要写 required,而不是 override。
实例
class Base {
var value: Int
// required 表示子类必须实现这个构造器
required init(value: Int) {
self.value = value
}
}
class Derived: Base {
// 子类实现时同样要写 required
required init(value: Int) {
super.init(value: value * 10)
}
}
let d = Derived(value: 5)
print("Derived 的值:\(d.value)")
运行结果:
Derived 的值:50
可失败构造器 init? 与 init!
如果构造过程可能因为参数不合法或资源不可用而失败,可以定义可失败构造器,在失败时返回 nil。
可失败构造器用 init? 声明,它的返回值类型是可选类型。
实例
struct Animal {
let species: String
// 可失败构造器:条件不满足时返回 nil
init?(species: String) {
if species.isEmpty { return nil }
self.species = species
}
}
let giraffe = Animal(species: "长颈鹿")
let unknown = Animal(species: "")
if let giraffe {
print("构造成功:\(giraffe.species)")
}
if unknown == nil {
print("构造失败:species 为空")
}
// 枚举的可失败构造器:用 self 直接选定某个成员
enum TemperatureUnit {
case kelvin, celsius, fahrenheit
init?(symbol: Character) {
switch symbol {
case "K": self = .kelvin
case "C": self = .celsius
case "F": self = .fahrenheit
default: return nil
}
}
}
let unit = TemperatureUnit(symbol: "F")
print(unit == .fahrenheit)
print(TemperatureUnit(symbol: "X") == nil)
运行结果:
构造成功:长颈鹿 构造失败:species 为空 true true
还有一种是 init!,它返回隐式解包可选值,失败时如果不检查就直接使用会崩溃,现在已经不推荐使用。
实例
class Student {
let name: String
// init! 返回隐式解包可选,为 nil 时会在使用处直接崩溃,不推荐
init!(name: String) {
if name.isEmpty { return nil }
self.name = name
}
}
// 显式标注为非可选类型,init! 的结果会被隐式解包
let student: Student = Student(name: "Runoob")
print("学生:\(student.name)")
let empty = Student(name: "")
print("空名字构造结果是否为 nil:\(empty == nil)")
运行结果:
学生:Runoob 空名字构造结果是否为 nil:true
类的可失败构造器还有一个限制:失败只能在所有属性初始化完成、并且所有构造器代理调用结束之后触发。
常见问题
构造过程相关的报错大多集中在下面几个场景。
提示 property not initialized at super.init call
调用 super.init 之前,必须先给子类自己引入的所有存储属性赋值,这是安全检查的第一条。
结构体的成员逐一构造器不见了
只要给结构体写了任意一个自定义构造器,成员逐一构造器就不再自动生成。需要它的话,把自定义构造器移到扩展里即可保留。
子类为什么不能直接用父类的便利构造器
因为子类还没有实现父类的全部指定构造器。按继承规则二补齐全部指定构造器后,便利构造器就会自动继承。
构造器里改 let 属性报错
常量属性只能在定义它的类的构造过程中赋值一次。如果是在子类中修改父类的常量属性,编译器会直接拒绝。
