Swift 进阶:枚举 & Optional

1,942 阅读10分钟

一、Enum 原始值

1.1 基本用法

Swift 中通过 enum 关键字来声明一个枚举:

enum SSLEnum {
    case test_one
    case test_two
    case test_three
}

我们知道在 COC 中默认是接受整数类型,也就意味着下面的例子中:A,B,C 分别默认代表 0,1,2

typedef NS_ENUM(NSInteger, SSLEnum) {
    A,
    B,
    C,
};

Swift 中的枚举则更加灵活,并且不需给枚举中的每一个成员都提供值。如果要为枚举成员提供值(所谓原始值),那么这个值可以是字符串、字符、任意的整数值,或者是浮点类型。

enum Color: String {
    case red = "Red"
    case amber = "Amber"
    case green = "Green"
}

enum SSLEnum: Double {
    case a = 10.0
    case b = 20.0
    case c = 30.0
    case d = 40.0
}

隐式 RawValue 分配是建立在 Swift 的类型推断机制上的。

enum DayOfWeek: String {
    case mon, tue, wed, thu, fri = "Hello World", sat, sun
}

print(DayOfWeek.rawValue)
print(DayOfWeek.fri.rawValue) 
print(DayOfWeek.rawValue)

输出结果:
mon
Hello World
sat

我们可以看到 mon、sta 的枚举值和原始值是一样的,接下来看一看这是如何做到的。

1.2 sil 分析取值过程

添加如下代码:

enum DayOfWeek: String {
    case mon, tue, wed, thu, fri = "Hello World", sat, sun
}

var x = DayOfWeek.mon.rawValue

swiftc -emit-sil main.swift > ./main.sil && open main.sil 生成 sil 文件:

enum DayOfWeek : String {
  case mon, tue, wed, thu, fri, sat, sun // case 值
  init?(rawValue: String) // 可失败初始化器
  typealias RawValue = String // 取别名
  var rawValue: String { get } // get 函数
} 

由上可知获取 rawValue 的本质,就是调用这个计算属性的 get 方法,在 sil 文件中找到 get 方法:

image.png

这里的取值过程是先判断是不是 mon,如果是就调用 bb1 代码块,代码块中取的是 “mon”字符串常量,字符串常量存在哪儿里呢?

存储位置见下面:

image.png

1.3 枚举值和原始值

枚举值和原始值不能相互赋值:

image.png

二、关联值

2.1 基本用法

有的时候我们想用枚举值表达一个更复杂的东西,比如说形状:

enum Shape {
    case circle(radios: Double) // 圆形
    case rectangle(width: Double, height: Double) // 长方形
}

// 半径为 10 的圆形
var circle = Shape.circle(radios: 10)

2.2 模式匹配

switch 模式匹配基本用法:

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.MONDAY.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,使用 default 关键字:

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 .SATDAY, .SUNDAY: print("Happy Day")
    default : print("SAD Day")
}

如果我们要匹配关联值的话:

enum Shape{
    case circle(radious: Double)
    case rectangle(width: Int, height: Int)
}

let shape = Shape.circle(radious: 10.0)

switch shape{
    case let .circle(radious):
        print("Circle radious:\(radious)")
    case let .rectangle(width, height):
        print("rectangle width:\(width),height\(height)")
}

还可以这么写:

enum Shape{
    case circle(radious: Double)
    case rectangle(width: Int, height: Int)
}

var shape = Shape.circle(radious: 10.0)

switch shape{
    case .circle(let radious):
        print("Circle radious:\(radious)")
case .rectangle(let width, let height):
        print("rectangle width:\(width),height\(height)")
}

三、枚举大小

3.1 No-payload enums

接下来我们来讨论一下枚举占用的内存大小,这里我们区分几种不同的情况,首先第一种就是 No-payload enums 也就是没有关联值的枚举。

enum Week {
    case MONDAY
    case TUEDAY
    case WEDDAY
    case THUDAY
    case FRIDAY
    case SATDAY
    case SUNDAY
}

大家可以看到这种枚举类型类似我们在 C 语言中的枚举,当前类型默认是 Int 类型,那么对于这一类的枚举在内存中是如何布局?以及在内存中占用的大小是多少那?这里我们就可以直接使用 MemoryLayout 来测量一下当前枚举

image.png

  • 可以看到这里我们测试出来的不管是 size 还是 stribe 都是 1
  • 在 Swift 中进行枚举布局的时候一直是尝试使用最少的空间来存储 enum,1 字节能够表示 256 个case
  • 也就是说一个默认枚举类型且没有关联值且 case 少于 256,当前枚举类型的大小都是 1 字节,枚举类型 UInt8

image.png

通过上面的打印我们可以直观的看到,当前变量 a,b,c 这三个变量存储的内容分别是 00,01,02 这和我们上面说的布局理解是一致的。

3.2 Single-payload enums

接下来我们再来看一下 Single-payload enums 的内存布局,字面意思就是只有一个负载的 enum, 比如下面这个例子

enum SSLEnum {
    case test_one(Bool)
    case test_two
    case test_three
    case test_four
}

enum SSLEnum2 {
    case test_one(Int)
    case test_two
    case test_three
    case test_four
}

print(MemoryLayout<SSLEnum>.size)
print(MemoryLayout<SSLEnum2>.size)

输出结果:
1
9
  • 这里为什么都是单个负载,但是当前占用的大小却不一致呢!
  • 注意,Swift 中的 enum 中的 Single-payload enums 会使用负载类型中的额外空间来记录没有负载的 case 值。
  • 对于 Bool 类型对负载, Bool 类型是 1 字节,但其实它只需要 1 位就够存了,对于 UInt8 类型的枚举来说,还有 7 位的空间可以用来表示 128 个 case,所以 1 个字节就可以了。
  • 对于 Int 类型的负载来说,其实系统是没有办法推算当前的负载所要使用的位数,也就意味着当前 Int 类型的负载是没有额外的剩余空间的,这个时候我们就需要额外开辟内存空间来存储我们的 case 值,也就是 8 + 1 = 9 字节。

3.3 Mutil-payload enums

接下来我们说第三种情况 Mutil-payload enums,有多个负载的情况时,enum 是如何进行布局的。

看下面的例子:

enum SSLEnum {
    case test_one(Bool)
    case test_two(Bool)
    case test_three
    case test_four
}

enum SSLEnum2 {
    case test_one(Int)
    case test_two(Int)
    case test_three(Int)
    case test_four(Int)
}

enum SSLEnum3 {
    case test_one(Bool)
    case test_two(Int)
    case test_three
    case test_four
}

enum SSLEnum4 {
    case test_one(Int, Int, Int)
    case test_two
    case test_three
    case test_four
}

print(MemoryLayout<SSLEnum>.size)
print(MemoryLayout<SSLEnum2>.size)
print(MemoryLayout<SSLEnum3>.size)
print(MemoryLayout<SSLEnum4>.size)

输出结果:
1
9
9
25
  • 通过输出结果我们可以了解到,有多个负载的枚举时,枚举的大小取决于当前最大关联值的大小
  • 如果最大关联值需要的位数小于 8 位,并且剩下的空间仍然可以表示所有的 case,枚举的大小就是 1
  • 如果最大关联值需要的位数大于 8 位,枚举的大小 = 最大关联值 + 1

3.4 特殊情况

最后这里还有一个特殊情况我们需要理解一下,我们来看下面的案例

enum SSLEnum {
    case test_one
}

对于当前的 SSLEnum 只有一个 case,我们不需要用任何东⻄来去区分当前的 case,所以当我们打印当前的 SSLEnum 大小你会发现是 0。

四、indirect关键字

我们先看下面报错的代码:

image.png

为什么报错呢,因为枚举是值类型,编译时期大小就要确定完成,而这里的关联值也是枚举类型,这样的话就没法确定了。

在枚举前面加上 indirect 就不会报错了,indirect 的作用就是把 BinaryTree 分配在堆空间上

image.png

打印一下 BinaryTree 的大小:

print(MemoryLayout<BinaryTree<Int>>.size)

输出结果:8

可以看到示例大小是 8,是引用类型。

indirect 除了放在枚举前面,还可以放在 case 的前面

enum BinaryTree<T> {
    case empty(Int)
    indirect case node(left: BinaryTree, value: T, right: BinaryTree)
} 

这个时候就不是整个 enum 都是引用类型了,只有 node 是引用类型。

五、Optional

5.1 可选值

5.1.1 认识可选值

之前我们在写代码的过程中早就接触过可选值,比如我们在代码这样定义:

class SSLTeacher {
    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
    }
} 

这个时候给定一个数组,我们想删除数组中所有的偶数:

image.png

这个时候编译器就会检查我们当前的 value 会发现他的类型和系统编译器期望的类型不符,这个时候我们就能使用 MyOptional 来限制语法的安全性。

通过 enum 的模式匹配来取出对应的值:

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)!)
    default:
        print("value not exist")
    }
} 

如果我们把上述的返回值更换一下,其实就和系统的 Optional 使用无疑

func getOddValue(_ value: Int) -> Int? {
    if value & 2 == 0 {
        return .some(value)
    } else {
        return .none
    }
}

这样我们其实是利用当前编译器的类型检查来达到语法书写层面的安全性。

5.1.2 if let

当然如果每一个可选值都用模式匹配的方式来获取值在代码书写上就比较繁琐,我们还可以使用 if let 的方式来进行可选值绑定:

if let value = value {
    array.remove(at: array.firstIndex(of: value)!)
}

5.1.3 gurad let

除了使用 if let 来处理可选值之外,我们还可以使用 gurad let 来简化我们的代码,

  • gurad let 和 if let 刚好相反
  • gurad let 守护一定有值。如果没有,直接返回
  • 通常判断如果有值之后,会做具体的逻辑实现,通常代码多

我们来看一个具体的案例:

let name: String? = "ssl"
let age: Int? = 18

guard let newName = name else {
    print("姓名 为空")
    return
}

guard let newAge = age else {
    print("年龄 为空")
    return
}
// 代码执行至此, nameNew 和 ageNew 一定有值
print(newName + String(newAge))

5.1.3 可选链

我们知道在 OC 中给一个 nil 对象发送消息什么也不会发生, Swift 中我们是没有办法向一个 nil 对象直接发送消息,但是借助可选链可以达到类似的效果。

我们先来看下面代码

let str: String? = "abc"
let upperStr = str?.uppercased() 
print(upperStr)  // 输出:Optional<"ABC">

var str1: String?
let upperStr1 = str1?.uppercased() 
print(upperStr1) // 输出:nil

我们再来看下面这段代码输出什么

let str: String? = "ssl"
let upperStr = str?.uppercased().lowercased()
print(upperStr) // 输出:Optional("ssl")

同样的可选链对于下标和函数调用也适用

var closure: ((Int) -> ())?
closure?(1) // closure 为 nil 不执行

let dict: NSDictionary? = ["one": 1, "two": 2]
print(dict?["one"]) // 输出:Optional(1)
print(dict?["three"]) // 输出:nil

5.2 ?? 运算符 (空合并运算符)

( a ?? b ) 将对可选类型 a 进行空判断,如果 a 包含一个值就进行解包,否则就返回一个默认值 b 

  • 表达式 a 必须是 Optional 类型
  • 默认值 b 的类型必须要和 a 存储值的类型保持一致

看下面示例:

var age: Int?

var x = age ?? 10

print(x) // 输出 10

5.3 运算符重载

在源码中我们可以看到除了重载了 ?? 运算符, Optional 类型还重载了 == , ?= 等等运算符,实际开发中我们可以通过重载运算符简化我们的表达式。

比如在开发中我们定义了一个二维向量,这个时候我们想对两个向量进行基本的操作,那么我们就可以通过重载运算符来达到我们的目的

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 v1 = Vector(x: 10, y: 20)
var v2 = Vector(x: 20, y: 30)
var v3 = v1 + v2
print(v3)  // 输出 Vector(x: 30, y: 50)
var v4 = -v3 
print(v4)  // 输出 Vector(x: -30, y: -50)

我们还可以自定义运算符,看下面示例:

infix operator --- : AdditionPrecedence
precedencegroup SSLPrecedence {
    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)
    }
}

var v1 = Vector(x: 10, y: 20)
var v2 = Vector(x: 20, y: 30)

var v5 = v1 --- v2
print(v5) // 输出 Vector(x: 200, y: 600)

关于自定义运算符,可以查看 官方文档

5.4 隐式解析可选类型

隐式解析可选类型是可选类型的一种,使用的过程中和非可选类型无异。它们之间唯一的区别是,隐式解析可选类型其实就是告诉 Swift 编译器,在运行时访问时,值不会为 nil。

看下面示例代码:

image.png

age1 为隐式解析可选类型,我们不需要再对它做解包操作了,编译器已经帮我们做了。

运行项目会发现,虽然编译期没有问题,但是运行的时候如果没有值依然会报错:

image.png