Swift優雅的進行解包
https://www.jianshu.com/p/4bfbb0ba4d32
對於Swift學習而言,可選型別Optional是永遠繞不過的坎,特別是從OC剛剛轉Swift的時候,可能就會被程式碼行間的?與!,有的時候甚至是??搞得稀裡糊塗的.
這篇文章會給各位帶來我對於可選型別的一些認識以及如何進行解包,其中會涉及到Swift中if let以及guard let的使用以及思考,還有涉及OC部分的nullable和nonnull兩個關鍵字,以及一點點對兩種語言的思考.
var num: Int?它是什麼型別?
在進行解包前,我們先來理解一個概念,這樣可能更有利於對於解包.
首先我們來看看這樣一段程式碼:
var num: Int?
num = 10
if num is Optional<Int> {
print("它是可選型別")
}else {
print("它是Int型別")
}
請先暫時不要把這段程式碼複製到Xcode中,先自問自答,num是什麼型別,是Int型別嗎?
好了,你可以將這段程式碼複製到Xcode裡去了,然後在IDE中的if上一定會出現這樣一段話:
'is' test is always true
不是Int類,它是Optiona型別
那麼Optional型別是啥呢--可選型別,具體Optional是啥,點進去看看你就知道了.在這裡我就不多做解釋了.
var num: Int?這是Optional的宣告,意思不是"我聲明瞭一個Optional的Int值",而是”我聲明瞭一個Optional型別值,它可能包含一個Int值,也可能什麼都不包含”,也就是說實際上我們宣告的是Optional型別,而不是聲明瞭一個Int型別!
以此類推String?是什麼型別,T?是什麼型別,答案各位心中已經明瞭吧.
正是因為num是一個可選型別,所以它才能賦值為nil, var num: Int = nil,這樣是不可能賦值成功的,因為Int型別中沒有nil這個概念!
這就是Swift與OC一個很大區別,在OC中我們的物件都可以賦值為nil,而在Swift中,能賦值為nil只有Optional型別!
解包的基本思路,使用if let或者guard let,而非強制解包
我們先來看一個簡單的需求,雖然這個需求在實際開發中意義不太大:
我們需要從網路請求獲取到的一個人的身高(cm為單位)以除以100倍,以獲取m為單位的結果然後將其結果進行返回.
設計思路:
由於實際網路請求中,後臺可能會返回我們的身高為空(即nil),所以在轉模型的時候我們不能定義Float型別,而是定義Float?便於接受資料
如果身高為nil,那麼nil除以100是沒有意義的,在編譯器中Float?除以100會直接報錯,那麼其返回值也應該為nil,所以函式的返回值也是Float?型別
那麼函式應該設計成為這個樣子是這樣的:
func getHeight(_ height: Float?) -> Float?
如果一般解包的話,我們的函式實現大概會寫成這樣:
func getHeight(_ height: Float?) -> Float? {
if height != nil {
return height! / 100
}
return nil
}
使用!進行強制解包,然後進行運算
我想說的是使用強制解包固然沒有錯,不過如果在實際開發中這個height引數可能還要其他用途,那麼是不是每使用一次都要進行強制解包?
強制解包是一種很危險的行為,一旦解包失敗,就有崩潰的可能,也許你會說這不是有if判斷,然而實際開發中,情況往往比想的複雜的多,所以安全的解包行為應該是通過if let 或者guard let來進行
func getHeight(_ height: Float?) -> Float? {
if let unwrapedHeight = height {
return unwrapedHeight / 100
}
return nil
}
或者
func getHeight(_ height: Float?) -> Float? {
guard let unwrapedHeight = height else {
return nil
}
return unwrapedHeight / 100
}
那麼if let和guard let 你更傾向使用哪個呢?
在本例子中,其實感覺二者的差別不大,不過我個人更傾向於使用guard let.
原因如下:
在使用if let的時候其大括號類中的情況才是正常情況,而外部主體是非正常情況的返回的nil;
而在使用guard let的時候,guard let else中的大括號是異常情況,而外部主體返回的是正常情況.
對於一個以返回結果為目的的函式,函式主體展示正常返回值,而將異常丟擲在判斷中,這樣不僅邏輯更清晰,而且更加易於程式碼閱讀
解包深入
有這麼一個需求,從本地路徑獲取一個json檔案,最終將其轉為字典,準備進行轉模型操作
在這個過程中我們大概有這麼幾個步驟:
- 獲取本地路徑
func path(forResource name: String?, ofType ext: String?) -> String?
2. 將本地路徑讀取轉為Data
init(contentsOf url: URL, options: Data.ReadingOptions = default) throws
3. JSON序列化
class func jsonObject(with data: Data, options opt: JSONSerialization.ReadingOptions = []) throws -> Any
4. 是否可以轉為字典型別
我們可以看到以上幾個函式中,獲取路徑獲取返回的路徑結果是一個可選型別,而轉Data的方法是丟擲異常,JSON序列化也是丟擲異常,至於最後一步的型別強轉是使用as! [Sting: Any]這樣的操作
這個函式我是這來進行設計與步驟分解的:
函式的返回型別為可選型別,因為下面的4步中都有可能失敗進而返回nil
雖然有人會說第一步獲取本地路徑,一定是本地有的才會進行讀取操作,但是作為一個嚴謹操作,凡事和字串打交道的書寫都是有隱患的,所以我這裡還是用了guard let進行守護
這個函式看起來很不簡潔,每一個guard let 後面都跟著一個異常返回,甚至不如使用if let看著簡潔
但是這麼寫的好處是:在除錯過程中你可以明確的知道自己哪一步出錯
func getDictFromLocal() -> [String: Any]? {
//1 獲取路徑
guard let path = Bundle.main.path(forResource: "test", ofType:"json") else {
return nil
}
//2 獲取json檔案裡面的內容
guard let jsonData = try? Data.init(contentsOf: URL.init(fileURLWithPath: path)) else {
return nil
}
//3 解析json內容
guard let json = try? JSONSerialization.jsonObject(with: jsonData, options:[]) else {
return nil
}
//4 將Any轉為Dict
guard let dict = json as? [String: Any] else {
return nil
}
return dict
}
當然,如果你要追求簡潔,這麼寫也未嘗不可,一波流帶走
func getDictFromLocal() -> [String: Any]? {
guard let path = Bundle.main.path(forResource: "test", ofType:"json"),
let jsonData = try? Data.init(contentsOf: URL.init(fileURLWithPath: path)),
let json = try? JSONSerialization.jsonObject(with: jsonData, options:[]),
let dict = json as? [String: Any] else {
return nil
}
return dict
}
guard let與if let不僅可以判斷一個值的解包,而是可以進行連續操作
像下面這種寫法,更加最求的是結果,對於一般的除錯與學習,多幾個guard let進行拆分,也未嘗不可
至於哪種用法更適合,因人而異
可選鏈的解包
至於可選鏈的解包是完全可以一步到位,假設我們有以下這個模型
class Person {
var phone: Phone?
}
class Phone {
var number: String?
}
Person類中有一個手機物件屬性,手機類中有個手機號屬性,現在我們有位小明同學,我們想知道他的手機號
小明他不一定有手機,可能有手機而手機並沒有上手機號碼
let xiaoming = Person()
guard let number = xiaoming.phone?.number else {
return
}
這裡只是拋磚引玉,更長的可選鏈也可以一步到位,而不必一層層進行判斷,因為可選鏈中一旦有某個鏈為nil,那麼就會返回nil
nullable和nonnull
我們先來看這兩個函式,PHImageManager在OC與Swift中通過PHAsset例項獲取圖片的例子
[[PHImageManager defaultManager] requestImageForAsset:asset
targetSize:size
contentMode:PHImageContentModeDefault
options:options
resultHandler:^(UIImage * _Nullable result, NSDictionary * _Nullable info) {
// 非空才進行操作 注意_Nullable,Swift中即為nil,注意判斷
if (result) {
}
}];
PHImageManager.default().requestImage(for: asset,
targetSize: size,
contentMode: .default,
options: options,
resultHandler: { (result: UIImage?, info: [AnyHashable : Any]?) in
guard let image = result else { return }
})
在Swift中閉包返回的是兩個可選型別result: UIImage?與info: [AnyHashable : Any]?
而在OC中返回的型別是 UIImage * _Nullable result, NSDictionary * _Nullable info
注意觀察OC中返回的型別UIImage * 後面使用了_Nullable來修飾,至於Nullable這個單詞是什麼意思,我想稍微有點英文基礎的應該一看就懂--"可能為空",這不恰恰和Swift的可選型別呼應嗎?
另外還有PHFetchResult遍歷這個函式,我們再來看看在OC與Swift中的表達
PHFetchResult *fetchResult;
[fetchResult enumerateObjectsUsingBlock:^(id _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
}];
let fetchResult: PHFetchResult
fetchResult.enumerateObjects({ (obj, index, stop) in
})
看見OC中Block中的回撥使用了Nonnull來修飾,即不可能為空,不能為nil,一定有值,對於使用這樣的字元修飾的物件,我們就不必為其做健壯性判斷了.
這也就是nullable與nonnull兩個關鍵字出現的原因吧--與Swift做橋接使用以及顯式的提醒物件的狀態
一點點Swift與OC的語言思考
我之前寫過一篇文章,是說有關於一個字串拼接函式的
OC函式是這樣的
- (NSString *)stringByAppendingString:(NSString *)aString;
Swift中函式是這樣的
public mutating func append(_ other: String)
僅從API來看,OC的入參是很危險的,因為型別是NSString *
那麼nil也可以傳入其中,而傳入nil的後果就是崩掉,我覺得對於這種傳入引數為nil會崩掉的函式需要特別提醒一下,應該寫成這樣:
- (NSString *)stringByAppendingString:(NSString * _Nonnull)aString;
或者這樣
- (NSString *)stringByAppendingString:(nonnull NSString *)aString;
以便告訴程式設計師,入參不能為空,不能為空,不能為空.重要的事情說三遍!!!
反觀Swift就不會出現這種情況,other後面的型別為String,而不是String?,說明入參是一個非可選型別.
基於以上對於程式碼的嚴謹性,所以我才更喜歡使用Swift進行程式設計.
當然,Swift的嚴謹使得它失去部分的靈活性,OC在靈活性上比Swift卓越,但是從安全形度和編碼的長遠意義看Swift才是現在與未來.
最後想說的是Swift在國內的使用並不是很受擁戴,這點很無奈,因為和整個的大環境有關.
作者:seasonZhu
連結:https://www.jianshu.com/p/4bfbb0ba4d32
來源:簡書
著作權歸作者所有。商業轉載請聯絡作者獲得授權,非商業轉載請註明出處。