Swift中非可選的可選值類型處理方法詳解
前言
在我們使用objective-c表示字符串信息的時(shí)候,可以用下面方法書寫。
NSString *str = @"秋恨雪"; str = nil;
因?yàn)閛bjective-c是弱類型語言,所以這里的str既可以是具體的字符串也可以是nil。但到了Swift中就不可以了,因?yàn)镾wift是類型安全的語言,一個(gè)String類型的變量不可能既能是具體的字符串,又可以為nil(更嚴(yán)格的說String類型的內(nèi)容只能是字符串)。所以,在Swift中有了可選類型的概念。(其實(shí)這一概念也是“借鑒”于其他編程語言,比如C#,只不過在C#中稱之為可空類型)。
大家在初看Optionals的感覺很陌生,在我第一眼看到它的時(shí)候,我就在想...這是什么鬼...但是仔細(xì)想想的話,可選值Optionals類型的引入,為我們也帶了便利.
可選值(optionals)無可爭議的是 swift 語言中最重要的特性之一,也是和其他語言,例如 Objective-C 的最大區(qū)別。通過強(qiáng)制處理那些有可能出現(xiàn) nil 的地方,我們就能寫出更有預(yù)測性的以及更健壯的代碼。
然而,有些時(shí)候可選值可能會(huì)致你于尷尬的境地,尤其是你作為開發(fā)者了解(甚至是有些猜測的成分在),有的特定變量始終是非空(non-nil)的,即使它是一個(gè)可選類型。例如,我們在一個(gè)視圖控制器中處理視圖的時(shí)候:
class TableViewController: UIViewController { var tableView: UITableView? override func viewDidLoad() { super.viewDidLoad() tableView = UITableView(frame: view.bounds) view.addSubview(tableView!) } func viewModelDidUpdate(_ viewModel: ViewModel) { tableView?.reloadData() } }
這也是對于很多 Swift 程序員爭論比較激烈的地方,程度不亞于討論 tabs 和 spaces 的用法。有的人會(huì)說:
既然它是一個(gè)可選值,你就應(yīng)該時(shí)刻使用 if let 或者 guard let 的方式進(jìn)行解包。
然而另外一些人則采用完全相反,說:
既然你知道這個(gè)變量在使用的時(shí)候不會(huì)為 nil,使用 ! 強(qiáng)制解包多好。崩潰也要比讓你的程序處于一個(gè)未知狀態(tài)要好吧。
本質(zhì)上來講,我們這里討論的是要不要采用防御性編程(defensive programming)的問題。我們是試圖讓程序從一個(gè)未知狀態(tài)恢復(fù)還是簡單的放棄,然后讓它崩潰掉?
如果非得讓我對這個(gè)問題給出一個(gè)答案的話,我更傾向于后者。未知狀態(tài)真的很難追蹤 bug,會(huì)導(dǎo)致執(zhí)行很多不想執(zhí)行的邏輯,采用防御性編程就會(huì)使得你的代碼很難追蹤,出現(xiàn)問題很難追蹤。
但是,我不太喜歡給出一個(gè)二選一的答案。相反,我們可以尋找一些技術(shù)手法,用更精妙的方式的解決上面提到的問題。
它真的可選的嗎?
那些可選類型的,但是被代碼邏輯真實(shí)需要的變量和屬性,實(shí)際上是架構(gòu)瑕疵的一個(gè)體現(xiàn)。如果在某些地方確實(shí)需要它,但是它又不在,就會(huì)使得你的代碼邏輯處于未知狀態(tài),那么它就不應(yīng)該是可選類型的。
當(dāng)然,在某些特定場景下,可選值確實(shí)很難避免(尤其是和特定的系統(tǒng) API 交互的時(shí)候),那對于大部分這種情況,我們有一些技術(shù)來處理從而避免可選值。
lazy 要比非可選的可選值更好
某些屬性的值需要在其父類創(chuàng)建之后再生成(比如視圖控制器中的那些視圖,應(yīng)該在 loadView()或者 viewDidLoad()方法中被創(chuàng)建),對于這種屬性要避免其可選類型的方法就是使用 lazy 屬性。一個(gè)lazy屬性是可以是非可選類型的,同時(shí)也不在其父類的初始化方法里被需要,它會(huì)在其第一次被獲取的時(shí)候創(chuàng)建出來。
讓我們改一下上面的代碼,使用 lazy 來改造 tableView 屬性:
class TableViewController: UIViewController { lazy var tableView = UITableView() override func viewDidLoad() { super.viewDidLoad() tableView.frame = view.bounds view.addSubview(tableView) } func viewModelDidUpdate(_ viewModel: ViewModel) { tableView.reloadData() } }
這樣,沒有可選值了,也不會(huì)有未知狀態(tài)咯🎉
適當(dāng)?shù)囊蕾嚬芾硪确强蛇x的可選值要好
可選值類型另外一種常用的場景就是用來打破循環(huán)依賴(circular dependencies)。有的時(shí)候,你就陷入 A 依賴 B,B 又依賴 A 的情況,如下:
class UserManager { private weak var commentManager: CommentManager? func userDidPostComment(_ comment: Comment) { user.totalNumberOfComments += 1 } func logOutCurrentUser() { user.logOut() commentManager?.clearCache() } } class CommentManager { private weak var userManager: UserManager? func composer(_ composer: CommentComposer didPostComment comment: Comment) { userManager?.userDidPostComment(comment) handle(comment) } func clearCache() { cache.clear() } }
從上面的代碼,我們可以看到,UserManager 和 CommentManager 之間有一個(gè)循環(huán)依賴的問題,它們二者都沒法假設(shè)自己擁有對方,但是它們都在各自的代碼邏輯里依賴彼此。這里就很容易產(chǎn)生 bug。
那要解決上面的問題,我們創(chuàng)建一個(gè) CommentComposer 來做一個(gè)協(xié)調(diào)者,負(fù)責(zé)通知UserManager 和 CommentManager二人一個(gè)評論產(chǎn)生了。
class CommentComposer { private let commentManager: CommentManager private let userManager: UserManager private lazy var textView = UITextView() init(commentManager: CommentManager, userManager: UserManager) { self.commentManager = commentManager self.userManager = userManager } func postComment() { let comment = Comment(text: textView.text) commentManager.handle(comment) userManager.userDidPostComment(comment) } }
通過這種形式,UserManager 可以強(qiáng)持有 CommentManager 也不產(chǎn)生任何依賴循環(huán)。
class UserManager { private let commentManager: CommentManager init(commentManager: CommentManager) { self.commentManager = commentManager } func userDidPostComment(_ comment: Comment) { user.totalNumberOfComments += 1 } }
我們又一次的移除了所有的可選類型,代碼也更好預(yù)測了🎉。
優(yōu)雅的崩潰(Crashing gracefully)
通過上面幾個(gè)例子,我們通過對代碼做一些調(diào)整,移除了可選類型從而排除了不確定性。然而,有的時(shí)候,移除可選類型是不可能的。讓我們舉個(gè)例子,比如你在加載一個(gè)本地的包含針對你 App 的配置項(xiàng)的 JSON 文件,這個(gè)操作本身一定會(huì)存在失敗的情況,我們就需要添加錯(cuò)誤處理。
繼續(xù)上面這個(gè)場景,加載配置文件失敗的時(shí)候繼續(xù)執(zhí)行代碼就會(huì)使得你的 app 進(jìn)入一個(gè)未知狀態(tài),在這種情況下,最好的方式讓它崩潰。這樣,我們會(huì)得到一個(gè)崩潰日志,希望這個(gè)問題能夠在用戶感知之前早早的被我們的測試人員以及 QA 處理掉。
所以,我們?nèi)绾伪罎?。。。最簡單的方式就是添?! 操作符,針對這個(gè)可選值強(qiáng)制解包,就會(huì)在其是 nil 的時(shí)候發(fā)生崩潰:
let configuration = loadConfiguration()!
雖然這個(gè)方法比較簡單,但是它有個(gè)比較大的問題,就是一旦這段代碼崩潰,我們能得到的只有一個(gè)錯(cuò)誤信息:
fatal error: unexpectedly found nil while unwrapping an Optional value
這個(gè)錯(cuò)誤信息并不告訴我們?yōu)槭裁窗l(fā)生這個(gè)錯(cuò)誤,在哪里發(fā)生的,給不了我們什么線索來解決它。這個(gè)時(shí)候,我們可以使用 guard 關(guān)鍵字,結(jié)合 preconditionFailure() 函數(shù),在程序退出的時(shí)候給出定制消息。
guard let configuration = loadConfiguration() else { preconditionFailure("Configuration couldn't be loaded. " + "Verify that Config.JSON is valid.") }
上面這段代碼發(fā)生崩潰的時(shí)候,我們就能獲得更多更有效的錯(cuò)誤信息:
fatal error: Configuration couldn't be loaded. Verify that Config.JSON is valid.: file /Users/John/AmazingApp/Sources/AppDelegate.swift, line 17
這樣,我們現(xiàn)在有了一個(gè)更清晰的解決問題的辦法,能夠準(zhǔn)確的知道這個(gè)問題在我們代碼里的哪個(gè)未知發(fā)生的。
引入 Require 庫
使用上面的 guard-let-preconditionFailure 的方案還是有一些冗長,確實(shí)讓我們呃代碼更難駕馭。我們也確實(shí)不希望在我們的代碼里占很多篇幅去些這種代碼,我們想更專注于我們的代碼邏輯上。
我的解決方案就是使用 Require。它只是簡單的在可選值添加簡單的 require() 方法,但能夠使得調(diào)用的地方更簡潔。用這種方法來處理上面加載 JSON 文件的代碼就可以這樣寫:
let configuration = loadConfiguration().require(hint: "Verify that Config.JSON is valid")
當(dāng)出現(xiàn)異常的時(shí)候,會(huì)給出下面的錯(cuò)誤信息:
fatal error: Required value was nil. Debugging hint: Verify that Config.JSON is valid: file /Users/John/AmazingApp/Sources/AppDelegate.swift, line 17
Require 的另一個(gè)優(yōu)勢就是它和調(diào)用 preconditionFailure() 方法一樣也會(huì)拋異常 NSException,就能使得那些異常上報(bào)工具能夠捕獲異常發(fā)生時(shí)候的元數(shù)據(jù)。
你如果想在自己代碼中使用的話,Require 現(xiàn)在在 Github 上開源了
總結(jié)
所以,總結(jié)來看,在 Swift 語言里處理那些非可選的可選值,我有幾點(diǎn)自己的貼心小提示給大家:
- lazy 屬性要比非可選的可選值要更好
- 適當(dāng)?shù)囊蕾嚬芾硪确强蛇x的可選值要好
- 當(dāng)你使用非可選的可選值的時(shí)候,優(yōu)雅的崩潰
好了,以上就是這篇文章的全部內(nèi)容了,希望本文的內(nèi)容對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,如果有疑問大家可以留言交流,謝謝大家對腳本之家的支持。
相關(guān)文章
Objective-C和Swift的轉(zhuǎn)換速查手冊(推薦)
這篇文章主要給大家介紹了關(guān)于Objective-C和Swift的轉(zhuǎn)換速查手冊的相關(guān)資料,文中通過示例代碼介紹的非常詳細(xì),非常推薦給大家參考學(xué)習(xí)使用,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)不2018-06-06Swift學(xué)習(xí)筆記之邏輯分支與循環(huán)體
這篇文章主要給大家介紹了關(guān)于Swift學(xué)習(xí)筆記之邏輯分支與循環(huán)體的相關(guān)資料,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧。2017-12-12Swift在什么情況會(huì)發(fā)生內(nèi)存訪問沖突詳解
這篇文章主要給大家介紹了關(guān)于Swift在什么情況會(huì)發(fā)生內(nèi)存訪問沖突的相關(guān)資料,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2021-01-01Swift中static和class關(guān)鍵字的深入講解
這篇文章主要給大家介紹了關(guān)于Swift中static和class關(guān)鍵字的相關(guān)資料,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者使用Java具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面來一起學(xué)習(xí)學(xué)習(xí)吧2019-03-03Swift HTTP加載請求Loading Requests教程
這篇文章主要為大家介紹了Swift HTTP加載請求Loading Requests教程示例,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-02-02Swift簡單快速的動(dòng)態(tài)更換app圖標(biāo)AppIcon方法示例
這篇文章主要為大家介紹了Swift動(dòng)態(tài)更換app圖標(biāo)AppIcon的簡單快速方法示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-06-06