Swift-枚举(enum)

194 阅读6分钟

一、Enum 原始值

1.1 基本用法

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

enum SSLEnum {
    case test_one
    case test_two
    case test_three
}

我们知道在 C 和 OC 中默认是接受整数类型,也就意味着下面的例子中: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 是引用类型。

总结:

1.原始值不占用枚举变量的内存;

2.枚举使用1个字节来存储成员值;

3.枚举使用N个字节来存储关联值;