在掘金裏面看見iOS各路大神各類底層與runtime,看得就算工做了好幾年的我也一臉蒙圈,因而只好從簡單的入手。程序員
文章最初發布在簡書上面,有段時間了,考慮之後大部分時間都會在掘金學習,因而把文章搬過來了。稍微作了點潤色與排版。web
對於Swift學習而言,可選類型Optional是永遠繞不過的坎,特別是從OC剛剛轉Swift的時候,可能就會被代碼行間的?與!,有的時候甚至是??搞得稀裏糊塗的。編程
這篇文章會給各位帶來我對於可選類型的一些認識以及如何進行解包,其中會涉及到Swift中if let以及guard let的使用以及思考,還有涉及OC部分的nullable和nonnull兩個關鍵字,以及一點點對兩種語言的思考。json
在進行解包前,咱們先來理解一個概念,這樣可能更有利於對於解包。安全
首先咱們來看看這樣一段代碼:markdown
var num: Int?
num = 10
if num is Optional<Int> {
print("它是Optional類型")
}else {
print("它是Int類型")
}
複製代碼
請先暫時不要把這段代碼複製到Xcode中,先自問自答,num是什麼類型,是Int類型嗎?網絡
好了,你能夠將這段代碼複製到Xcode裏去了,而後在Xcode中的if上必定會出現這樣一段話:閉包
'is' test is always true
複製代碼
num不是Int類,它是Optional類型。app
那麼Optional類型是啥呢--可選類型,具體Optional是啥,Optional類型的本質實際上就是一個帶有泛型參數的enum類型,各位去源碼中仔細看看就能瞭解到,這個類型和Swift中的Result類有殊途同歸之妙。函數
var num: Int?這是一我的Optional的聲明,意思不是「我聲明瞭一個Optional的Int值」,而是「我聲明瞭一個Optional類型,它可能包含一個Int值,也可能什麼都不包含」,也就是說實際上咱們聲明的是Optional類型,而不是聲明瞭一個Int類型!
至於像Int!或者Int?這種寫法,只是一種Optional類型的糖語法寫法。
以此類推String?是什麼類型,泛型T?是什麼類型,答案各位心中已經明瞭吧。
正是由於num是一個可選類型。因此它才能賦值爲nil, var num: Int = nil。
這樣是不可能賦值成功的。由於Int類型中沒有nil這個概念!
這就是Swift與OC一個很大區別,在OC中咱們的對象均可以賦值爲nil,而在Swift中,能賦值爲nil只有Optional類型!
咱們先來看一個簡單的需求,雖然這個需求在實際開發中意義不太大:
咱們須要從網絡請求獲取到的一我的的身高(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?
複製代碼
init(contentsOf url: URL, options: Data.ReadingOptions = default) throws
複製代碼
class func jsonObject(with data: Data, options opt: JSONSerialization.ReadingOptions = []) throws -> Any
複製代碼
咱們能夠看到以上幾個函數中,獲取路徑獲取返回的路徑結果是一個可選類型而轉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
}
print(number)
複製代碼
這裏只是拋磚引玉,更長的可選鏈也能夠一步到位,而沒必要一層層進行判斷,由於可選鏈中一旦有某個鏈爲nil,那麼就會返回nil。
咱們先來看這兩個函數,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作橋接使用以及顯式的提醒對象的狀態
我以前寫過一篇文章,是說有關於一個字符串拼接函數的
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卓越。