初始化
在 iOS 裡面,無論是 Objective-C 還是 Swift,類(結構體、列舉)的初始化都有一定的規則要求,只不過在 Objective-C 中會比較寬鬆,如果不按照規則也不會報錯,但會存在隱患,而在 Swift 則需要嚴格按照規則要求程式碼才能編譯通過,極大提高了程式碼的安全性。
類(結構體、列舉)的初始化有兩種初始化器(初始化方法):指定初始化器(Designated Initializers )、便利初始化器(Convenience Initializers)
Designated Initializers
指定初始化器是類(結構體、列舉)的主初始化器,類(結構體、列舉)初始化的時候必須呼叫自身或者父類的指定初始化器。一個類(結構體、列舉)可以有多個指定初始化器,作用是代表從不同的源進行初始化。一個類(結構體、列舉)除非有多種不同的源進行初始化,否則不建議建立多個指定初始化器。在 iOS 裡,檢視控制元件類,如:UIView
、UIViewController
就有兩個指定初始化器,分別代表從程式碼初始化、從Nib
初始化
Convenience Initializers
便利初始化器是類(結構體、列舉)的次要初始化器,作用是使類(結構體、列舉)在初始化時更方便設定相關的屬性(成員變數)。既然便利初始化器是為了便利,那麼一個類(結構體、列舉)就可以有多個便利初始化器,這些便利初始化器裡面最後都需要呼叫自身的指定初始化器
核心規則
iOS 的初始化最核心兩條的規則:
- 必須至少有一個指定初始化器,在指定初始化器裡保證所有非可選型別屬性都得到正確的初始化(有值)
- 便利初始化器必須呼叫其他初始化器,使得最後肯定會呼叫指定初始化器
所有的其他規則都根據這兩條規則而展開,只是 Objective-C 沒有那麼多安全檢查,顯得比較隨意、寬鬆,而 Swift 則有一堆的限制。
Objective-C
Objective-C 在初始化時,會自動給每個屬性(成員變數)賦值為 0 或者 nil
,沒有強制要求額外為每個屬性(成員變數)賦值,方便的同時也缺少了程式碼的安全性。
Objective-C 中的指定初始化器會在後面被NS_DESIGNATED_INITIALIZER
修飾,以下為NSObject
和UIView
的指定初始化器
// 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
或者其他類。
無論繼承自什麼類,都經常需要新的初始化方法,而這個新的初始化方法其實就是新的指定初始化器。如果存在一個新的指定初始化器,那麼原來的指定初始化器就會自動退化成便利初始化器。為了遵循必須要呼叫指定初始化器的規則,就必須重寫舊的定初始化器,在裡面呼叫新的指定初始化器,這樣就能確保所有屬性(成員變數)被初始化
根據這條規則,可以從NSObject
、UIView
中看出,由於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
修飾的初始化器