因為一個Crash引發(fā)對Swift構(gòu)造器的思考分析
前言
不久前,公司決定在一個 Objective-C 老工程中,開始使用 Swift 進行混合開發(fā)。期間,碰到一個與 Swift 類構(gòu)造過程相關(guān)的 Crash。在解決的過程中,對 Swift 構(gòu)造過程有了更深刻的理解,特作此記錄,期望對剛?cè)肟?Swift 開發(fā)的同學能有所幫助。
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,并且自定義了構(gòu)造器 initWithParamenterA。
// AViewController.swift import UIKit class AViewController: BaseViewController { let count: Int init(count: Int, parameterA: Int) { self.count = count super.init(paramenterA: parameterA) } // 后面的 “initCoder 從哪兒來” 小節(jié)會講講這個構(gòu)造器 required init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } }
第二塊代碼段定義了 Swift 類 AViewController,繼承自 BaseViewController,并且自定義了構(gòu)造器 init(count: Int, parameterA: Int),這個構(gòu)造器還調(diào)用到了父類的 initWithParamenterA 構(gòu)造器。細心的同學可能發(fā)現(xiàn)了,代碼中還出現(xiàn)了 init?(coder aDecoder: NSCoder) 構(gòu)造器,對此,在 initCoder 從哪兒來小節(jié)會有詳細解釋。
代碼就這么多。構(gòu)建運行工程,前往 AViewController 頁面,出乎意料,Crash??刂婆_輸出:
`Fatal error: Use of unimplemented initializer 'init(nibName:bundle:)' for class 'XXX.AViewController'`
意思是 AViewController 沒有實現(xiàn) init(nibName:bundle:) 方法,從而導致了 Crash。
對于剛?cè)肟?Swift 不久的同學可能就會有些懵逼。明明在 Objective-C 的時候這樣寫根本沒有問題啊,怎么到 Swift 這兒就 Crash 了呢?
Swift 類類型的構(gòu)造過程回顧
如果想要了解 Crash 的原因,就需要了解 UIViewController 所屬的類類型(class)構(gòu)造器的相關(guān)知識。
注:本小節(jié)大部分內(nèi)容摘自Swift 官方中文教程。
指定構(gòu)造器和便利構(gòu)造器
Swift 為類類型提供了兩種構(gòu)造器,分別是指定構(gòu)造器和便利構(gòu)造器。
類傾向于擁有極少的指定構(gòu)造器,普遍的是一個類只擁有一個指定構(gòu)造器。每一個類都必須至少擁有一個指定構(gòu)造器。指定構(gòu)造器語法如下:
init(parameters) { statements }
便利構(gòu)造器是類中比較次要的、輔助型的構(gòu)造器。你可以定義便利構(gòu)造器來調(diào)用同一個類中的指定構(gòu)造器,并為部分形參提供默認值。一般只在必要的時候為類提供便利構(gòu)造器。
便利構(gòu)造器也采用相同樣式的寫法,但需要在 init 關(guān)鍵字之前放置 convenience 關(guān)鍵字,并使用空格將它們倆分開:
convenience init(parameters) { statements }
類類型的構(gòu)造器代理
規(guī)則 1
指定構(gòu)造器必須調(diào)用其直接父類的的指定構(gòu)造器。
規(guī)則 2
便利構(gòu)造器必須調(diào)用同類中定義的其它構(gòu)造器。
規(guī)則 3
便利構(gòu)造器最后必須調(diào)用指定構(gòu)造器。
一個更方便記憶的方法是:
- 指定構(gòu)造器必須總是向上代理
- 便利構(gòu)造器必須總是橫向代理
這些規(guī)則可以通過下面圖例來說明:
類類型的繼承和重寫
跟 Objective-C 中的子類不同,Swift 中的子類默認情況下不會繼承父類的構(gòu)造器。Swift 的這種機制可以防止一個父類的簡單構(gòu)造器被一個更精細的子類繼承,而在用來創(chuàng)建子類時的新實例時沒有完全或錯誤被初始化。
構(gòu)造器的自動繼承
如上所述,子類在默認情況下不會繼承父類的構(gòu)造器。但是如果滿足特定條件,父類構(gòu)造器是可以被自動繼承的。事實上,這意味著對于許多常見場景你不必重寫父類的構(gòu)造器,并且可以在安全的情況下以最小的代價繼承父類的構(gòu)造器。
假設(shè)你為子類中引入的所有新屬性都提供了默認值,以下 2 個規(guī)則將適用:
規(guī)則 1
如果子類沒有定義任何指定構(gòu)造器,它將自動繼承父類所有的指定構(gòu)造器。(反之,如果定義了指定構(gòu)造器,就不會繼承父類的指定構(gòu)造器)
規(guī)則 2
如果子類提供了所有父類指定構(gòu)造器的實現(xiàn)——無論是通過規(guī)則 1 繼承過來的,還是提供了自定義實現(xiàn)——它將自動繼承父類所有的便利構(gòu)造器。
即使你在子類中添加了更多的便利構(gòu)造器,這兩條規(guī)則仍然適用。
注意
子類可以將父類的指定構(gòu)造器實現(xiàn)為便利構(gòu)造器來滿足規(guī)則 2。
UIViewController 的指定構(gòu)造器
UIViewController 在 Swift 中定義了兩個指定構(gòu)造器。
當使用 StoryBoard 創(chuàng)建 UIViewController 時,最終會調(diào)用:
init?(coder: NSCoder)
在使用除了 StoryBoard 之外的其它方式創(chuàng)建時,包括代碼、Xib 的創(chuàng)建,最終會調(diào)用:
init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?)
分析與解決
講完了 Swift 類類型構(gòu)造器知識,先來分析一下 Swift 類 AViewController 。AViewController 定義了一個指定構(gòu)造器 init(count: Int, parameterA: Int),因此根據(jù)構(gòu)造器的自動繼承的規(guī)則 1, AViewController 不會自動繼承父類的指定構(gòu)造器,包括 init(nibName:bundle:)。也就是說 AViewController 沒有實現(xiàn) init(nibName:bundle:)。
其次 BaseViewController 是 Objective-C 類,所以可以不遵循 Swift 構(gòu)造器的規(guī)則。我們可以看到在 BaseViewController 的指定構(gòu)造器 initWithParamenterA 中,調(diào)用的是 [super init] ,這個方法并不是其父類的指定構(gòu)造器,不過就算這樣寫,編譯器也不會報錯。
@implementation BaseViewController - (instancetype)initWithParamenterA:(NSInteger)parameterA { // 在 Objective-C 中,子類的指定構(gòu)造器,不需要強制調(diào)用父類的指定構(gòu)造器。 // 調(diào)用 init,編譯允許通過 self = [super init]; if (self) { self.parameterA = parameterA; } return self; } @end
而在 AViewController 的構(gòu)造過程中,BaseViewController 的指定構(gòu)造器中 [super init] 這句代碼最終會調(diào)用當前類(AViewController)并沒有實現(xiàn)的 init(nibName:bundle:) ,從而導致了 Crash。這也就對應(yīng)了控制臺輸出的信息:
Fatal error: Use of unimplemented initializer 'init(nibName:bundle:)' for class 'XXX.AViewController'
再來簡單總結(jié)一下 Crash 的原因:
- 子類 AViewController 自定義了指定構(gòu)造器,但沒有實現(xiàn)父類的指定構(gòu)造器 init(nibName:bundle:)
- 父類 BaseViewController 的構(gòu)造器中直接調(diào)用了 [super init],導致最終調(diào)用了 AViewController 沒有實現(xiàn)的 init(nibName:bundle:) ,從而 Crash。
換句話說,如果子類 AViewController 沒有自定義指定構(gòu)造器或者父類 BaseViewController 遵循了類類型的構(gòu)造器代理的規(guī)則1,就不會發(fā)生 Crash。
據(jù)此,解決的方案也呼之欲出啦:
方法一:此處定義一個 SwiftBaseViewController 來替代 BaseViewController,其指定構(gòu)造器不允許調(diào)用 super.init ,因此也就避免了 Crash:
import UIKit class SwiftBaseViewController: UIViewController { let parameterA: Int init(parameterA: Int) { self.parameterA = parameterA // 調(diào)用 super.init(),編譯不通過 // 報錯信息:Must call a designated initializer of the superclass 'UIViewController' // super.init() // 必須調(diào)用父類的指定構(gòu)造器 super.init(nibName: nil, bundle: nil) } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } }
這個方法的好處是可以從編譯器層面阻止直接調(diào)用 super.init,避免了程序員犯錯的可能。
不過這個方法的缺點是需要改變 BaseViewController 的編寫語言。遷移成本較大。
方法二:修改 BaseViewController 的構(gòu)造器實現(xiàn),將 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 構(gòu)造器的規(guī)則,調(diào)用了父類的指定構(gòu)造器。
方法三:在子類 AViewController 中修改:
class AViewController: BaseViewController { var count: Int = 0 // 使用便利構(gòu)造器 convenience init(count: Int, parameterA: Int) { self.init(paramenterA: parameterA) self.count = count } }
使用便利構(gòu)造器代替了原先的指定構(gòu)造器,根據(jù)構(gòu)造器的自動繼承規(guī)則 1,AViewController 自動繼承了父類所有的指定構(gòu)造器,包括 init(nibName:bundle:)。這個方法的缺點是,原本的常量屬性 count 需要變更為變量,并被賦予默認值。
initCoder 從哪兒來
在 Swift 的 UIViewController 子類中,如果自定義指定構(gòu)造器后,就必須實現(xiàn)構(gòu)造器 init?(coder aDecoder: NSCoder),這是為什么呢?
我們可以查看 UIViewController 的接口文件,其遵循 NSCoding 協(xié)議:
class UIViewController : NSCoding, ...
再來看一下 NSCoding 協(xié)議的內(nèi)容:
protocol NSCoding { func encode(with coder: NSCoder) init?(coder: NSCoder) // NS_DESIGNATED_INITIALIZER }
其中定義了一個指定構(gòu)造器 init?(coder: NSCoder)。因為還需要遵循協(xié)議,這個構(gòu)造器同時是一個必要構(gòu)造器。
必要構(gòu)造器
在類的構(gòu)造器前添加 required 修飾符表明所有該類的子類都必須實現(xiàn)該構(gòu)造器。
根據(jù)構(gòu)造器的自動繼承規(guī)則 1,如果子類自定義了指定構(gòu)造器,那么就無法繼承父類的指定構(gòu)造器,恰巧 init?(coder: NSCoder) 還是一個必要構(gòu)造器,所以就必須在子類中實現(xiàn)該方法。
那么,這種情況就比較尷尬啦。明明就沒有在項目中使用到 StoryBoard??墒敲看味家由线@么一段代碼,顯得非常冗余:
required init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") }
那么有什么辦法可以避免重復寫這段代碼嗎?
答案是有的!方法是在 BaseViewController 中聲明該方法不可用,那么繼承自 BaseViewController 的所有子類都不需要實現(xiàn)這個方法。
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 構(gòu)造器知識拾遺
除了上面講到的一些構(gòu)造器知識,這里還會再講講一些其它比較重要的點。
默認構(gòu)造器
如果結(jié)構(gòu)體或類為所有屬性提供了默認值,又沒有提供任何自定義的構(gòu)造器,那么 Swift 會給這些結(jié)構(gòu)體或類提供一個默認構(gòu)造器。這個默認構(gòu)造器將簡單地創(chuàng)建一個所有屬性值都設(shè)置為它們默認值的實例。
class ShoppingListItem { var name: String? var quantity = 1 var purchased = false } var item = ShoppingListItem()
逐一構(gòu)造器
只要你曾經(jīng)了解過 Swift,肯定聽說過許許多多關(guān)于類和結(jié)構(gòu)體的區(qū)別。對于習慣使用類的同學來說,這里不妨再多告訴你一個使用結(jié)構(gòu)體的理由。
官方文檔中提到,結(jié)構(gòu)體如果沒有定義任何自定義構(gòu)造器,它們將自動獲得逐一成員構(gòu)造器(memberwise initializer)。不像默認構(gòu)造器,即使存儲型屬性沒有默認值,結(jié)構(gòu)體也能會獲得逐一成員構(gòu)造器。
struct Size { var width = 0.0, height = 0.0 } let twoByTwo = Size(width: 2.0, height: 2.0) // Swift 5.1 甚至會為你生成省去了有默認值屬性的逐一構(gòu)造器。省去的屬性將會直接使用默認值 let zeroByTwo = Size(height: 2.0) let twoByZero = Size(width: 2.0)
某些場景下,如果確實需要自定義一個構(gòu)造器,但又想保留逐一成員構(gòu)造器,那么請在 extension 中自定義構(gòu)造器。
不過對于類來說,所有的構(gòu)造器都必須自己來實現(xiàn)。所以從使用便利性的角度來說,結(jié)構(gòu)體無疑是一個更好的選擇。
可失敗構(gòu)造器
在 Swift 中可以定義一個構(gòu)造器可失敗的類,結(jié)構(gòu)體或者枚舉。這里的“失敗”指的是,如給構(gòu)造器傳入無效的形參,或缺少某種所需的外部資源,又或是不滿足某種必要的條件等。
為了妥善處理這種構(gòu)造過程中可能會失敗的情況。你可以在一個類,結(jié)構(gòu)體或是枚舉類型的定義中,添加一個或多個可失敗構(gòu)造器。其語法為在 init 關(guān)鍵字后面添加問號(init?)。比如 Int 存在如下可失敗構(gòu)造器:
init?(exactly source: Float)
推薦閱讀
想要更全面深入了解 Swift 的構(gòu)造過程,請閱讀下面的中英文教程:
總結(jié)
以上就是這篇文章的全部內(nèi)容了,希望本文的內(nèi)容對大家的學習或者工作具有一定的參考學習價值,謝謝大家對腳本之家的支持。
相關(guān)文章
Swift中的可選項Optional解包方式實現(xiàn)原理
這篇文章主要為大家介紹了Swift中的可選項Optional解包方式實現(xiàn)原理示例分析,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2023-03-03詳解在swift中實現(xiàn)NSCoding的自動歸檔和解檔
本篇文章主要介紹了在swift中實現(xiàn)NSCoding的自動歸檔和解檔,具有一定的參考價值,感興趣的小伙伴們可以參考一下。2017-03-03