從 Objective-C 轉 Swift 開發已經有一段時間了,這兩門語言在總體的理念上差別仍是蠻大的。在這之中,可選類型的處理是每個使用 Swift
的開發者天天都要面臨的問題,理解並正確處理好可選類型對於寫出高質量的 Swift
代碼和保證 iOS
項目的健壯性都是相當重要的。git
要想處理好可選類型,就要先理解可選類型。github
一個可選類型表明有兩種可能性:有一個值,你能夠解包可選類型來訪問該值;或者根本沒有值。數據庫
在 Objective-C
中不存在可選類型的概念,Objective-C
中最接近的東西就是 nil
, nil
的意思是「沒有有效的對象」。可是,這隻適用於對象——它不適用於結構、基本數據類型或枚舉值。對於這些類型,Objective-C
方法一般會返回一個特殊值(如 NSNotFound
)來指示缺乏值。這種方法假設方法的調用者知道有一個特殊的值來測試,並記得檢查它。Swift
的可選值可讓你指出可能爲 nil
的任何類型的值,而不須要特殊的常量。安全
例如,Swift
的 Int
類型有一個初始化方法,它試圖將一個 String
值轉換成一個 Int
值。可是,並非每一個字符串均可以轉換成一個整數。字符串 "123" 能夠轉換爲數字值 123,但字符串 "Hello, world" 沒有一個明顯的數值要轉換。bash
下面的例子使用初始化方法來嘗試將一個字符串轉換爲一個 Int
:app
let possibleNumber = "123"
let convertedNumber = Int(possibleNumber)
// convertedNumber 被推斷爲 "Int?" 類型或 「可選的 Int」
複製代碼
由於初始化方法可能會失敗,因此它返回一個可選的 Int
,而不是一個 Int
。可選的 Int
被寫爲 Int?
,而不是 Int
。問號表示它所包含的值是可選的,這意味着它可能包含一個 Int
值,或者它可能根本不包含任何值。ide
經過賦值給它一個特殊的值 nil
來設置一個可選變量爲無值狀態:性能
var serverResponseCode: Int? = 404
// serverResponseCode 包含一個實際的 Int 值爲 404
serverResponseCode = nil
// serverResponseCode 如今不包含任何值
複製代碼
若是你定義了一個可選變量而不提供默認值,則該變量會自動設置爲 nil
:測試
var surveyAnswer: String?
// surveyAnswer 自動設置爲 nil
複製代碼
Swift
的 nil
與 Objective-C
中的 nil
不相同。在 Objective-C
中,nil
是一個指向不存在對象的指針。在 Swift
中,nil
不是一個指針,它是缺乏某種類型的值。任何類型的可選值均可以被設置爲 nil
,而不只僅是對象類型。fetch
在 Swift
中,處理可選類型整體而言有五種方式:強制解包、可選綁定、隱式解包、Nil-Coalescing 運算符和可選鏈。接下來咱們將簡要介紹一下這五種方式:
一旦肯定可選值包含值,能夠經過在可選值名稱的末尾添加感嘆號(!
)來訪問其內部值。這被稱爲強制解包一個可選的值。
print("convertedNumber has an integer value of \(convertedNumber!).")
複製代碼
試着用 !
訪問不存在的可選值會觸發運行時錯誤。在使用強制解包以前,必定要確保一個可選值不爲 nil
if convertedNumber != nil {
print("convertedNumber has an integer value of \(convertedNumber!).")
}
// 打印 "convertedNumber has an integer value of 123."
複製代碼
你可使用可選綁定來發現可選值是否包含值,若是有,則使用該值用做臨時常量或變量。可選綁定能夠與 if
和 while
語句一塊兒使用,以檢查可選值內部的值,並將該值提取爲常量或變量,做爲單次操做的一部分。
使用 if
語句編寫一個可選綁定,以下所示:
if let actualNumber = Int(possibleNumber) {
print("\"\(possibleNumber)\" has an integer value of \(actualNumber)")
} else {
print("\"\(possibleNumber)\" could not be converted to an integer")
}
// 打印 ""123" has an integer value of 123"
複製代碼
若是轉換成功,那麼 actualNumber
常量能夠在 if
語句的第一個分支中使用。它已經被初始化爲包含在非可選的值中,因此沒有必要使用 !
後綴來訪問它的值。
你可使用可選綁定的常量和變量。若是你想在 if
語句的第一個分支內操做 actualNumber
的值,你能夠寫 if var actualNumber
,使得可選值做爲一個變量而很是量。
你能夠根據須要在單個 if
語句中包含儘量多的可選綁定和布爾條件,並用逗號分隔。若是可選綁定中的任何值爲 nil
,或者任何布爾條件的計算結果爲 false
,則整個 if
語句的條件被認爲是錯誤的。如下 if
語句是等價的:
if let firstNumber = Int("4"), let secondNumber = Int("42"), firstNumber < secondNumber && secondNumber < 100 {
print("\(firstNumber) < \(secondNumber) < 100")
}
// 打印 "4 < 42 < 100"
if let firstNumber = Int("4") {
if let secondNumber = Int("42") {
if firstNumber < secondNumber && secondNumber < 100 {
print("\(firstNumber) < \(secondNumber) < 100")
}
}
}
// 打印 "4 < 42 < 100"
複製代碼
有時從程序的結構中能夠清楚的看到,在第一次設置值以後,可選值將始終有一個值。在這些狀況下,每次訪問時都不須要檢查和解包可選值,由於能夠安全地假定全部的時間都有一個值。
這些可選值被定義爲隱式解包可選值。你寫一個隱式解包的可選值,在你想要的可選類型以後放置一個感嘆號(String!
)而不是一個問號(String?
)
隱式解包可選值的背後是普通可選值,但也能夠像非可選值同樣使用,而沒必要在每次訪問時解包可選值。
let possibleString: String? = "An optional string."
let forcedString: String = possibleString! // 須要感嘆號
let assumedString: String! = "An implicitly unwrapped optional string."
let implicitString: String = assumedString // 不須要感嘆號
複製代碼
若是隱式解包可選值爲 nil
,而且你嘗試訪問其包裝的值,則會觸發運行時錯誤。
你仍然能夠對隱式解包可選值使用強制解包和可選綁定。
Nil-Coalescing 運算符(a ?? b
)若是 a
包含一個值則解包它,或者返回一個默認值 b
(若是 a
是 nil
)。表達式 a
始終是可選的類型,表達式 b
必須匹配存儲在 a
中的類型。
Nil-Coalescing 運算符是如下代碼的簡寫:
a != nil ? a! : b
複製代碼
上面的代碼使用三元條件運算符,並強制解包(a!
)來訪問 a
來訪問 a
不爲 nil
時包裝的值,不然返回 b
。Nil-Coalescing 運算符提供了一種更簡潔的方式來以簡潔易懂的形式封裝這個條件檢查和解包。
若是 a
的值不是 nil
,則不計算 b
的值。這就是所謂的短路計算。
可選鏈是查詢和調用可能當前爲 nil
的可選屬性,方法和下標的過程。若是可選值包含一個值,那麼屬性,方法和下標調用將會成功;若是可選值爲 nil
,則屬性,方法和下標調用返回 nil
。多個查詢能夠連接在一塊兒,若是鏈中的任何連接有一個爲 nil
,則整個連接將優雅的失敗。
可選鏈能夠做爲強制解包的替代。
定義兩個名爲 Person
和 Residence
的類:
class Person {
var residence: Residence?
}
class Residence {
var numberOfRooms = 1
}
複製代碼
建立一個新的 Person
示例,因爲是它是可選類型,因此它的 residence
屬性默認初始化爲 nil
。
let john = Person()
複製代碼
若是採用強制解包的方式訪問 john
的 numberOfRooms
屬性,則會觸發運行時錯誤:
let roomCount = john.residence!.numberOfRooms
// 這會觸發運行時錯誤
複製代碼
可選鏈提供了另外一種訪問 numberOfRooms
值的方法。要使用可選鏈,請使用問號代替感嘆號:
if let roomCount = john.residence?.numberOfRooms {
print("John's residence has \(roomCount) room(s).")
} else {
print("Unable to retrieve the number of rooms.")
}
// 打印 "Unable to retrieve the number of rooms."
複製代碼
可選鏈能夠訪問屬性:
if let roomCount = john.residence?.numberOfRooms {
print("John's residence has \(roomCount) room(s).")
} else {
print("Unable to retrieve the number of rooms.")
}
john.residence?.numberOfRooms = 2
複製代碼
可選鏈能夠調用方法:
class Person {
var residence: Residence?
}
class Residence {
var numberOfRooms = 1
func printNumberOfRooms () {
print("John's residence has \(numberOfRooms) room(s).")
}
}
...
john.residence?.printNumberOfRooms()
複製代碼
可選鏈能夠訪問下標:
var testScores = ["Dave": [86, 82, 84], "Bev": [79, 94, 81]]
testScores["Dave"]?[0] = 91
testScores["Bev"]?[0] += 1
testScores["Brian"]?[0] = 72
// the "Dave" array is now [91, 82, 84] and the "Bev" array is now [80, 94, 81]
複製代碼
就是這樣。
由於 Objective-C
中的 nil
對於開發者來講是相對安全的,雖然向集合類型中添加 nil
會形成異常,可是對 nil
發送消息並不會有任何的問題(固然業務上可能會有問題)。但在 Swift
中,就像大多數其餘語言同樣,向 nil
發送消息會形成 crash
。並且做爲典型的現代強類型語言,可選類型的加入更是給以前長期使用 Objective-C
這種算是弱類型語言的 iOS
開發者帶來了困擾。再此給開發者們一些處理可選類型的建議:
除非一些必要的場景(例如代理模式,過程當中對象可能爲 nil
),儘量的使用非可選類型,包括但不限於屬性聲明和方法參數。
Swift
選用 「!
」 做爲強制解包和隱式解包的標誌是有緣由的,這是在提醒咱們這是一種很危險的操做,每每會在乎想不到的時候給咱們的應用帶來額外的 crash
。
在進行可選綁定時,可選類型不爲 nil
的場景咱們都會進行處理,但每每會忽視 else
的狀況,儘量也進行處理,那怕只是一句 log
。
最佳實踐是用同名的局部變量來可選綁定可選值,這樣能夠保證上下文清晰,不會由於出現了新的局部變量致使閱讀代碼的人反覆對照。
if let serverResponseCode = serverResponseCode {
print("serverResponseCode is (\serverResponseCode)")
} else {
print("serverResponseCode is nil")
}
複製代碼
至此,關於 Swift
中可選類型的處理就告一段落了,因爲 Swift
是一門強類型的語言,若是有哪些場景是咱們處理的不正確的,編譯器也會給出相應的提示,可是真正的危險可能不只止於此……
除了一些在最近一段時間剛剛從零啓動的項目,絕大多數的項目都是處於從 Objective-C
向 Swift
代碼過渡的階段,這裏面涉及到了對原有 Objective-C
代碼進行可選非可選區分的問題。
在 Objective-C
中,你使用可能爲 NULL
的原始指針(在 Objective-C
中稱爲 nil
)來處理對象的引用。 在 Swift
中,全部值(包括結構和對象引用)都保證爲非 nil
值。 相反,你表示能夠經過將值的類型包裝爲可選類型表示其可能缺失。 當你須要表示缺乏某個值時,可使用值 nil
。
若是讀過一些進行過適配 Swift
的 Objective-C
寫的三方庫的源代碼以後會發現,不少都用到了這樣的一對宏:
NS_ASSUME_NONNULL_BEGIN
...
NS_ASSUME_NONNULL_END
複製代碼
這對宏的意思是,在這對宏之間聲明的屬性和方法,其中涉及到的類型都是非可選類型的。不少開發的同窗發現這樣一種簡單而又粗暴的將 Objective-C
一鍵適配到 Swift
的方法以後,果斷的在全部的頭文件中的開始和結尾處加上這對宏。而後悲劇就發生了,好比:
NS_ASSUME_NONNULL_BEGIN
// 若是設備的內存處於極小的狀況下,會返回 nil
@property (nonatomic, strong) DataBase *dataBase;
NS_ASSUME_NONNULL_END
複製代碼
在尋常的狀況下調用數據庫屬性並不會有任何的問題,若是設備的內存處於極小的狀況下,會返回 nil
,這在純 Objective-C
的代碼中也不會有什麼問題,可是當混編時:
let x = object.dataBase().fetchUserInfo() // 當 dataBase 返回 nil 時,會 crash。
複製代碼
由於你已經經過宏聲明瞭 dataBase
屬性是非可選的,因此編譯器就會認爲這個屬性是非可選的,不會給出任何處理可選的提示。
看到這裏你可能會說,對於這類狀況,能夠經過判斷是否爲 nil
來進行處理,好比:
if object.dataBase() != nil {
...
}
複製代碼
這在 debug
模式下是行的通的,可是在 release
模式下,iOS
系統爲了優化性能,會對全部標記了非可選類型的對象的與 nil
的比較直接認爲是 true
,直接落入了括號中,形成更不可查的 crash
。因此最好的處理方式是對任何可能出現 nil
可能的屬性或方法參數都加上 nullable
:
@property (nonatomic, strong, nullable) DataBase *dataBase;
複製代碼
這樣就能夠通知編譯器這是一個可選類型屬性,該有的一些提示和處理也會由編譯器來提供。從而避免了 release
以後出現線上 crash
的悲劇。
原文地址:Objective-C 轉 Swift 的第一道坎——論如何正確的處理可選類型
若是以爲我寫的還不錯,請關注個人微博@小橘爺,最新文章即時推送~