Swift类型初始化

目录

引入

初始化是准备使用类、 结构或枚举实例的过程。 这个过程包括为该实例的每个存储属性设置初始值, 以及在新实例准备就绪可以使用之前执行任何其他必要的设置或初始化。

可以通过定义初始化器来实现这一初始化过程, 初始化器就像一种特殊的方法, 可以被调用来创建特定类型的新实例。

为存储属性设置初始值

在创建类或结构的实例时, 类和结构必须将其所有存储属性设置为适当的初始值。 存储属性不能处于不确定状态。

可以在初始化器中为存储属性设置初始值, 也可以在属性定义中指定默认属性值。

初始化器

调用初始化器是为了创建特定类型的新实例。 在最简单的形式中, 初始化器就像一个没有参数的实例方法,使用 init 关键字编写。

struct User {
    var name: String
    
    init() {
        self.name = "hzclog"
    }
}

let user = User()
print(user.name) // hzclog

默认属性值

可以在初始化器中设置存储属性的初始值。 或者在属性声明中指定默认属性值。

如果一个属性总是使用相同的初始值, 应提供一个默认值, 而不是在初始化器中设置一个值。

struct User {
    var kind = "human"
}

let user = User()
print(user.kind) // human

自定义初始化

可以通过输入参数和可选属性类型, 或在初始化过程中分配常量属性来自定义初始化过程。

初始化参数

作为初始化器定义的一部分, 你可以提供初始化参数, 以自定义初始化过程的值的类型和名称。

struct User {
    var name: String
    
    init(name: String) {
        self.name = name
    }
}

let user = User(name:"hzclog")
print(user.name) // hzclog

参数名称

与函数和方法参数一样, 初始化参数既可以有一个用于初始化器正文的参数名, 也可以有一个用于调用初始化器时的参数标签。

但是, 初始化器不像函数和方法那样, 在括号前有一个可识别的函数名。 因此, 初始化器参数的名称和类型对于确定调用哪个初始化器尤为重要。

因此, 如果没有提供参数标签,Swift会为初始化器中的每个参数自动提供一个参数标签。

struct User {
    var name: String
    var age: Int?
    
    init(name: String) {
        self.name = name
    }
    
    init(name: String, age: Int) {
        self.name = name
        self.age = age
    }
}

// 调用第一个init
let user = User(name:"hzclog")
print(user.name, user.age ?? 0) // hzclog 0

// 根据参数调用第二个init
let anotherUser = User(name: "陶喆", age: 54)
print(anotherUser.name, anotherUser.age ?? 0) // 陶喆 54

可选属性类型

如果自定义类型有一个逻辑上允许 “无值 ”的存储属性 — 也许是因为它的值不能在初始化过程中设置, 或者是因为它允许在以后的某个时刻 “无值” — 可以用可选类型声明该属性。

可选类型的属性会被自动初始化为 nil 值,这表明该属性在初始化过程中被故意设置为 “暂无值”。

struct User {
    var name: String?
}

let user = User()
print(user.name) // nil

初始化时分配常量属性

可以在初始化过程中为常量属性赋值。 只要在初始化结束时将其设置为一个确定的值即可。

struct User {
    let name: String
    
    init(name: String) {
        self.name = name
    }
}

let user = User(name: "王二小")
print(user.name) // 王二小

默认初始化器

如果结构或类为所有的属性提供了默认值, 但自身没有提供初始化器, Swift就会提供默认的初始化器。

默认初始化器会在创建新实例时, 将所有属性设置为默认值。

struct User {
    var name: String = "hzclog"
    var age: Int = 18
}

let user = User()
print(user.name, user.age) // hzclog 18

结构类型的成员初始化器

如果结构类型没有定义自己的初始化器, 会自动接受一个成员初始化器。 与默认初始化器不同, 即使存储的属性没有默认值, 结构也会接受成员初始化器。

成员初始化器是对新结构实例初始化的速记方法。 新实例属性的初始值可以通过名称传给成员初始化器。

struct User {
    var name: String
    var age: Int
}

let user = User(name: "hzclog", age: 18)
print(user.name, user.age) // hzclog 18

初始化器委托

初始化程序可以调用其他初始化程序来执行实例的部分初始化。 这一过程被称为初始化器委托, 可以避免在多个初始化器中重复编写代码。

对于值类型和类类型, 初始化器委托的工作方式和允许的委托形式的规则是不同的。 值类型(结构和枚举)不支持继承, 因此它们的初始化器委托过程相对简单, 因为它们 只能委托给自己提供的另一个初始化器。 然而, 类可以从其他类继承。这意味着类有额 外的责任确保在初始化过程中为其继承的所有存储属性分配一个合适的值。

对于值类型, 在编写自己的自定义初始化程序时, 可以使用 self.init 引用同一值类型的其他初始化程序。 你只能在初始化器中调用 self.init

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

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

struct Rect {
    var origin = Point()
    var size = Size()
    
    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)
    }
}

类的继承和初始化

类的所有存储属性(包括类从其超类继承的任何属性)都必须在初始化过程中分配初始值。

Swift为类定义了两种初始化器, 以帮助确保所有存储的属性都能获得初始值。 这两种初始化器被称为指定初始化器和方便初始化器。

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

指定初始化程序是类的主要初始化程序。 指定初始化程序会完全初始化该类引入的所有属性, 并调用适当的超类初始化程序来继续超类链上的初始化过程。

类的指定初始化程序往往很少, 一个类只有一个指定初始化程序也很常见。 指定初始化器是进行初始化的 “漏斗 ”点, 通过它, 初始化过程可以沿超类链向上继续。

每个类必须至少有一个指定初始化器。 在某些情况下, 可以通过从超类继承一个或多个 指定初始化器来满足这一要求。

方便初始化器是一个类的辅助初始化器。 你可以定义一个方便初始化器, 从与方便初始化 器相同的类中调用指定初始化器, 并将指定初始化器的某些参数设置为默认值。 你还可以 定义一个方便初始化程序, 为特定的用例或输入值类型创建该类的实例。

如果你的类不需要方便初始化器, 你就不必提供方便初始化器。 只要常用初始化模式的快 捷方式可以节省时间或使类的初始化意图更明确, 就可以创建方便初始化器。

指定初始化和方便初始化的语法

类的指定初始化器与值类型的简单初始化器相同。

init(<#parameters#>) {
   <#statements#>
}

方便初始化器在init之前, 有一个 convenience 修饰符, 使用空格隔开。

convenience init(<#parameters#>) {
   <#statements#>
}

类类型的初始化器委托

为了简化指定初始化器和方便初始化器之间的关系, Swift对初始化器之间的委托调用 应用了以下三条规则:

  1. 指定初始化程序必须调用其直属超类中的指定初始化程序。
  2. 方便初始化程序必须调用同一类中的另一个初始化程序。
  3. 方便初始化程序必须最终调用指定初始化程序。

记住这一点的简单方法是: 指定初始化器必须始终向上委托。 方便初始化器必须始终跨类委托。

两阶段初始化

Swift中的类初始化分为两个阶段。 在第一阶段, 每个存储属性都由引入它的类分配一个初始值。 一旦确定了每个存储属性的初始状态, 第二阶段就开始了, 在新实例被认为可以使用之前,每个 类都有机会进一步自定义其存储属性。

两阶段初始化过程的使用使初始化变得安全, 同时也为类层次结构中的每个类提供了完全的灵活性。 两阶段初始化可防止属性值在初始化前被访问, 并防止属性值被另一个初始化程序意外设置为不同的值。

Swift的编译器会执行四项有用的安全检查, 以确保两阶段初始化无差错地完成:

  1. 指定的初始化程序必须确保其类引入的所有属性都已初始化, 然后再委托给超类初始化程序。
  2. 指定初始化程序在为继承属性赋值之前, 必须将其委托给超类初始化程序。
  3. 在为任何属性(包括由同一类定义的属性)赋值之前, 方便初始化程序必须委托给另一个初始化程序。
  4. 在初始化的第一阶段完成之前, 初始化器不能调用任何实例方法、 读取任何实例属性的值或将 self 作为值引用。

在第一阶段结束之前, 类实例并不完全有效。 只有在第一阶段结束时知道类实例有效后, 才能访问属性和调用方法。

初始化器继承和重载

Swift子类默认不继承超类的初始化器。 Swift的这种方法可以防止出现这样的情况: 超类中的简单初始化程序被更专业的子类继承, 并被用于创建未完全或正确初始化的子类新实例。

如果想让自定义子类提供一个或多个与超类相同的初始化器, 可以在子类中提供这些初始化器的自定义实现。

当你编写的子类初始化器与超类指定的初始化器相匹配时, 你实际上是在提供对该指定初始化器的覆盖。 因此, 必须在子类的初始化器定义之前写入覆盖修饰符。 即使覆盖的是自动提供的默认初始化器,情况也是如此。

与覆盖属性、 方法一样, 覆盖修饰符的存在会提示 Swift 检查超类是否有匹配的指定初始化器被覆盖, 并验证覆盖初始化器的参数是否已按预期指定。

相反, 如果你编写的子类初始化程序与超类的方便初始化程序相匹配, 那么按照上文 “类类型的初始化程序委托 ”中所述的规则, 超类的方便初始化程序永远不能被你的子类直接调用。 因此, 你的子类并没有(严格来说)提供对超类初始化器的覆盖。 因此, 在提供超类方便初始化器的匹配实现时, 不需要写重载修饰符。

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

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

class Hoverboard: Vehicle {
    var color: String
    init(color: String) {
        self.color = color
    }
    override var description: String {
        return "\(super.description) in a beautiful \(color)"
    }
}

let vehicle = Vehicle()
print("vehicle: \(vehicle.description)") // vehicle: 0 wheels

let bicycle = Bicycle()
print("bicycle: \(bicycle.description)") // bicycle: 2 wheels

let hoverboard = Hoverboard(color: "silver")
print("Hoverboard: \(hoverboard.description)") // Hoverboard: 0 wheels in a beautiful silver

自动继承初始化器

子类默认不继承超类的初始化器。 不过, 如果满足某些条件, 超类的初始化程序会自动继承。 在实践中, 这意味着在许多常见情况下, 你不需要编写初始化重载, 只要在安全的情况下, 继承超类的初始化只需花费很少的精力。

假设在子类中引入的任何新属性都提供了默认值, 那么以下两条规则就适用:

  1. 如果子类没有定义任何指定的初始化器,它将自动继承所有超类指定的初始化器。
  2. 如果你的子类提供了所有超类指定初始化器的实现 — 或者按照规则 1 继承了它们, 或者提供了自定义实现作为其定义的一部分—那么它就自动继承了所有超类的方便初始化器。

即使你的子类添加了更多的方便初始化器,这些规则仍然适用。

可失效初始化器

有时定义一个初始化可能失败的类、 结构或枚举非常有用。 初始化失败的原因可能是初始化参数值无效、 缺少所需的外部资源或其他妨碍初始化成功的情况。

为了应对可能失败的初始化条件, 可以定义一个或多个可失败初始化器, 作为类、 结构或枚举定义的一部分。

可失效初始化程序会创建一个与初始化类型相同的可选值。 在可失效初始化器中写入 return nil 表示初始化失败的触发点。

struct Animal {
    let species: String
    // 可失效初始化器,init后面加问号
    init?(species: String) {
        if species.isEmpty { return nil }
        self.species = species
    }
}

初始化失败的传播

一个类、 结构或枚举的可失效初始化器可以委托给同一类、 结构或枚举的另一个可失效初始化器。 同样, 子类的可失效初始化程序也可以委托给超类的可失效初始化程序。

无论是哪种情况, 如果委托给另一个初始化器导致初始化失败, 整个初始化过程都会立即失败, 并且不会再执行任何初始化代码。

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

如果数量小于1或者名字为空, 初始化器都会失效。

必需的初始化器

在类初始化器的定义前写入 required 修饰符, 表示该类的每个子类都必须实现该初始化器。

class SomeClass {
    required init() {
        // initializer implementation goes here
    }
}

子类实现初始化器之前, 也要写上 required 修饰符。

class SomeSubclass: SomeClass {
    required init() {
        // subclass implementation of the required initializer goes here
    }
}

使用闭包或函数设置属性默认值

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

每当属性所属类型的新实例被初始化时, 闭包或函数就会被调用, 其返回值被指定为属性的默认值。

class SomeClass {
    let someProperty: SomeType = {
        return someValue
    }()
}

闭包的结尾大括号后面是一对空的括号。 这会告诉 Swift 立即执行闭包。 如果省略这些括号, 就会试图将闭包本身赋值给属性, 而不是闭包的返回值。