Swift 专栏Swift探索

Swift探索(五): Enum & Optional

2022-01-17  本文已影响0人  Lee_Toto

一:枚举(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 一样都是从 012 开始,当指定值时,后面的枚举值的 RawValue 会在当前值的基础上进行累加操作,因此 fourfive 的值为 1112

将枚举类型改成 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 字节。

No-payload enum.png
通过上面的打印我们可以看到,当前变量 abc 三个变量的地址相差 1 位,并且存储的内容分别是 000102 ,这与我们上面说的布局理解是一致的
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 ,所以当前能表达 256case 的情况,对于 Bool 类型来说,只需要使用低位的 01 这两种情况,其他剩余的空间就可以用来表示没有负载的 case 值。

LJLBoolEnum.png
可以看到不同的 case 值确实是按照我们在开始得出来的那个结论进行布局的。
对于 Int 类型的负载来说,其实系统是没有办法推算当前的负载所要使用的位数,也就意味着当前 Int 类型的负载是没有额外的剩余空间的,这个时候我们就需要额外开辟内存空间来去存储我们的 case 值,也就是 8 + 1 = 9 字节。
LJLIntEnum.png
可以看出变量 abcde 的地址相差 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)")

打印结果

打印结果.png
我们可以看到两个 Bool 关联值的枚举的大小为 1,根据上面单个关联值枚举的所述不难理解。对于两个 Int 关联值的枚举的大小为 9,是因为创建一个枚举值时有且只有一个关联值,但是还需要 1 个字节去存储其他 case 枚举值( threefour ) 所以当前只需要 8 字节 + 1 字节
LJLDoubleBoolEnum.png
我们可以看到当前内存存储的分别是 000140418081 ,这里在存储当前的 case 时候会使用到 common spare bits,首先 bool 类型需要 1 字节,也就是 8 位,对于 bool 类型来说,我们存储的无非就是 01 ,只需要用到 1 位,所以剩余的 7 位,这里我们都统称为 common spare bits ,对于当前的 case 数量来说我们完全可以把所有的情况放到 common spare bits 所以这里只需要 1 字节就可以存储所有的内容了。
对于 000140418081 ,其中 048 称之为 tag value01 称之为 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)

在汇编中查看

汇编中查看.png
可以发现这里调用了 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
}

agename 我们就称之为可选值

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

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. 可选值有关的高阶函数

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?? 类型。

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]
上一篇下一篇

猜你喜欢

热点阅读