Swift 中的枚舉及可選型

枚舉可選型Swift 中兩個很重要的概念兼蕊,前者與 Objective-C 中的概念大不相同洛口,后者完全不存在慌烧,因此需要詳細介紹下這兩個概念炫乓。

為什么將可選型枚舉放在一起呢?因為可選型 Optional 本質就是枚舉 Emun闸衫,接下來詳細介紹涛贯。

枚舉(Enum)

Enum 基本語法

Swift 中枚舉的語法非常簡潔:

// 定義一個表示季節(jié)的枚舉
enum Season {
    case spring    // 春
    case summer    // 夏
    case autumn    // 秋
    case winter    // 冬
}

// 創(chuàng)建一個 Season 實例變量
var s = Season.spring
s = Season.summer // 重新賦值
s = .winter // 在確定 s 的類型為 Season 后可以直接使用.語法
print("\(s) is coming!") // winter is coming

// 在 switch 語句中使用 Enum
switch s {
case .spring:
    print("春天")
case .summer:
    print("夏天")
case .autumn:
    print("秋天")
case .winter:
    print("冬天")
}

如果讓枚舉遵循 CaseIterable 協(xié)議,即可獲得一個 allCases 屬性蔚出,用于表示一個包含枚舉所有成員的集合:

// 枚舉成員的遍歷
enum Season: CaseIterable {
    case spring, summer, autumn, winter
}

// 通過 allCases 屬性獲得枚舉成員的數(shù)量
let seasonNumber = Season.allCases.count

// 通過 allCases 屬性在 for 循環(huán)中遍歷枚舉成員
for s in Season.allCases {
    print("Current season is \(s)")
}

關聯(lián)值(Associated Values)

// 定義一個日期枚舉弟翘,有兩種 case : 1.數(shù)字表示的日期 2.字符串表示的日期
enum Date {
    case digit(year: Int, month: Int, dat: Int)
    case string(String)
}

var digitDate = Date.digit(year: 2019, month: 8, dat: 28)
var stringDate = Date.string("2019-08-28")

需要注意關聯(lián)值是存儲在枚舉變量的內存里 ,比如上面的 2019 骄酗、8 稀余、28"2019-08-28" 都是存儲在變量 digitDate

原始值(Raw Values)

可以指定 Enum 各個 case 的原始值類型

// 枚舉 PokerSuit 原始值的類型為 Character
enum PokerSuit: Character {
    case spade = "?"
    case heart = "?"
    case diamond = "?"
    case club = "?"
}

let suit = PokerSuit.heart
print(suit)           // heart
print(suit.rawValue)  // ?

需要注意原始值不存儲在枚舉變量的內存里 趋翻,比如下面的枚舉:

enum Level: String {
    case low = "low level"
    case middle = "middle level"
    case high = "high level"
}
print(MemoryLayout<Level>.stride)    // 1
print(MemoryLayout<Level>.size)      // 1
print(MemoryLayout<Level>.alignment) // 1

隱式原始值(Implicitly Assigned Raw Values)

如果指定的類型為 String 或者 Int睛琳,Swift 編譯器會隱式(自動)添加原始值:

enum Direction : String {
    case north
    case south
    case east
    case west
}
// 完全等價于
enum Direction : String {
    case north = "north"
    case south = "south"
    case east = "east"
    case west = "west"
}

enum Season : Int {
    case spring
    case summer
    case autumn
    case winter
}
// 也完全等價于
enum Season : Int {
    case spring = 0
    case summer = 1
    case autumn = 2
    case winter = 3
}

遞歸枚舉(Recursive Enumeration)

遞歸枚舉是一種特殊的枚舉類型,它有一個或多個枚舉成員使用該枚舉類型的實例作為關聯(lián)值踏烙。使用遞歸枚舉時师骗,編譯器會插入一個間接層。你可以在枚舉成員前加上 indirect 來表示該成員可遞歸宙帝。

例如丧凤,下面的例子中,枚舉類型存儲了簡單的算術表達式:

enum ArithmeticExpression {
    case number(Int)
    indirect case addition(ArithmeticExpression, ArithmeticExpression)
    indirect case multiplication(ArithmeticExpression, ArithmeticExpression)
}

也可以在枚舉類型開頭加上 indirect 關鍵字來表明它的所有成員都是可遞歸的:

indirect enum ArithmeticExpression {
    case number(Int)
    case addition(ArithmeticExpression, ArithmeticExpression)
    case multiplication(ArithmeticExpression, ArithmeticExpression)
}

可選型(Optional)

Swift 標準庫源碼 Optional 的定義:

public enum Optional<Wrapped>: ExpressibleByNilLiteral {
  /// The absence of a value.
  ///
  /// In code, the absence of a value is typically written using the `nil`
  /// literal rather than the explicit `.none` enumeration case.
  case none

  /// The presence of a value, stored as `Wrapped`.
  case some(Wrapped)
  
  ...
}

很明顯 Optional 就是一個 enum 類型步脓,且有兩個 casenonesome
其中 some 的關聯(lián)值為泛型 < Wrapped >浩螺,即 Optional 內存儲的數(shù)據(jù)類型為 Wrapped

從定義中不難理解可選型表示 一個類型即可能有值又可能為 nil 靴患。

基本概念

定義一個普通的 String 類型變量:

var str: String = "hello"

然后將 str 變量清空:

str = nil

編譯器會立馬報錯:

'nil' cannot be assigned to type 'String'

說明 Swift 不允許普通類型的變量賦值為 nil,要想 即可能有值又可能為空 要出,需要使用可選類型 Optional鸳君,其基本語法如下:

var name: String?  // 默認為 nil
name = "CodingIran"
name = nil

var age: Int?  // 默認值為 nil
age = 30

可選型不僅可以表示這個類型的值可能為 nil,而且一旦聲明為可選型之后患蹂, 其默認值即為 nil 或颊。

另外需要注意:任何類型(如:Int砸紊、DoubleStruct 等)的可選狀態(tài)都可以被設置為 nil囱挑,不只是對象類型醉顽!

可選型的概念有點類似薛定諤的貓,可以將其想象成一個盒子平挑,其中可能裝著某個具體類型的數(shù)據(jù)游添,也有可能什么都沒有。那如何才能打開盒子呢通熄?

強制解包(Forced Unwrapping)

Swift 提供如下語法可以強制打開一個可選類型:

// 定一個可選 Int 類型的變量唆涝,表示網絡請求的返回狀態(tài)碼
var responseCode: Int? = 404
print(responseCode!) // 404

responseCode = nil
print(responseCode!) // Fatal error: Unexpectedly found nil while unwrapping an Optional value

很顯然是強制解包是不安全的, 除非能夠確保一個可選類型一定有值 唇辨, 否則不要輕易嘗試強制解包 廊酣!在使用可選類型的值時應該先進行判斷:

// 判斷可選型是否為空
if responseCode != nil {
    print("responseCode's value is \(responseCode!)")
} else {
    print("responseCode has no value!")
}

這么寫雖然解決了 nil 導致的錯誤,但顯得很笨拙赏枚,對此 Swift 提供了一些更優(yōu)雅的語法亡驰。

可選綁定(Optional Binding)

使用可選綁定來判斷可選類型是否包含值,如果包含就把值賦給一個臨時常量或者變量嗡贺。

可選綁定可以用在 ifwhile 語句中隐解,這條語句不僅可以用來判斷可選類型中是否有值,同時可以將可選類型中的值賦給一個常量或者變量:

// 創(chuàng)建 unwrappedResponseCode 常量用來接受 responseCode 解包成功后結果
if let unwrappedResponseCode = responseCode {
    print("responseCode's value is \(unwrappedResponseCode)")
} else {
    // 解包失敗(nil)會進入 else 分支
    print("responseCode has no value!")
}

這段代碼可以理解為:

  • 如果 responseCode 返回的可選 Int 包含一個值诫睬,則創(chuàng)建一個叫做 unwrappedResponseCode 的新常量并將可選包含的值賦給它煞茫。

  • 如果解包成功,unwrappedResponseCode 常量可以在 if 語句的第一個分支中使用摄凡。它已經被可選類型包含的值初始化過续徽,所以不需要再使用 ! 后綴來獲取它的值。

  • 可以在可選綁定中使用常量變量亲澡。如果想在 if 語句的第一個分支中操作 unwrappedResponseCode 的值钦扭,可以改成

    if var unwrappedResponseCode...
    

憑空冒出一個新的變量(or 常量)名unwrappedResponseCode 顯得有些奇怪,所以通常都讓創(chuàng)建出的新名稱與可選型的名稱保持一致:

// 等號左右的名稱一樣
if let responseCode = responseCode {
    print("responseCode's value is \(responseCode)")
} else {
    print("responseCode has no value!")
}

第一次看到if let responseCode = responseCode可能會覺得『臥槽床绪,還可以這樣客情?』。

別急癞己,你甚至還可以這樣:

var responseCode: Int?
var responseData: Dictionary<String, Any>?

// do someting...

if let responseCode = responseCode, // 可選綁定 responseCode
   let responseData = responseData, // 可選綁定 responseData
   responseCode != 404,             // 判斷 responseCode
   responseData.count != 0 {        // 判斷 responseData
    // do someting...
}

guard 語句

在我們平時寫代碼的時候經常會遇到類型下面的代碼:

/// 定義一個計算形狀邊數(shù)的函數(shù)
///
/// - Parameter shape: 形狀字符串
/// - Returns: 形狀的變數(shù)(由于傳入的 shape 位置可能導致返回 nil膀斋,因此返回的是可選 Int)
func calculateNumberOfSides(shape: String) -> Int? {
    switch shape {
    case "Triangle":  // △
        return 3
    case "Square":    // □
        return 4
    case "Rectangle": // ?
        return 4
    case "Pentagon":  // ?
        return 5
    case "Hexagon":
        return 6      // ?
    default:
        return nil
    }
}

func maybePrintSides(shape: String) {
    let sides = calculateNumberOfSides(shape: shape)
    
    if let sides = sides {
        print("A \(shape) has \(sides) sides.")
    } else {
        print("I don't know the number of sides for \(shape).")
    }
}

上面的代碼沒毛病,在 oc 時代我們幾乎每天都在寫這樣的 if else 語句痹雅,但 Swift 給了一種新的條件判斷的語句 guard

func maybePrintSides(shape: String) {
    guard let sides = calculateNumberOfSides(shape: shape) else {
        print("I don't know the number of sides for \(shape).")
        return
    }
    
    print("A \(shape) has \(sides) sides.")
}

有沒有發(fā)現(xiàn)使用 guard 有什么好處仰担?

當使用 guard 語句進行可選項綁定時,綁定的常量(let)绩社、變量(var)也能 在外層作用域中使用 摔蓝!

隱式解包(Implicitly Unwrapped Optional)

可選型的引入對于程序安全性有很大的提升赂苗,但即使使用可選綁定,有時依然覺得有些繁瑣:

let number: Int? = 10

if let number = number {
    print("number is \(number)")
} else {
    print("number is non-existent!")
}

上面這段代碼中定義了一個 Int 可選型的常量 number贮尉,并將 10 賦值給它拌滋。

既然是常量,那 number 永遠都是 10绘盟,不可能出現(xiàn) nil 的情況鸠真,這時候再去擔心就顯得很多余。對此 Swift 推出了隱式解包的概念:

let number: Int! = 10
let otherNumber: Int = number
print("otherNumber is \(otherNumber)")

需要注意:number 變量依然是一個可選 Int龄毡,只是在將它賦值給 otherNumber 時會 自動強制解包 7途怼(因為你已經確保 number 不會為 nil 了)

空合并運算符(Nil Coalescing)

Swift 中還有一個更加方便的方法可以對可選類型進行解包。如果想獲取一個可選類型的值沦零,并且希望在它為空時 給它一個默認值 的話祭隔,空合并運算符是最好的方法:

var optionalInt: Int? = 10
var mustHaveResult = optionalInt ?? 0
// mustHaveResult 類型為 Int,值為 10

上面的代碼optionalInt ?? 0表示如果 optionalInt 有值路操,則返回自動解包的 optionalInt10疾渴,如果為空則返回 0,其完全等價于下面的代碼:

var optionalInt: Int? = 10
var mustHaveResult: Int
if let unwrapped = optionalInt {
  mustHaveResult = unwrapped
} else {
  mustHaveResult = 0
}

再看一種情況:

var a: Int? = 1
var b: Int? = 2

let c = a ?? b

此時 c 是什么類型屯仗?值又是多少搞坝?

普通青年這時候會開始瞎幾把猜了,文藝青年已經打開 Xcode 進行實戰(zhàn)操作魁袜,二逼青年直接去看 Swift 定義 ?? 運算符的源碼桩撮。沒錯!我就是二逼青年:

// 可選型 ?? 非可選型
public func ?? <T>(optional: T?, defaultValue: @autoclosure () throws -> T)
    rethrows -> T {
  switch optional {
  case .some(let value):
    return value
  case .none:
    return try defaultValue()
  }
}

// 可選型 ?? 可選型
public func ?? <T>(optional: T?, defaultValue: @autoclosure () throws -> T?)
    rethrows -> T? {
  switch optional {
  case .some(let value):
    return value
  case .none:
    return try defaultValue()
  }
}

上面就是 ?? 的實現(xiàn)源碼峰弹,可以在 Swift 源碼文件夾(swift->stdlib->public->core->Optional.swift)查看店量。簡簡單單的10幾行代碼中有2個關鍵:

  • 空合并運算符 ?? 有兩個函數(shù),對比發(fā)現(xiàn)區(qū)別在于自動閉包內的返回值類型如果為 T 鞠呈,整個 ?? 函數(shù)返回 T融师,如果自動閉包內的返回值類型如果為 T? ,整個 ?? 函數(shù)也返回 T?

  • ?? 函數(shù)體內使用 switch 語句對 optional 做了判斷(如前面所說蚁吝, 可選型的本質是枚舉 ):如果有值則返回這個值旱爆,如果為空則返回默認值 defaultValue

因此對于 a ?? b 可以總結如下:

  • a 是必須是可選項(見源碼函數(shù)的第一個參數(shù))
  • b 可以是可選項也可以不是(源碼的兩個函數(shù)的第二個參數(shù)自動閉包的返回值類型,一個是可選型窘茁,另一個是非可選型)
  • ab 儲存的類型必須相同(源碼唯一的泛型 T
  • 如果 a 不為 nil疼鸟,就返回 a(源碼第一個 case 語句)
  • 如果 anil,就返回 b(源碼第二個case 語句)
  • 如果 b 不是可選項庙曙,返回 a 時會自動解包(源碼兩個函數(shù) ?? 函數(shù)的返回值一個為 T,另一個為 T?
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末浩淘,一起剝皮案震驚了整個濱河市捌朴,隨后出現(xiàn)的幾起案子吴攒,更是在濱河造成了極大的恐慌,老刑警劉巖砂蔽,帶你破解...
    沈念sama閱讀 221,273評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件洼怔,死亡現(xiàn)場離奇詭異,居然都是意外死亡左驾,警方通過查閱死者的電腦和手機镣隶,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,349評論 3 398
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來诡右,“玉大人安岂,你說我怎么就攤上這事》牵” “怎么了域那?”我有些...
    開封第一講書人閱讀 167,709評論 0 360
  • 文/不壞的土叔 我叫張陵,是天一觀的道長猜煮。 經常有香客問我次员,道長,這世上最難降的妖魔是什么王带? 我笑而不...
    開封第一講書人閱讀 59,520評論 1 296
  • 正文 為了忘掉前任淑蔚,我火速辦了婚禮,結果婚禮上愕撰,老公的妹妹穿的比我還像新娘刹衫。我一直安慰自己,他們只是感情好盟戏,可當我...
    茶點故事閱讀 68,515評論 6 397
  • 文/花漫 我一把揭開白布绪妹。 她就那樣靜靜地躺著,像睡著了一般柿究。 火紅的嫁衣襯著肌膚如雪邮旷。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 52,158評論 1 308
  • 那天蝇摸,我揣著相機與錄音婶肩,去河邊找鬼。 笑死貌夕,一個胖子當著我的面吹牛律歼,可吹牛的內容都是我干的。 我是一名探鬼主播啡专,決...
    沈念sama閱讀 40,755評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼险毁,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起畔况,我...
    開封第一講書人閱讀 39,660評論 0 276
  • 序言:老撾萬榮一對情侶失蹤鲸鹦,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后跷跪,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體馋嗜,經...
    沈念sama閱讀 46,203評論 1 319
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 38,287評論 3 340
  • 正文 我和宋清朗相戀三年吵瞻,在試婚紗的時候發(fā)現(xiàn)自己被綠了葛菇。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 40,427評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡橡羞,死狀恐怖眯停,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情尉姨,我是刑警寧澤庵朝,帶...
    沈念sama閱讀 36,122評論 5 349
  • 正文 年R本政府宣布,位于F島的核電站又厉,受9級特大地震影響九府,放射性物質發(fā)生泄漏。R本人自食惡果不足惜覆致,卻給世界環(huán)境...
    茶點故事閱讀 41,801評論 3 333
  • 文/蒙蒙 一侄旬、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧煌妈,春花似錦儡羔、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,272評論 0 23
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至之宿,卻和暖如春族操,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背比被。 一陣腳步聲響...
    開封第一講書人閱讀 33,393評論 1 272
  • 我被黑心中介騙來泰國打工色难, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人等缀。 一個月前我還...
    沈念sama閱讀 48,808評論 3 376
  • 正文 我出身青樓枷莉,卻偏偏與公主長得像,于是被迫代替她去往敵國和親尺迂。 傳聞我的和親對象是個殘疾皇子笤妙,可洞房花燭夜當晚...
    茶點故事閱讀 45,440評論 2 359

推薦閱讀更多精彩內容