Swift 中 Enum 及 Optional 介绍
Enum
枚举的基本用法
swift
中通过 enum
关键字来声明一个枚举
enum CXEnum {
case test_one
case test_two
case test_three
}
而在 C
或者 OC
中默认受整数支持,也就意味着下面的例子中: A
、B
、C
分别代表 0、 1、2。
typedef NS_ENUM(NSUInteger, CXEnum) {
A,
B,
C,
};
Swift
中的枚举则更加灵活,并且不需给枚举中的每一个成员都提供值。如果一个值(所谓“原 始”值)要被提供给每一个枚举成员,那么这个值可以是字符串、字符、任意的整数值,或者是浮点类型。
enum Color : String {
case red = "Red"
case amber = "Amber"
case green = "Green"
}
enum CXEnum: Double {
case a = 10.0
case b= 20.0
case c = 30.0
case d = 40.0
}
这里其实是因为编译器做了很多的操作,第一个就是隐士 RawValue
分配,是建立在 Swift
的类型推断机制上的。
enum DayOfWeek: Int {
case mon, tue, wed, thu, fri = 10, sat, sun
}
这里从 mon
开始对应的值是从 0 开始的,但是也可以自己指定枚举的值,当指定 fri = 10
之后,从 fri
开始 sat
、sun
就分别对应 11、12。
对于 DayOfWeek
,也可以指定为 String
类型,也可以指定枚举的值,如下所示:
enum DayOfWeek: String {
case mon, tue, wed, thu, fri = "Hello world", sat, sun
}
这里输出枚举值的 RawValue
,当没指定枚举成员的原始值的时候 RawValue
的值跟枚举成员值的字符串是一样的, 这是编译器默认分配的。
关联值
enum Shape {
case circle(radius: Double)
case rectangle(width: Int, height: Int)
}
var circle = Shape.circle(radius: 15.0)
var rectangle = Shape.rectangle(width: 15, height: 7)
如果我们想用枚举类型来表示更复杂的类型,例如圆形跟矩形,就可以如上,通过关联值的形式进行实现,circle
代表圆形,可以为它关联一个 Double
类型的 radius
来代表半径。rectangle
代表矩形,可以为它关联 Int
类型的 width
、height
来代表宽高。
模式匹配
enum Weak: String {
case MONDAY
case TUEDAY
case WEDDAY
case THUDAY
case FRIDAY
case SATDAY
case SUNDAY
}
let currentWeak: Weak = Weak.MONDAY
switch currentWeak {
case .MONDAY: print(Weak.MONDAY.rawValue)
case .TUEDAY: print(Weak.TUEDAY.rawValue)
case .WEDDAY: print(Weak.WEDDAY.rawValue)
case .THUDAY: print(Weak.THUDAY.rawValue)
case .FRIDAY: print(Weak.FRIDAY.rawValue)
case .SUNDAY: print(Weak.SUNDAY.rawValue)
case .SATDAY: print(Weak.SUNDAY.rawValue)
}
如上代码所示,是穷尽了所有的枚举成员值,如果不想匹配所有的 case
,可以使用 defalut
关键字。
switch currentWeak{
case .SATDAY, .SUNDAY: print("Happy Day")
default : print("SAD DAY")
}
如果我们要匹配关联值的话,示例代码如下:
enum Shape {
case circle(radius: Double)
case rectangle(width: Int, height: Int)
}
var shape = Shape.circle(radius: 15.0)
switch shape {
case let .circle(radious):
print("Circle radious:\(radious)")
case let .rectangle(width, height):
print("rectangle width:\(width),height\(height)")
}
当然还可以这么写:
switch shape{
case .circle(let radious):
print("Circle radious:\(radious)")
case .rectangle(let width, var height):
height += 10
print("rectangle width:\(width),height\(height)")
}
跟类跟结构体一样,枚举中也可以添加异变方法(mutaing
)、属性、扩展(extension
),也可以遵循协议。枚举是值类型,存储在栈区。
枚举的大小
No-payload enums
接下来我们来讨论一下枚举占用的内存大小,这里我们区分几种不同的情况,首先第一种就是
No-payload enums
,没有关联值。
enum Weak: String {
case MONDAY
case TUEDAY
case WEDDAY
case THUDAY
case FRIDAY
case SATDAY
case SUNDAY
}
大家可以看到这种枚举类型类似我们在 C
语言中的枚举,当前类型默认是 Int
类型,那么对于这一类的枚举在内存中是如何布局?以及在内存中占用的大小是多少那?这里我们就可以直接使用 MemoryLayout
来测量一下当前枚举内存大小。
可以看到这里我们测试出来的不管是 size
还是 stride
都是 1 ,这个地方我们也很好理解,当前的 enum
有几个 case
? 是不是 8 个,在 Swift
中进行枚举布局的时候一直是尝试使用最少的空间来存储 enum
,对于当前的 case
数量来说, UInt8
能够表示 256 个 case
,也就意味着如果一个默认枚举类型且没有关联值的 case
少于 256 ,当前枚举类型的大小都是 1 字节,所以可以由 UInt8
的方式来存储当前枚举的值。当 case
大于 256 的时候存储方式就会由 UInt8
升级为 UInt16
。
通过上面的打印我们可以直观的看到,当前变量 a
、 b
、 c
这三个变量存储的内容分别是 00
、01
、02
,分别相差 1 个字节, 这和我们上面说的布局理解是一致的。
Single-payload enums
No-payload enums
的布局比较简单,我们也比较好理解,接下来我们来理解一下 Single-payload enums
的内存布局, 字面意思就是有一个成员负载的 enum
,比如下面这个例子:
这个时候可以看到枚举的大小是 1 字节,下面我们将关联值改为 Int
类型再来看一下:
这里我们可以看到,同样是一个负载,只是因为关联值类型不同,就会造成内存的大小不同。 这是因为在 Swift
中的 enum
中的 Single-payload enums
会使用负载类型中的额外空间来记录没有负载的 case
值。这句话该怎么理解?首先 Bool
类型是 1 字节,也就是 UInt8
,所以当前能表达 256 个 case
的情况,对于布尔类型来说,只需要使用低位的 0, 1 这两种情况,其他 7 位剩余的空间就可以用来表示没有负载的 case
值。对于 Int
类型的负载来说,其实系统是没有办法推算当前的负载所要使用的位数,Int
类型就是 8 字节,也就意味着当前 Int
类型的负载是没有额外的剩余空间的,这个时候我们就需要额外开辟内存空间来去存储我们的 case
值,也就是 8+1 =9
字节。
Mutil-payload enums
上面说完了 Single-payload enums
, 接下来我们说第三种情况 Mutil-payload enums
, 有多个负载的情况产生时,当前的 enum
是如何进行布局的呢?
这里我们可以看到当前内存存储的分别是 00, 01, 40, 41, 80,81 , 这里在存储当前的 case
时候会使用到 common spare bits
,什么意思?其实在上一个案例我们也讲过了,首先 bool
类型需要 1 字节,也就是 8 位。
对于 bool
类型来说,我们存储的无非就是 0 或 1 ,只需要用到 1 位,所以剩余的 7 位这里我们都统称为 common spare bits
,对于当前的 case
数量来说我们完全可以把所有的情况放到 common spare bits
中,所以这里我们只需要 1 字节就可以存储所有的内容了。
接下来我们来看一下 00、01、40、41、80、81
分别代表的是什么?首先 0、4、8
这里我们叫做 tag value
, 0, 1
这里我们就叫做 tag index
,至于这个 tag value
怎么来的, 目前在源码中还没有找到验证,如果大家感兴趣的的话也可以阅读一下源码中的 Enum.cpp
和 GenEnum.cpp
这两个文件,找到了验证大家可以一起交流。
当前一般来说,我们有多个负载的枚举时,当前枚举类型的大小取决于当前最大关联值的大小。
我们来看一个例子
enum CXEnum{
case test_one(Bool)
case test_two(Int)
case test_three
case test_four
}
当前 CXEnum
的大小就等于 sizeof(Int) + sizeof(rawVlaue) = 9
,在比如下面这个例子:
enum CXEnum{
case test_one(Bool)
case test_two(Int, Int, Int)
case test_three
case test_four
}
当前大小就是 sizeof(Int) * 3 + sizeof(rawVlaue) = 25
。
最后这里有我们再来看下下面的案例:
enum CXEnum{
case test_one
}
对于当前的 CXEnum
只有一个 case
,我们不需要用任何东⻄来去区分当前的 case
, 所以当我们打印当前的 CXEnum
,可以发现大小是 0。
indirect 关键字
indirect enum BinaryTree<T> {
case empty
case node(left: BinaryTree, value: T, right: BinaryTree)
}
如上所示,我们也可以用枚举来表示树中的一个节点,不过需要用到 indirect
关键字,indirect
的意思是表示当前枚举是引用类型,分配在堆空间。
enum BinaryTree<T> {
case empty
indirect case node(left: BinaryTree, value: T, right: BinaryTree)
}
我们也可以修改 indirect
关键字的位置,当 indirect
关键字放在 case
的前面的时候,表示只有是 node
这种 case
的时候枚举才会是引用类型,为 empty
的时候依然是值类型。
Optional
认识可选值
之前我们在写代码的过程中早就接触过可选值,比如我们在代码这样定义:
class LGTeacher{
var age: Int?
}
当前的 age 我们就称之为可选值,当然可选值的写法这两者是等同的
var age: Int? = var age: Optional<Int>
那对于 Optional
的本质是什么?我们直接跳转到源码,打开 Optional.swift
文件,可以看到源码中 Optional
的本质是枚举。
@frozen
public enum Optional<Wrapped>: ExpressibleByNilLiteral {
case none
case some(Wrapped)
}
既然 Optional
的本质是枚举,那么我们也可以仿照系统的实现制作一个自己的 Optional
enum MyOptional<Value> {
case some(Value)
case none
}
比如给定任意一个自然数,如果当前自然数是偶数返回,否则为 nil
,我们应该怎么表达这个案 例
enum MyOptional<Value> {
case some(Value)
case none
}
func getOddValue(_ value: Int) -> MyOptional<Int> {
if value % 2 == 0 {
return .some(value) }
else{
return .none
}
}
这个时候给定一个数组,我们想删除数组中所有的偶数
var array = [1, 2, 3, 4, 5, 6]
for element in array {
let value = getOddValue(element)
array.remove(at: array.firstIndex(of: value))
}
这个时候编译器就会检查我们当前的 value
会发现他的类型和系统编译器期望的类型不符,这个时候我们就能使用 MyOptional
来限制语法的安全性。
于此同时我们通过 enum
的模式匹配来取出对应的值
for element in array {
let value = getOddValue(element)
switch value {
case .some(let value):
array.remove(at: array.firstIndex(of: value)!)
case .none:
print("vlaue not exist") }
}
如果我们把上述的返回值更换一下,其实就和系统的 Optional
使用无异
func getOddValue(_ value: Int) -> Int? {
if value % 2 == 0 {
return .some(value)
}else{
return .none
}
}
这样我们其实是利用当前编译器的类型检查来达到语法书写层面的安全性。
当然如果每一个可选值都用模式匹配的方式来获取值在代码书写上就比较繁琐,我们还可以使 用 if let
的方式来进行可选值绑定
if let value = value{
array.remove(at: array.firstIndex(of: value)!)
}
除了使用 if let
来处理可选值之外,我们还可以使用 gurad let
来简化我们的代码,我们来看一个具体的案例
gurad let
和 if let
刚好相反,守护一定有值。如果没有,直接返回。通常判断是否有值之后,会做具体的逻辑实现,通常代码多,如果用 if let
凭空多了一层分支,guard let
是降低分支层次的办法。
可选链
我们都知道在 OC
中我们给一个 nil
对象发送消息什么也不会发生,Swift
中我们是没有办法向一个 nil
对象直接发送消息,但是借助可选链可以达到类似的效果。我们看下面两段代码
let str: String? = "abc"
let upperStr = str?.uppercased() // Optional<"ABC">
var str: String?
let upperStr = str?.uppercased() // nil
我们再来看下面这段代码输出什么
let str: String? = "kody"
let upperStr = str?.uppercased().lowercased()
同样的可选链对于下标和函数调用也适用
var closure: ((Int) -> ())? closure?(1) // closure 为 nil 不执行
let dict = ["one": 1, "two": 2] dict?["one"] // Optional(1) dict?["three"] // nil
?? 运算符 (空合并运算符)
( a ?? b )
将对可选类型 a
进行空判断,如果 a
包含一个值就进行解包,否则就返回 一个默认值 b
。
- 表达式
a
必须是Optional
类型 - 默认值
b
的类型必须要和a
存储值的类型保持一致
运算符重载
在源码中我们可以看到除了重载了 ??
运算符,Optional
类型还重载了 == , ?=
等运算符,实际开发中我们可以通过重载运算符简化我们的表达式。
比如在开发中我们定义了一个二维向量,这个时候我们想对两个向量进行基本的操作,那么我们就可以通过重载运算符来达到我们的目的
struct Vector {
let x: Int
let y: Int
}
extension Vector {
static func + (fistVector: Vector, secondVector: Vector) -> Vector {
return Vector(x: fistVector.x + secondVector.x, y: fistVector.y + seco
}
static prefix func - (vector: Vector) -> Vector {
return Vector(x: -vector.x, y: -vector.y)
}
static func - (fistVector: Vector, secondVector: Vector) -> Vector {
return fistVector + -secondVector
}
}
通过以上代码我们可以对 Vector
进行相加、相减、或者取负。
更多关于自定义运算符的规则也可以看下官方文档 自定义运算符。
隐士解析可选类型
隐式解析可选类型是可选类型的一种,使用的过程中和非可选类型无异。它们之间唯一 的区别是,隐式解析可选类型是你告诉对 Swift
编译器,我在运行时访问时,值不会为 nil
。
var age: Int?
var age1: Int!
age = nil
age1 = nil
其实日常开发中我们比较常⻅这种隐士解析可选类型
IBOutlet
类型是 Xcode
强制为可选类型的,因为它不是在初始化时赋值的,而是在加载视图的时候。你可以把它设置为普通可选类型,但是如果这个视图加载正确,它是不会为空的。
与可选值有关的高阶函数
-
map
: 这个方法接受一个闭包,如果可选值有内容则调用这个闭包进行转换
var dict = ["one": "1", "two": "2"]
let result = dict["one"].map{ Int($0) } // Optional(Optional(1))
上面的代码中我们从字典中取出字符串 "1"
,并将其转换为 Int
类型,但因为 String
转换成 Int
不一定能成功,所以返回的是 Int?
类型,而且字典通过键不一定能取得到值,所以 map
返回的也是一个 Optional
,所以最后上述代码 result
的类型为 Int??
类型。
那么如何把我们的双重可选展平开来,这个时候我们就需要使用到 flatMap
。
-
flatMap
: 可以把结果展平成为单个可选值
var dict = ["one": "1", "two": "2"]
let result = dict["one"].flatMap{ Int($0) } // Optional(1)
-
注意,这个方法是作用在
Optioanl
上的 -
作用在
Sequence
上的flatMap
方法在Swift4.1
中被更名为compactMap
,该方法可以将序列中的nil
过滤出去。
let array = ["1", "2", "3", nil]
let result = array.compactMap{ $0 } // ["1", "2", "3"]
let array = ["1", "2", "3", "four"]
let result = array.compactMap{ Int($0) } // [1, 2, 3]