现在位置: 首页 > Swift 教程 > 正文

Swift 构造过程

构造过程是创建类、结构体或枚举实例时,为每个存储属性设置初始值并完成必要准备工作的过程。

这个过程由构造器(initializer)完成,用关键字 init 声明。构造器不返回任何值,它的职责是保证实例在第一次使用前已经处于有效状态。


存储属性初始赋值

实例创建时,所有存储属性都必须有初始值,有两种给法:在声明时直接给默认值,或者在构造器里赋值。

在构造器中给属性赋值是直接设置,不会触发属性观察器

实例

import Foundation

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)

实例

import Foundation

// 所有存储属性都有默认值,类自动获得默认构造器 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

一旦你为结构体写了任意一个自定义构造器,成员逐一构造器就不再自动生成。


构造参数与外部参数名

构造器可以带参数,用来在创建实例时传入不同的初始值。

和普通函数不同,构造器的参数默认同时是局部名和外部名,调用时每个参数都要写标签;用下划线 _ 可以显式省略某个参数的外部名。

实例

import Foundation

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,因此不需要在构造器里手动赋值。

实例

import Foundation

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 声明的常量属性并不是完全不能赋值,只要在构造过程结束前确定值,就可以在构造器里给它赋值一次。

对类来说,常量属性只能在定义它的那个类的构造器中修改,不能在子类中修改。

实例

import Foundation

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 的类构造过程分成两个阶段,目的是防止属性在还没初始化完时就被访问。

Swift 两段式构造过程流程图:第一阶段自下而上,子类先初始化自己的存储属性再向上调用 super.init 直到根类;第二阶段自上而下,从根类开始逐层定制实例;右侧标注三条安全检查

第一阶段自下而上:子类先初始化自己的存储属性,然后向上调用父类构造器,逐层完成整条继承链的属性初始化。

第二阶段自上而下:从最顶层父类开始,每一层都有机会在新实例准备就绪后进一步定制属性。

实例

import Foundation

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 调用。

实例

import Foundation

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 通过覆盖父类的全部指定构造器,继承了父类的便利构造器。

实例

import Foundation

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

实例

import Foundation

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? 声明,它的返回值类型是可选类型。

实例

import Foundation

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!,它返回隐式解包可选值,失败时如果不检查就直接使用会崩溃,现在已经不推荐使用。

实例

import Foundation

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 属性报错

常量属性只能在定义它的类的构造过程中赋值一次。如果是在子类中修改父类的常量属性,编译器会直接拒绝。