Swift 中的枚举及可选型

枚举可选型Swift 中两个很重要的概念,前者与 Objective-C 中的概念大不相同,后者完全不存在,因此需要详细介绍下这两个概念。

为什么将可选型枚举放在一起呢?因为可选型 Optional 本质就是枚举 Emun,接下来详细介绍。

枚举(Enum)

Enum 基本语法

Swift 中枚举的语法非常简洁:

// 定义一个表示季节的枚举
enum Season {
    case spring    // 春
    case summer    // 夏
    case autumn    // 秋
    case winter    // 冬
}

// 创建一个 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 协议,即可获得一个 allCases 属性,用于表示一个包含枚举所有成员的集合:

// 枚举成员的遍历
enum Season: CaseIterable {
    case spring, summer, autumn, winter
}

// 通过 allCases 属性获得枚举成员的数量
let seasonNumber = Season.allCases.count

// 通过 allCases 属性在 for 循环中遍历枚举成员
for s in Season.allCases {
    print("Current season is \(s)")
}

关联值(Associated Values)

// 定义一个日期枚举,有两种 case : 1.数字表示的日期 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")

需要注意关联值是存储在枚举变量的内存里 ,比如上面的 2019828"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 或者 IntSwift 编译器会隐式(自动)添加原始值:

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)

递归枚举是一种特殊的枚举类型,它有一个或多个枚举成员使用该枚举类型的实例作为关联值。使用递归枚举时,编译器会插入一个间接层。你可以在枚举成员前加上 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 的关联值为泛型 < Wrapped >,即 Optional 内存储的数据类型为 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

另外需要注意:任何类型(如:IntDoubleStruct 等)的可选状态都可以被设置为 nil,不只是对象类型!

可选型的概念有点类似薛定谔的猫,可以将其想象成一个盒子,其中可能装着某个具体类型的数据,也有可能什么都没有。那如何才能打开盒子呢?

强制解包(Forced Unwrapping)

Swift 提供如下语法可以强制打开一个可选类型:

// 定一个可选 Int 类型的变量,表示网络请求的返回状态码
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 提供了一些更优雅的语法。

可选绑定(Optional Binding)

使用可选绑定来判断可选类型是否包含值,如果包含就把值赋给一个临时常量或者变量。

可选绑定可以用在 ifwhile 语句中,这条语句不仅可以用来判断可选类型中是否有值,同时可以将可选类型中的值赋给一个常量或者变量:

// 创建 unwrappedResponseCode 常量用来接受 responseCode 解包成功后结果
if let unwrappedResponseCode = responseCode {
    print("responseCode's value is \(unwrappedResponseCode)")
} else {
    // 解包失败(nil)会进入 else 分支
    print("responseCode has no value!")
}

这段代码可以理解为:

  • 如果 responseCode 返回的可选 Int 包含一个值,则创建一个叫做 unwrappedResponseCode 的新常量并将可选包含的值赋给它。

  • 如果解包成功,unwrappedResponseCode 常量可以在 if 语句的第一个分支中使用。它已经被可选类型包含的值初始化过,所以不需要再使用 ! 后缀来获取它的值。

  • 可以在可选绑定中使用常量变量。如果想在 if 语句的第一个分支中操作 unwrappedResponseCode 的值,可以改成

    if var unwrappedResponseCode...
    

凭空冒出一个新的变量(or 常量)名unwrappedResponseCode 显得有些奇怪,所以通常都让创建出的新名称与可选型的名称保持一致:

// 等号左右的名称一样
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 语句

在我们平时写代码的时候经常会遇到类型下面的代码:

/// 定义一个计算形状边数的函数
///
/// - Parameter shape: 形状字符串
/// - Returns: 形状的变数(由于传入的 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.")
}

有没有发现使用 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,不可能出现 nil 的情况,这时候再去担心就显得很多余。对此 Swift 推出了隐式解包的概念:

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

需要注意:number 变量依然是一个可选 Int,只是在将它赋值给 otherNumber 时会 自动强制解包 !(因为你已经确保 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 进行实战操作,二逼青年直接去看 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()
  }
}

上面就是 ?? 的实现源码,可以在 Swift 源码文件夹(swift->stdlib->public->core->Optional.swift)查看。简简单单的10几行代码中有2个关键:

  • 空合并运算符 ?? 有两个函数,对比发现区别在于自动闭包内的返回值类型如果为 T ,整个 ?? 函数返回 T,如果自动闭包内的返回值类型如果为 T? ,整个 ?? 函数也返回 T?

  • ?? 函数体内使用 switch 语句对 optional 做了判断(如前面所说, 可选型的本质是枚举 ):如果有值则返回这个值,如果为空则返回默认值 defaultValue

因此对于 a ?? b 可以总结如下:

  • a 是必须是可选项(见源码函数的第一个参数)
  • b 可以是可选项也可以不是(源码的两个函数的第二个参数自动闭包的返回值类型,一个是可选型,另一个是非可选型)
  • ab 储存的类型必须相同(源码唯一的泛型 T
  • 如果 a 不为 nil,就返回 a(源码第一个 case 语句)
  • 如果 anil,就返回 b(源码第二个case 语句)
  • 如果 b 不是可选项,返回 a 时会自动解包(源码两个函数 ?? 函数的返回值一个为 T,另一个为 T?
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,324评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,303评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,192评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,555评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,569评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,566评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,927评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,583评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,827评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,590评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,669评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,365评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,941评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,928评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,159评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,880评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,399评论 2 342

推荐阅读更多精彩内容