Swift 进阶(三)枚举

枚举的基本用法

enum Direction {
    case north
    case south
    case east
    case west
}

// 简便写法
enum Direction {
    case north, south, east, west
}

var dir = Direction.west
dir = Direction.east
dir = .north

switch dir {
case .north:
    print("north")
case .south:
    print("south")
case .east:
    print("east")
case .west:
    print("west")
}

关联值(Associated Values)

有时会将枚举的成员值其他类型的值关联存储在一起,会非常有用

enum Score {
	case points(Int)
	case grade(Character)
}

var score = Score.points(96)
score = .grade("A")

switch score {
case let .points(i):
  debugPrint(i)
case let .grade(i):
  debugPrint(i)
}
enum Date {
    case digit(year: Int, month: Int, day: Int)
    case string(String)
}

var date = Date.digit(year: 2020, month: 12, day: 5)
date = .string("2020-12-5")
   
switch date {
case .digit(let year, let month, let day):
  debugPrint(year, month, day)
case let .string(value):
  debugPrint(value)
}
enum Password {
    case number(Int, Int, Int, Int)
    case gesture(String)
}

var pwd = Password.number(5, 6, 4, 7)
pwd = .gesture("12369")

switch pwd {
case let .number(n1, n2, n3, n4):
    print("number is ", n1, n2, n3, n4)
case let .gesture(str):
    print("gesture is ", str)
}

必要时let可以改成var

原始值(Raw Values)

枚举成员可以使用相同类型的默认值预先关联,这个默认值叫做原始值

enum PokerSuit: String {
    case spade = "1"
    case heart = "2"
    case diamond = "3"
    case club = "4"
}

let suit = PokerSuit.heart
debugPrint(suit)
debugPrint(suit.rawValue)
debugPrint(PokerSuit.spade.rawValue)

原始值不占用枚举变量的内存,原始值只是关联上了枚举变量,所以原始值占用内存的大小并不是枚举变量的大小

隐式原始值(Implicitly Assigned Raw Values)

如果枚举的原始值类型是Int、String,Swift会自动分配原始值

字符串默认分配的原始值就是其变量名

enum Direction: String {
    case north = "north"
    case south = "south"
    case east = "east"
    case west = "west"
}

// 等价于上面
enum Direction: String {
	case north, south, east, west
}

debugPrint(Direction.north.rawValue) // north

Int类型默认分配的原始值是从0开始递增的数字

enum Season: Int {
    case spring, summer, autumn, winter
}

print(Season.spring.rawValue) // 0
print(Season.summer.rawValue) // 1
print(Season.autumn.rawValue) // 2
print(Season.winter.rawValue) // 3

如果有指定原始值的,下一个就会按照已经指定的值递增分配

enum Season: Int {
    case spring = 1, summer, autumn = 4, winter
}

print(Season.spring.rawValue) // 1
print(Season.summer.rawValue) // 2
print(Season.autumn.rawValue) // 4
print(Season.winter.rawValue) // 5

递归枚举(Recursive Enumeration)

递归枚举要用indirect关键字来修饰enum,不然会报错

indirect enum ArithExpr {
    case number(Int)
    case sum(ArithExpr, ArithExpr)
    case difference(ArithExpr, ArithExpr)
}

或者

enum ArithExpr {
	case number(Int)
	indirect case sum(ArithExpr, ArithExpr)
	indirect case difference(ArithExpr, ArithExpr)
}

let five = ArithExpr.number(5)
let four = ArithExpr.number(4)
let sum = ArithExpr.sum(five, four)

func calculate(_ expr: ArithExpr) -> Int {
    switch expr {
    case let .number(value):
        return value
    case let .sum(left, right):
        return calculate(left) + calculate(right)
    case let .difference(left, right):
        return calculate(left) - calculate(right)
    }
}

calculate(difference)

MemoryLayout

可以使用MemoryLayout获取数据类型占用的内存大小

64bitInt类型8个字节

let age = 10

MemoryLayout<Int>.stride // 8, 分配占用的空间大小
MemoryLayout<Int>.size // 8, 实际用到的空间大小
MemoryLayout<Int>.alignment // 8, 对齐参数

等同于

MemoryLayout.size(ofValue: age)
MemoryLayout.stride(ofValue: age)
MemoryLayout.alignment(ofValue: age)

关联值和原始值的区别:关联值类型会存储到枚举变量里面,原始值因为一开始就会知道默认值是多少,所以只做记录,不会存储

enum Password {
	case number(Int, Int, Int, Int)
	case other
}

MemoryLayout<Password>.stride // 40,分配占用的空间大小
MemoryLayout<Password>.size // 33,实际用到的空间大小
MemoryLayout<Password>.alignment // 8,对齐参数
enum Session: Int {
	case spring, summer, autnmn, winter
}

MemoryLayout<Session>.stride // 1,分配占用的空间大小
MemoryLayout<Session>.size // 1,实际用到的空间大小
MemoryLayout<Session>.alignment // 1,对齐参数

枚举变量的内存布局

我们知道通过MemoryLayout可以获取到枚举占用内存的大小,那枚举变量分别占用多少内存呢?

要想知道枚举变量的大小,我们需要通过查看枚举变量的内存布局来进行分析

枚举变量的分析准备

我们可以需要通过Xcode里的View Memory选项来查看详细的内存布局

1.可以在运行程序时,通过控制台打印的枚举变量右键选择View Memory of *进入到内存布局的页面

-w440

2.还可以从Xcode标题栏中选择Debug -> Debug Workflow -> View Memory进入到内存布局的页面

-w569

3.进入到该页面,然后通过输入变量的内存地址来查看

-w1129

4.我们可以下载一个小工具来获取到变量的内存地址

下载地址:https://github.com/CoderMJLee/Mems

5.然后将下载好的这个文件Mems.swift拖到自己的Xcode

调用这个函数就可以了

print(Mems.ptr(ofVal: &t))

我们来分析下面的枚举变量的情况

enum TestEnum {
    case test1, test2, test3
}

var t = TestEnum.test1
print(Mems.ptr(ofVal: &t))

t = TestEnum.test2
t = TestEnum.test3

print(MemoryLayout<TestEnum>.stride) // 1
print(MemoryLayout<TestEnum>.size) // 1
print(MemoryLayout<TestEnum>.alignment) // 1

分别将断点打在给枚举变量t赋值的三句代码上,然后运行程序观察每次断点之后的内存布局有什么变化

-w1127

-w1124

-w1124

通过上图可以看到,每个枚举变量都分配了一个字节的大小,并且存储的值分别是0、1、2,我们可以知道这一个字节的大小就是用来存储枚举成员值

我们再来分析一个枚举变量的情况

enum TestEnum: Int {
    case test1 = 1, test2 = 2, test3 = 3
}

var t = TestEnum.test1
print(Mems.ptr(ofVal: &t))

t = TestEnum.test2
t = TestEnum.test3

print(MemoryLayout<TestEnum>.stride) // 1
print(MemoryLayout<TestEnum>.size) // 1
print(MemoryLayout<TestEnum>.alignment) // 1

-w1131

-w1126

-w1125

通过上图可以看到,每个枚举变量存储的值也是0、1、2,并且分配了一个字节的大小

可以证明枚举变量的内存大小和原始值类型无关,而且枚举变量里存储的值和原始值也无关

我们再来分析一个枚举变量的情况

enum TestEnum {
    case test1(Int, Int, Int) // 24
    case test2(Int, Int) // 16
    case test3(Int) // 8
    case test4(Bool) // 1
    case test5 // 1
}

var t = TestEnum.test1(1, 2, 3)
print(Mems.ptr(ofVal: &t))

t = TestEnum.test2(4, 5)
t = TestEnum.test3(6)
t = TestEnum.test4(true)
t = TestEnum.test5

MemoryLayout<TestEnum>.size // 25
MemoryLayout<TestEnum>.stride // 32
MemoryLayout<TestEnum>.alignment // 8

我们先通过打印了解到枚举类型总共分配了32个字节,然后我们通过断点分别来观察枚举变量的内存布局

-w773
-w1124

执行完第一句我们可以看到,前面24个字节分别用来存储关联值1、2、3,第25个字节用来存储成员值0,之所以分配32个字节是因为内存对齐的原因

// 调整排版后的内存布局如下所示
01 00 00 00 00 00 00 00
02 00 00 00 00 00 00 00
03 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00

-w719
-w1193

执行完第二句我们可以看到,前面16个字节分半用来存储关联值4、5,然后第25个字节用来存储成员值1

// 调整排版后的内存布局如下所示
04 00 00 00 00 00 00 00
05 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00
01 00 00 00 00 00 00 00

-w563
-w1196

执行完第三句我们可以看到,前面8个字节分半用来存储关联值6,然后第25个字节用来存储成员值2

// 调整排版后的内存布局如下所示
06 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00
02 00 00 00 00 00 00 00

-w665
-w1192

执行完第四句我们可以看到,由于是Bool类型,那么只用了第一个字节来存储关联值1,然后第25个字节用来存储成员值3

// 调整排版后的内存布局如下所示
01 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00
03 00 00 00 00 00 00 00

-w676
-w1191

执行完最后一句我们可以看到,由于没有关联值,那么只用了第25个字节存储成员值4

// 调整排版后的内存布局如下所示
00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00
04 00 00 00 00 00 00 00

总结:内存分配情况:一个字节存储成员值,n个字节存储关联值(n取占用内存最大的关联值),任何一个case的关联值都共有这n个字节

我们再来看几个情况

enum TestEnum {
    case test
}

MemoryLayout<Session>.stride // 1,分配占用的空间大小
MemoryLayout<Session>.size // 0,实际用到的空间大小
MemoryLayout<Session>.alignment // 1,对齐参数

如果枚举里只有一个case,那么实际用到的空间为0,都不用特别分配内存来进行存储

enum TestEnum {
    case test(Int)
}

MemoryLayout<Session>.stride // 8,分配占用的空间大小
MemoryLayout<Session>.size // 8,实际用到的空间大小
MemoryLayout<Session>.alignment // 8,对齐参数

可以看到分配的内存大小就是关联值类型决定的,因为只有一个case,所以都不需要再额外分配内存来存储是哪个case

posted on 2021-03-14 17:42  FunkyRay  阅读(203)  评论(0编辑  收藏  举报