阅读 1010

Swift 5.1 (14) - 初始化和反初始化

级别: ★☆☆☆☆
标签:「iOS」「Swift 」「便利初始化」「初始化」「反初始化」
作者: 沐灵洛
审校: QiShare团队


初始化Initialization

初始化是准备类,结构体或枚举类型实例的过程。该过程中涉及:设置存储属性初始值,初始化实例所需的配置项。

为存储属性设置初始值

因为在创建类或结构体的实例后,类或结构体的所有存储属性必须要要有初始值,故,在类和结构体定义时就必须为其所有存储属性设置适当的初始值。存储属性不能保留在不确定的状态(无初始值的状态),否则编译器会提示我们:Class '*' has no initializers

  1. 初始化方法中为存储属性设置初始值。
class Initializers {
    //! 属性声明时就设置属性的默认值
    var storeProperty : String = "变量存储属性声明时就设置属性的默认值"
    let constantStoreProperty : String = "常量存储属性声明时就设置属性的默认值"
    //! 属性声明为可选类型,可选类型的属性将自动初始化为nil
    var optionalProperty : Array<Int>?
}
复制代码
  1. 属性声明时指定默认属性值为其初始值。
class Initializers {
    var storeProperty : String
    let constantStoreProperty : String
    //! 属性声明为可选类型,可选类型的属性将自动初始化为nil。可以在初始化方法中设置其他值,也可以不管,看需要。
    var optionalProperty : Array<Int>?
    init() {
        storeProperty = "在初始化方法中设置了存储属性的初始值"
        constantStoreProperty = "在初始化方法中设置了常量存储属性的初始值"
    }
}
复制代码

####自定义初始化

class Initializers {
    var storeProperty : String
    let constantStoreProperty : String
    var optionalProperty : Array<Int>?
    // 无参数,无标签
    init() {
        storeProperty = "在初始化方法中设置存储属性的初始值"
        constantStoreProperty = "在初始化方法中设置常量存储属性的初始值"
    }
    //!有参数,有参数标签的自定义初始化方法
    init(prefixed prefix:String) {
        storeProperty = prefix + "在初始化方法中设置存储属性的初始值"
        constantStoreProperty = prefix + "在初始化方法中设置常量存储属性的初始值"
    }
    //!有参数,无参数标签的自定义初始化方法
    init(_ prefix:String) {
        storeProperty = prefix + "在初始化方法中设置存储属性的初始值"
        constantStoreProperty = prefix + "在初始化方法中设置常量存储属性的初始值"
    }
    //!多参数,有标签
    init(prefixed prefix:String, suffixed suffix:String) {
        storeProperty = prefix + "在初始化方法中设置存储属性的初始值" + suffix
        constantStoreProperty = prefix + "在初始化方法中设置常量存储属性的初始值" + suffix
    }
    init(prefix:String,suffix:String) {
        storeProperty = prefix + "在初始化方法中设置存储属性的初始值" + suffix
        constantStoreProperty = prefix + "在初始化方法中设置常量存储属性的初始值" + suffix
    }
    //! 多参数,无标签
    init(_ prefix:String, _ suffix:String) {
        storeProperty = prefix + "在初始化方法中设置存储属性的初始值" + suffix
        constantStoreProperty = prefix + "在初始化方法中设置常量存储属性的初始值" + suffix
    }
    class func usage(){
        //!调用:有参数,有参数标签的自定义初始化方法
        let obj = Initializers("QiShare")
        print(obj.storeProperty + "\n" + obj.constantStoreProperty)
        //!调用:有参数,无参数标签的自定义初始化方法
        let obj1 = Initializers(prefixed: "hasArgumentLabels")
        print(obj1.storeProperty + "\n" + obj1.constantStoreProperty)
        //!调用:多参数,有参数标签的自定义初始化方法
        let obj2 = Initializers(prefixed: "QiShare", suffixed: "end")
        print(obj2.storeProperty + "\n" + obj2.constantStoreProperty)
        //!调用:多参数,无参数标签的自定义初始化方法
        let obj3 = Initializers("Qishare","end")
        print(obj3.storeProperty + "\n" + obj3.constantStoreProperty)
    }
}
复制代码

默认初始化方法

Swift 可以为任何结构体和类提供默认的初始化方法,前提是结构体或类中的所有属性都被赋了初始值,并且结构体和类也没有提供任何初始化方法。

结构体类型的成员初始化方法

如果结构类型没有定义任何自定义的初始化方法,它们会自动生成接收成员属性初始值的初始化方法。与结构体默认初始化方法不同,即使结构体的存储属性没有默认值,结构体类型也会生成接收成员属性初始值的初始化方法。

struct Size {
    var width = 0.0
    var height : Double
}
//! 使用
let size1 = Size.init(width: 3.0, height: 3.0)
let size2 = Size(height: 3.0)
print(size1)
复制代码

注意:值类型中自定义了初始化方法,则将无法再访问该类型的默认初始化方法,或结构体自动生成的接收成员属性初始值的初始化方法。此约束为了保证自定义的初始化方法的优先级。

//自定义初始化方法
struct Size {
    var width = 0.0
    var height : Double
    init(wid:Double,hei:Double) {
        width = wid
        height = hei
    }
}
//原始生成的初始化方法失效
let size1 = Size.init(width: 3.0, height: 3.0) //< 报错
复制代码

值类型初始化方法嵌套调用Initializer Delegation

Initializer Delegation:值类型初始化方法中调用其他初始化方法来执行实例初始化的一部分。 作用:避免跨多个初始化方法时出现重复代码。 注意:初始化方法的嵌套调用在值类型和类类型中规则不同。值类型(结构体和枚举)不涉及继承,相对简单。类类型涉及继承,我们需要额外的操作,来保证实例对象初始化的正确性。

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)
    }
}
复制代码

类继承和初始化

类的所有存储属性,包括类从其父类继承的任何属性,都必须在初始化期间分配初始值。Swift为类类型定义了两种初始化方法:指定初始化方法和便利初始化方法,来帮助确保类的所有存储属性都能设置初始值。

指定初始化方法和便利初始化方法

指定初始化方法:是类的主要初始化方法,负责完全初始化类的所有属性,且会调用super引入父类适当的初始化方法。 便利初始化方法:是类次要的初始化方法,便利初始化方法可以定义默认值作为初始化方法调用的参数值。方便我们使用给定的默认值创建一个实例对象。

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

指定初始化方法:

init(`parameters`) {
}
复制代码

便利初始化方法:使用convenience关键字来指明

convenience init(`parameters`) {
    //statements
}
复制代码

类类型初始化方法嵌套调用Initializer Delegation

为了简化初始化方法与便利初始化方法之间的关系,Swift对于Initializer Delegation制定了三个规则:

  • 指定初始化方法必须调用其直接父类的指定初始化方法。
  • 便利初始化方法中必须调用同类的另一个初始化方法 。
  • 便利初始化方法最终必须调用到指定的初始化方法。
class Animal {
    var name : String
    var kind : String = "Unknown"
    init(name:String = "[UnNamed]") {
        self.name = name
    }
    convenience init(name : String, kind : String) {
       /*便利初始化方法中必须调用同类的另一个初始化方法
         便利初始化方法最终必须调用到指定的初始化方法
       */
        self.init(name: name)
        self.kind = kind
    }
}
class Dog: Animal {
    var nickName : String = "nihao"
    init(name : String, kind : String) {
        //指定初始化方法中,不能调用父类的便利初始化方法
        //super.init(name: name, kind: kind) // error:Must call a designated initializer of the superclass 'Animal'
        super.init(name: name)
    }
}

复制代码

总结:指定初始化方法必须始终向上使用super代理父级的初始化。便利初始化方法必须始终横向使用self代理同类的初始化。

`Initializer Delegation`三个规则阐述图.png

注意:这些规则不会影响类创建实例时的使用方式。上图中的任何初始值方法都能用于创建完全初始化的实例。规则仅影响类初始方法的实现方式。

下图会通过多类的复杂继承,来阐述指定初始化方法如何在类的初始化过程中扮演烟囱、漏斗的角色。

多类的复杂继承关系图.png

初始化的两个阶段

  • 第一阶段,设置所有存储属性的初始状态,赋初值。
  • 第二阶段,有机会进一步定制其存储属性。

Swift的编译器为确保完成这两个阶段的初始化而没有错误,会执行以下四个安全检查:

  • 指定初始化方法必须确保使用super向上代理父级初始化方法之前,完成初始化本类的所有属性。
  • 指定初始化方法中为继承属性赋值之前,必须使用super向上代理父级初始化方法。否则继承属性的新值会因调用父级super初始化方法而覆盖。
  • 便利初始化方法中为任何属性赋值之前,必须先调用同类的指定初始化方法。否则会被覆盖。
  • 初始化的第一阶段完成之前,不能在初始化方法中调用本类的任何实例方法,读取来自父类的任何实例属性。即:不能引用self作为本类的一个实例对象。理解起来比较抽象,举个例子来阐述,示例如下:
class BaseClass {
    var property1 : String = "defaultvalue"
    var property2 : String
    init() {
        property1 = "property1"
        property2 = "property2"
    }
}
class SubClass: BaseClass {
    var property3 : String
    override init() {
        //property3 = "property3"
        //super.init()
        someInstanceMethod(property2)
        /*
         报错信息为:
         1.'self' used in method call 'someInstanceMethod' before 'super.init' call
         2.'self' used in property access 'property2' before 'super.init' call
         */
        someInstanceMethod(property3)
        /*
         报错信息为:
         1.'self' used in method call 'someInstanceMethod' before 'super.init' call
         2.Variable 'self.property3' used before being initialized
         */
    }
    func someInstanceMethod(_ : String) -> Void {
    }
}
复制代码

在第一阶段结束之前,类实例不完全有效。

基于以上四个安全检查,以上两个阶段分别发挥的的作用总结如下:

第一阶段:

  1. 类的指定初始化方法或便利初始化方法会被调用。
  2. 为该类的新实例分配内存。内存尚未初始化。
  3. 类的指定初始化方法确认类中所有存储属性都具有值。至此所有存储属性的内存初始化完成。
  4. 类的指定初始化方法通过super委托父类的初始化方法完成父类所有存储属性的初始化。
  5. 类沿着继承链继续执行与4中相同的操作,直到继承链的顶端。
  6. 继承链中最顶端的类在确保其所有存储属性都有值时,意味着实例的内存已经完全初始化。至此第一阶段完成。

第二阶段:

  1. 初始化方法中可以访问self并可以修改其属性,调用实例方法等。
  2. 便利初始化方法中可以选择自定义实例并且使用self
   class convenienceClass: Initializers {
    /* 初始化与指定初始化之间的关系
     必须调用父类的指定初始化方法
     便利初始化方法只能使用`self.init`代理类的初始化而不是使用`super.init`
     便利初始化方法必须最终调用到同类的指定初始化方法*/
    var subClassStoreProperty : String
    override init(prefixed prefix:String) {
        subClassStoreProperty = "子类的属性"
        super.init(prefix)
        storeProperty = prefix + "在初始化方法中设置存储属性的初始值"
    }
    //父类中有相应的初始化方法 需要`override`
    convenience override init(_ name : String = "子类便利初始化前缀",_ suffix : String = "子类便利初始化后缀") {
        self.init(prefixed: name) //!< 必须是同类的初始化方法
        self.storeProperty = "子类的便利初始化中的存储属性重新赋值"
    }
}
复制代码

初始化方法继承和重写

子类对父类初始化方法的覆盖或重写:子类提供了与父类相同的初始化方法。必须要使用override修饰。 注意:即使是子类将父类的指定初始化方法实现为便利初始化方法也需要使用override修饰。

关于继承:与Objective-C不同,Swift中子类默认情况下是不会继承父类的初始化方法的。只有在某些特定的情况下才会继承。

class Animal {
    var name : String
    init(name:String = "[UnNamed]") {
        self.name = name
    }
}
class Dog: Animal {
    var nickName : String
    init(others:String) {
        self.nickName = others
    }
}
//调用父类的初始化方法会报错。
//let dog = Dog.init(name:"nihao")
let dog = Dog.init(others:"nihao")
print(dog.name) // 打印[UnNamed]
复制代码

注意:关于隐式调用父类的初始化方法,即省略super.init():当父类的指定初始化方法为零参数时,子类在其初始化方法中对其存储属性赋初值后,可以省略super.init()

class Animal {
    var name : String = "defaultValue"
  //以下参数赋初值也是符合`零参数`规则,调用时也可以对该参数进行忽略。该方法不写也是对的。
    init(name:String = "[UnNamed]") {
        self.name = name
    }
}
class Dog: Animal {
    var nickName : String
    init(others:String) {
        self.nickName = others
        //super.init(),此处可以省略
    }
}
//调用
let animals = Dog.init(others: "狗")
print(animals.name) // [UnNamed]
复制代码

自动继承初始化方法

自动继承初始化方法意味着不需要override进行重写。 前提:子类引入的新属性都确保提供了默认值。 基于前提需遵守两个规则:

  • 规则一:如果子类没有定义任何指定的初始化方法,则子类将自动继承其父类中所有的指定初始化方法,也会继承便利初始化方法。
class Animal {
    var name : String = "defaultValue"
    var kind : String = "Unknown"
    init(name:String = "[UnNamed]") {
        self.name = name
    }
    convenience init(name : String, kind : String) {
        self.init(name: name)
        self.kind = kind
    }
}
class Dog: Animal {
    var nickName : String = "defaultValue"
}
//子类使用继承自父类的便利初始化方法
let animals = Dog.init(name: "狗", kind: "狗类")
//子类使用继承自父类的指定初始化方法
let animals1 = Dog.init(name: "狗")
print(animals.name,animals1.kind)//!< 狗 Unknown
复制代码
  • 规则二:如果子类提供了所有父类指定初始化方法的实现。则子类会自动继承所有父类的便利初始化方法。在子类对于父类所有指定初始化方法的实现中,加入我们自定义的部分也是可以的。
class Animal {
    var name : String = "defaultValue"
    var kind : String = "Unknown"
    init(name:String = "[UnNamed]") {
        self.name = name
    }
    init(name2:String) {
        self.name = name2
    }
    convenience init(name : String, kind : String) {
        self.init(name: name)
        self.kind = kind
    }
}
class Dog: Animal {
    var nickName : String
    override init(name:String = "[UnNamed]") {
        self.nickName = "默认昵称"
        super.init(name: name)
        self.name = "自定义实现为:哈士奇"
    }
    override init(name2:String) {
        self.nickName = "默认昵称"
        super.init(name2: name2)
        self.name = "自定义实现为:哈士奇2"
    }
}
//子类使用继承自父类的便利初始化方法
let animals = Dog.init(name: "狗", kind: "狗类")
//子类使用继承自父类的指定初始化方法
let animals1 = Dog.init(name: "狗")
print(animals.name,animals1.kind)//!< 自定义实现为:哈士奇 Unknown
复制代码

注意点:子类可以将父类指定的初始化方法实现为子类便利初始化方法,也是满足规则二的。

class Animal {
    var name : String = "defaultValue"
    var kind : String = "Unknown"
    init(name:String = "[UnNamed]") {
        self.name = name
    }
    init(name2:String) {
        self.name = name2
    }
    convenience init(name : String, kind : String) {
        self.init(name: name)
        self.kind = kind
    }
}
class Dog: Animal {
    var nickName : String
    convenience override init(name:String = "[UnNamed]") {
        self.init(name2: name)
        self.nickName = "默认昵称"
        self.name = "自定义实现为:哈士奇"
    }
    override init(name2:String) {
        self.nickName = "默认昵称"
        super.init(name2: name2)
        self.name = "自定义实现为:哈士奇2"
    }
}
//子类使用继承自父类的便利初始化方法
let animals = Dog.init(name: "狗", kind: "狗类")
//子类使用继承自父类的指定初始化方法
let animals1 = Dog.init(name: "狗")
print(animals.name,animals1.kind)//!< 自定义实现为:哈士奇 Unknown
复制代码

实操中的指定和便利初始化方法

以下示例定义了三个类,分别为FoodRecipeIngredientShoppingListItem它们之间为继承关系。将用来展示指定初始化方法、便利初始化方法以及自动继承初始化方法之间的相互作用。

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

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

class ShoppingListItem: RecipeIngredient {
    var purchased = false
    var description: String {
        var output = "\(quantity) x \(name)"
        output += purchased ? " ✔" : " ✘"
        return output
    }
}
复制代码

初始化方法之间的作用解释如下:

Food为基类,定义了一个指定初始化方法init(name: String)和一个便利初始化方法convenience init()

RecipeIngredient继承自Food

  1. 定义了属于自己的初始化方法:init(name: String, quantity: Int)并在其中代理实现了父类的初始化super.init(name: name)。此处符合初始化的第一阶段:设置所有存储属性的初始状态,赋初值。
  2. 将父类的指定初始化方法实现为便利初始化方法:override convenience init(name: String)。该类在初始化的过程中,可以确保自增的属性quantity有初值;并且init(name: String)为父类Food的唯一指定初始化方法。这点满足了自动继承初始化方法的规则二:如果子类提供了所有父类指定初始化方法的实现,则子类会自动继承所有父类的便利初始化方法。故该类继承了父类的便利初始化方法convenience init()

ShoppingListItem继承自RecipeIngredient,未提供任何指定的初始化方法,此处满足了自动继承初始化方法的规则 一:如果子类没有定义任何指定的初始化方法,则子类将自动继承其父类中所有的指定初始化方法,也会继承便利初始化方法。

所以构建RecipeIngredientShoppingListItem的实例可以通过以下三种方式:

RecipeIngredient(),
RecipeIngredient(name: "醋"),
RecipeIngredient(name: "大葱", quantity: 6)
//`ShoppingListItem`的实例
ShoppingListItem(),
ShoppingListItem(name: "姜"),
ShoppingListItem(name: "鸡蛋", quantity: 6)
复制代码

以上阐述可以用一张图来展示:

初始化方法关系分析.png

可失败的初始化方法

使用init?来声明一个classstructureenumeration可失败的初始化方法。这种失败可能会被无效的初始化参数,必要资源的缺少等阻碍初始化成功的条件触发。

需要注意的是:

  1. 不能使用相同的参数类型和名称既定义可失败的又定义不可失败的初始化方法。
  2. 可失败的初始化方法将创建该类型的可选值,通过在可失败的初始化方法中return nil来表示初始化失败的条件被触发了。
class Animal {
    var name : String 
    init?(name:String) {
        if name.isEmpty {
            return nil
        }
        self.name = name
    }
}
if let _  = Animal.init(name: "") {
    print("初始化成功")
} else {
    print("初始化失败")//!< 输出
}
复制代码

枚举类型可失败的初始化方法

enum ResponseStatus {
    case ResponseStatus(Int,String)
    case ResponseFail
    case ResponseSuccess
    init?(code: Int,des:String){
        switch code {
        case 401:
             self = .ResponseStatus(code,des)
        case 500:
            self = .ResponseFail
        case 200:
            self = .ResponseSuccess
        default:
            return nil
        }
    }
}
//调用
if let status = ResponseStatus.init(code: 401, des: "未授权") {
    switch status {
    case .ResponseStatus(let code, let status):
        print(code,status) //401 未授权
    default:
        print("失败了")
    }
}
复制代码

具有原始值的枚举的可失败的初始化方法

enum Direction: Int {
    case east = 0,west = 1, south = 2, north = 3
}
//调用
if let dir = Direction.init(rawValue: 2) {
    print(dir.self) //!< south
} else {
    print("失败了")
}
复制代码

初始化失败的传播

值类型,类类型都可以使用初始化方法的委托,可以是同类委托,也可以是子类委托父类,不管是何种方式,若我们委托的初始化方法造成了初始化失败,整个初始化的过程会立即失败,不会再继续执行。 注意:可失败的初始化方法也可以委托一个不可失败的初始化方法,使用这种方式,我们需要在初始化的过程中添加潜在的失败操作,否则将不会失败。

class Animal {
    var name : String = "defaultValue"
    init(name:String) { //加问号也行
        self.name = name
    }
}
class Dog: Animal {
    var kind : String
    init?(name : String, kind : String) {
        if kind.isEmpty {
            return nil
        }
        self.kind = kind
        super.init(name: name)
    }
}
//调用
if let _ = Dog.init(name: "", kind: "") {
    print("初始化成功")
} else {
    print("初始化失败")//!< 输出
}
复制代码

重写可失败的初始化方法

子类可以重写父类可失败的初始化方法。子类也可以将其重写为不可失败的初始化方法来使用:意味着父类中此方法可fail,重写后便不可fail

注意:

  1. 可以将可失败的初始化方法覆盖为不可失败的初始化方法,但不能反过来。
  2. 将可失败的初始化方法覆盖为不可失败的初始化方法时,委托父类可失败的初始化方法时,需要强制解包可用的父类初始化结果。
class Animal {
    var animalName : String = "defaultValue"
    init?(name:String) {
        if name.isEmpty { return nil }
        animalName = name
    }
}
class Dog: Animal {
    var kind : String
    override init(name:String) {
        kind = "defaultKind"
        //处理父类可能初始化失败的情况
        if name.isEmpty {
            super.init(name: "[UnKnown]")!
        } else {
            super.init(name: name)!
        }
    }
}
//调用
let dog = Dog.init(name: "")
print(dog.animalName)
复制代码

init!可失败的初始化方法

定义一个可失败的初始化程序,通过关键字init?。当定义一个可失败的初始化方法,用于创建相应类型的隐式解包的可选实例时,通过关键字init!。 可以在init!中委托init?,反之亦然,可以重写init?init!反之亦然;也可以在init中委托init!,这样做,在init!初始化失败时,会触发断言。

必需的初始化方法

使用required关键字,来表示每个子类都必须实现该初始化方法。

class SomeClass {
    required init() {
    }
}
复制代码

子类在实现必需的初始化方法时,也必须使用required关键字来表示这个必需的初始化方法也适用于继承链中的其他子类。

class SomeSubclass: SomeClass {
    required init() {
        //super.init()
    }
}
复制代码

注意: 若满足初始化方法的继承条件,则不必显式实现required修饰的初始化方法。

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

如果存储属性的默认值需要某些自定义或设置,则可以使用闭包或全局函数为该属性提供自定义的默认值。每当初始化属性所属类型的新实例时,将调用闭包或函数,并将其返回值指定为属性的默认值。

使用闭包赋值的形式,函数与之类似:

class SomeClass {
    let someProperty: SomeType = {
        // `someValue` 必须是` SomeType`类型
        return someValue
    }()
}
复制代码

注意: 如果使用闭包来初始化属性,需要注意在执行闭包时尚未初始化实例的其余部分。意味着无法从闭包中访问任何其他属性值,即使这些属性具有默认值也是如此。也不能使用隐式self属性和调用任何实例的方法。

反初始化Deinitialization

Deinitialization可以理解为对象析构函数与对象初始化对应,在释放对象内存之前调用。当对象结束其生命周期,调用析构函数释放内存。Swift中通过自动引用计数处理实例的内存管理。 注意:

  1. 析构函数在实例释放内存之前自动调用,不允许手动调用。
  2. 父类的析构函数被子类继承,并在子类对析构函数的实现调用结束后自动调用。即使子类不提供自己的析构函数,父类的析构函数也总是会被调用。

析构函数仅在类类型中有效,使用deinit关键字。写法:

deinit {
}
复制代码

参考资料: swift 5.1官方编程指南


了解更多iOS及相关新技术,请关注我们的公众号:

image

可添加如下小编微信,并备注加入QiShare技术交流群,小编会邀请你加入《QiShare技术交流群》。

小编微信

关注我们的途径有:
QiShare(简书)
QiShare(掘金)
QiShare(知乎)
QiShare(GitHub)
QiShare(CocoaChina)
QiShare(StackOverflow)
QiShare(微信公众号)

推荐文章:
浅谈编译过程
深入理解HTTPS 浅谈 GPU 及 “App渲染流程”
iOS 查看及导出项目运行日志
Flutter Platform Channel 使用与源码分析
开发没切图怎么办?矢量图标(iconFont)上手指南
DarkMode、WKWebView、苹果登录是否必须适配?
奇舞团安卓团队——aTaller
奇舞周刊