Objective-C 轉 Swift 的第一道坎——論如何正確的處理可選類型

從 Objective-C 轉 Swift 開發已經有一段時間了,這兩門語言在總體的理念上差別仍是蠻大的。在這之中,可選類型的處理是每個使用 Swift 的開發者天天都要面臨的問題,理解並正確處理好可選類型對於寫出高質量的 Swift 代碼和保證 iOS 項目的健壯性都是相當重要的。git

可選類型

要想處理好可選類型,就要先理解可選類型。github

一個可選類型表明有兩種可能性:有一個值,你能夠解包可選類型來訪問該值;或者根本沒有值。數據庫

Objective-C 中不存在可選類型的概念,Objective-C 中最接近的東西就是 nil, nil 的意思是「沒有有效的對象」。可是,這隻適用於對象——它不適用於結構、基本數據類型或枚舉值。對於這些類型,Objective-C 方法一般會返回一個特殊值(如 NSNotFound)來指示缺乏值。這種方法假設方法的調用者知道有一個特殊的值來測試,並記得檢查它。Swift 的可選值可讓你指出可能爲 nil 的任何類型的值,而不須要特殊的常量。安全

例如,SwiftInt 類型有一個初始化方法,它試圖將一個 String 值轉換成一個 Int 值。可是,並非每一個字符串均可以轉換成一個整數。字符串 "123" 能夠轉換爲數字值 123,但字符串 "Hello, world" 沒有一個明顯的數值要轉換。bash

下面的例子使用初始化方法來嘗試將一個字符串轉換爲一個 Intapp

let possibleNumber = "123"
let convertedNumber = Int(possibleNumber)
// convertedNumber 被推斷爲 "Int?" 類型或 「可選的 Int」
複製代碼

由於初始化方法可能會失敗,因此它返回一個可選的 Int,而不是一個 Int。可選的 Int 被寫爲 Int?,而不是 Int。問號表示它所包含的值是可選的,這意味着它可能包含一個 Int 值,或者它可能根本不包含任何值。ide

nil

經過賦值給它一個特殊的值 nil 來設置一個可選變量爲無值狀態:性能

var serverResponseCode: Int? = 404
// serverResponseCode 包含一個實際的 Int 值爲 404
serverResponseCode = nil
// serverResponseCode 如今不包含任何值 
複製代碼

若是你定義了一個可選變量而不提供默認值,則該變量會自動設置爲 nil測試

var surveyAnswer: String?
// surveyAnswer 自動設置爲 nil
複製代碼

SwiftnilObjective-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."
複製代碼

可選綁定

你可使用可選綁定來發現可選值是否包含值,若是有,則使用該值用做臨時常量或變量。可選綁定能夠與 ifwhile 語句一塊兒使用,以檢查可選值內部的值,並將該值提取爲常量或變量,做爲單次操做的一部分。

使用 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 運算符

Nil-Coalescing 運算符(a ?? b)若是 a 包含一個值則解包它,或者返回一個默認值 b(若是 anil)。表達式 a 始終是可選的類型,表達式 b 必須匹配存儲在 a 中的類型。

Nil-Coalescing 運算符是如下代碼的簡寫:

a != nil ? a! : b
複製代碼

上面的代碼使用三元條件運算符,並強制解包(a!)來訪問 a 來訪問 a 不爲 nil 時包裝的值,不然返回 b。Nil-Coalescing 運算符提供了一種更簡潔的方式來以簡潔易懂的形式封裝這個條件檢查和解包。

若是 a 的值不是 nil,則不計算 b 的值。這就是所謂的短路計算。

可選鏈

可選鏈是查詢和調用可能當前爲 nil 的可選屬性,方法和下標的過程。若是可選值包含一個值,那麼屬性,方法和下標調用將會成功;若是可選值爲 nil,則屬性,方法和下標調用返回 nil。多個查詢能夠連接在一塊兒,若是鏈中的任何連接有一個爲 nil,則整個連接將優雅的失敗。

可選鏈能夠做爲強制解包的替代。

定義兩個名爲 PersonResidence 的類:

class Person {
    var residence: Residence?
}
 
class Residence {
    var numberOfRooms = 1
}
複製代碼

建立一個新的 Person 示例,因爲是它是可選類型,因此它的 residence 屬性默認初始化爲 nil

let john = Person()
複製代碼

若是採用強制解包的方式訪問 johnnumberOfRooms 屬性,則會觸發運行時錯誤:

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]
複製代碼

就是這樣。

Swift 中處理可選類型的建議

由於 Objective-C 中的 nil 對於開發者來講是相對安全的,雖然向集合類型中添加 nil 會形成異常,可是對 nil 發送消息並不會有任何的問題(固然業務上可能會有問題)。但在 Swift 中,就像大多數其餘語言同樣,向 nil 發送消息會形成 crash。並且做爲典型的現代強類型語言,可選類型的加入更是給以前長期使用 Objective-C 這種算是弱類型語言的 iOS 開發者帶來了困擾。再此給開發者們一些處理可選類型的建議:

儘量避免聲明可選類型的實例

除非一些必要的場景(例如代理模式,過程當中對象可能爲 nil),儘量的使用非可選類型,包括但不限於屬性聲明和方法參數。

多使用可選綁定、Nil-Coalescing 運算符和可選鏈處理可選類型,避免使用強制解包和隱式解包

Swift 選用 「!」 做爲強制解包和隱式解包的標誌是有緣由的,這是在提醒咱們這是一種很危險的操做,每每會在乎想不到的時候給咱們的應用帶來額外的 crash

不只要處理可選綁定和可選鏈的命中分支,對於 else 的狀況也要進行額外狀況的處理

在進行可選綁定時,可選類型不爲 nil 的場景咱們都會進行處理,但每每會忽視 else 的狀況,儘量也進行處理,那怕只是一句 log

進行可選綁定時,儘可能使用同名的局部變量

最佳實踐是用同名的局部變量來可選綁定可選值,這樣能夠保證上下文清晰,不會由於出現了新的局部變量致使閱讀代碼的人反覆對照。

if let serverResponseCode = serverResponseCode {
    print("serverResponseCode is (\serverResponseCode)")
} else {
    print("serverResponseCode is nil")
}
複製代碼

至此,關於 Swift 中可選類型的處理就告一段落了,因爲 Swift 是一門強類型的語言,若是有哪些場景是咱們處理的不正確的,編譯器也會給出相應的提示,可是真正的危險可能不只止於此……

Objective-C 和 Swift 混編時如何正確的處理可選類型

除了一些在最近一段時間剛剛從零啓動的項目,絕大多數的項目都是處於從 Objective-CSwift 代碼過渡的階段,這裏面涉及到了對原有 Objective-C 代碼進行可選非可選區分的問題。

Objective-C 中,你使用可能爲 NULL 的原始指針(在 Objective-C 中稱爲 nil)來處理對象的引用。 在 Swift 中,全部值(包括結構和對象引用)都保證爲非 nil 值。 相反,你表示能夠經過將值的類型包裝爲可選類型表示其可能缺失。 當你須要表示缺乏某個值時,可使用值 nil

若是讀過一些進行過適配 SwiftObjective-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 的第一道坎——論如何正確的處理可選類型

若是以爲我寫的還不錯,請關注個人微博@小橘爺,最新文章即時推送~

相關文章
相關標籤/搜索