枚举
为一组相关的值定义了一个共同的类型,使你可以在代码中以类型安全的方式来使用这些值。在C
或者Objective-C
语言中,枚举会为一组整型值分配相关联的名称。
枚举
枚举的基本用法
Swift
中的枚举则更加灵活,并且不需给枚举中的每一个成员都提供值。如果一个值(原始值
)要被提供给每一个枚举成员,那么这个值可以是字符串
、字符
、任意的整数值
,或者是浮点类型
。
下面看个例子,Swift
中通过enum
关键字来声明一个枚举:
enum ATEnum {
case one
case two
case three
}
这个是最简单的枚举定义,再看一下Objective-C
的枚举声明:
typedef NS_ENUM(NSInteger, ATEnum) {
A,
B,
C
}
这里的A、B、C分别默认代表0、1、2,而在Swift中,原始值可以声明成不同的类型。
// String类型
enum Color: String {
case red = "Red"
case amber = "Amber"
case green = "Green"
}
// Double类型
enum LGEnum: 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
}
从上面的原始值来看也是从0
、1
、2
开始的,然后fri
是10
,sat
是11
,这个也是和Objective-C
是一样的,我们把上面的Int
类型改成String
类型,把fri
改个String
类型的值,然后输出原始值看一下。
从输出可以看到系统会自动枚举成员值输出对应的字符串,如果成员枚举被单独赋值了那就读取它的值(
fri输出hello
),我们把上面代码通过命令编译成SIL
文件,来研究一下枚举是怎么读取原始值的。
SIL文件分析
访问枚举rawValue
的代码:
enum DayOfWeek: String {
case mon, tue, wed, thu, fri = "hello", sat, sun
}
var x = DayOfWeek.mon.rawValue
通过生成sil
文件的命令
// 生成sil命令
swiftc -emit-sil main.swift > ./main.sil
编译命令生成的sil
文件,看到枚举的定义:
enum DayOfWeek : String {
case mon, tue, wed, thu, fri, sat, sun
init?(rawValue: String) // 可失败初始化器
typealias RawValue = String // 取别名
var rawValue: String { get } // get rawValue函数
}
我们找到sil
文件中main
函数代码如下:
// main
sil @main : $@convention(c) (Int32, UnsafeMutablePointer<Optional<UnsafeMutablePointer<Int8>>>) -> Int32 {
bb0(%0 : $Int32, %1 : $UnsafeMutablePointer<Optional<UnsafeMutablePointer<Int8>>>):
alloc_global @$s4main1xSSvp // id: %2
%3 = global_addr @$s4main1xSSvp : $*String // user: %8
%4 = metatype $@thin DayOfWeek.Type
%5 = enum $DayOfWeek, #DayOfWeek.mon!enumelt // user: %7
// function_ref DayOfWeek.rawValue.getter (rawValue的getter方法)
%6 = function_ref @$s4main9DayOfWeekO8rawValueSSvg : $@convention(method) (DayOfWeek) -> @owned String // user: %7
%7 = apply %6(%5) : $@convention(method) (DayOfWeek) -> @owned String // user: %8
store %7 to %3 : $*String // id: %8
%9 = integer_literal $Builtin.Int32, 0 // user: %10
%10 = struct $Int32 (%9 : $Builtin.Int32) // user: %11
return %10 : $Int32 // id: %11
} // end sil function 'main'
在rawValue
的getter
方法里,通过获取mon
的枚举值传入到getter
方法(也就是上面的s4main9DayOfWeekO8rawValueSSvg
),搜索s4main9DayOfWeekO8rawValueSSvg
函数看一下它的定义:
self
就是DayOfWeek
%0
就是传进来的枚举成员值
- 通过模式匹配走到对应的分支(
bb1、bb2...
)- 不同的代码分支里获取到对应的字符串给到不同分支的返回值 而上面的字符串其实就是从
Mach-O
文件的Section64(__TEXT,__cstring)
里读取的。
枚举值&原始值
还是上面的代码,分别打印枚举值和原始值,看日志的输出:
结果发现输出的都是
mon
,打印的结果是一样的,但实际的类型是不一致的。我们不能把一个枚举值
赋值给一个String
类型的变量,也不能把一个String
类型的值赋值给一个枚举变量
。
从上面的
sil
文件在枚举定义中有个可失败初始化器
,我们在代码中加个初始化器的符号断点。
然后运行,发现这里可失败初始化方法并没有调用。那可失败初始化器什么时候才会调用?当我们给定一个原始值希望得到它的枚举值,可以通过下面方法:
DayOfWeek.init(rawValue: "mon")
为什么是可失败初始化器?我们在初始化过程中可能赋值一个不存在的枚举字符串,这样返回就是nil
了。
这种可以通过
String
值获取它的枚举值,一般的使用场景可以在模式匹配通过switch case
来做相应的操作。
关联值
有时候我们想通过枚举值来表达更复杂的案例,可以定义Swift
枚举来存储任意类型的关联值,每个枚举成员的关联值类型可以各不相同。通过下面的案例说明一下:
// 通过给定关联值来表示具体的图形
enum Shape {
case circle(radius: Double)
case rectangle(width: Double, height: Double)
}
上面的案例就是通过给定具体关联值来表示不同的形状,当给定关联值后对于枚举成员变量来说就没有原始值了。对于关联值来说可以让枚举成员携带更复杂的信息从而表达更复杂的案例。
关联值的使用
通过关联值定义的枚举,使用也很简单。
// 定义一个半径为10的圆形
var circle = Shape.circle(radius: 10.0)
// 定义一个宽高为10、20的矩形
var rectangle = Shape.rectangle(width: 10.0, height: 20.0)
关联值注意事项
模式匹配通过switch
关键字去匹配当前的枚举值,从而获取对应的分支。
enum Weak: String {
case MONDAY
case TUEDAY
case WEDDAY
case THUDAY
case FRIDAY
case SATDAY
case SUNDAY
}
let currentWeak: Weak = Weak.MONDAY
// 通过switch匹配具体的分支
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 .SATDAY:
print(Weak.SATDAY.rawValue)
case .SUNDAY:
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: Double, height: Double)
}
let shape = Shape.circle(radius: 10.0)
switch shape {
case let .circle(radius):
print("Circle radius: \(radius)")
case let .rectangle(width, height):
print("rectangle width:\(width),height\(height)")
}
switch
语句中可以从上面通过关联值拿到radius
的值然后打印出来。case let
就是模式匹配的写法,除了这种写法还可以把let
放在参数里面,相当于声明了一个临时变量去接收radius
的值,代码如下:
switch shape {
case .circle(let radius):
print("Circle radius: \(radius)")
case .rectangle(let width, let height):
print("rectangle width:\(width), height\(height)")
}
枚举的大小
接下来讨论一下枚举占用的内存大小,这里我们区分几种不同的情况,首先第一种就是No-payload enums
。
No-payload enums
没有负载的enum
,也就是只有隐式值没有关联值。下面通过一个案例说输出一下它的大小。
通过
MemoryLayout
输出枚举的大小,这里可以看到输出1字节
。计算枚举的大小其实就是计算枚举值的大小,枚举值默认以UInt8
来存储的,UInt8
就是1字节
。
下面通过断点运行看看在内存中枚举的存储信息。定义3个变量,把对应的枚举值赋值后,通过断点一步步打印。
当运行到给
a
赋值后,通过API输出a
的地址,memory read
读取出来的是0
,然后再过一个断点,同样看看b
的信息。
可以看到这里
b
输出的是1
,再跳到下一个断点,查看c
的信息。
这里看到
c
输出的是2
。因此在默认没有关联值的枚举成员来说,它是以UInt8
的方式来存取枚举值的。UInt8
最多能表示256
个case
,当case
超过了256
个后,就自动提升为UInt16
,然后UInt32
以此类推。
Single-payload enums
Single-payload enums
的内存布局,字面的意思就是有一个负载的enum
,比如下面的例子:
enum ATEnum {
case test_one(Bool)
case test_two
case test_three
case test_four
}
当前的案例enum
的内存大小是多少呢?我们在项目中运行看一下。
可以看到输出
1字节
,我们把上面的Bool
改成Int
,再次运行:
发现输出的是
9
,单个负载枚举的大小系统并不是把关联值加上枚举值的1字节
,这种类型枚举大小取决于关联类型是否有额外的空间来记录枚举的其他case
值。
什么意思呢?还是用上面的Bool
类型说明,Bool
类占用1字节
(8位),8位
中只使用了1位
标识,剩下的7位
未使用就可以提供给其他的case
。
enum ATEnum {
case test_one(Int)
case test_two
case test_three
case test_four
}
而对于Int
类型来说实际占用8字节
,下面其他的case
需要另外申请空间,增加1字节
,因此枚举大小占用8字节 + 1字节 = 9字节
。
Mutil-payload enums
上面说完了Single-payload enums
, 接下来我们说第三种情况Mutil-payload enums
, 有多个负载的情况产生时,当前的enum
是如何进行布局的?
// 这里定义了2个Bool类型
enum ATEnum {
case test_one(Bool)
case test_two(Bool)
case test_three
case test_four
}
结合多个负载的定义,在项目中运行一下看看输出。
结果输出还是
1字节
。还是和之前分析的一样,这里的1字节
的Bool
类型已经可以存放其他的case
。
再把上面的Bool
类型改成Int
,再看一下结果输出。
发现这里输出的是
9
,而不是8 + 8 + 1 = 17
字节,因为枚举的case
中只会计算一个相同的关联值大小,所以在同一个枚举类型中,如果多个相同关联值的枚举只会计算1个
。因此就是8 + 1 = 9
字节。
以上就是关于枚举不同payload
情况大小的案例分析。
递归枚举
递归枚举
是一种枚举类型,它有一个或多个枚举成员使用该枚举类型的实例作为关联值。使用递归枚举时,编译器会插入一个间接层。你可以在定义枚举前加上indirect
关键字来表示该成员可递归。
// 示例代码,二叉树
indirect enum BinaryTree<T> {
case empty
case node(left: BinaryTree, value: T, right: BinaryTree)
}
枚举类型是值类型,在编译的时候大小已经确定,但上面的二叉树
示例无法确定它的大小,因此需要用indirect
关键字来修饰,告诉编译器需要在堆空间上申请内存。结合实际例子来看一下:
这里定义了个
BinaryTree<Int>
类型的node
,通过格式化输出看内存地址很像个示例对象。再通过汇编的方式运行:
可以看到调用了
swift_allocObject
,也就知道确实是生成了实例对象,内存分配到堆空间上。
上面的indirect
关键字放在了枚举的外面,当然也可以放在case
的前面,两者有什么区别呢?
enum BinaryTree<T> {
case empty
indirect case node(left: BinaryTree, value: T, right: BinaryTree)
}
如果是放在枚举的外面,就相当于整个枚举大小都是引用类型存放在堆空间上,如果是放在case
前面,就只有当前的case
会以引用类型存放在堆空间上,接下来还是通过案例来证明一下。
这里定义
empty
变量读取二叉树的empty
节点,通过打印可以看到输出的是0x0
,所以它还是值类型,由栈分配空间。
Optional
认识可选值
之前我们在写代码的过程中早就接触过可选值,比如我们在代码这样定义:
class ATTeacher {
var age: Int?
}
当前的age
我们就称之为可选值,当然可选值的写法下面这两者是等同的
var age: Int? = var age: Optional<Int>
那对于Optional
的本质是什么?我们直接跳转到源码,打开Optional.swift
文件
@frozen
public enum Optional<Wrapped>: ExpressibleByNilLiteral {
case none
case some(Wrapped)
}
既然Optional
的本质是枚举,那么我们也可以仿照系统的实现制作一个自己的Optional
enum MyOptional<Value> {
case some(Value)
case none
}
比如给定任意一个自然数,如果当前自然数是偶数返回,否则为nil
,我们应该怎么表达这个案例
func getOddValue(_ value: Int) -> MyOptional<Int> {
if value % 2 == 0 {
return .some(value)
} else {
return .none
}
}
这个时候给定一个数组,我们想删除数组中所有的偶数
这个时候编译器就会检查我们当前的
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
来简化我们的代码,看一下下面的案例:
if let
方式guard let
方式
guard let
和if let
刚好相反,guard 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
同样的可选链对于下标和函数调用也适用
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 + (firstVector: Vector, secondVector: Vector) -> Vector {
return Vector(x: firstVector.x + secondVector.x, y: firstVector.y + secondVector.y)
}
static prefix func - (vector: Vector) -> Vector {
return Vector(x: -vector.x, y: -vector.y)
}
static func - (firstVector: Vector, secondVector: Vector) -> Vector {
return firstVector + -secondVector
}
}
根据上面的重载,实际定义2个Vector
来运行一下。
关于运算符重载可以参考官方说明文档,如果重载常用运算符可以直接重定义,如果定义自己的的运算符,需要按照文档的要求进行重载。
自定义运算符
运算符分为infix
(中缀)、prefix
(前缀)和postfix
(后缀)运算符,这里我们就定义个前缀运算符
infix operator operator
再指定运算符的优先级
// 优先级组声明有以下形式
precedencegroup precedence group name {
higherThan: lower group names
lowerThan: higher group names
associativity: associativity
assignment: assignment
}
根据声明我们定义自己的运算符号,代码如下:
// 自定义乘法
infix operator &*: AdditionPrecedence
precedencegroup AtomPrecedence {
lowerThan: AdditionPrecedence // 低于AdditionPrecedence优先级
associativity: left // 左结合
}
然后在定义一下&*
运算符的方法。
extension Vector {
static func &* (firstVector: Vector, secondVector: Vector) -> Vector {
return Vector(x: firstVector.x * secondVector.x, y: firstVector.y * secondVector.y)
}
}
再运行输出一下。
可以看到输出自定义运算符的结果。在实际开发中可以根据自己的需求定义相关的运算符。
隐式解析可选类型
隐式解析可选类型是可选类型的一种,使用的过程中和非可选类型无异。它们之间唯一的区别是,隐式解析可选类型是告诉Swift
编译器在运行访问时值不会为nil
。
在日常开发中比较常⻅这种隐士解析可选类型
IBOutlet
类型是Xcode
强制为可选类型的,因为它不是在初始化时赋值的,而是在加载视图的时候。你可以把它设置为普通可选类型,但是如果这个视图加载正确,它是不会为空的。