美文网首页Swift
Swift-进阶:枚举enum

Swift-进阶:枚举enum

作者: 辉辉岁月 | 来源:发表于2021-05-19 19:11 被阅读0次

    本文主要介绍enum的常见使用形式,以及枚举大小是如何计算的

    补充:添加脚本自动生成SIL

    • 通过target -> +,选择 other -> Aggregate,,然后命名为CJLScript

    选中CJLScript,选择Build Phases -> New Run Script Phase

    • 选中CJLScript,选择Build Phases -> New Run Script Phase

    • Run Script中输入以下命令

    swiftc -emit-sil ${SRCROOT}/06、EnumTest/main.swift | xcrun swift-demangle > ./main.sil && code main.sil
    
    

    然后我们就可以通过脚本自动生成SIL并自动打开啦 ✿✿ヽ(°▽°)ノ✿✿

    C中的枚举

    在介绍swift中的枚举之前,首先我们来回顾下C中的枚举写法,如下所示

    enum 枚举名{
        枚举值1,
        枚举值2,
        ......
    };
    
    <!--举例:表示一周7天-->
    enum Week{
        MON, TUE, WED, THU, FRI, SAT, SUN
    };
    
    <!--更改C中枚举默认值-->
    //如果没有设置枚举默认值,一般第一个枚举成员的默认值为整型0,后面依次递推
    enum Week{
        MON = 1, TUE, WED, THU, FRI, SAT, SUN
    };
    
    <!--C中定义一个枚举变量-->
    //表明创建了一个枚举,并声明了一个枚举变量Week
    enum Week{
        MON = 1, TUE, WED, THU, FRI, SAT, SUN
    }week;
    //或者下面这种写法,省略枚举名称
    enum{
        MON = 1, TUE, WED, THU, FRI, SAT, SUN
    }week;
    
    

    Swift中的枚举

    在swift中,枚举的创建方式如下所示,如果没有指定枚举值的类型,那么enum默认枚举值是整型

    <!--1、写法一-->
    enum Week{
        case MON
        case TUE
        case WED
        case THU
        case FRI
        case SAT
        case SUN
    }
    
    <!--2、写法二-->
    //也可以直接一个case,然后使用逗号隔开
    enum Week{
        case MON, TUE, WED, THU, FRI, SAT, SUN
    }
    
    <!--定义一个枚举变量-->
    var w: Week = .MON
    
    
    • 如果此时想创建一个枚举值是String类型的enum,可以通过指定enum的枚举值的类型来创建,其中枚举值和原始值rawValue的关系为case 枚举值 = rawValue原始值
    /*
    - =左边的值是枚举值,例如 MON
    - =右边的值在swift中称为 RawValue(原始值),例如 "MON"
    - 两者的关系为:case 枚举值 = 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"
    }
    
    
    • 如果不想写枚举值后的字符串,也可以使用隐式RawValue分配,如下所示
    <!--String类型-->
    enum Week: String{
        case MON, TUE, WED = "WED", THU, FRI, SAT, SUN
    }
    
    <!--Int类型-->
    //MON是从0开始一次递推,而WED往后是从10开始一次递推
    enum Week: Int{
        case MON, TUE, WED = 10, THU, FRI, SAT, SUN
    }
    

    枚举的访问

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

    枚举的访问方式如下所示

    enum Week: String{
        case MON, TUE, WED, THU, FRI, SAT, SUN
    }
    var w = Week.MON.rawValue
    <!--访问-->
    print(w)
    
    <!--打印结果-->
    MON
    

    这里就有一个疑问,swift是如何做到打印 MON的?我们通过SIL文件分析

    • 首先查看SIL文件中的enum,底层多增加了一些东西

      • 1、给枚举值的类型,通过typealias取了一个别名RawValue

      • 2、默认添加了一个可选类型的init方法

      • 3、增加一个计算属性rawValue,用于获取枚举值的原始值

    查看SIL中的main方法,可以得知w是通过枚举值的rawValueget方法获取

    • 查看SIL文件rawValue的get方法,主要有以下几步:

      • 1、接收一个枚举值,用于匹配对应的分支

      • 2、在对应分支创建对应的String

      • 3、返回对应的String

    结论1:使用rawValue的本质是调用get方法

    但是get方法中的String是从哪里来的呢?String存储在哪里?

    • 其实这些对应分支的字符串在编译时期就已经存储好了,即存放在Mach-O文件的__TEXT.cstring中,且是连续的内存空间,可以通过编译后查看Mach-O文件来验证

    结论2rawValueget方法中的分支构建的字符串,主要是从Mach-O文件对应地址取出的字符串,然后再返回给w

    总结

    • 使用rawValue的本质就是在底层调用get方法,即在get方法中从Mach-O对应地址中取出字符串并返回的操作

    区分 case枚举值 & rawValue原始值

    请问下面这段代码的打印结果是什么?

    //输出 case枚举值
    print(Week.MON)
    //输出 rawValue 
    print(Week.MON.rawValue)
    
    <!--打印结果-->
    MON
    MON
    
    

    虽然这两个输出的值从结果来看是没有什么区别的,虽然输出的都是MON,但并不是同一个东西

    • 第一个输出的case枚举值

    • 第二个是通过rawValue访问的rawValueget方法

    如果我们像下面这种写法,编译器就会报错

    枚举的init调用时机

    主要是探索枚举的init会在什么时候调用

    • 定义一个符号断点Week.init

    • 定义如下代码

    print(Week.MON.rawValue)
    
    let w = Week.MON.rawValue
    
    

    通过运行结果发现,都是不会走init方法的

    • 如果是通过init方式创建enum呢?
    print(Week.init(rawValue: "MON"))
    

    运行结果如下

    注:这个断点首先需要通过init前的一个断点 + Week.init符号断点+init符号断点,一起配合,才能断住

    总结:enum中init方法的调用是通过枚举.init(rawValue:)或者枚举(rawValue:)触发的

    我们再继续来分析init方法,来看下面这段代码的打印结果是什么?

    print(Week.init(rawValue: "MON"))
    print(Week.init(rawValue: "Hello"))
    
    <!--打印结果-->
    Optional(_6_EnumTest.Week.MON)
    nil
    
    

    从结果中可以看出,第一个输出的可选值,第二个输出的是nil,表示没有找到对应的case枚举值。为什么会出现这样的情况呢?

    • 首先分析SIL文件中的Week.init方法,主要有以下几步:

      • 1、在init方法中是将所有enum的字符串从Mach-O文件中取出,依次放入数组中

      • 2、放完后,然后调用_findStringSwitchCase方法进行匹配

    其中

    • index_addr 表示获取当前数组中的第n个元素值的地址,然后再把构建好的字符串放到当前地址中
    - `struct_extract` 表示`取出当前的Int值`,Int类型在系统中也是结构体
    - `cond_br` 表示比较的表达式,即分支条件跳转
        - 如果匹配成功,则构建一个`.some的Optional`返回
        - 如果匹配不成功,则继续匹配,知道最后还是没有匹配上,则构建一个`.none的Optional`返回
    
    
    • swift-source中查找_findStringSwitchCase方法,接收两个参数,分别是 数组 + 需要匹配的String
      • 1、遍历数组,如果匹配则返回对应的index
      • 2、如果不匹配,则返回-1
    @_semantics("findStringSwitchCase")
    public // COMPILER_INTRINSIC
    // 接收一个数组 + 需要匹配的string
    func _findStringSwitchCase( 
      cases: [StaticString],
      string: String) -> Int {
    // 遍历之前创建的字符串数组,如果匹配则返回对应的index
      for (idx, s) in cases.enumerated() {
        if String(_builtinStringLiteral: s.utf8Start._rawValue,
                  utf8CodeUnitCount: s._utf8CodeUnitCount,
                  isASCII: s.isASCII._value) == string {
          return idx
        }
      }
      // 如果不匹配,则返回-1
      return -1
    }
    
    • 继续分析SIL中的week.init方法

      • 1、如果没有匹配成功,则构建一个.none类型的Optional,表示nil
      • 2、如果匹配成功,则构建一个.some类型的Optional,表示有值

    所以,这也是为什么一个打印可选值,一个打印nil的原因

    枚举的遍历

    CaseIterable协议通常用于没有关联值的枚举,用来访问所有的枚举值,只需要对应的枚举遵守该协议即可,然后通过allCases获取所有枚举值,如下所示

    <!--1、定义无关联值枚举,并遵守协议-->
    enum Week: String{
        case MON, TUE, WED, THU, FRI, SAT, SUN
    }
    extension Week: CaseIterable{}
    
    <!--2、通过for循环遍历-->
    var allCase = Week.allCases
    for c in allCase{
        print(c)
    }
    
    <!--3、通过函数式编程遍历-->
    let allCase = Week.allCases.map({"\($0)"}).joined(separator: ", ")
    print(allCase)
    //******打印结果******
    MON, TUE, WED, THU, FRI, SAT, SUN
    
    

    关联值

    如果希望用枚举表示复杂的含义,关联更多的信息,就需要使用关联值了

    例如,使用enum表达一个形状,其中有圆形、长方形等,圆形有半径,长方形有宽、高,我们可以通过下面具有关联值的enum来表示

    //注:当使用了关联值后,就没有RawValue了,主要是因为case可以用一组值来表示,而rawValue是单个的值
    enum Shape{
        //case枚举值后括号内的就是关联值,例如 radius
        case circle(radius: Double)
        case rectangle(width: Int, height: Int)
    }
    
    

    注:具有关联值的枚举,就没有rawValue属性了,主要是因为一个case可以用一个或者多个值来表示,而rawValue只有单个的值

    这一点我们也可以通过SIL文件 来验证

    • 首先查看SIL文件,发现此时的enum中既没有别名,也没有init方法、计算属性rawValue了,简称三无枚举(个人叫法,大家随意哈)

    • 其中关联值中radius、width、height这些都是自定义的标签,也可以不写,如下所示,但并不推荐这种方式,因为`可读性非常差

    enum Shape{
        //case枚举值后括号内的就是关联值,例如 radius
        case circle(Double)
        case rectangle(Int, Int)
    }
    
    

    那么如何创建一个有关联值的枚举值呢?可以直接在使用时给定值来创建一个关联的枚举值

    <!--创建-->
    var circle = Shape.circle(radius: 10.0)
    
    <!--重新分配-->
    circle = Shape.rectangle(width: 10, height: 10)
    

    枚举的其他用法

    模式匹配

    enum中的模式匹配其实就是匹配case枚举值

    简单enum的模式匹配

    注:swift中的enum模式匹配需要将所有情况都列举,或者使用default表示默认情况,否则会报错

    enum Week: String{
        case MON
        case TUE
        case WED
        case THU
        case FRI
        case SAT
        case SUN
    }
    
    var current: Week?
    switch current {
        case .MON:print(Week.MON.rawValue)
        case .TUE:print(Week.MON.rawValue)
        case .WED:print(Week.MON.rawValue)
        default:print("unknow day")
    }
    
    <!--打印结果-->
    unknow day
    
    

    查看其SIL文件,其内部是将nil放入current全局变量,然后匹配case,做对应的代码跳转

    具有关联值enum的模式匹配

    关联值的模式匹配主要有两种:

    • 通过switch匹配所有case
    enum Shape{
        case circle(radius: Double)
        case rectangle(width: Int, height: Int)
    }
    
    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)")
    }
    
    <!--打印结果-->
    circle radius: 10.0
    
    

    也可以这么写,将关联值的参数使用let、var修饰

    enum Shape{
        case circle(radius: Double)
        case rectangle(width: Int, height: Int)
    }
    
    let shape = Shape.circle(radius: 10)
    switch shape{
        //做了Value-Binding,相当于将10.0赋值给了声明的radius常量
        case .circle(let radius):
            print("circle radius: \(radius)")
        case .rectangle(let width, var height):
            height += 1
            print("rectangle width: \(width) height: \(height)")
    }
    
    <!--打印结果-->
    circle radius: 10.0
    
    

    然后查看SIL中的关联值的模式匹配,如下图所示

    • 1、首先构建一个关联值的元组

    • 2、根据当前case枚举值,匹配对应的case,并跳转

    • 3、取出元组中的值,将其赋值给匹配case中的参数

    • 通过if case匹配单个case,如下所示

    enum Shape{
        case circle(radius: Double)
        case rectangle(width: Int, height: Int)
    }
    
    let circle = Shape.circle(radius: 10)
    
    <!--匹配单个case-->
    if case let Shape.circle(radius) = circle {
        print("circle radius: \(radius)")
    }
    
    
    • 如果我们只关心不同case的相同关联值(即关心不同case的某一个值),需要使用同一个参数,例如案例中的x,如果分别使用x、y, 编译器会报错
    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:
        break
    }
    
    

    也可以使用通配符_(表示匹配一切)的方式

    enum Shape{
        case circle(radius: Double)
        case rectangle(width: Double, height: Double)
        case square(width: Double, height: Double)
    }
    let shape = Shape.rectangle(width: 10, height:20)
    switch shape{
    case let .rectangle(_, x), let .square(_, x):
        print("x = \(x)")
    default:
        break
    }
    
    <!--另一种方式-->
    enum Shape{
        case circle(radius: Double)
        case rectangle(width: Double, height: Double)
        case square(width: Double, height: Double)
    }
    let shape = Shape.rectangle(width: 10, height:20)
    switch shape{
    case let .rectangle(x, _), let .square(_, x):
        print("x = \(x)")
    default:
        break
    }
    
    

    注:

    • 枚举使用过程中不关心某一个关联值,可以使用通配符_表示
    • OC只能调用swift中Int类型的枚举

    枚举的嵌套

    枚举的嵌套主要用于以下场景:

    • 1、【枚举嵌套枚举】一个复杂枚举是由一个或多个枚举组成

    • 2、【结构体嵌套枚举】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)
    
    

    结构体嵌套枚举

    //结构体嵌套枚举
    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")
            }
        }
    }
    
    

    枚举中包含属性

    enum中只能包含计算属性、类型属性,不能包含存储属性

    enum Shape{
        case circle(radius: Double)
        case rectangle(width: Double, height: Double)
    
        //编译器报错:Enums must not contain stored properties 不能包含存储属性,因为enum本身是值类型
    //    var radius: Double
    
        //计算属性 - 本质是方法(get、set方法)
        var with: Double{
            get{
                return 10.0
            }
        }
        //类型属性 - 是一个全局变量
        static let height = 20.0
    }
    
    

    为什么struct中可以放存储属性,而enum不可以?

    主要是因为struct中可以包含存储属性是因为其大小就是存储属性的大小。而对enum来说就是不一样的(请查阅后文的enum大小讲解),enum枚举的大小是取决于case的个数的,如果没有超过255,enum的大小就是1字节(8位)

    枚举中包含方法

    可以在enum中定义实例方法、static修饰的方法

    enum Week: Int{
        case MON, TUE, WED, THU, FRI, SAT, SUN
    
        mutating func nextDay(){
            if self == .SUN{
                self = Week(rawValue: 0)!
            }else{
                self = Week(rawValue: self.rawValue+1)!
            }
        }
    }
    
    <!--使用-->
    var w = Week.MON
    w.nextDay()
    print(w)
    
    

    indirect关键字

    如果我们想要表达的enum是一个复杂的关键数据结构时,可以通过indirect关键字来让当前的enum更简洁

    //用枚举表示链表结构
    enum List<T>{
        case end
        //表示case是引用来存储
        indirect case node(T, next: List<T>)
    }
    
    <!--也可以将indirect放在enum前-->
    //表示整个enum是用引用来存储
    indirect enum List<T>{
        case end
        case node(T, next: List<T>)
    }
    
    

    为什么呢?

    • 因为enum值类型,也就意味着他们的大小在编译时期就确定了,那么这个过程中对于当前的enum的大小是不能确定的,从系统的角度来说,不知道需要给enum分配多大的空间,以下是官方文档的解释
    You indicate that an enumeration case is recursive by writing indi rect before it, which tells the compiler to insert the necessary l ayer of indirection.
    
    
    • 打印enum的大小
    enum List<T>{
        case end
        indirect case node(T, next: List<T>)
    }
    print(MemoryLayout<List<Int>>.size)
    print(MemoryLayout<List<Int>>.stride)
    
    <!--打印结果-->
    8 //size大小是8
    8 //stride大小是8
    
    

    如果传入的类型是String呢?

    从结果发现,换成其他类型,其结果依旧是8,这是为什么呢?

    下面来分析其内存结构,首先需要定义一个全局变量

    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))
    
    

    通过lldb分析其内存

    所以indirect关键字其实就是通知编译器,我当前的enum是递归的,大小是不确定的,需要分配一块堆区的内存空间,用来存放enum

    • 如果是end,此时存储的是case值,而case为node时存储的是引用地址

    然后再通过插件来查看哪个地址在堆上,哪个地址在栈上

    这一点也可以通过SIL来验证

    也可以通过node的断点来验证,确实是执行了swift_allocObject

    swift和OC混编enum

    在swift中,enum非常强大,可以添加方法、添加extension
    而在OC中,enum仅仅只是一个整数值

    如果想将swift中的enum暴露给OC使用:

    • @objc关键字标记enum
    • 当前enum应该是Int类型

    OC调用Swift的enum

    <!--swift中定义-->
    @objc enum Week: Int{
        case MON, TUE, WED, THU, FRI, SAT, SUN
    }
    
    <!--OC使用-->
    - (void)test{
        Week mon = WeekMON;
    }
    
    

    Swift调用OC的enum
    OC中的枚举会自动转换成swift中的enum

    <!--OC定义-->
    //会自动转换成swift的enum
    NS_ENUM(NSInteger, OCENUM){
        Value1,
        Value2
    };
    
    <!--swift使用-->
    //1、将OC头文件导入桥接文件
    #import "CJLTest.h"
    //2、使用
    let ocEnum = OCENUM.Value1
    
    

    如果OC中是使用typedef enum定义的,自动转换成swift就成了下面这样

    typedef enum {
        Num1,
        Num2
    }OCNum;
    
    <!--swift中使用-->
    let ocEnum = OCNum.init(0)
    print(ocEnum)
    
    //*******打印结果*******
    OCNum(rawValue: 0)
    
    

    自动转换成swift中的如下所示,通过typedef enum定义的enum,在swift中变成了一个结构体,并遵循了两个协议:EquatableRawRepresentable

    如果在OC中使用typedef NS_ENUM定义枚举呢?

    typedef NS_ENUM(NSInteger, CENUM){
        CEnumInvalid = 0,
        CEnumA = 1,
        CEnumB,
        CEnumC
    };
    
    

    自动转换成swift后的结果如下

    问题:OC如何访问swift中String类型的enum?

    • swift中的enum尽量声明成Int整型
    • 然后OC调用时,使用的是Int整型的
    • enum在声明一个变量/方法,用于返回固定的字符串,用于在swift中使用
    @objc enum Week: 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中使用-->
    Week mon = WeekMON;
    
    <!--swift中使用-->
    let Week = Week.MON.val
    
    

    枚举的大小

    主要分析以下几种情况的大小:

    • 1、普通enum

    • 2、具有关联值的enum

    • 3、enum嵌套enum

    • 4、struct嵌套enum

    1、普通enum大小分析

    在前面提及enum中不能包含存储属性,其根本在于enum的大小与Struct的计算方式是不一样的,这里我们将展开详细的分析

    • 首先,我们先来看看下面这段代码的打印结果是什么?
    enum NoMean{
        case a
    }
    print(MemoryLayout<NoMean>.size)
    print(MemoryLayout<NoMean>.stride)
    
    <!--打印结果-->
    0 //size大小是0
    1 //表示访问下一个NoMean的case时,需要跨越1字节的步长
    
    
    • 如果此时增加一个 case b,此时的打印结果是什么?
    enum NoMean{
        case a
        case b
    }
    print(MemoryLayout<NoMean>.size)
    print(MemoryLayout<NoMean>.stride)
    
    <!--打印结果-->
    1 //size大小是1
    1 //步长是1
    
    
    • 如果在增加多个呢?
    enum NoMean{
        case a
        case b
        case c
        case d
    }
    print(MemoryLayout<NoMean>.size)
    print(MemoryLayout<NoMean>.stride)
    
    <!--打印结果-->
    1
    1
    
    

    从结果来看,仍然是1,说明enum就是以1字节存储在内存中的,这是为什么呢?我们来分析下

    断点分析

    • 首先通过断点来分析,case分别a、b、c的情况

      从断点可以看出,

      • case是UInt8,即1字节(8位),最大可以存储255

      • 如果超过了255,会自动从UInt8 -> UInt16 -> UInt32 -> UInt64 ...

    LLDB分析

    • 分别定义4个全局变量tmp、tmp1、tmp2、tmp3
    enum NoMean{
        case a
        case b
        case c
        case d
    }
    
    var tmp = NoMean.a
    var tmp1 = NoMean.b
    var tmp2 = NoMean.c
    var tmp3 = NoMean.d
    
    

    通过lldb查看内存情况如下,case都是1字节大小

    普通enum总结

    • 1、如果enum中有原始值,即rawValue,其大小取决于case的多少,如果没有超过UInt8即255,则就是1字节存储case

    • 2、Int标识的其实就是 RawValue的值

    • 3、当只有一个case的情况下,size0,表示这个enum是没有意义的,

    • 4、当有两个及以上case时,此时的enum是有意义的,如果没有超过255,则case的步长是1字节,如果超过,则UInt8->UInt16...,以此类推

    2、具有关联值enum的大小分析

    如果enum中有关联值,其大小又是多少呢?有如下代码,打印其size和stride

    enum Shape{
        case circle(radius: Double)
        case rectangle(width: Double, height: Double)
    }
    print(MemoryLayout<Shape>.size)
    print(MemoryLayout<Shape>.stride)
    
    <!--打印结果-->
    17 //size的大小是17
    24 //stride的步长是24
    
    

    说明从打印结果可以说明 enum中有关联值时,其内存大小取决于关联值的大小

    • enum有关联值时,关联值的大小 取 对应枚举关联值 最大的,例如circle中关联值大小是8,而rectangle中关联值大小是16,所以取16。所以enum的size = 最大关联值大小 + case(枚举值)大小 = 16 + 1 = 17,而stride由于8字节对齐,所以自动补齐到24
      • 定义一个全局变量,观察其内存

    总结

    • 1、具有关联值的enum大小,取决于最大case的内存大小【枚举大小的本质】

    • 2、关联值枚举的大小 = 最大case的内存大小 + 1(case的大小)

    • 3、size 表示 实际大小

    • 4、stride 表示 对齐后的大小(内存空间中真实占用的大小)

    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)
    
    <!--打印结果-->
    2 //size大小,enum有关联值取决于关联值的大小,每个case都有2个大小为1的enum,所以为2
    2 //stride大小
    

    从结果中说明enum嵌套enum同具有关联值的enum是一样的,同样取决于关联值的大小,其内存大小是最大关联值的大小

    通过嵌套枚举定义一个全局变量

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

    查看其内存情况如下

    这里我们会有一个疑问,其中的81到底指的是什么?这里先提前剧透下:8表示 case leftDown的枚举值,1表示其中down的枚举值,下面我们来验证

    在上面这个例子中,是有4个case,其case在内存中是用0、4、8、12体现的,如果是有很多个case,是否还满足我们现在这样的规律呢?

    • 【尝试1】:在4个case的基础上增加了10个case

      • 查看case downDown1,在内存中为0x1,即1

    • 查看case rightUp,在内存中为0xb,即11

      从这里可以发现case是从0、1、2....这样依次往后的顺序

    【尝试2】:如果去掉其中的几种情况呢,发现case依旧是0、1、2....

    【尝试3】:当只有2个case时,发现case的枚举值是0、8

    • 【尝试4】:当有3个case时,发现case的枚举值是 0、4、8

    PS:至于为什么会是这样的结果,目前也没找到任何依据,后续如果有了依据,再来补充吧(有知道的童鞋,欢迎留言~)

    总结

    • enum嵌套enum同样取决于最大case的关联值大小

    • 当嵌套enum的case只有2个时,case在内存中的存储是0、8

    • 当嵌套enum的case大于2,小于等于4时,case在内存中的存储是 0、4、8、12

    • 当嵌套enum的case大于4时,case在内存中的存储是从0、1、2...以此类推

    4、结构体嵌套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)
    
    <!--打印结果-->
    1
    1
    
    
    • 如果只嵌套了enum,没有声明变量,结构体的大小是多少呢?
    struct Skill {
        enum KeyType{
            case up
            case down
            case left
            case right
        }
    }
    print(MemoryLayout<Skill>.size)
    print(MemoryLayout<Skill>.stride)
    
    <!--打印结果-->
    0 //size的大小取决于成员变量,但是struct中目前没有属性
    1
    
    
    • 如果不仅有枚举变量,还有其他属性,结构体的大小是多少呢?
    struct Skill {
        enum KeyType{
            case up
            case down
            case left
            case right
        }
    
        let key: KeyType //1字节
    
        var height: UInt8 //1字节
    
        func launchSkill(){
            switch key {
            case .left, .right:
                print("left, right")
            case .up, .down:
                print("up, down")
            }
        }
    }
    print(MemoryLayout<Skill>.size)
    print(MemoryLayout<Skill>.stride)
    
    <!--打印结果-->
    2
    2
    
    
    • 如果在增加一个Int类型的属性呢?
    struct Skill {
        enum KeyType{
            case up
            case down
            case left
            case right
        }
    
        var width: Int //8字节
    
        let key: KeyType //1字节
    
        var height: UInt8 //1字节
    
        func launchSkill(){
            switch key {
            case .left, .right:
                print("left, right")
            case .up, .down:
                print("up, down")
            }
        }
    }
    print(MemoryLayout<Skill>.size)
    print(MemoryLayout<Skill>.stride)
    
    <!--打印结果-->
    10 //size大小(与OC中的结构体大小计算是一致的,min(m,n),其中m表示存储的位置,n表示属性的大小,要求是:m必须整除n)
    16 //stride大小
    
    

    结论

    • 1、如果结构体中没有其他属性,只有枚举变量,那么结构体的大小就是枚举的大小,即size为1

    • 2、如果结构体中嵌套了enum,但是没有声明变量,此时的size是0,stride是1

    • 3、如果结构体中还有其他属性,则按照OC中的结构体内存对齐三原则进行分析(参考iOS 内存对齐原理这篇文章)

    内存对齐 & 字节对齐 区分

    • 内存对齐:iOS中是8字节对齐,苹果实际分配采用16字节对齐,这种只会在分配对象时出现

    • 字节对齐:存储属性的位置必须是地址,即OC内存对齐中的min(m,n),其中m表示存储的位置,n表示属性的大小,需要满足位置m整除n时,才能从该位置存放属性。简单来说,就是必须在自身的倍数位置开始

    • 外部调用对象时,对象是服从内存对齐

    • 单纯从结构上说,结构内部服从最大字节对齐

    例如下面这个例子

    struct Skill {
        var age: Int //8字节
        var height: UInt8 //1字节
        var width: UInt16 //2字节
    }
    print(MemoryLayout<Skill>.size)
    print(MemoryLayout<Skill>.stride)
    
    <!--打印结果-->
    12
    16
    
    
    • size为12的原因是:内存从0位置开始Int是占据0-7,UInt8占据8,下一个位置是9,但是UInt16是2字节对齐的要在它的倍数位置开始所以找下一个可以整除它的位置也就是UInt16占据10-11正好整个size在0-11,所以size为12

    • stride为16的原因:stride是实际分配的,必须是最大属性大小的整数倍,即8的倍数,所以是16

    总结

    • 枚举说明:

      • 1、enum中使用rawValue的本质是调用get方法,即在get方法中从Mach-O对应地址中取出字符串并返回的操作

      • 2、enum中init方法的调用是通过枚举.init(rawValue:)或者枚举(rawValue:)触发的

      • 3、没有关联值的enum,如果希望获取所有枚举值,需要遵循CaseIterable协议,然后通过枚举名.allCase的方式获取

      • 4、case枚举值和rawValue原始值的关系:case 枚举值 = rawValue原始值

      • 5、具有关联值的枚举,可以成为三无enum,因为没有别名RawValue、init、计算属性rawValue

      • 6、enum的模式匹配方式,主要有两种:switch / if case

      • 7、enum可以嵌套enum,也可以在结构体中嵌套enum,表示该enum是struct私有的

      • 8、enum中还可以包含计算属性、类型属性,但是不能包含存储属性

      • 9、enum中可以定义实例 + static修饰的方法

    • 枚举内存大小结论:

      • 1、普通enum的内存大小一般是1字节,如果只有一个case,则为0,表示没有意义,如果case个数超过255,则枚举值的类型由UInt8->UInt16->UInt32...

      • 2、具有关联值的enum大小,取决于最大case的内存大小+case的大小(1字节)

      • 3、enum嵌套enum同样取决于最大case的关联值大小

      • 4、结构体嵌套enum,如果没有属性,则size为0,如果只有enum属性,size为1,如果还有其他属性,则按照OC中内存对齐原则进行计算

    相关文章

      网友评论

        本文标题:Swift-进阶:枚举enum

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