枚举及可选型是 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")
需要注意: 关联值是存储在枚举变量的内存里 ,比如上面的 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)
递归枚举是一种特殊的枚举类型,它有一个或多个枚举成员使用该枚举类型的实例作为关联值。使用递归枚举时,编译器会插入一个间接层。你可以在枚举成员前加上 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 类型,且有两个 case :none 、 some
其中 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 。
另外需要注意:任何类型(如:Int、Double、Struct 等)的可选状态都可以被设置为 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)
使用可选绑定来判断可选类型是否包含值,如果包含就把值赋给一个临时常量或者变量。
可选绑定可以用在 if 和 while 语句中,这条语句不仅可以用来判断可选类型中是否有值,同时可以将可选类型中的值赋给一个常量或者变量:
// 创建 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
有值,则返回自动解包的 optionalInt 值 10,如果为空则返回 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 可以是可选项也可以不是(源码的两个函数的第二个参数自动闭包的返回值类型,一个是可选型,另一个是非可选型)
- a 和 b 储存的类型必须相同(源码唯一的泛型 T )
- 如果 a 不为 nil,就返回 a(源码第一个 case 语句)
- 如果 a 为 nil,就返回 b(源码第二个case 语句)
- 如果 b 不是可选项,返回 a 时会自动解包(源码两个函数 ?? 函数的返回值一个为 T,另一个为 T?)