Swift 枚举

230 阅读7分钟

简介

枚举是一种数据类型,只包含自定义的特定数据,它是一组有共同特性的数据的集合。

语法

Swift 中使用 enum 关键词来创建枚举并且把它们的整个定义放在一对大括号内:

enum enumname {
   // 枚举成员
}

下面两种写法等价

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

等价于

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

测试代码:

var direction = Direction.east
direction = .south // direction为已知类型,再次赋值可以省略枚举名

switch direction {
case .east:
    print("东")
case .south:
    print("南")
case .west:
    print("西")
case .north:
    print("北")
}

分类

关联值(相关值)

枚举的成员值和其它类型的值存储在一起。

下面我们定义一个名为 Student 的枚举类型,它可以是 Name 的一个字符串相关值(String),或者是 number 的一个相关值(Int)

enum Student {
    case name(String)
    case number(Int)
}
var s = Student.name("张三")
s = .number(20081024)

switch s {
case let .name(i):
    print("名字:\(i)")
case let .number(i):
    print("学号:\(i)")
}

输出:

学号:20081024

上面我们可以看到,枚举变量需要通过不同枚举成员传入的值,而这个值就是关联值(如,张三、20081024)。

不难发现:
传入的关联值都会存储在枚举变量的内存中,所以针对于关联值的枚举,其内存和将来存储的关联值类型相关的。

原始值

枚举成员可以使用相同类型的默认值预先对应,这个默认值叫做:原始值
注意其定义: 在枚举名称后面有 : [数据类型(此处是String)],这里并不表示继承,而是原始值类型的枚举的定义格式,表示其原始值的类型。

enum Grade : String {
    case perfect = "A"
    case great = "B"
    case good = "C"
    case bad = "D"
}
print(Grade.perfect.rawValue) // A
print(Grade.great.rawValue) // B
print(Grade.good.rawValue) // C
print(Grade.bad.rawValue) // D

上面定义一个学生成绩的等级,原始值分别是 "A"、"B"、"C"、"D"。

原始值可以是字符串,字符,或者任何整型值或浮点型值。每个原始值在它的枚举声明中必须是唯一的。

当然,在原始值为整数的枚举时,不需要显式的为每一个成员赋值,Swift会自动为你赋值。

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

隐式原始值

原始值类型为 String

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

等价于

enum Direction : String {
    case east, south, west, north
}

测试代码

print(Direction.north) // north
print(Direction.north.rawValue) // north

原始值类型为 Int

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

可以看到:当使用整数作为原始值时,隐式赋值的值依次递增1。如果第一个值没有被赋初值,将会被自动置为0。


当然,你可以手动指定枚举成员的原始值

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

注意:原始值不占用枚举变量的内存。
原始值:固定死的,和枚举成员永远绑定在一起,不允许后续绑定新值,而不是存 储在每一个枚举变量内存中,也就不会占用每一个枚举变量的内存。类似于标序号

枚举内存布局

无原始值和无关联值枚举的内存布局

定义枚举 EnumTest

enum EnumTest {
    case e1, e2, e3
}

分别打印 EnumTest 实际占用的内存、分配的内存、和内存对齐值

print("实际占用:", MemoryLayout<EnumTest>.size)
print("实际分配:", MemoryLayout<EnumTest>.stride)
print("内存对齐:", MemoryLayout<EnumTest>.alignment)

输出:
实际占用: 1
实际分配: 1
内存对齐: 1

既没有原始值,又没有关联值的枚举类型,只占用一个字节。

通过内存视图查看这一个字节里面存储的具体值: Swift 没法直接通过 Xcode 打印枚举变量的内存地址,借用 MJLee 写的一个小工具:Mems

添加测试代码,断点调试

输出信息可以看到变量 e 的内存地址为:0x0000000100007710

查看内存视图,输入 e = .e1 的内存信息:

断点下一个,可以看到 e = .e2 这个字节的内存值是 1

同理,e = .e3 的那个字节值为 2

所以:在没有原始值和关联值的枚举,枚举占用一个字节的内存大小(枚举成员不超过256个的情况下),该字节的存储的值为依次存储 枚举成员值,即:分别为0、1、2、3...

特殊情况(这种情况在开发中没有存在的意义) 只有一个成员的枚举

enum Person {
    case person
}

print("实际占用:", MemoryLayout<Person>.size)
print("实际分配:", MemoryLayout<Person>.stride)
print("内存对齐:", MemoryLayout<Person>.alignment)

输出信息:

实际占用: 0
实际分配: 1
内存对齐: 1

可以看到虽然分配了一个字节,但实际上没有使用,只有一个成员,就没有必要存储成员对应的值。

总结:在没有原始值和关联值的枚举,枚举占用一个字节的内存大小(枚举成员不超过256个的情况下),该字节的存储的值为依次存储 枚举成员值(0,1,2..)

关联值枚举的内存布局

定义 Time 枚举类型

enum Time {
    case hour(Int, Int, Int)
    case day(Int)
    case moth(Int, Int)
    case unknown(Bool)
}

查看 Time 实际占用的内存、分配的内存、和内存对齐值

print("实际占用:", MemoryLayout<Time>.size)
print("实际分配:", MemoryLayout<Time>.stride)
print("内存对齐:", MemoryLayout<Time>.alignment)

实际占用: 25
实际分配: 32
内存对齐: 8

同样内存视图查看该枚举变量实际适用的 25 个字节的内存中的具体值

var time = Time.hour(9, 10, 11)
print(Mems.ptr(ofVal: &time))

time = .day(1)
time = .moth(2, 3)
time = .unknown(true)

time 变量的内存地址:0x00000001000076f0

可以看到 var time = Time.hour(9, 10, 11) 时,三个值分别存储在对应的 8 字节中(09、0A、0B),最后 8 字节(实际只使用了其中的一个字节)存储的值为 00

下一步断点,即 time = .day(1) 时,

time = .day(1)
01 00 00 00 00 00 00 00    -- 存储关联值 1
00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00
01 00 00 00 00 00 00 00    -- 存储成员值,值为1


time = .moth(2, 3) 时,

time = .moth(2, 3)

02 00 00 00 00 00 00 00    -- 存储关联值 2
03 00 00 00 00 00 00 00    -- 存储关联值 3
00 00 00 00 00 00 00 00
02 00 00 00 00 00 00 00    -- 存储成员值,值为2

time = .unknown(true)

time = .unknown(true)
01 00 00 00 00 00 00 00    -- 存储关联值 1,true为1,false为0
00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00
03 00 00 00 00 00 00 00    -- 存储成员值,值为3

可以发现,关联值枚举类型,会分配内存空间用于存储成员值,同时分配一部分内存空间存储关联值。

总结:关联值枚举所占空间:‘1个字节存储成员值’ + ‘N个字节存储关联值’。
 1)其中 N 为占用内存最大的关联值,上面的例子为 case hour(Int, Int, Int),8 * 3 = 24 字节;
 2)任何一个 case 的关联值都共用这 N 个字节。

原始值枚举的内存布局

创建 Sex 枚举类型,并添加测试代码,进行断点调试

实际占用: 1
实际分配: 1
内存对齐: 1

0x0000000100007718

sex = Sex.male 所占用的一字节中存储的值为:0

sex = .female 所占用的一字节中存储的值为:1

sex = .renyao 所占用的一字节中存储的值为:2

可以看出来,原始值枚举,其原始值并不占用枚举的内存空间,而只是分配了一个字节用于存储成员值。

总结:原始值枚举只会分配相应的内存空间(不超过256个成员的情况下分配一个字节)用于枚举成员值,而不会针对原始值分配空间。