Swift

初始化

深入理解 Swift 中的初始化过程,包括存储属性初始化、构造器类型、两段式初始化、继承规则、可失败构造器等核心概念。

初始化是为类、结构体或枚举准备新实例的过程。这个过程包括为每个存储属性设置初始值,以及执行其他必要的设置工作。

存储属性的初始赋值

类和结构体在创建实例时,必须为所有存储属性设置合适的初始值。存储属性不能处于未定义状态。

初始化器

初始化器使用 init 关键字定义,在创建实例时被调用。

struct Fahrenheit {
    var temperature: Double
    
    init() {
        temperature = 32.0
    }
}

let f = Fahrenheit()
print("默认温度是 \(f.temperature)° Fahrenheit")
// 输出:默认温度是 32.0° Fahrenheit

默认属性值

可以在属性声明时直接提供默认值,这样更简洁。

struct Fahrenheit {
    var temperature = 32.0
}

当属性总是使用相同的初始值时,使用默认属性值比在初始化器中设置更好。两种方式的最终结果相同,但默认值将属性的初始化与声明更紧密地联系在一起。

初始化参数

初始化器可以接受参数来自定义初始化过程。

struct Celsius {
    var temperatureInCelsius: Double
    
    init(fromFahrenheit fahrenheit: Double) {
        temperatureInCelsius = (fahrenheit - 32.0) / 1.8
    }
    
    init(fromKelvin kelvin: Double) {
        temperatureInCelsius = kelvin - 273.15
    }
    
    init(_ celsius: Double) {
        temperatureInCelsius = celsius
    }
}

let boilingPoint = Celsius(fromFahrenheit: 212.0)
let freezingPoint = Celsius(fromKelvin: 273.15)
let bodyTemperature = Celsius(37.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
    }
}

let magenta = Color(red: 1.0, green: 0.0, blue: 1.0)
let halfGray = Color(white: 0.5)

可选属性类型

如果属性在逻辑上允许"无值",可以声明为可选类型。可选类型属性会自动初始化为 nil

class SurveyQuestion {
    var text: String
    var response: String?
    
    init(text: String) {
        self.text = text
    }
    
    func ask() {
        print(text)
    }
}

let question = SurveyQuestion(text: "你喜欢什么颜色?")
question.ask()
// response 自动初始化为 nil

默认初始化器

如果结构体或类为所有属性提供了默认值,且没有提供任何自定义初始化器,Swift 会提供默认初始化器。

class ShoppingListItem {
    var name: String?
    var quantity = 1
    var purchased = false
}

let item = ShoppingListItem()

结构体的成员逐一初始化器

结构体如果没有定义任何自定义初始化器,会自动获得一个成员逐一初始化器。

struct Size {
    var width = 0.0
    var height = 0.0
}

// 自动获得成员逐一初始化器
let twoByTwo = Size(width: 2.0, height: 2.0)

即使属性有默认值,成员逐一初始化器也会包含这些属性。

struct Size {
    var width = 0.0
    var height = 0.0
}

let zeroByTwo = Size(height: 2.0)
let zeroByZero = Size()

值类型的构造器代理

构造器可以调用其他构造器来完成实例的部分初始化,这称为构造器代理。

对于值类型(结构体和枚举),使用 self.init 引用同一类型中的其他构造器。

struct Point {
    var x = 0.0
    var y = 0.0
}

struct Size {
    var width = 0.0
    var height = 0.0
}

struct Rect {
    var origin = Point()
    var size = Size()
    
    init() {}
    
    init(origin: Point, size: Size) {
        self.origin = origin
        self.size = size
    }
    
    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 basicRect = Rect()
let originRect = Rect(origin: Point(x: 2.0, y: 2.0),
                      size: Size(width: 5.0, height: 5.0))
let centerRect = Rect(center: Point(x: 4.0, y: 4.0),
                      size: Size(width: 3.0, height: 3.0))

如果为值类型定义了自定义初始化器,将无法访问默认初始化器和成员逐一初始化器。如果希望保留这些初始化器,可以在扩展中定义自定义初始化器。

struct Rect {
    var origin = Point()
    var size = Size()
}

extension Rect {
    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 defaultRect = Rect()
let memberRect = Rect(origin: Point(), size: Size(width: 10, height: 10))
let centerRect = Rect(center: Point(x: 5, y: 5), size: Size(width: 10, height: 10))

类的继承和初始化

类的所有存储属性(包括从超类继承的)都必须在初始化期间分配初始值。

Swift 为类定义了两种初始化器:指定初始化器和便利初始化器。

指定初始化器和便利初始化器

指定初始化器是类的主要初始化器,它初始化该类引入的所有属性,并调用合适的超类初始化器。

init(parameters) {
    // 初始化语句
}

便利初始化器是次要的、辅助性的初始化器,可以调用同一个类中的指定初始化器。

convenience init(parameters) {
    // 初始化语句
}

初始化器链

为了简化指定初始化器和便利初始化器之间的调用关系,Swift 采用以下三条规则:

规则 1:指定初始化器必须调用其直接超类的指定初始化器。

规则 2:便利初始化器必须调用同一类中定义的其他初始化器。

规则 3:便利初始化器最终必须调用一个指定初始化器。

简单记忆:

  • 指定初始化器必须总是向上代理
  • 便利初始化器必须总是横向代理
class Vehicle {
    var numberOfWheels = 0
    var description: String {
        return "\(numberOfWheels) 个轮子"
    }
}

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

class Hoverboard: Vehicle {
    var color: String
    
    init(color: String) {
        self.color = color
        // super.init() 在这里被隐式调用
    }
    
    override var description: String {
        return "\(super.description),颜色是 \(color)"
    }
}

两段式初始化

Swift 中类的初始化是一个两段式过程:

第一阶段:每个存储属性被引入它的类分配初始值。

第二阶段:每个类都有机会在新实例准备使用之前进一步自定义其存储属性。

两段式初始化过程的使用让初始化更安全,同时在类层次结构中给予了每个类完全的灵活性。

Swift 编译器执行四项安全检查:

安全检查 1:指定初始化器必须确保其所在类引入的所有属性都完成初始化,然后才能将初始化任务向上代理给超类。

安全检查 2:指定初始化器必须先向上代理调用超类初始化器,然后才能为继承的属性设置新值。

安全检查 3:便利初始化器必须先代理调用同一类中的其他初始化器,然后才能为任何属性赋值。

安全检查 4:初始化器在第一阶段初始化完成之前,不能调用任何实例方法,不能读取任何实例属性的值,不能引用 self

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

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)
    }
}

初始化器的继承和重写

Swift 中的子类默认不会继承超类的初始化器。这防止了超类的简单初始化器被更专业的子类继承,并被用来创建未完全或正确初始化的子类实例。

如果要提供与超类相同的初始化器,需要重写该初始化器。

class Vehicle {
    var numberOfWheels = 0
    var description: String {
        return "\(numberOfWheels) 个轮子"
    }
}

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

当重写超类的指定初始化器时,必须加上 override 修饰符。即使是重写自动提供的默认初始化器,也需要加上 override

当重写超类的便利初始化器时,不需要加 override 修饰符,因为子类不能直接调用超类的便利初始化器。

自动初始化器继承

虽然子类默认不继承超类的初始化器,但在特定条件下会自动继承。

规则 1:如果子类没有定义任何指定初始化器,它将自动继承超类所有的指定初始化器。

规则 2:如果子类提供了所有超类指定初始化器的实现(无论是通过规则 1 继承,还是提供自定义实现),它将自动继承超类所有的便利初始化器。

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

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)
    }
}

// RecipeIngredient 继承了 Food 的便利初始化器
let mysteryItem = RecipeIngredient()  // 使用继承的 init()
let oneBacon = RecipeIngredient(name: "Bacon")
let sixEggs = RecipeIngredient(name: "Eggs", quantity: 6)

可失败构造器

有时定义一个初始化可能失败的类、结构体或枚举很有用。失败可能由无效的初始化参数值、缺少必需的外部资源或其他阻止初始化成功的条件触发。

使用 init? 定义可失败构造器。

struct Animal {
    let species: String
    
    init?(species: String) {
        if species.isEmpty {
            return nil
        }
        self.species = species
    }
}

let someCreature = Animal(species: "长颈鹿")
// someCreature 是 Animal? 类型

if let giraffe = someCreature {
    print("动物的种类是 \(giraffe.species)")
}

let anonymousCreature = Animal(species: "")
// anonymousCreature 是 nil

枚举的可失败构造器

可以使用可失败构造器来选择合适的枚举成员。

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 fahrenheitUnit = TemperatureUnit(symbol: "F")
// fahrenheitUnit 是 TemperatureUnit? 类型,值为 .fahrenheit

let unknownUnit = TemperatureUnit(symbol: "X")
// unknownUnit 是 nil

带原始值的枚举的可失败构造器

带原始值的枚举会自动获得一个可失败构造器 init?(rawValue:)

enum TemperatureUnit: Character {
    case kelvin = "K"
    case celsius = "C"
    case fahrenheit = "F"
}

let fahrenheitUnit = TemperatureUnit(rawValue: "F")
// fahrenheitUnit 是 TemperatureUnit? 类型,值为 .fahrenheit

let unknownUnit = TemperatureUnit(rawValue: "X")
// unknownUnit 是 nil

可失败构造器的传播

类、结构体或枚举的可失败构造器可以横向代理到同一类型的其他可失败构造器。子类的可失败构造器也可以向上代理到超类的可失败构造器。

class Product {
    let name: String
    
    init?(name: String) {
        if name.isEmpty { return nil }
        self.name = name
    }
}

class CartItem: Product {
    let quantity: Int
    
    init?(name: String, quantity: Int) {
        if quantity < 1 { return nil }
        self.quantity = quantity
        super.init(name: name)
    }
}

重写可失败构造器

可以用子类的可失败构造器重写超类的可失败构造器,也可以用非可失败构造器重写可失败构造器。这让你能够定义一个初始化不会失败的子类,即使超类的初始化允许失败。

class Document {
    var name: String?
    
    init() {}
    
    init?(name: String) {
        if name.isEmpty { return nil }
        self.name = name
    }
}

class AutomaticallyNamedDocument: Document {
    override init() {
        super.init()
        self.name = "[未命名]"
    }
    
    override init(name: String) {
        super.init()
        if name.isEmpty {
            self.name = "[未命名]"
        } else {
            self.name = name
        }
    }
}

注意,可以用非可失败构造器重写可失败构造器,但反过来不行。

必要构造器

在类的构造器前添加 required 修饰符表明所有该类的子类都必须实现该构造器。

class SomeClass {
    required init() {
        // 构造器实现
    }
}

class SomeSubclass: SomeClass {
    required init() {
        // 子类构造器实现
    }
}

在子类重写必要构造器时,必须在前面加上 required 修饰符,以确保继承链上的子类也必须提供该构造器的实现。重写必要构造器时,不需要添加 override 修饰符。

class SomeClass {
    required init() {}
}

class SomeSubclass: SomeClass {
    required init() {
        // 必须实现
    }
}

如果子类继承的构造器能满足必要构造器的要求,则无需显式提供必要构造器的实现。

通过闭包或函数设置属性默认值

如果某个存储属性的默认值需要一些自定义或设置,可以使用闭包或全局函数来提供默认值。

class SomeClass {
    let someProperty: SomeType = {
        // 在这个闭包中创建默认值
        // someValue 必须和 SomeType 类型相同
        return someValue
    }()
}

注意闭包结尾的圆括号,这告诉 Swift 立即执行闭包。如果省略圆括号,你是将闭包本身赋值给属性,而不是闭包的返回值。

struct Chessboard {
    let boardColors: [Bool] = {
        var temporaryBoard = [Bool]()
        var isBlack = false
        for i in 1...8 {
            for j in 1...8 {
                temporaryBoard.append(isBlack)
                isBlack = !isBlack
            }
            isBlack = !isBlack
        }
        return temporaryBoard
    }()
    
    func squareIsBlackAt(row: Int, column: Int) -> Bool {
        return boardColors[(row * 8) + column]
    }
}

let board = Chessboard()
print(board.squareIsBlackAt(row: 0, column: 1))  // true
print(board.squareIsBlackAt(row: 7, column: 7))  // false

闭包在实例的其余部分还没有初始化时执行,因此不能在闭包中访问其他属性值,即使这些属性有默认值。也不能使用隐式的 self 属性,或调用任何实例方法。

最佳实践

1. 优先使用默认属性值

// ✅ 好的做法 - 使用默认值
struct Temperature {
    var celsius = 0.0
}

// ❌ 避免 - 不必要的初始化器
struct Temperature {
    var celsius: Double
    init() {
        celsius = 0.0
    }
}

2. 合理使用构造器代理

// ✅ 好的做法 - 避免重复代码
struct User {
    let name: String
    let email: String
    let age: Int
    
    init(name: String, email: String, age: Int) {
        self.name = name
        self.email = email
        self.age = age
    }
    
    init(name: String, email: String) {
        self.init(name: name, email: email, age: 0)
    }
}

3. 可失败构造器用于验证

// ✅ 好的做法 - 验证输入
struct Email {
    let address: String
    
    init?(address: String) {
        guard address.contains("@") else {
            return nil
        }
        self.address = address
    }
}

4. 在扩展中添加便利初始化器

// ✅ 好的做法 - 保留默认初始化器
struct Point {
    var x: Double
    var y: Double
}

extension Point {
    init(value: Double) {
        self.init(x: value, y: value)
    }
}

let origin = Point(x: 0, y: 0)  // 成员逐一初始化器
let diagonal = Point(value: 5)   // 扩展中的初始化器

总结

Swift 的初始化系统确保所有属性在使用前都有明确的值:

  • 存储属性必须在初始化完成前设置初始值
  • 指定初始化器是类的主要初始化器,负责初始化所有属性
  • 便利初始化器是辅助初始化器,最终调用指定初始化器
  • 两段式初始化确保初始化过程的安全性
  • 可失败构造器允许初始化在特定条件下返回 nil
  • 必要构造器确保子类必须实现特定的初始化器

理解这些概念对于编写安全、可维护的 Swift 代码至关重要。

在 GitHub 上编辑

上次更新于