Swift探索(五): Enum & Optional
一:枚举(Enum)
1. 枚举的基本用法
Swift
中通过 enum
关键字来声明一个枚举
enum LJLEnum {
case test_one
case test_two
case test_three
}
Swift
中的枚举则更加灵活,并且不需给枚举中的每一个成员都提供值。如果一个值(所谓“原始”值)要被提供给每一个枚举成员,那么这个值可以是字符串、字符、任意的整数值,或者是浮点类型。
enum Color: String {
case red = "Red"
case blue = "Blue"
case green = "Grren"
}
enum LJLEnum: Double {
case a = 10.0
case b = 22.2
case c = 33.3
case d = 44.4
}
2. 原始值 RawValue
隐士 RawValue
是建立在 Swift
的类型判断机制上的
enum LJLEnum: Int {
case one, two, three = 10, four, five
}
print(LJLEnum.one.rawValue)
print(LJLEnum.two.rawValue)
print(LJLEnum.three.rawValue)
print(LJLEnum.four.rawValue)
print(LJLEnum.five.rawValue)
// 打印结果
0
1
10
11
12
可以看到 RawValue
原始值跟 OC
一样都是从 0
、 1
、2
开始,当指定值时,后面的枚举值的 RawValue
会在当前值的基础上进行累加操作,因此 four
和 five
的值为 11
和 12
。
将枚举类型改成 String
类型
enum LJLEnum: String {
case one, two, three = "Hello World", four, five
}
print(LJLEnum.one.rawValue)
print(LJLEnum.two.rawValue)
print(LJLEnum.three.rawValue)
print(LJLEnum.four.rawValue)
print(LJLEnum.five.rawValue)
// 打印结果
one
two
Hello World
four
five
可以看出系统已经默认给了每一个枚举成员分配了一个字符串,并且该字符串与枚举成员值的字符串一致。
将上述代码简化一下,通过 LLDB
命令 swiftc xxx.swift -emit-sil
编译成 sil
文件
enum LJLEnum: String {
case one, two = "Hello World", three
}
var x = LJLEnum.one.rawValue
sil
文件代码
// 枚举的声明
enum LJLEnum : String {
case one, two, three
init?(rawValue: String)
typealias RawValue = String
var rawValue: String { get }
}
// LJLEnum.rawValue.getter
sil hidden @$s4main7LJLEnumO8rawValueSSvg : $@convention(method) (LJLEnum) -> @owned String {
// %0 "self" // users: %2, %1
bb0(%0 : $LJLEnum):
debug_value %0 : $LJLEnum, let, name "self", argno 1 // id: %1
switch_enum %0 : $LJLEnum, case #LJLEnum.one!enumelt: bb1, case #LJLEnum.two!enumelt: bb2, case #LJLEnum.three!enumelt: bb3 // id: %2
bb1: // Preds: bb0
%3 = string_literal utf8 "one" // user: %8
%4 = integer_literal $Builtin.Word, 3 // user: %8
%5 = integer_literal $Builtin.Int1, -1 // user: %8
%6 = metatype $@thin String.Type // user: %8
// function_ref String.init(_builtinStringLiteral:utf8CodeUnitCount:isASCII:)
%7 = function_ref @$sSS21_builtinStringLiteral17utf8CodeUnitCount7isASCIISSBp_BwBi1_tcfC : $@convention(method) (Builtin.RawPointer, Builtin.Word, Builtin.Int1, @thin String.Type) -> @owned String // user: %8
%8 = apply %7(%3, %4, %5, %6) : $@convention(method) (Builtin.RawPointer, Builtin.Word, Builtin.Int1, @thin String.Type) -> @owned String // user: %9
br bb4(%8 : $String) // id: %9
bb2: // Preds: bb0
%10 = string_literal utf8 "Hello World" // user: %15
%11 = integer_literal $Builtin.Word, 11 // user: %15
%12 = integer_literal $Builtin.Int1, -1 // user: %15
%13 = metatype $@thin String.Type // user: %15
// function_ref String.init(_builtinStringLiteral:utf8CodeUnitCount:isASCII:)
%14 = function_ref @$sSS21_builtinStringLiteral17utf8CodeUnitCount7isASCIISSBp_BwBi1_tcfC : $@convention(method) (Builtin.RawPointer, Builtin.Word, Builtin.Int1, @thin String.Type) -> @owned String // user: %15
%15 = apply %14(%10, %11, %12, %13) : $@convention(method) (Builtin.RawPointer, Builtin.Word, Builtin.Int1, @thin String.Type) -> @owned String // user: %16
br bb4(%15 : $String) // id: %16
bb3: // Preds: bb0
%17 = string_literal utf8 "three" // user: %22
%18 = integer_literal $Builtin.Word, 5 // user: %22
%19 = integer_literal $Builtin.Int1, -1 // user: %22
%20 = metatype $@thin String.Type // user: %22
// function_ref String.init(_builtinStringLiteral:utf8CodeUnitCount:isASCII:)
%21 = function_ref @$sSS21_builtinStringLiteral17utf8CodeUnitCount7isASCIISSBp_BwBi1_tcfC : $@convention(method) (Builtin.RawPointer, Builtin.Word, Builtin.Int1, @thin String.Type) -> @owned String // user: %22
%22 = apply %21(%17, %18, %19, %20) : $@convention(method) (Builtin.RawPointer, Builtin.Word, Builtin.Int1, @thin String.Type) -> @owned String // user: %23
br bb4(%22 : $String) // id: %23
// %24 // user: %25
bb4(%24 : $String): // Preds: bb3 bb2 bb1
return %24 : $String // id: %25
} // end sil function '$s4main7LJLEnumO8rawValueSSvg'
我们可以看到 rawValue.getter
函数的调用,根据传进来的枚举成员值,通过模式匹配的方式走到不同的代码分支,在不同的代码分支中把不同的字符串给到当前对应的代码分支返回值。
3. 关联值
用枚举表达更复杂的情况,
enum Shape {
case circle(radius: Double)
case rectangle(width: Int, height: Int)
}
var circle = Shape.circle(radius: 10.0)
var square = Shape.rectangle(width: 5, height: 5)
4. 模式匹配
enum Week: String {
case MONDAY
case TUEDAY
case WEDDAY
case THUDAY
case FRIDAY
case SATDAY
case SUNDAY
}
通过 Switch
关键字进行匹配
let currentWeek: Week
switch currentWeek {
case .MONDAY: print(Week.MONDAY.rawValue)
case .TUEDAY: print(Week.TUEDAY.rawValue)
case .WEDDAY: print(Week.WEDDAY.rawValue)
case .THUDAY: print(Week.THUDAY.rawValue)
case .FRIDAY: print(Week.FRIDAY.rawValue)
case .SUNDAY: print(Week.SUNDAY.rawValue)
case .SATDAY: print(Week.SUNDAY.rawValue)
}
如果不想匹配所有的 case
,使用 defalut
关键字
let currentWeek: Week = Week.MONDAY
switch currentWeak {
case .SATDAY, .SUNDAY: print("Happy Day")
default : print("SAD DAY")
}
匹配关联值
enum Shape {
case circle(radius: Double)
case rectangle(width: Int, height: Int)
}
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 shape {
case .circle(let radius):
print("Circle radius:\(radius)")
case .rectangle(let width, let height):
print("rectangle width:\(width), height\(height)")
}
5. 枚举的大小
5.1 没有关联值枚举(No-payload enums)
enum Week: String {
case MONDAY
case TUEDAY
case WEDDAY
case THUDAY
case FRIDAY
case SATDAY
case SUNDAY
}
print(MemoryLayout<Week>.size)
print(MemoryLayout<Week>.stride)
// 打印结果
1
1
可以看出不管是大小( size
) 还是步长 stride
都是 1
,在 Swift
中进行枚举布局的时候一直是尝试使用最少的空间来存储 enum
,对于当前的 case
数量来说, UInt8
能够表示 256 cases
,也就意味着如果一个默认枚举类型且没有关联值的 case
少于 256
,当前枚举类型的大小都是 1
字节。
通过上面的打印我们可以看到,当前变量
a
, b
, c
三个变量的地址相差 1
位,并且存储的内容分别是 00
, 01
, 02
,这与我们上面说的布局理解是一致的
5.2 单个关联值枚举(Single-payload enums)
enum LJLBoolEnum {
case one(Bool)
case two
case three
case four
}
enum LJLIntEnum {
case one(Int)
case two
case three
case four
}
print("BoolEnum.Size:\(MemoryLayout<LJLBoolEnum>.size)")
print("BoolEnum.stride:\(MemoryLayout<LJLBoolEnum>.stride)")
print("IntEnum.Size:\(MemoryLayout<LJLIntEnum>.size)")
print("IntEnum.stride:\(MemoryLayout<LJLIntEnum>.stride)")
打印结果.png
Swift
中的 enum
中的 Single-payload enums
会使用负载类型中的额外空间来记录没有负载的 case 值。比如这里的 BoolEnum
,首先 Bool
类型是 1
字节,也就是 UInt8
,所以当前能表达 256
个 case
的情况,对于 Bool
类型来说,只需要使用低位的 0
, 1
这两种情况,其他剩余的空间就可以用来表示没有负载的 case
值。
可以看到不同的
case
值确实是按照我们在开始得出来的那个结论进行布局的。对于
Int
类型的负载来说,其实系统是没有办法推算当前的负载所要使用的位数,也就意味着当前 Int
类型的负载是没有额外的剩余空间的,这个时候我们就需要额外开辟内存空间来去存储我们的 case
值,也就是 8 + 1 = 9
字节。LJLIntEnum.png
可以看出变量
a
、 b
、 c
、 d
、 e
的地址相差 16
位,这和上面打印的步长信息相一致。
5.3 多个关联值枚举(Mutil-payload enums)
enum LJLDoubleBoolEnum {
case one(Bool)
case two(Bool)
case three
case four
}
enum LJLDoubleIntEnum {
case one(Int)
case two(Int)
case three
case four
}
print("DoubleBoolEnum.Size:\(MemoryLayout<LJLDoubleBoolEnum>.size)")
print("DoubleBoolEnum.stride:\(MemoryLayout<LJLDoubleBoolEnum>.stride)")
print("DoubleIntEnum.Size:\(MemoryLayout<LJLDoubleIntEnum>.size)")
print("DoubleIntEnum.stride:\(MemoryLayout<LJLDoubleIntEnum>.stride)")
打印结果
我们可以看到两个
Bool
关联值的枚举的大小为 1
,根据上面单个关联值枚举的所述不难理解。对于两个 Int
关联值的枚举的大小为 9
,是因为创建一个枚举值时有且只有一个关联值,但是还需要 1
个字节去存储其他 case
枚举值( three
和 four
) 所以当前只需要 8
字节 + 1
字节LJLDoubleBoolEnum.png
我们可以看到当前内存存储的分别是
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
。
一般来说,有多个负载的枚举时,当前枚举类型的大小取决于当前最大关联值的大小。
enum LJLEnum{
case one(Bool)
case two(Int)
case three
case four
}
print("LJLEnum.Size:\(MemoryLayout<LJLEnum>.size)")
// 打印结果
LJLEnum.Size:9
当前 LJLEnum
的大小就等于 sizeof(Int)
+ sizeof(rawVlaue)
= 9
enum LJLEnum{
case one(Bool)
case two(Int, Bool, Int)
case three
case four
}
print("LJLEnum.Size:\(MemoryLayout<LJLEnum>.size)")
// 打印结果
LJLEnum.Size:24
这里为什么不是 sizeof(Int) * 2 + sizeof(rawVlaue)
= 17
呢?对于 two (Int, Bool, Int)
类型的由于字节对齐的原因所以它的存储大小为 8 * 3
= 24
,又由于中间的 Bool
实际值占用 1
个字节因此中间 8
字节还有剩余控件去存储其他的 case
值。所以这里是 24
字节。那么将 bool
放在后面呢?我们试一试
enum LJLEnum{
case one(Bool)
case two(Int, Int, Bool)
case three
case four
}
print("LJLEnum.Size:\(MemoryLayout<LJLEnum>.size)")
print("LJLEnum.Stride:\(MemoryLayout<LJLEnum>.stride)")
打印结果.png
可以看到大小为
17
,其实也不难理解,bool
放后面那么它的 1
字节就能放下其他的 case
,那为什么这里不字节对齐呢?其实是因为最后会因为步长 24
的原因会对这里进行补齐。
注意对于只有一个 case
的枚举,不需要用任何东⻄来去区分当前的 case
,所以它的大小是 0
。
enum LJLEnum{
case one
}
print("LJLEnum.Size:\(MemoryLayout<LJLEnum>.size)")
// 打印结果
LJLEnum.Size:0
6. 递归枚举
递归枚举是一种枚举类型,它有一个或多个枚举成员使用该枚举类型的实例作为关联值。使用递归枚举时,编译器会插入一个间接层。你可以在枚举成员前加上 indirect
来表示该成员可递归。
enum BinaryTree<T> {
case empty
indirect case node(left: BinaryTree, value: T, right: BinaryTree)
}
var node = BinaryTree<Int>.node(left: BinaryTree<Int>.empty, value: 10, right: BinaryTree<Int>.empty)
也可以在枚举类型开头加上 indirect
关键字来表明它的所有成员都是可递归的。
indirect enum BinaryTree<T> {
case empty
case node(left: BinaryTree, value: T, right: BinaryTree)
}
var node = BinaryTree<Int>.node(left: BinaryTree<Int>.empty, value: 10, right: BinaryTree<Int>.empty)
在汇编中查看
可以发现这里调用了
swift_allocObject
, 在之前的文章 Swift探索(一): 类与结构体(上) 中我们就探讨过, swift_allocObject
就是在堆空间中分配内存空间。对于 indirect
关键字放在 enum
前面也就意味着当前这个 enum
的大小都是用引用类型在堆空间中存储。当 indirect
关键字放在 case
前面,那么就只有这个 case note
是存储在堆空间中,其中 case empty
是存储在 __DATA.__common
(存储没有初始化过的符号声明)的 section
中
二:可选值(Optional)
1. 什么是可选值
class Person {
var age: Int?
var name: Optional<String> = nil
}
age
和 name
我们就称之为可选值
2.可选值的本质
swift源码.png在
Swift
源码中可以发现可选值实际上就是一个枚举,并且有两个 case
一个 none
,一个 some
3. 可选值的基本使用
func getOddValue(_ value: Int) -> 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)
switch value {
case .some(let value):
array.remove(at: array.firstIndex(of: value)!)
case .none:
print("vlaue not exist")
}
}
如果每一个可选值都用模式匹配的方式来获取值在代码书写上就比较繁琐,还可以使用 if let
的方式来进行可选值绑定
func getOddValue(_ value: Int) -> Int? {
if value % 2 == 0 {
return .some(value)
} else {
return .none
}
}
var array = [1, 2, 3, 4, 5, 6]
for element in array {
if let value = getOddValue(element) {
array.remove(at: array.firstIndex(of: value)!)
}
}
除了使用 if let
还可以使用 gurad let
,和 if let
刚好相反,gurad let
守护一定有值。如果没有,直接返回。 通常判断是否有值之后,会做具体的逻辑实现,通常代码多如果用 if let
凭空多了一层分支, gurad let
是降低分支层次的办法
var name: String?
var age: Int?
var height: Double?
func testIfLet() {
if let name1 = name {
if let age1 = age {
if let height1 = height {
print("姓名: \(name1), 年龄:\(age1), 身高:\(height1)cm")
} else {
print("height 为空")
}
} else {
print("age 为空")
}
} else {
print("name 为空")
}
}
func testGuardLet() {
guard let name1 = name else {
print("name 为空")
return
}
guard let age1 = age else {
print("age 为空")
return
}
guard let height1 = height else {
print("height 为空")
return
}
print("姓名: \(name1), 年龄:\(age1), 身高:\(height1)cm")
}
2. 可选链
在 OC
中我们给一个 nil
对象发送消息什么也不会发生,但是在 Swift
中是没有办 法向一个 nil
对象直接发送消息,但是借助可选链可以达到类似的效果。
let str: String? = "abc"
let upperStr = str?.uppercased()
print(upperStr)
var str2: String?
let upperStr2 = str2?.uppercased()
print(upperStr2)
// 打印结果
Optional("ABC")
nil
同意可选链对数组和喜爱
var closure: ((Int) -> ())?
print(closure?(1)) // closure 为 nil 不执行
let dict = ["one": 1, "two": 2]
print(dict["one"]) // Optional(1)
print(dict["three"]) // nil
// 打印结果
nil
Optional(1)
nil
3. ?? 运算符(空合并运算符)
( a ?? b
) 将对可选类型 a
进行空判断,如果 a
包含一个值就进行解包,否则就返回一个默认值 b
。
- 表达式
a
必须是Optional
类型 - 默认值
b
的类型必须要和a
存储值的类型保持一致
var age: Int? = 10
var x = age ?? 0
print(x)
// 打印结果
10
?? 空运算符源码.png
4. 运算符重载
源码中我们可以看到除了重载了 ??
运算符, 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 + secondVector.y)
}
static prefix func - (vector: Vector) -> Vector {
return Vector(x: -vector.x, y: -vector.y)
}
static func - (fistVector: Vector, secondVector: Vector) -> Vector {
return fistVector + -secondVector
}
}
var x = Vector(x: 10, y: 20)
var y = Vector(x: 20, y: 30)
var z = x + y
print(z)
var w = -z
print(w)
// 打印结果
Vector(x: 30, y: 50)
Vector(x: -30, y: -50)
infix operator **: AdditionPrecedence
// 运算组名称: LJLPrecedence 优先级低于: AdditionPrecedence 结合方式: 左结合
precedencegroup LJLPrecedence {
lowerThan: AdditionPrecedence
associativity: left
}
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 + secondVector.y)
}
static prefix func - (vector: Vector) -> Vector {
return Vector(x: -vector.x, y: -vector.y)
}
static func - (fistVector: Vector, secondVector: Vector) -> Vector {
return fistVector + -secondVector
}
static func ** (fistVector: Vector, secondVector: Vector) -> Vector {
return Vector(x: fistVector.x * secondVector.x, y: fistVector.y * secondVector.y)
}
}
var x = Vector(x: 10, y: 20)
var y = Vector(x: 20, y: 30)
var z = x ** y
var w = x + y ** x
print(z)
print(w)
// 打印结果
Vector(x: 200, y: 600)
Vector(x: 300, y: 1000)
5. 隐士解析可选类型
隐式解析可选类型是可选类型的一种,使用的过程中和非可选类型无异。它们之间唯一的区别是,隐式解析可选类型是你告诉对 Swift
编译器,在运行时访问时,值不会为 nil
。
var age: Int
var age1: Int!
var age2: Int?
let x = age1 % 2
let y = age2 % 2
隐式解析.png
其中
age1
不用做解包的操作,编译器已经做了
@IBOutlet weak var btn: UIButton!
IBOutlet
类型是 Xcode
强制为可选类型的,因为它不是在初始化时赋值的,而是在加载视图的时候。可以把它设置为普通可选类型,但是如果这个视图加载正确,它是不会为空的。
6. 可选值有关的高阶函数
-
map
:这个方法接受一个闭包,如果可选值有内容则调用这个闭包进行转换
var dict = ["one": "1", "two": "2"]
let result = dict["one"].map{ Int($0) }
print(result)
// 打印结果
Optional(Optional(1))
上面的代码中我们从字典中取出字符串 1
,并将其转换为 Int
类型,但因为 String
转换成 Int
不一定能成功,所以返回的是 Int?
类型,而且字典通过键不一定能取得到值,所以 map
返回的也是一个 Optional
,所以最后上述代码 result
的类型为 Int??
类型。
-
flatMap
:可以把结果展平成为单个可选值
var dict = ["one": "1", "two": "2"]
let result = dict["one"].flatMap{ Int($0) } // Optional(1)
print(result)
// 打印结果
Optional(1)
注意这个方法是作用在 Optional
的方法,而不是作用在 Sequence
上的
作用在 Sequence
上的 flatMap
方法在 Swift4.1
中被更名为 compactMap
,该方法可以将序列中的 nil
过滤出去
let array = ["1", "2", "3", nil]
let result = array.compactMap{ $0 } // ["1", "2", "3"]
print(result)
let array1 = ["1", "2", "3", "four"]
let result1 = array1.compactMap{ Int($0) } // [1, 2, 3]
print(result1)
// 打印结果
["1", "2", "3"]
[1, 2, 3]