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

