深入瞭解 iOS 的初始化

Danie1s發表於2019-11-18

初始化

在 iOS 裡面,無論是 Objective-C 還是 Swift,類(結構體、列舉)的初始化都有一定的規則要求,只不過在 Objective-C 中會比較寬鬆,如果不按照規則也不會報錯,但會存在隱患,而在 Swift 則需要嚴格按照規則要求程式碼才能編譯通過,極大提高了程式碼的安全性。

類(結構體、列舉)的初始化有兩種初始化器(初始化方法):指定初始化器(Designated Initializers )、便利初始化器(Convenience Initializers)

Designated Initializers

指定初始化器是類(結構體、列舉)的主初始化器,類(結構體、列舉)初始化的時候必須呼叫自身或者父類的指定初始化器。一個類(結構體、列舉)可以有多個指定初始化器,作用是代表從不同的源進行初始化。一個類(結構體、列舉)除非有多種不同的源進行初始化,否則不建議建立多個指定初始化器。在 iOS 裡,檢視控制元件類,如:UIViewUIViewController就有兩個指定初始化器,分別代表從程式碼初始化、從Nib初始化

Convenience Initializers

便利初始化器是類(結構體、列舉)的次要初始化器,作用是使類(結構體、列舉)在初始化時更方便設定相關的屬性(成員變數)。既然便利初始化器是為了便利,那麼一個類(結構體、列舉)就可以有多個便利初始化器,這些便利初始化器裡面最後都需要呼叫自身的指定初始化器

核心規則

iOS 的初始化最核心兩條的規則:

  • 必須至少有一個指定初始化器,在指定初始化器裡保證所有非可選型別屬性都得到正確的初始化(有值)
  • 便利初始化器必須呼叫其他初始化器,使得最後肯定會呼叫指定初始化器

Initialization

所有的其他規則都根據這兩條規則而展開,只是 Objective-C 沒有那麼多安全檢查,顯得比較隨意、寬鬆,而 Swift 則有一堆的限制。

Objective-C

Objective-C 在初始化時,會自動給每個屬性(成員變數)賦值為 0 或者 nil,沒有強制要求額外為每個屬性(成員變數)賦值,方便的同時也缺少了程式碼的安全性。

Objective-C 中的指定初始化器會在後面被NS_DESIGNATED_INITIALIZER修飾,以下為NSObjectUIView的指定初始化器

// NSObject
@interface NSObject <NSObject> 

- (instancetype)init
#if NS_ENFORCE_NSOBJECT_DESIGNATED_INITIALIZER
    NS_DESIGNATED_INITIALIZER
#endif
    ;
@end
  
  
// UIView
@interface UIView : UIResponder

- (instancetype)initWithFrame:(CGRect)frame NS_DESIGNATED_INITIALIZER;
- (nullable instancetype)initWithCoder:(NSCoder *)coder NS_DESIGNATED_INITIALIZER;

@end
複製程式碼

在 Objective-C 裡面,幾乎所有類都繼承自NSObject。當自定義一個類的時候,要麼直接繼承自NSObject,要麼繼承自UIView或者其他類。

無論繼承自什麼類,都經常需要新的初始化方法,而這個新的初始化方法其實就是新的指定初始化器。如果存在一個新的指定初始化器,那麼原來的指定初始化器就會自動退化成便利初始化器。為了遵循必須要呼叫指定初始化器的規則,就必須重寫舊的定初始化器,在裡面呼叫新的指定初始化器,這樣就能確保所有屬性(成員變數)被初始化

根據這條規則,可以從NSObjectUIView中看出,由於UIView擁有新的指定初始化器-initWithFrame:,導致父類NSObject的指定初始化器-init退化成便利初始化器。所以當呼叫[[UIView alloc] init]時,-init裡面必然呼叫了-initWithFrame:

當存在一個新的指定初始化器的時候,推薦在方法名後面加上NS_DESIGNATED_INITIALIZER,主動告訴編譯器有一個新的指定初始化器,這樣就可以使用 Xcode 自帶的Analysis功能分析,找出初始化過程中可能存在的漏洞

@interface MyView : UIView

@property (nonatomic, strong) NSString *name;

// 推薦加上NS_DESIGNATED_INITIALIZER
- (instancetype)initWithFrame:(CGRect)frame name:(NSString *)name NS_DESIGNATED_INITIALIZER;

@end


@implementation MyView

// 初始化時加入引數name,這個方法已經成為新的指定初始化器
- (instancetype)initWithFrame:(CGRect)frame name:(NSString *)name {
    if (self = [super initWithFrame:frame]) {
        self.name = name;
    }
    return self;
}

// 舊的指定初始化器就自動退化成便利初始化器,必須在裡面呼叫新的指定初始化器
- (instancetype)initWithFrame:(CGRect)frame {
    return [self initWithFrame:frame name:@"Daniels"];
}

// 舊的指定初始化器就自動退化成便利初始化器,必須在裡面呼叫新的指定初始化器
- (instancetype)initWithCoder:(NSCoder *)coder {
    // 這裡的實現是虛擬碼,只是為了滿足規則
    return [self initWithFrame:CGRectNull name:@"Daniels"];
}

@end
複製程式碼

如果不想去重寫舊的指定初始化器,但又不想存在漏洞和隱患,那麼可以使用NS_UNAVAILABLE把舊的指定初始化器都廢棄,外界就無法呼叫舊的指定初始化器

@interface MyView : UIView

@property (nonatomic, strong) NSString *name;



// 推薦加上NS_DESIGNATED_INITIALIZER
- (instancetype)initWithFrame:(CGRect)frame name:(NSString *)name NS_DESIGNATED_INITIALIZER;

// 廢棄舊的指定初始化器
- (instancetype)init NS_UNAVAILABLE;

// 廢棄舊的指定初始化器
- (instancetype)initWithFrame:(CGRect)frame NS_UNAVAILABLE;

// 廢棄舊的指定初始化器
- (instancetype)initWithCoder:(NSCoder *)coder NS_UNAVAILABLE;

@end


@implementation MyView

// 初始化時加入引數name,這個方法已經成為新的指定初始化器
- (instancetype)initWithFrame:(CGRect)frame name:(NSString *)name {
    if (self = [super initWithFrame:frame]) {
        self.name = name;
    }
    
    return self;
}


@end
複製程式碼

當然,一個新的類也可以不增加新的初始化方法,在 Objective-C 中,子類會直接繼承父類所有的初始化方法

Swift

在 Swift 中,初始化器的規則嚴格且複雜,目的就是為了使程式碼更加安全,如果不符合規則,會直接報錯,常常會讓剛接手 Swift 或者一直對 iOS 的初始化沒有深入理解的人很頭疼。其實核心規則還是一樣,只要理解了各個規則的含義和作用,寫起來還是沒有壓力。

從 iOS 初始化的核心規則展開而來,Swift 多了一些規則:

  • 初始化的時候需要保證類(結構體、列舉)的所有非可選型別屬性都會有值,否則會報錯。
  • 在沒有給所有非可選型別屬性賦值(初始化完成)之前,不能呼叫self相關的任何東西,例如:呼叫例項屬性,呼叫例項方法。

不存在繼承

這種情況處理就十分簡單,自己裡面的init方法就是它的指定初始化器,而且可以隨意建立多個它的指定初始化器。如果需要建立便利初始化器,則在方法名前面加上convenience,且在裡面必須呼叫其他初始化器,使得最後肯定呼叫指定初始化器

class Person {

    var name: String

    var age: Int

    // 可以存在多個指定初始化器
    init(name: String, age: Int) {
        self.name = name;
        self.age = age;
    }

    // 可以存在多個指定初始化器
    init(age: Int) {
        self.name = "Daniels";
        self.age = age;
    }

    // 便利初始化器
    convenience init(name: String) {
        // 必須要呼叫自己的指定初始化器
        self.init(name: name, age: 18)
        // 必須在初始化完成後才能呼叫例項方法
        jump()
    }
  
    func jump() {

    }
}
複製程式碼

存在繼承

如果子類沒有新的非可選型別屬性,或者保證所有非可選型別屬性都已經有預設值,則可以直接繼承父類的指定初始化器和便利初始化器

class Student: Person {

    var score: Double = 100
  
}
複製程式碼

如果子類有新的非可選型別屬性,或者無法保證所有非可選型別屬性都已經有預設值,則需要新建立一個指定初始化器,或者重寫父類的指定初始化器

  • 新建立一個指定初始化器,會覆蓋父類的指定初始化器,需要先給當前類所有非可選型別屬性賦值,然後再呼叫父類的指定初始化器
  • 重寫父類的指定初始化器,需要先給當前類所有非可選型別屬性賦值,然後再呼叫父類的指定初始化器
  • 在保證子類有指定初始化器,才能建立便利初始化器,且在便利初始化器裡面必須呼叫指定初始化器
class Student: Person {

    var score: Double
		
    // 新的指定初始化器,如果有新的指定初始化器,就不會繼承父類的所有初始化器,除非重寫
    init(name: String, age: Int, score: Double) {
        self.score = score
        super.init(name: name, age: age)
    }
  
    // 重寫父類的指定初始化器,如果不重寫,則子類不存在這個方法
    override init(name: String, age: Int) {
        score = 100
        super.init(name: name, age: age)
    }
  
  
    // 便利初始化器
    convenience init(name: String) {
        // 必須要呼叫自己的指定初始化器
        self.init(name: name, age: 10, score: 100)
    }
}
複製程式碼

需要注意的是,如果子類重寫父類所有指定初始化器,則會繼承父類的便利初始化器。原因也是很簡單,因為父類的便利初始化器,依賴於自己的指定初始化器

Failable Initializers

在 Swift 中可以定義一個可失敗的初始化器(Failable Initializers),表示在某些情況下會建立例項失敗。

只有在表示建立失敗的時候才有返回值,並且返回值為nil

子類可以把父類的可失敗的初始化器重寫為不可失敗的初始化器,但不能把父類的不可失敗的初始化器重寫為可失敗的初始化器

class Animal {
    
    let name: String
    // 可失敗的初始化器,如果把 ? 換成 !,則為隱式的可失敗的初始化器
    init?(name: String) {
        if name.isEmpty {
            return nil
        }
        self.name = name
    }
}

class Dog: Animal {

    override init(name: String) {
        if name.isEmpty {
            super.init(name: "旺財")!
        } else {
            super.init(name: name)!
        }
    }
}
複製程式碼

Required Initializers

在 Swift 中,可以使用required修飾初始化器,來指定子類必須實現該初始化器。需要注意的是,如果子類可以直接繼承父類的指定初始化器和便利初始化器,所以也就可以不用額外實現required修飾的初始化器

子類實現該初始化器時,也必須加上required修飾符,而不是override

class MyView: UIView {

    var name: String


    init(frame: CGRect, name: String) {
        self.name = name;
        super.init(frame: frame)
    }

    // 必須實現此初始化器,但由於是可失敗的初始化器,所以裡面可以不做具體實現
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
}
複製程式碼

總結

iOS 的初始化最核心兩條的規則:

  • 必須至少有一個指定初始化器,在指定初始化器裡保證所有非可選型別屬性都得到正確的初始化(有值)
  • 便利初始化器必須呼叫其他初始化器,使得最後肯定會呼叫指定初始化器

展開而來的多條規則:

  • 無論在 Objective-C 還是 Swift 中,都可以有多個指定初始化器和多個便利初始化器。如果不是可以從多個不同的源初始化,最好只建立一個指定初始化器
  • 無論在 Objective-C 還是 Swift 中,都需要在便利初始化器中呼叫指定初始化器
  • 在 Objective-C 中,初始化的時候不需要保證所有屬性(成員變數)都有值
  • 在 Objective-C 中,如果存在一個新的指定初始化器,那麼原來的指定初始化器就會自動退化成便利初始化器。必須重寫舊的定初始化器,在裡面呼叫新的指定初始化器
  • 在 Swift 中,初始化的時候需要保證類(結構體、列舉)的所有非可選型別屬性都會有值
  • 在 Swift 中,必須在初始化完成後才能呼叫例項屬性,呼叫例項方法
  • 在 Swift 中,如果存在繼承,並且子類有新的非可選型別屬性,或者無法保證所有非可選型別屬性都已經有預設值,那麼就需要新建立一個指定初始化器,或者重寫父類的指定初始化器,並且在裡面呼叫父類的指定初始化器
  • 在 Swift 中,子類如果沒有新建立一個指定初始化器,並且沒有重寫父類的指定初始化器,則會繼承父類的指定初始化器和便利初始化器
  • 在 Swift 中,子類如果新建立一個指定初始化器,或者重寫了父類的某個指定初始化器,那麼就不會繼承父類的指定初始化器和便利初始化器;但是如果重寫了父類的所有指定初始化器,就會繼承父類的便利初始化器
  • 在 Swift 中,子類可以把父類的指定初始化器重寫成便利初始化器
  • 在 Swift 中,如果子類沒有直接繼承父類的指定初始化器和便利初始化器,則必須實現父類中required修飾的初始化器

參考資料

Initialization

相關文章