Swift 枚举(enum)详解

Swift 枚举(enum)详解

[TOC]

本文将介绍Swift中枚举的一些用法和其底层原理的一些探索,以及探索一下OC中的枚举与Swift中枚举互相调用和枚举类型的内存占用情况。

1. 枚举

1.1 C中枚举

首先我们来看看C语言中枚举的写法。这里我们以一周7天作为示例。

普通写法:

enum Week {
    MON, TUE, WED, THU, FRI, SAT, SUN
}

以上就是C语言中枚举的常见写法enum关键字,加上枚举名称,大括号里面的不同的枚举值使用逗号分隔开来。此时的枚举值默认从0开始,依次是1,2,3……

自定义枚举值:

如果我们不想使用默认的枚举值,则可以这样写

enum Week {
    MON = 1, TUE, WED, THU, FRI, SAT, SUN
};

此时枚举值就会从1开始依次向后排列,你也可以给每个枚举都定义不同的枚举值,如果直接给TUE定义为2,而没给MON定义,则MON的枚举值会是0。

枚举变量的定义:

enum Week {
    MON = 1, TUE, WED, THU, FRI, SAT, SUN
};

enum Week week;

enum Week{
    MON = 1, TUE, WED, THU, FRI, SAT, SUN
}week;

enum{
    MON = 1, TUE, WED, THU, FRI, SAT, SUN
}week;

我们可以通过以上三种方法创建枚举变量:

  1. 创建一个枚举,然后声明一个枚举变量
  2. 创建一个枚举并声明一个枚举变量
  3. 也可以省略枚举名称,直接声明一个枚举变量

1.2 Swift中枚举

Swift中最常见的枚举写法:

enum Week{
    case MON
    case TUE
    case WED
    case THU
    case FRI
    case SAT
    case SUN
}

Swift中也可以简化为如下写法:

enum Week{
    case MON, TUE, WED, THU, FRI, SAT, SUN
}

Swift中枚举很强大,我们可以创建一个枚举值是String类型的enum,其实也不应该说是枚举值,而是枚举的RawValue

enum Week: String{
    case MON = "MON"
    case TUE = "TUE"
    case WED = "WED"
    case THU = "THU"
    case FRI = "FRI"
    case SAT = "SAT"
    case SUN = "SUN"
}

当然,如果我们不想写后面的字符串,也可以简写成如下的形式:

enum Week: String{
    case MON, TUE, WED, THU, FRI, SAT, SUN
}

定义枚举变量:

var w: Week = .MON

枚举变量的定义很简单,跟普通变量的定义没什么差别。

枚举的访问:

我们可以访问枚举的变量和枚举的rawValue
首先我们需要注意的是如果没有声明枚举的类型,是没有rawValue属性可以访问的。

image

一般情况下我们可以通过以下方式访问枚举:

enum Week: String{
    case MON, TUE, WED, THU, FRI, SAT, SUN
}

print(Week.MON)
print(Week.MON.rawValue)

打印结果如下:

image

1.3 枚举值和其RawValue的存储

在上面枚举的访问中我们可以看到关于MON字符串的打印,既然可以打印出来,说明是存储了相关的字符串的,那么是怎么存储的呢?又是怎么获取出来的呢?下面我们通过sil代码进行分析(使用如下命令生成并打开sil代码)

swiftc -emit-sil main.swift >> ./main.sil && open main.sil

为了方便分析我们将代码修改为如下:

enum Week: String{
    case MON, TUE, WED, THU, FRI, SAT, SUN
}
var w = Week.MON.rawValue
print(w)

执行生成sil代码的名后:

enum week : String {
  case MON
  case TUE
  case WED
  case SUN
  typealias RawValue = String
  init?(rawValue: String)
  var rawValue: String { get }
}

通过sil代码中对枚举的定义可以看到:

  1. Swift中一致的枚举
  2. 取了一个别名,也就是String类型是RawValue
  3. 添加了一个可选类型的init方法
  4. 一个计算属性rawValue,通过其get方法获取枚举的原始值

下面我们在main函数中看看:

image

关于w变量的初始化即分析注释写在了截图中。

  1. 首先创建一个全局变量w,并为变量w开辟内存地址
  2. 将枚举类型Week.MON存储到%5
  3. 将枚举Week的rawValue.getter函数存储到%6
  4. 调用%6中存储的函数,%5作为参数,返回值存储到%7
  5. 将%7中获取到额值存储到%3,至此变量w初始化完成

下面我们看看rawValuegetter方法:

image

我们可以看到在rawValuegetter方法中主要实现是:

  1. 通过接收到的枚举值去匹配一个分支
  2. 在分支中构建对于的String
  3. 返回上一步构建的String

那么这个字符串是从哪里来的呢?根据匹配的分支中的方法名称我们可以知道这是获取一个内置的字符串的字面量。其实就是从Mach-O文件的__TEXT.cstring中。下面我们通过查看Mach-O来验证。

image

所以说rawValue的值是通过调用枚举的rawValue。getter函数,从Mach-O对应的地址中取出字符串并返回。

那么枚举值呢?其实在上面关于rawValue探索的时候就可以知道了,枚举值在sil代码中就是:#Week.MON!enumelt,枚举值和rawValue本质上是不一样的,从下面的例子可以得到结论:

image

按照以上的写法是会报编译错误的。

1.4 枚举.init

1.4.1 触发方式

在上面的分析时我们知道枚举会有一个init方法,那么这个方法是什么时候调用的呢?我们添加如下符号断点:

image

添加如下代码:

var w: Week = .MON
print(w.rawValue)

运行后并没有触发该符号断点。

下面我们在添加如下代码:

var w = Week(rawValue: "MON")
print(w)

运行后即可触发符号断点:

image

所以这里init方法是为枚举通过rawValue初始化的时候调用的。

1.4.2 init分析

首先我们来看看如下代码的打印结果:

print(Week.init(rawValue: "MON"))
print(Week.init(rawValue: "Hello"))


Optional(SwiftEnum.Week.MON)
nil

从打印结果中可以看到,第一个输出的是可选值SwiftEnum.Week.MON,第二个是nil,很显然Hello不是我们的枚举,那么这些是怎么实现的呢?我们再次查看sil代码,此时我们可以直接看Week.init方法的实现。

Xnip2021-03-04_15-28-16 2

方法比较长,在里面添加了相关的注释和分析,折叠的代码基本上是与其上面的代码一致。现在总结如下:

  1. 首先开辟一块内存用于后续存储构建出来的枚举
  2. 通过_allocateUninitializedArray函数创建一个元组,元组中包含
    1. 与枚举个数大小一样的数组,用于存储枚举中的rawValue在本示例中是staticString
    2. 数组的首地址
  3. 开始一个一个的构建枚举rawValue存储到数组中
  4. 通过_findStringSwitchCase函数查找处要构建的枚举在数组中的位置index
  5. 从0到count-1依次与index作比较
    1. 如果相等则构建对于的枚举
    2. 如果不相等则构建一个Optional.none!enumelt的枚举
  6. 将构建的枚举存储到开辟的地址
  7. 最后返回构建的枚举

关于上面提到的两个函数源码可以Swift源码中找到

_allocateUninitializedArray源码:

@inlinable @inline(__always) @_semantics("array.uninitialized_intrinsic") public func _allocateUninitializedArray(_ builtinCount: Builtin.Word) -> (Swift.Array, Builtin.RawPointer) {
  let count = Int(builtinCount)
  if count > 0 {
    // Doing the actual buffer allocation outside of the array.uninitialized
    // semantics function enables stack propagation of the buffer.
    let bufferObject = Builtin.allocWithTailElems_1(
      _ContiguousArrayStorage.self, builtinCount, Element.self)

    let (array, ptr) = Array._adoptStorage(bufferObject, count: count)
    return (array, ptr._rawValue)
  }
  // For an empty array no buffer allocation is needed.
  let (array, ptr) = Array._allocateUninitialized(count)
  return (array, ptr._rawValue)
}

可以看到此处就是根据传入的countBuiltin.Word初始化一个数组,将其以元组的形式返回数组和数组首地址。

_findStringSwitchCase源码:

/// The compiler intrinsic which is called to lookup a string in a table
/// of static string case values.
@_semantics("findStringSwitchCase")
public // COMPILER_INTRINSIC
func _findStringSwitchCase(
  cases: [StaticString],
  string: String) -> Int {

  for (idx, s) in cases.enumerated() {
    if String(_builtinStringLiteral: s.utf8Start._rawValue,
              utf8CodeUnitCount: s._utf8CodeUnitCount,
              isASCII: s.isASCII._value) == string {
      return idx
    }
  }
  return -1
}

我们可以看到这里接收一个数组和要匹配的字符串,然后通过一个for循环匹配字符串,如果匹配到了则返回数组中对应的index,否则返回-1。

1.5 枚举的遍历

一般我们很少会对枚举进行遍历操作,在Swift中可以通过遵守CaseIterable协议来实现对枚举的遍历。

enum Week: String, CaseIterable{
    case MON, TUE, WED, THU, FRI, SAT, SUN
}
// 使用for循环遍历
var allCase = Week.allCases
for c in allCase{
    print(c)
}

// 函数是编程遍历
let allCase = Week.allCases.map({"\($0)"}).joined(separator: ", ")
print(allCase)

1.6 关联值

Swift中如果想要表示复杂的含义,可以在枚举中关联更多的信息。下面我们举个例子,如果需要有一个形状的枚举,里面有圆形和矩形。圆形有半径,矩形有长宽,那么这个枚举就可以写成如下代码:

enum Shape{
    case circle(radius: Double)
    case rectangle(width: Int, height: Int)
}
  • 其中括号中的radius以及widthheight就是关联值
  • 如果没枚举中使用关联值则枚举就没有rawValue属性了,因为关联值是一组值,而rawValue是单个值,可以通过sil代码验证
image

sil代码中我们并没有发现init方法RawValue别名以及rawValueget方法。

在这个枚举中radius、width、height这些都是自定义的标签,也可以不写,如下所示,但并不推荐这种方式,因为可读性非常差

enum Shape{
    case circle(Double)
    case rectangle(Int, Int)
}

那么有关联值的枚举该如何初始化呢?其实也很简单,下面我们就来创建一下

var shape = Shape.circle(radius: 10.0)
shape = Shape.circle(radius: 15)
shape = Shape.rectangle(width: 10, height: 10)

2. 其他用法

2.1 模式匹配

2.1.1 简单的模式匹配

顾明思议,模式匹配就是匹配每一个枚举值,通常我们可以使用switch语句来进行模式匹配。如果使用switch进行模式匹配:

  1. 必须列举当前所有可能的情况,否则就会报编译错误
  2. 如果不想匹配这么多case则可以使用defalut
  3. 在同一个case中可以列举多种情况
enum Week{
    case MON
    case TUE
    case WED
    case THU
    case FRI
    case SAT
    case SUN
}

var week = Week.MON

switch week {
    case .MON:
        print("周一")
    case .TUE:
        print("周二")
    case .WED:
        print("周三")
    case .SAT, .SUN:
        print("happy day")
    default : print("unknow day")
}

其实这个匹配也很简单,我们通过查看sil代码就可以知道:

image

2.1.2 关联值枚举的模式匹配

如果我们不关心关联值,关联值枚举的写法与普通枚举没有什么区别:

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

let shape = Shape.circle(radius: 10.0)

switch shape {
    case .circle:
        print("the shape is circle")
    case .rectangle:
        print("the shape is rectangle")
}

但是我们使用关联值枚举,肯定是会关心关联值的,当关心关联值时其写法如下:

switch shape{
    case let .circle(radius):
        print("circle radius: \(radius)")
    case .rectangle(let width, var height):
        height += 1
        print("rectangle width: \(width) height: \(height)")
}

可以发现,这里的每个case中都使用了let或者var,这里因为要使用关联值,所以需要使用let声明一下。或者放在最前面,或者对每个需要使用的变量前都添加let。如果使用var则可在当前case中修改其修饰的关联值。当然你也可以不使用枚举定义中的关联值的名字,可以自定义。

关于关联值枚举的模式匹配我们也可以看看sil代码:

image

2.1.3 其他匹配

有时候在业务逻辑处理中,我们只是想匹配单个case,我们可以这样写:

if case let Shape.circle(radius) = shape {
    print("circle radius: \(radius)")
}

当然如果我们只关心不同case的相同关联值时就可以这样写:

enum Shape{
    case circle(radius: Double)
    case rectangle(width: Double, height: Double)
    case square(width: Double, width: Double)
}

let shape = Shape.rectangle(width: 20, height: 10)

switch shape {
    case let .rectangle(x, 10), let .square(x, 10):
        print(x)
    default: break
}

此时的打印是20,对于上面的例子,必须caserectanglesquare,而且rectangle必须是10,square后面的width是10。

如果对于10的匹配不那么严格我们则可以使用通配符_

switch shape {
    case let .rectangle(x, _), let .square(x, _):
        print(x)
    default: break
}

注意: 以上命名必须一致,比如都使用x,如果一个x一个y就不行了。

2.2 枚举的嵌套

2.2.1 枚举嵌套枚举

顾名思义,枚举嵌套枚举就是在枚举中还有枚举,比如我们玩游戏时会有上下左右四个方向键,有时候也需要两两组合去使用,所以我们通过这个例子可以编写如下枚举:

enum CombineDirect{
    enum BaseDirect{
        case up
        case down
        case left
        case right
    }

    case leftUp(combineElement1: BaseDirect, combineElement2: BaseDirect)
    case rightUp(combineElement1: BaseDirect, combineElement2: BaseDirect)
    case leftDown(combineElement1: BaseDirect, combineElement2: BaseDirect)
    case rightDown(combineElement1: BaseDirect, combineElement2: BaseDirect)
}

使用起来也很简单:

let leftup = CombineDirect.leftUp(combineElement1: .left, combineElement2: .up)

2.2.1 结构体嵌套枚举

Swift允许在结构体中嵌套枚举,具体使用如下:

struct Skill{
   enum KeyType{
          case up
          case down
          case left
          case right
   }
   
    let key: KeyType
    
    func launchSkill(){
        switch key {
        case .left,.right:
            print("left, right")
        case .down,.up:
            print("up, down")
        }
    }
}

使用起来也很简单:

let s = Skill(key: .up)
s.launchSkill()

2.3 枚举中包含属性

Swift中允许在枚举中包含计算属性和类型属性,但不能包含存储属性。

enum Shape {
    case circle(radius: Double)
    case rectangle(width: Double, height: Double)
    
//    var radius: Double // Enums must not contain stored properties
    
    var width: Double{
        get {
            return 10.0
        }
    }
    static let height = 20.0
}

2.3 枚举中包含方法

Swift中的枚举也可以包含方法,可以是实例方法也可以是类方法

enum Week: Int{
    case MON, TUE, WED, THU, FRI, SAT, SUN
    
    mutating func nextDay(){
        if self == .SUN{
            self = Week.MON
        }else{
            self = Week(rawValue: self.rawValue+1)!
        }
    }
    
    static func test() {
        print("test")
    }
}

使用起来依旧很简单:

var w = Week.SUN
w.nextDay()
print(w) 

Week.test()

此处的方法都是静态调用:

image

image
image
image

2.4 indiret在枚举中的应用

2.4.1 indiret

如果我们想要使用enum表达一个复杂的关键数据结构的时候,我们可以通过使用indrect关键字来让enum更简洁。

比如我们想要通过枚举来表达一个链表的结构,链表需要存储数据以及指向它的下一个节点的指针,如果不使用indiret修饰则会报编译错误:

image

此时我们可以写成如下两种方式就不会报错了:

enum List {
    case end
    indirect case node(T, next: List)
}

indirect enum List {
    case end
    case node(T, next: List)
}

那么为什么要添加indirect关键字呢?

因为enum是值类型,它的大小在编译期就需要确定,如果按照开始的写法是不能够确定当前enum的大小的,所以从系统的角度来说,在不知道给enum分配多大的空间,所以就需要使用indirect关键字,官方文档是这样解释的:

You indicate that an enumeration case is recursive by writing indirect before it, which tells the compiler to insert the necessary layer of indirection.

译:您可以通过在枚举案例之前写indirect来表明枚举案例是递归的,这告诉编译器插入必要的间接层。

2.4.2 内存占用

我们打印一下使用indirect修饰的枚举内存占用是多少呢?

enum List {
    case end
    indirect case node(T, next: List)
}

print(MemoryLayout>.size)
print(MemoryLayout>.stride)


8
8

如果我们的泛型使用的是String呢?

print(MemoryLayout>.size)
print(MemoryLayout>.stride)


8
8

此时我们发现泛型的更换内存占用保持不变,此时我们创建一个使用indirect修饰的枚举类型的变量:

var node = List.node(10, next: List.end)

通过lldb查看node的内存:

image

可以看到node像一个对象的结构,所以说这里面存储的是一个指针,当不确定枚举类型大小的时候,将分配一个8字节大小的指针,指向一块堆空间用于存储这不确定大小的枚举。

如果是end,此时存储的就是case

image

那么这些是如何实现的呢?我们通过sil代码来看一下:

image

这里我们可以看到使用了alloc_box,我们打开SIL参考文档,并找到alloc-box

我们可以看到alloc_box就是在堆上分配一个引用计数@box,该值足够大,可以容纳T类型的值,以及一个retain count和运行时所需的任何其他元数据。

其本质是调用了swift_allocObject,这点可以通过汇编代码验证:

image

3. 枚举的大小

3.1 普通枚举大小分析

首先看看下面这段代码的打印结果:

enum NoMean{
    case a
}
print(MemoryLayout.size)
print(MemoryLayout.stride)


0
1

如果我们在增加一个case呢?

enum NoMean{
    case a
    case b
}
print(MemoryLayout.size)
print(MemoryLayout.stride)


1
1

如果在多增加几个呢?

enum NoMean{
    case a
    case b
    case c
    case d
    case e
}
print(MemoryLayout.size)
print(MemoryLayout.stride)


1
1

可以看到,打印结果还是1,所以普通枚举应该就是以1字节存储在内存中的,下面我们来分析一下:

首先我们添加如下代码:

var a = NoMean.a
var b = NoMean.b
var c = NoMean.c
var d = NoMean.d

lldb调试:

image

所以这里当前枚举的步长是1字节,也就意味着如果内存中连续存储NoMean,需要跨越一个字节的长度。一个字节也就是8位,最大可以表达255个数字。由于太长就不测试了,如果真的需要写255及以上,还是建议以别的方式优化一下。

如果枚举后面写了类型,比如:

enum NoMean: Int{
    case a
    case b
    case c
    case d
}

此时打印枚举的大小和步长还是1,这里面的类型指的是rawValue,并不是case的值。

  • 所以枚举中默认是以UInt8存储的,最大可以存储0~255,如果不够则会自动转换为UInt16,以此类推。
  • 当只有一个case的时候,size是0,表示这个枚举是没有意义的
  • 枚举中后面声明的类型只的是rawValue的类型,不会影响枚举的大小
  • 这些rawValue的值会存储在Mach-O文件中,在使用的时候取查找,这个在上面提到过,与枚举大小没有关系

3.2 关联值枚举的大小

如果枚举中有关联值,那么它的大小是多少呢?

enum Shape{
    case circle(radius: Int)
    case rectangle(width: Int, height: Int)
}
print(MemoryLayout.size)
print(MemoryLayout.stride)


17
24

从打印结果我们可以知道,具有关联值的枚举的大小取决于关联值的大小,此时circle中的关联值Int占用内存大小是8,而rectangle中两个Int加起来是16,那么打印的这个17是怎么来的呢?其实还有存储枚举值,所以枚举的大小此处枚举的size = 8+8+1 = 17,由于内存对齐,所以要分配8的整数倍,所以stride就是24。这是该枚举中最大需要的内存,这个内存足够容纳circle需要的9字节的大小。

下面我们,修改一下代码顺序,创建一下具有关联值的枚举,看看其内存分布:

image

我们可以看到circle是分配了24字节的内存空间的,内存分布首先是存储关联自,然后在存储枚举值,circle的枚举值是存储在第三个8字节上的,也就是存储在最后。

  • 具有关联值的枚举的大小取决于关联值的大小
  • 具有关联值的枚举的大小是枚举中最大的那个关联值枚举的大小 + 1(case 需要占用1字节),
  • 如果大于255可能需要占用2字节,这里没有进行测试

3.3 枚举嵌套枚举的大小分析

下面我们看看枚举嵌套枚举中内存占用的大小:

enum CombineDirect{
    enum BaseDirect{
        case up
        case down
        case left
        case right
    }

    case leftUp(combineElement1: BaseDirect, combineElement2: BaseDirect)
    case rightUp(combineElement1: BaseDirect, combineElement2: BaseDirect)
    case leftDown(combineElement1: BaseDirect, combineElement2: BaseDirect)
    case rightDown(combineElement1: BaseDirect, combineElement2: BaseDirect)
}

print(MemoryLayout.size)
print(MemoryLayout.stride)


2
2

根据打印结果我们可以知道嵌套的枚举,其实也就是枚举中关联了枚举,它的大小同样取决于关联值的大小,因为BaseDirect是基本的枚举,其内存占用为1,那么按照关联值枚举中的内存占用应该是1+1+1 = 3,那么为什么是2呢?

下面我们通过创建枚举变量,看看其内存分布是什么样的,首先添加如下代码:

// 2 0  3 0   2 1  3 1
var a = CombineDirect.leftUp(combineElement1: .left, combineElement2: .up)
var b = CombineDirect.rightUp(combineElement1: .right, combineElement2: .up)
var c = CombineDirect.leftDown(combineElement1: .left, combineElement2: .down)
var d = CombineDirect.rightDown(combineElement1: .right, combineElement2: .down)

lldb查看内存

image

通过lldb调试的结果我们可以看到,在每个字节的低4位上存储着关联值的值,而在最后那个关联值的高四位分别存储了0,4,8,12(c),所以对于枚举中嵌套枚举应该是做了相应的优化,借用未使用的高位存储关联值枚举的枚举值。

下面我们测试一下,多写几个:

enum CombineDirect{
    enum BaseDirect{
        case up
        case down
        case left
        case right
    }
    
    case upup(combineElement1: BaseDirect, combineElement2: BaseDirect)
    case updown(combineElement1: BaseDirect, combineElement2: BaseDirect)
    case upleft(combineElement1: BaseDirect, combineElement2: BaseDirect)
    case upright(combineElement1: BaseDirect, combineElement2: BaseDirect)
    case downup(combineElement1: BaseDirect, combineElement2: BaseDirect)
    case downdown(combineElement1: BaseDirect, combineElement2: BaseDirect)
    case downleft(combineElement1: BaseDirect, combineElement2: BaseDirect)
    case downright(combineElement1: BaseDirect, combineElement2: BaseDirect)
    case leftUp(combineElement1: BaseDirect, combineElement2: BaseDirect)
    case leftDown(combineElement1: BaseDirect, combineElement2: BaseDirect)
    case leftleft(combineElement1: BaseDirect, combineElement2: BaseDirect)
    case leftright(combineElement1: BaseDirect, combineElement2: BaseDirect)
    case rightup(combineElement1: BaseDirect, combineElement2: BaseDirect)
    case rightdown(combineElement1: BaseDirect, combineElement2: BaseDirect)
    case rightleft(combineElement1: BaseDirect, combineElement2: BaseDirect)
    case rightright(combineElement1: BaseDirect, combineElement2: BaseDirect)
}

print(MemoryLayout.size)
print(MemoryLayout.stride)

var a = CombineDirect.upup(combineElement1: .up, combineElement2: .up)
var b = CombineDirect.updown(combineElement1: .up, combineElement2: .down)
var c = CombineDirect.upleft(combineElement1: .up, combineElement2: .left)
var d = CombineDirect.upright(combineElement1: .up, combineElement2: .right)
var e = CombineDirect.downup(combineElement1: .down, combineElement2: .up)
var f = CombineDirect.downdown(combineElement1: .down, combineElement2: .down)
var g = CombineDirect.downleft(combineElement1: .down, combineElement2: .left)
var h = CombineDirect.downright(combineElement1: .down, combineElement2: .right)
var i = CombineDirect.leftUp(combineElement1: .left, combineElement2: .up)
var j = CombineDirect.leftDown(combineElement1: .left, combineElement2: .down)
var k = CombineDirect.leftleft(combineElement1: .left, combineElement2: .left)
var l = CombineDirect.leftright(combineElement1: .left, combineElement2: .right)
var m = CombineDirect.rightup(combineElement1: .right, combineElement2: .up)
var n = CombineDirect.rightdown(combineElement1: .right, combineElement2: .down)
var o = CombineDirect.rightleft(combineElement1: .right, combineElement2: .left)
var p = CombineDirect.rightright(combineElement1: .right, combineElement2: .right)

lldb调试结果:

image

此时我们发现,最后那个关联值的高四位分别存储了0,1,2,3,4,5,6,7,8,9,a,b,c,d,e,f。

如果我们随便注释几个:

image

我们发现,对应的结果与枚举中的顺序是一致的。

如果只剩一个,但不是第一个:

image

此时我们发现是7,与枚举中的属性还是一致的。

下面我们开始注释枚举中的:

最后发现:

  • 如果是两个枚举就是0,8
  • 如果是3或4的时候是按照0,4,8,c
  • 大约4小于等于16的时候是0,1,2,3,4......f
  • 如果大于16就不能看出上面的规律了,所以从二进制位看

对于枚举中嵌套枚举,使用关联值,又或者说不嵌套,具有关联值的枚举中的关联值是枚举类型的时候,会优先借用最后关联的那个枚举的二进制位存储具有关联值枚举的值,借用的位数为关联值枚举的个数小于等于2的幂最小值,也就是2的几次幂才能大于等于关联枚举的个数。

这里我有进一步测试,如果普通枚举的个数不足以使用低四位表示,比如低四位最少表示16个,如果多了的话,就会借用关联值中倒数第二个,也就上面例子中的第一个关联值的高位进行借位存储。按照这个逻辑,大胆猜想,如果普通枚举的个数为256个,也就是不能借任何一个位,这种具有关联值枚举是不是会另外开辟内存存关联值枚举的值?其实不需要是256个,只要不够借的时候就会开辟内存去存储关联值枚举的值。

举个例子:

enum BaseDirect{
    case up
    case down
    case left
    case right
    case a
    case b
    case c
    case d
    case e
    case f
    case g
    case h
    case i
    case j
    case k
    case l,m,n,o,p,q,r,s,t,u,v,w,x,y,z
    case l1,m1,n1,o1,p1,q1,r1,s1,t1,u1,v1,w1,x1,y1,z1
    case l2,m2,n2,o2,p2,q2,r2,s2,t2,u2,v2,w2,x2,y2,z2
    case l3,m3,n3,o3,p3,q3,r3,s3,t3,u3,v3,w3,x3,y3,z3
}

enum CombineDirect{
    case upup(combineElement1: BaseDirect, combineElement2: BaseDirect)
    case updown(combineElement1: BaseDirect, combineElement2: BaseDirect)
    case upleft(combineElement1: BaseDirect, combineElement2: BaseDirect)
    case upright(combineElement1: BaseDirect, combineElement2: BaseDirect)
    case downup(combineElement1: BaseDirect, combineElement2: BaseDirect)
    case downdown(combineElement1: BaseDirect, combineElement2: BaseDirect)
    case downleft(combineElement1: BaseDirect, combineElement2: BaseDirect)
    case downright(combineElement1: BaseDirect, combineElement2: BaseDirect)
    case leftUp(combineElement1: BaseDirect, combineElement2: BaseDirect)
    case leftDown(combineElement1: BaseDirect, combineElement2: BaseDirect)
    case leftleft(combineElement1: BaseDirect, combineElement2: BaseDirect)
    case leftright(combineElement1: BaseDirect, combineElement2: BaseDirect)
    case rightup(combineElement1: BaseDirect, combineElement2: BaseDirect)
    case rightdown(combineElement1: BaseDirect, combineElement2: BaseDirect)
    case rightleft(combineElement1: BaseDirect, combineElement2: BaseDirect)
    case rightright(combineElement1: BaseDirect, combineElement2: BaseDirect)
    case aa(combineElement1: BaseDirect, combineElement2: BaseDirect)
}

print(MemoryLayout.size)
print(MemoryLayout.stride)


3
3

这个例子并没有嵌套,其实与嵌套没有任何关系,嵌套的枚举也是单独存储的,只不过嵌套的枚举作用域只在嵌套的大括号内。

另外,如果枚举值过多的时候,我们看sil代码:

image

此时我们可以发现:

  • 多了一个hashValue的计算属性
  • 一个遵守了Equatable协议的__derived_enum_equalsimp
  • 以及一个hash函数

我猜想,对于过多case的枚举,swift为了更好更快的匹配,使用了苹果惯用的哈希。我尝试在源码中搜索了一下derived_enum_equals并没有找到相关方法,貌似是过期被移除了,后面使用==代替。

3.4 结构体嵌套枚举的大小分析

首先还是看一下打印结果:

struct Skill{

   enum KeyType{
          case up
          case down
          case left
          case right
   }


    let key: KeyType

    func launchSkill(){
        switch key {
        case .left,.right:
            print("left, right")
        case .down,.up:
            print("up, down")
        }
    }
}
print(MemoryLayout.size)
print(MemoryLayout.stride)


1
1

如果只是嵌套了枚举呢?

struct Skill{
   enum KeyType{
          case up
          case down
          case left
          case right
   }
}

print(MemoryLayout.size)
print(MemoryLayout.stride)


0
1

如果添加了其他属性,则打印结果与添加的属性类型有关系,这里就不一一验证了。

总的来说,结构体中嵌套枚举与枚举嵌套枚举是一样的,他们都不存储枚举,只是作用域在其中而已。

4. 与OC混编

综上,我们可以看到Swift中的枚举非常强大,而在OC中枚举仅仅只是一个整数值,那么在与OC混编的时候,该如何在OC中使用Swift的枚举呢?下面我们就来探索一下:

4.1 OC调用Swift中的枚举

如果想要在OC中使用Swift枚举要求会很严格:

  1. 使用@objc标记
  2. 类型必须是Int,也就是SwiftrawValue
  3. 必须导入import Foundation

也就是这样:

import Foundation

@objc enum Week: Int{
    case MON, TUE, WED, THU, FRI, SAT, SUN
}

此时编译后就可以正在project-Swift.h中看到转换后的对应的OC枚举:

image
image

调用的话就是:

4.2 Swift调用OC中的枚举

4.2.1 NS_ENUM

NS_ENUM(NSInteger, OCENUM){
    Value1,
    Value2
};

如果使用NS_ENUM创建的枚举会自动转换成swift中的enum

可以在ocfileName.h中查看转换后的枚举:

image
image

使用的话需要在桥接文件中导入OC头文件,然后在swift中使用:

let value = OCENUM.Value1

4.2.2 使用typedef enum

typedef enum {
    Enum1,
    Enum2,
    Enum3
}OCENum;

如果使用typedef enum这种形式的枚举,会转换成结构体,同样可以在ocfileName.h中查看转换后的结果,转换后的代码如下:

image

可以看到里面有一个rawValue属性,以及init方法。还遵守了Equatable, RawRepresentable两个协议。

使用的话也是需要导入头文件:

let num = OCEnum(0)
let num1 = OCEnum.init(0)
let num2 = OCEnum.init(rawValue: 3)

print(num)


OCNum(rawValue: 0)

这里我们只能通过init方法去初始化,不能访问枚举中的变量。

4.2.3 使用typedef NS_ENUM

typedef NS_ENUM(NSInteger, OCENUM){
    OCEnumInvalid = 0,
    OCEnumA = 1,
    OCEnumB,
    OCEnumC
};

使用typedef NS_ENUM也会自动转换为Swift的枚举,转换后的代码如下:

image

使用也是需要导入头文件:

let ocenum = OCENUM.OCEnumInvalid
let ocenumRawValue = OCENUM.OCEnumA.rawValue

4.3 混编时需要使用String类型的枚举

这里的意思是,Swift中需要使用String类型的枚举,但是又要与OC混编,暴露给OC使用。

如果直接声明为String类型编译时不会通过的,这里只能弄个假的。Swift中的枚举还是声明为Int,可以在枚举中声明一个变量或者方法,用于返回想要的字符串。

@objc enum Week: Int{
    case MON, TUE, WED
    
    var value: String?{
            switch self {
            case .MON:
                return "MON"
            case .TUE:
                return "TUE"
            case .WED:
                return "WED"
            default:
                return nil
            }
        }
    
    func weekName() -> String? {
        switch self {
        case .MON: return "MON"
        case .TUE: return "TUE"
        case .WED: return "WED"
        default:
            return nil
       }
    }
}

用法:


Week mon = WeekMON;


let value = Week.MON.value
let value1 = Week.MON.weekName()

5. 总结

其实感觉该篇不太适合总结,因为直接给结论会使人不是很好理解,但是还是记录一下吧。

  1. Swift中的枚举很强大
  2. enum中的rawValue是其中的计算属性
  3. 如果声明的时候不指定枚举类型就没有rawValue属性(包括关联值)
  4. rawValue中的值存储在Mach-O中,不占用枚举的存储空间
  5. 枚举值与rawValue不是同一个东西
  6. rawValue可以不写,如果是Int默认0,1,2...String等于枚举名称的字符串
  7. 如果枚举中存在rawValue同时也会存在init(rawValue:)方法,用于通过rawValue值初始化枚举
  8. 如果枚举遵守了CaseIterable协议,且不是关联值的枚举,我们可以通过enum.allCases获取到所有的枚举,然后通过for循环遍历
  9. 我们可以使用switch对枚举进行模式匹配,如果只关系一个枚举还可以使用if case
  10. 关联值枚举可以表示复杂的枚举结构
  11. 关联值的枚举没有init方法,没有RawValue别名,没有rawValue计算属性
  12. enum可以嵌套enum,被嵌套的作用域只在嵌套内部
  13. 结构体也可以嵌套enum,此时enum的作用域也只在结构体内
  14. enum中可以包含计算属性类型属性但不能包含存储属性
  15. enum中可以定义实例方法和使用static修饰的方法,不能定义class修饰的方法
  16. 如果想使用复杂结构的枚举,或者说是具有递归结构的枚举可以使用indirect关键字
  • 关于枚举的大小

    1. 默认情况下枚举占用1字节也就是UInt8,如果不够用也就是超过256个的时候会使用UInt16,UInt32... (太多没有去验证,如果真的有这么多枚举,建议通过其他方式去优化)
    2. 如果枚举个数过多会使用哈希来进行优化,以便快速匹配
    3. 使用关联值的枚举大小取决于关联值的大小,还要考虑内存对齐
    4. 关联值的枚举中的关联值如果是普通枚举类型,系统会通过借位优化的方式节省内存的占用
    5. 如果借位不够了,会单独开辟内存存储关联值枚举的枚举值
    6. 在嵌套的时候,无论结构体还是枚举中都是不占用内存的,被嵌套的枚举是单独存储的,只是作用域在其内部而已
  • 关于和OC混编

    1. OC中只能使用SwiftInt类型的枚举
    2. 需要使用@objc关键字进行修饰
    3. 还要import Foundation
    4. swift中使用OC中的NS_ENUM的枚举就跟普通枚举一致
    5. 如果使用OCtypedef enum枚举则需要通过init方法进行初始化

你可能感兴趣的:(Swift 枚举(enum)详解)