Swift 枚举

作者: 深圳_你要的昵称 | 来源:发表于2021-02-01 15:11 被阅读0次

    前言

    本篇文章将讲述Swift中很常用的也很重要的一个知识点 👉 Enum枚举。首先会介绍与OC中枚举的差别,接着会从底层分析Enum的使用场景,包含枚举的嵌套递归与OC的混编的场景,最后分析枚举的内存大小的计算方式,希望大家能够掌握。

    一、OC&Swift枚举的区别

    1.1 OC中的NS_ENUM

    OC中的枚举和C/C++中的枚举基本一样,具有以下特点👇

    • 仅支持Int类型,默认首元素值为0,后续元素值依次+1
    • 中间的元素有赋值,那么以此赋值为准,后续没赋值的元素值依旧依次+1
      枚举使用示例代码👇
    typedef NS_ENUM(NSInteger, WEEK) {
        Mon,
        Tue = 10,
        Wed,
        Thu,
        Fri,
        Sat,
        Sun
    };
    // 调用代码👇
    WEEK a = Mon; 
    WEEK b = Tue;
    NSLog(@"a = %d, b = %d", (int)a, (int)b);
    

    1.2 Swift中的Enum

    Swift中的枚举比OC的强大很多!其特点如下👇

    • 格式: 不用逗号分隔,类型需使用case声明
    • 内容:
      1. 支持Int、Double、String基础类型,也有默认枚举值String类型默认枚举值为key的名称Int、Double数值型默认枚举值为0开始+1递增
      2. 支持自定义选项 👉 不指定支持类型,就没有rawValue,但同样支持case枚举,可自定义关联内容

    注意:rawValue在后面枚举的访问中会详细的讲解。

    示例代码👇

    // 写法一
    // 不需要逗号隔开
    enum Weak1 {
        case MON
        case TUE
        case WED
        case THU
        case FRI
        case SAT
        case SUN
    }
    
    // 写法二
    // 也可以直接一个case,然后使用逗号隔开
    enum Weak2 {
        case MON, TUE, WED, THU, FRI, SAT, SUN
    }
    
    // 定义一个枚举变量
    var w: Weak1 = .MON
    

    1.2.2 自定义选项类型

    如果在声明枚举时不指定类型,那么可给枚举项添加拓展内容(即自定义类型)。switch-case访问时,可取出拓展类型进行相应的操作。例如👇

    // 自定义类型的使用
    enum Shape {
        case square(width: Double)
        case circle(radius: Double, borderWidth:Double)
    }
    
    func printValue(_ v: Shape) {
        // switch区分case(不想每个case处理,可使用default)
        switch v {
        case .square(let width):
            print(width)
        case .circle(let radius, _):
            print(radius)
        }
    }
    
    let s = Shape.square(width: 10)
    let c = Shape.circle(radius: 20, borderWidth: 1)
    printValue(s)
    printValue(c)
    

    二、Swift枚举的使用

    接下来我们看看Swift枚举的使用,包含一些特殊的场景的情况。

    2.1 枚举的访问

    说到枚举的访问,就必须得提一个关键字rawValue,使用案例👇

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

    运行结果👇


    注意:如果enum没有声明类型,是没有rawValue属性的👇

    现在问题来了 👉 rawValue对应在底层是如何做到读取到MON值的?

    rawValue的取值流程

    老规矩,找入口,之前我们都是查看SIL,当然这里也不例外👇

    swiftc -emit-sil xx.swift | xcrun swift-demangle >> ./xx.sil && vscode xx.sil

    先看看枚举Week👇

    接着看看main函数的流程👇

    最后看看rawValue的getter方法👇

    然后看bb8代码段👇

    至此,我们现在知道了,rawValue的底层就是调用的getter方法,getter方法中构造了字符串,但是这个字符串的值(例如“MON”)从哪里取出的呢?其实我们能猜出来,应该是在编译期确定了的,所以,我们打开工程的exec可执行文件,查看Mach-O👇

    可见,在__TEXT, __cstring的section段,这些字符串在编译期已经存储好了,而且内存地址是连续的。所以,rawValue的getter方法 👉 case分支中构建的字符串,主要是在Mach-O文件中从对应地址取出的字符串,然后再返回给变量w

    case值 & rawValue值

    现在我们弄清楚了rawValue值的来源,那么又有一个问题:枚举case值和 rawValue值如何区分呢?下面的代码输出打印结果是什么?

    //输出 case值
    print(Weak.MON)
    //输出 rawValue值
    print(Weak.MON.rawValue)
    

    虽然输出的都是MON,但其实并不是相同的,why?看下图👇

    上图可知,并不能将枚举的case值赋给字符串类型常量w,同时,也不能将字符串"MON"赋给枚举值t

    2.2 枚举初始化init

    在OC中,枚举没有初始化一说,而在Swift中,枚举是有init初始化方法👇

    Weak.init(rawValue:)
    

    接下来我们来看看这个初始化方法在底层的流程,首先添加代码👇,打上断点

    print(Weak.init(rawValue: "MON")!)
    

    打开汇编,运行👇

    接着我们还是看SIL代码,关于Weak.init(rawValue:)部分👇

    其中,上图中涉及的SIL的指令释义👇

    指令名称 指令释义
    index_addr 获取当前数组中的第n个元素值的地址(即指针),存储到当前地址中
    struct_extract 表示在结构体中取出当前的Int值,Int类型在系统中也是结构体
    cond_br 表示比较的表达式,即分支条件跳转(类似于三元表达式)

    接着来看看这个关键的函数_findStringSwitchCase的源码👇

    我们继续看Weak.init的最终处理代码 👉 bb29代码段👇

    至此,我们分析完了Weak.init的底层流程,于是修改之前的调用代码(去掉了之前的感叹号!)👇

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

    编译器会爆出警告(返回的结果是可选型),运行结果👇

    所以,现在我们就能明白,为什么一个打印的是可选值,一个打印的是nil。

    2.3 枚举遍历:CaseIterable协议

    CaseIterable协议,有allCases属性,支持遍历所有case,例如👇

    // Double类型
    enum Week1: Double, CaseIterable {
        case Mon,Tue, Wed, Thu, Fri, Sat, Sun
    }
    Week1.allCases.forEach { print($0.rawValue)}
    
    // String类型
    enum Week2: String, CaseIterable {
        case Mon,Tue, Wed, Thu, Fri, Sat, Sun
    }
    Week2.allCases.forEach { print($0.rawValue)}
    

    2.4 枚举关联值

    关联值就是上面讲过的自定义类型的枚举,它能表示更复杂的信息,与普通类型的枚举不同点在于👇

    1. 没有rawValue
    2. 没有rawValue的getter方法
    3. 没有初始化init方法

    例如

    // 自定义类型的使用
    enum Shape {
        case square(width: Double)
        case circle(radius: Double, borderWidth:Double)
    }
    

    查看其SIL代码👇

    中间层代码真的什么都没有!😂

    2.5 模式匹配

    模式匹配就是针对case的匹配,根据枚举类型,分为2种:

    1. 简单类型的枚举的模式匹配
    2. 自定义类型的枚举(关联值)的模式匹配

    2.5.1 简单类型

    swift中的简单类型enum匹配需要将所有情况都列举,或者使用default表示默认情况,否则会报错

    enum Weak: String{
        case MON
        case TUE
        case WED
        case THU
        case FRI
        case SAT
        case SUN
    }
    
    var current: Weak?
    switch current {
        case .MON:print(Weak.MON.rawValue)
        case .TUE:print(Weak.MON.rawValue)
        case .WED:print(Weak.MON.rawValue)
        default:print("unknow day")
    }
    

    如果去掉default,会报错👇

    我们看看SIL代码👇

    所以运行上面代码,应该匹配的是default分支👇

    2.5.2 关联值类型

    关联值类型的模式匹配有两种方式👇

    • switch - case 👉 匹配所有case
    • if - case 👉 匹配单个case
    switch - case
    enum Shape{
        case circle(radius: Double)
        case rectangle(width: Int, height: Int)
    }
    

    let修饰case值👇

    let shape = Shape.circle(radius: 10.0)
    switch shape{
        //相当于将10.0赋值给了声明的radius常量
        case let .circle(radius):
            print("circle radius: \(radius)")
        case let .rectangle(width, height):
            print("rectangle width: \(width) height: \(height)")
    }
    

    也可以let var修饰关联值的入参👇

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

    查看SIL层的代码,看看是怎么匹配的👇

    if - case
    let circle = Shape.circle(radius: 10.0)
    if case let Shape.circle(radius) = circle {
        print("circle radius: \(radius)")
    }
    
    通用关联值

    如果只关心不同case下的某一个关联值,可以将该关联值用同一个入参替换,例如下面例子中的x👇

    enum Shape{
        case circle(radius: Double)
        case rectangle(width: Double, height: Double)
        case square(width: Double, height: Double)
    }
    let shape = Shape.circle(radius: 10)
    switch shape{
    case let .circle(x), let .square(20, x):
        print(x)
    default:
        print("未匹配")
        break
    }
    

    注意:不能使用多于1个的通用入参,例如下面的y👇

    也可以使用通配符 _👇

    let shape = Shape.rectangle(width: 10, height:20)
    switch shape{
    case let .rectangle(_, x), let .square(_, x):
        print("x = \(x)")
    default:
        break
    }
    

    还可以这么写👇

    let shape = Shape.rectangle(width: 10, height:20)
    switch shape{
    case let .rectangle(x, _), let .square(_, x):
        print("x = \(x)")
    default:
        break
    }
    

    大家平时在使用枚举时,还是要注意下面2点👇

    1. 枚举使用过程中不关心某一个关联值,可以使用通配符_标识
    2. OC只能调用Swift中Int类型的枚举

    2.6 支持计算型属性 & 函数

    Swift枚举中还支持计算属性函数,例如👇

    enum Direct: Int {
        case up
        case down
        case left
        case right
        
        // 计算型属性
        var description: String{
            switch self {
            case .up:
                return "这是上面"
            default:
                return "这是\(self)"
            }
        }
        
        // 函数
        func printSelf() {
            print(description)
        }
    }
    
    Direct.down.printSelf() 
    

    三、枚举嵌套

    枚举的嵌套主要有2种场景👇

    1. 枚举嵌套枚举
    2. 结构体嵌套枚举

    3.1 enum嵌套enum

    我们先来看看枚举嵌套枚举,我们继续改下上面的例子👇

    enum CombineDirect{
        //枚举中嵌套的枚举
        enum BaseDirect{
            case up
            case down
            case left
            case right
        }
        //通过内部枚举组合的枚举值
        case leftUp(baseDirect1: BaseDirect, baseDirect2: BaseDirect)
        case leftDown(baseDirect1: BaseDirect, baseDirect2: BaseDirect)
        case rightUp(baseDirect1: BaseDirect, baseDirect2: BaseDirect)
        case rightDown(baseDirect1: BaseDirect, baseDirect2: BaseDirect)
    }
    

    如果初始化一个左上方向,代码👇

    let leftUp = CombineDirect.leftUp(baseDIrect1: CombineDirect.BaseDirect.left, baseDirect2: CombineDirect.BaseDirect.up)
    

    3.2 struct嵌套enum

    接下来就是结构体嵌套枚举了,例如👇

    //结构体嵌套枚举
    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 .up, .down:
                print("up, down")
            }
        }
    }
    

    3.3 枚举的递归(indirect)

    还有一种特殊的场景 递归 👉 枚举中case关联内容使用自己的枚举类型。例如👇

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

    一个结构,其左右节点的类型也是自己本身,这时编译器会报错👇

    报错原因 👉 使用该枚举时,enum的大小需要case来确定,而case的大小又需要使用到enum大小。所以无法计算enmu的大小,于是报错!
    安排 👉 根据编译器提示,需要使用关键字indirect,意思就是将该枚举标记位递归,同时也支持标记单个case,所以可以👇

    那么问题来了,indirect在底层干了什么呢?

    indirect底层原理

    我们先来看这个例子👇

    enum List<T>{
        case end
        indirect case node(T, next: List<T>)
    }
    
    var node = List<Int>.node(10, next: List<Int>.end)
    
    print(MemoryLayout.size(ofValue: node))
    print(MemoryLayout.stride(ofValue: node))
    

    size和stride都是8,换成String类型👇

    仍然也是8,看来枚举的大小不受其模板类型大小的影响。

    lldb分析

    我们先lldb看看其内存的分布👇

    上图中我们发现,node的metadata对应的地址0x0000000100562660是分配在上的,所以,indirect关键字其实就是通知编译器,需要分配一块堆区的内存空间,用来存放enumcase。此时case为node时,存储的是引用地址0x0000000100562660,而case为end时,则👇

    那为何说地址是堆区呢?我们接着看看SIL代码👇

    SIL代码中,是通过alloc_box申请的内存,alloc_box底层调用的是swift_allocObject,所以是堆区,我们可以再node打上断点,查看汇编👇

    四、##swift和OC混编枚举

    接下来我们看看swift和OC的枚举的混编场景。

    4.1 OC使用Swift枚举

    首先看看OC调用Swift枚举,那么此时枚举必须具备以下2个条件👇

    1. @objc关键字标记enum
    2. 当前enum必须是Int类型
    // Swift中定义枚举
    @objc enum Weak: Int{
        case MON, TUE, WED, THU, FRI, SAT, SUN
    }
    
    // OC使用
    - (void)test{
        Weak mon = WeakMON;
    }
    

    4.2 Swift使用OC枚举

    反过来,就没限制了,OC中的枚举会自动转换成swift中的enum。

    // OC定义
    NS_ENUM(NSInteger, OCENUM){
        Value1,
        Value2
    };
    
    // swift使用
    //1、将OC头文件导入桥接文件
    #import "OCFile.h"
    //2、使用
    let ocEnum = OCENUM.Value1
    
    typedef enum
    // OC定义
    typedef enum {
        Num1,
        Num2
    }OCNum;
    
    // swift使用
    let ocEnum = OCNum.init(0)
    print(ocEnum)
    

    上图可知,通过typedef enum定义的enum,在swift中变成了一个结构体,并遵循了两个协议:EquatableRawRepresentable

    typedef NS_ENUM
    // OC定义
    typedef NS_ENUM(NSInteger, OCNum) {
        Num1,
        Num2
    };
    // swift使用
    let ocEnum = OCNum.init(rawValue: 0)
    print(ocEnum!)
    

    那么自动生成的swift中是这样👇

    并没有遵循任何协议!

    4.3 OC使用Swift中String类型的枚举

    这也是一种常见的场景,解决方案👇

    1. swift中的enum尽量声明成Int整型
    2. 然后OC调用时,使用的是Int整型的
    3. enum再声明一个变量/方法,用于返回固定的字符串,给swift中使用
      示例👇
    @objc enum Weak: Int{
        case MON, TUE, WED
        
        var val: String?{
            switch self {
            case .MON:
                return "MON"
            case .TUE:
                return "TUE"
            case .WED:
                return "WED"
            default:
                return nil
            }
        }
    }
    
    // OC中使用
    Weak mon = WeakMON;
    
    // swift中使用
    let weak = Weak.MON.val
    

    五、枚举的大小

    主要分析以下几种情况👇

    1. 普通enum
    2. 具有关联值的enum
    3. enum嵌套enum
    4. struct嵌套enum

    枚举的大小也是面试中经常问到的问题,重点在于两个函数的区别👇

    size: 实际占用内存大小
    stride:系统分配的内存大小

    5.1 普通enum

    最普通的情况,即非嵌套,非自定义类型的枚举,例如👇

    enum Weak {
        case MON
    }
    
    print(MemoryLayout<Weak>.size)
    print(MemoryLayout<Weak>.stride)
    

    再添加一个case,运行👇

    继续增加多个case,运行👇

    以上可以看出,当case个数为1时,枚举size为0,个数>=2时,size的大小始终是1,why?下面我们来分析分析👇

    上图打断点,读取内存可以看出,case都是1字节大小,1个字节是8个byte,按照二进制转换成十进制,那么有255种排列组合(0x00000000 - 0x11111111),所以当case为1个的时候,size的大小是0(二进制是0x0),case数<=255时,size都是1。而超过255个时,会自动扩容sizestride都会增加

    5.2 具有关联值的enum

    如果是自定义类型的枚举,即关联值类型,size和stride的值会发生什么变化呢?看下面的例子👇

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

    看来关联值的枚举大小和关联值入参有关系,👇

    5.3 enum嵌套enum

    枚举嵌套枚举,是一种特殊的情况,下面示例大小是多少?👇

    enum CombineDirect{
        enum BaseDirect{
            case up, down, left, right
        }
        
        case leftUp(baseDirect1: BaseDirect, baseDirect2: BaseDirect)
        case rightUp(baseDirect1: BaseDirect, baseDirect2: BaseDirect)
        case leftDown(baseDirect1: BaseDirect, baseDirect2: BaseDirect)
        case rightDown(baseDirect1: BaseDirect, baseDirect2: BaseDirect)
    }
    
    print(MemoryLayout<CombineDirect>.size)
    print(MemoryLayout<CombineDirect>.stride)
    

    从结果中可以看出,enum嵌套enum,和具有关联值的enum的情况是一样的,同样取决于关联值的大小,其内存大小是最大关联值的大小
    接着我们看看具体的分布,可以先定义一个变量👇

    var combine = CombineDirect.leftDown(baseDirect1: .left, baseDirect2: .down)
    

    lldb查看其内存分布👇

    将第一个入参left 改为 up👇

    所以,02表示是caseleftDown的关联值的第一个入参的枚举值,那第2个入参是.down,按照规律来算应该是01,但却是81,why?接下来我们看看81代表的是什么值?

    • 在enum CombineDirect中多加4个case

    结果是c1

    • 减少一个case

    减少一个case项后,是a1

    • 再减少一个case

    再减少一个是81,说明81中的8

    • 继续减少,保证case只有2个

    结果页是81

    • 添加保证case>10个

    如果leftDown 的case索引值大于8,例如上图,leftDown是第1个case,结果e1中的e就是15(十进制),即case选项的索引值。

    • 再减少,保证case leftDown在第9个

    果然,上图中leftDown的case索引值是9,打印出来的91中的第一位也是9

    综上, 81中的第一位8这个值,有以下几种情况区分👇

    1. 当嵌套enum的case只有2个时,case在内存中的存储是0、8
    2. 当嵌套enum的case大于2,小于等于4时,case在内存中的存储是0、4、8、12
    3. 当嵌套enum的case大于4时,case在内存中的存储是从0、1、2...类推

    81中的1,代表什么意思呢?我们改变下关联值入参👇

    所以,leftDown减少一个入参,结果是80,加一个入参,结果是01 80,继续再加一个入参👇

    关联值的入参是up,down,right,right,对应的枚举值是0,1,3,3,所以可以得出结论👇

    1. enum嵌套enum同样取决于最大case的关联值大小
    2. case中关联值的内存分布,又是根据入参的个数大小来分布的
      2.1 每个入参占一个字节大小的空间(即2个byte位),第2位byte里面存储的是内层枚举的case值,第1位的byte值通常是0
      2.2 最后一个入参的byte空间分布 👉 第2位是内层枚举的case值,第1位是外层枚举的case值,其规律又如下👇
    • 当外层enum的case只有2个时,第1位byte值按照0、8依次分布
    • 当外层enum的case个数>2,<=4时,第1位byte值按照0、4、8、12依次分布
    • 当外层enum的case个数>4时,第1位byte值按照0、1、2、3、...依次分布

    5.4 struct嵌套enum

    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 .up, .down:
                print("up, down")
            }
        }
    }
    print(MemoryLayout<Skill>.size)
    print(MemoryLayout<Skill>.stride)
    

    size和stride都是1。结构体的大小计算,跟函数无关,所以只看成员变量key的大小,key是枚举Skill类型,大小为1,所以结构体大小为1。继续,去掉成员key👇

    没有任何成员变量时,size为0,stride为1(系统默认分配的)。如果加一个成员👇

    因为添加的是UInt8,占1个字节,所以size和stride都+1,均为2。再添加一个成员👇

    添加的成员是Int类型,占8字节,8+1+1=10,而stride是系统分配的,8的倍数来分配,所以是16。你以为就这么简单的相加吗?我们换一下width的位置👇

    将width成员放到最后面,size变为16,why?因为size的大小,是按照结构体内存对齐原则来计算的,可参考我之前的文章内存对齐分析

    总结

    本篇文章主要讲解了Swift中的枚举,开始与OC的枚举作比较,引出Swift枚举的不同点,进而分析了rawValue和初始化init的底层实现流程,然后讲解了几个重要的场景 👉 OC和Swift的桥接场景,枚举嵌套的场景,最后重点分析了枚举的大小,即内存分布的情况,这也是面试中经常出的题目,希望大家掌握,谢谢!

    相关文章

      网友评论

        本文标题:Swift 枚举

        本文链接:https://www.haomeiwen.com/subject/nurttltx.html