由一個Crash引發對 Swift 構造器的思考
不久前,公司決定在一個 Objective-C 老工程中,開始使用 Swift 進行混合開發。期間,碰到一個與 Swift 類構造過程相關的 Crash。在解決的過程中,對 Swift 構造過程有了更深刻的理解,特作此記錄,期望對剛入坑 Swift 開發的同學能有所幫助。
Crash 回顧
先來看一下程式碼,以下定義了 BaseiewController
和 AViewController
兩個類:
// BaseViewController.h
#import <UIKit/UIKit.h>
NS_ASSUME_NONNULL_BEGIN
@interface BaseViewController : UIViewController
- (instancetype)initWithParamenterA:(NSInteger)parameterA;
@end
NS_ASSUME_NONNULL_END
// BaseViewController.m
#import "BaseViewController.h"
@interface BaseViewController ()
@property (nonatomic,assign) NSInteger parameterA;
@end
@implementation BaseViewController
- (instancetype)initWithParamenterA:(NSInteger)parameterA {
self = [super init];
if (self) {
self.parameterA = parameterA;
}
return self;
}
@end
複製程式碼
以上程式碼段定義了 Objective-C 類 BaseViewController
,並且自定義了構造器 initWithParamenterA
。
// AViewController.swift
import UIKit
class AViewController: BaseViewController {
let count: Int
init(count: Int,parameterA: Int) {
self.count = count
super.init(paramenterA: parameterA)
}
// 後面的 “initCoder 從哪兒來” 小節會講講這個構造器
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
複製程式碼
第二塊程式碼段定義了 Swift 類 AViewController
,繼承自 BaseViewController
,並且自定義了構造器 init(count: Int,parameterA: Int)
,這個構造器還呼叫到了父類的 initWithParamenterA
構造器。細心的同學可能發現了,程式碼中還出現了 init?(coder aDecoder: NSCoder)
程式碼就這麼多。構建執行工程,前往 AViewController
頁面,出乎意料,Crash。控制檯輸出:
`Fatal error: Use of unimplemented initializer 'init(nibName:bundle:)' for class 'XXX.AViewController'`
複製程式碼
意思是 AViewController
沒有實現 init(nibName:bundle:)
方法,從而導致了 Crash。
對於剛入坑 Swift 不久的同學可能就會有些懵逼。明明在 Objective-C 的時候這樣寫根本沒有問題啊,怎麼到 Swift 這兒就 Crash 了呢?
Swift 類型別的構造過程回顧
如果想要了解 Crash 的原因,就需要了解 UIViewController
所屬的類型別(class)構造器的相關知識。
注:本小節大部分內容摘自Swift 官方中文教程。
指定構造器和便利構造器
Swift 為類型別提供了兩種構造器,分別是指定構造器和便利構造器。
類傾向於擁有極少的指定構造器,普遍的是一個類只擁有一個指定構造器。每一個類都必須至少擁有一個指定構造器。指定構造器語法如下:
init(parameters) {
statements
}
複製程式碼
便利構造器是類中比較次要的、輔助型的構造器。你可以定義便利構造器來呼叫同一個類中的指定構造器,併為部分形參提供預設值。一般只在必要的時候為類提供便利構造器。
便利構造器也採用相同樣式的寫法,但需要在 init
關鍵字之前放置 convenience
關鍵字,並使用空格將它們倆分開:
convenience init(parameters) {
statements
}
複製程式碼
類型別的構造器代理
規則 1
指定構造器必須呼叫其直接父類的的指定構造器。
規則 2
便利構造器必須呼叫同類中定義的其它構造器。
規則 3
便利構造器最後必須呼叫指定構造器。
一個更方便記憶的方法是:
- 指定構造器必須總是向上代理
- 便利構造器必須總是橫向代理
這些規則可以通過下面圖例來說明:
類型別的繼承和重寫
跟 Objective-C 中的子類不同,Swift 中的子類預設情況下不會繼承父類的構造器。Swift 的這種機制可以防止一個父類的簡單構造器被一個更精細的子類繼承,而在用來建立子類時的新例項時沒有完全或錯誤被初始化。
構造器的自動繼承
如上所述,子類在預設情況下不會繼承父類的構造器。但是如果滿足特定條件,父類構造器是可以被自動繼承的。事實上,這意味著對於許多常見場景你不必重寫父類的構造器,並且可以在安全的情況下以最小的代價繼承父類的構造器。
假設你為子類中引入的所有新屬性都提供了預設值,以下 2 個規則將適用:
規則 1
如果子類沒有定義任何指定構造器,它將自動繼承父類所有的指定構造器。(反之,如果定義了指定構造器,就不會繼承父類的指定構造器)
規則 2
如果子類提供了所有父類指定構造器的實現——無論是通過規則 1 繼承過來的,還是提供了自定義實現——它將自動繼承父類所有的便利構造器。
即使你在子類中添加了更多的便利構造器,這兩條規則仍然適用。
注意
子類可以將父類的指定構造器實現為便利構造器來滿足規則 2。
UIViewController 的指定構造器
UIViewController 在 Swift 中定義了兩個指定構造器。
當使用 StoryBoard 建立 UIViewController
時,最終會呼叫:
init?(coder: NSCoder)
複製程式碼
在使用除了 StoryBoard 之外的其它方式建立時,包括程式碼、Xib 的建立,最終會呼叫:
init(nibName nibNameOrNil: String?,bundle nibBundleOrNil: Bundle?)
複製程式碼
分析與解決
講完了 Swift 類型別構造器知識,先來分析一下 Swift 類 AViewController
。AViewController
定義了一個指定構造器 init(count: Int,parameterA: Int)
,因此根據構造器的自動繼承的規則 1, AViewController
不會自動繼承父類的指定構造器,包括 init(nibName:bundle:)
。也就是說 AViewController
沒有實現 init(nibName:bundle:)
。
其次 BaseViewController
是 Objective-C 類,所以可以不遵循 Swift 構造器的規則。我們可以看到在 BaseViewController
的指定構造器 initWithParamenterA
中,呼叫的是 [super init]
,這個方法並不是其父類的指定構造器,不過就算這樣寫,編譯器也不會報錯。
@implementation BaseViewController
- (instancetype)initWithParamenterA:(NSInteger)parameterA {
// 在 Objective-C 中,子類的指定構造器,不需要強制呼叫父類的指定構造器。
// 呼叫 init,編譯允許通過
self = [super init];
if (self) {
self.parameterA = parameterA;
}
return self;
}
@end
複製程式碼
而在 AViewController
的構造過程中,BaseViewController
的指定構造器中 [super init]
這句程式碼最終會呼叫當前類(AViewController
)並沒有實現的 init(nibName:bundle:)
,從而導致了 Crash。這也就對應了控制檯輸出的資訊:
Fatal error: Use of unimplemented initializer 'init(nibName:bundle:)' for class 'XXX.AViewController'
複製程式碼
再來簡單總結一下 Crash 的原因:
- 子類
AViewController
自定義了指定構造器,但沒有實現父類的指定構造器init(nibName:bundle:)
- 父類
BaseViewController
的構造器中直接呼叫了[super init]
,導致最終呼叫了AViewController
沒有實現的init(nibName:bundle:)
,從而 Crash。
換句話說,如果子類 AViewController
沒有自定義指定構造器或者父類 BaseViewController
遵循了類型別的構造器代理的規則1,就不會發生 Crash。
據此,解決的方案也呼之欲出啦:
方法一:此處定義一個 SwiftBaseViewController
來替代 BaseViewController
,其指定構造器不允許呼叫 super.init
,因此也就避免了 Crash:
import UIKit
class SwiftBaseViewController: UIViewController {
let parameterA: Int
init(parameterA: Int) {
self.parameterA = parameterA
// 呼叫 super.init(),編譯不通過
// 報錯資訊:Must call a designated initializer of the superclass 'UIViewController'
// super.init()
// 必須呼叫父類的指定構造器
super.init(nibName: nil,bundle: nil)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
複製程式碼
這個方法的好處是可以從編譯器層面阻止直接呼叫 super.init
,避免了程式設計師犯錯的可能。
不過這個方法的缺點是需要改變 BaseViewController
的編寫語言。遷移成本較大。
方法二:修改 BaseViewController
的構造器實現,將 self = [super init]
替換為 self = [super initWithNibName:nil bundle:nil]
。
@implementation BaseViewController
- (instancetype)initWithParamenterA:(NSInteger)parameterA {
//self = [super init];
self = [super initWithNibName:nil bundle:nil];
if (self) {
self.parameterA = parameterA;
}
return self;
}
@end
複製程式碼
這種方法是讓 Objective-C
類 BaseViewController
強制遵循 Swift 構造器的規則,呼叫了父類的指定構造器。
方法三:在子類 AViewController
中修改:
class AViewController: BaseViewController {
var count: Int = 0
// 使用便利構造器
convenience init(count: Int,parameterA: Int) {
self.init(paramenterA: parameterA)
self.count = count
}
}
複製程式碼
使用便利構造器代替了原先的指定構造器,根據構造器的自動繼承規則 1,AViewController
自動繼承了父類所有的指定構造器,包括 init(nibName:bundle:)
。這個方法的缺點是,原本的常量屬性 count
需要變更為變數,並被賦予預設值。
initCoder 從哪兒來
在 Swift 的 UIViewController
子類中,如果自定義指定構造器後,就必須實現構造器 init?(coder aDecoder: NSCoder)
,這是為什麼呢?
我們可以檢視 UIViewController
的介面檔案,其遵循 NSCoding
協議:
class UIViewController : NSCoding,...
複製程式碼
再來看一下 NSCoding
協議的內容:
protocol NSCoding {
func encode(with coder: NSCoder)
init?(coder: NSCoder) // NS_DESIGNATED_INITIALIZER
}
複製程式碼
其中定義了一個指定構造器 init?(coder: NSCoder)
。因為還需要遵循協議,這個構造器同時是一個必要構造器。
必要構造器
在類的構造器前新增 required
修飾符表明所有該類的子類都必須實現該構造器。
根據構造器的自動繼承規則 1,如果子類自定義了指定構造器,那麼就無法繼承父類的指定構造器,恰巧 init?(coder: NSCoder)
還是一個必要構造器,所以就必須在子類中實現該方法。
那麼,這種情況就比較尷尬啦。明明就沒有在專案中使用到 StoryBoard。可是每次都要加上這麼一段程式碼,顯得非常冗餘:
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
複製程式碼
那麼有什麼辦法可以避免重複寫這段程式碼嗎?
答案是有的!方法是在 BaseViewController
中宣告該方法不可用,那麼繼承自 BaseViewController
的所有子類都不需要實現這個方法。
Swift 版本:
@available(*,unavailable,message: "Unsupported init(coder:)")
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
複製程式碼
Objective-C 版本:
- (nullable instancetype)initWithCoder:(NSCoder *)coder NS_UNAVAILABLE;
複製程式碼
Swift 構造器知識拾遺
除了上面講到的一些構造器知識,這裡還會再講講一些其它比較重要的點。
預設構造器
如果結構體或類為所有屬性提供了預設值,又沒有提供任何自定義的構造器,那麼 Swift 會給這些結構體或類提供一個預設構造器。這個預設構造器將簡單地建立一個所有屬性值都設定為它們預設值的例項。
class ShoppingListItem {
var name: String?
var quantity = 1
var purchased = false
}
var item = ShoppingListItem()
複製程式碼
逐一構造器
只要你曾經瞭解過 Swift,肯定聽說過許許多多關於類和結構體的區別。對於習慣使用類的同學來說,這裡不妨再多告訴你一個使用結構體的理由。
官方文件中提到,結構體如果沒有定義任何自定義構造器,它們將自動獲得逐一成員構造器(memberwise initializer)。不像預設構造器,即使儲存型屬性沒有預設值,結構體也能會獲得逐一成員構造器。
struct Size {
var width = 0.0,height = 0.0
}
let twoByTwo = Size(width: 2.0,height: 2.0)
// Swift 5.1 甚至會為你生成省去了有預設值屬性的逐一構造器。省去的屬性將會直接使用預設值
let zeroByTwo = Size(height: 2.0)
let twoByZero = Size(width: 2.0)
複製程式碼
某些場景下,如果確實需要自定義一個構造器,但又想保留逐一成員構造器,那麼請在 extension
中自定義構造器。
不過對於類來說,所有的構造器都必須自己來實現。所以從使用便利性的角度來說,結構體無疑是一個更好的選擇。
可失敗構造器
在 Swift 中可以定義一個構造器可失敗的類,結構體或者列舉。這裡的“失敗”指的是,如給構造器傳入無效的形參,或缺少某種所需的外部資源,又或是不滿足某種必要的條件等。
為了妥善處理這種構造過程中可能會失敗的情況。你可以在一個類,結構體或是列舉型別的定義中,新增一個或多個可失敗構造器。其語法為在 init
關鍵字後面新增問號(init?
)。比如 Int
存在如下可失敗構造器:
init?(exactly source: Float)
複製程式碼
推薦閱讀
想要更全面深入瞭解 Swift 的構造過程,請閱讀下面的中英文教程: