美文网首页
Swift枚举

Swift枚举

作者: YY323 | 来源:发表于2021-08-22 22:12 被阅读0次

    CObjective-C中的枚举相比,Swift枚举功能更强大。它支持很多只有才有的特性,如:PropertiesMethodsInitializationExtensionsProtocols...

    • C语言枚举的写法

    只支持Int一种类型:

    // WEEK:枚举名,可省略
    // MON:枚举成员
    // week:定义枚举变量
    enum WEEK {
        MON, TUE, WED, THU, FRI, STA, SUN
    } week;
    
    • Swift枚举写法

    支持整型(Integer)浮点数(Float Point)字符串(String)布尔类型(Boolean)四种基本类型,如果想要支持其它自定义的类型,需实现 StringLiteralConvertible 协议:

    enum WEEK {
       case MON, TUE, WED, THU, FRI, STA, SUN
    }
    // 或者
    enum WEEK {
       case MON
       case TUE
       case WED
       case THU
       case FRI
       case STA 
       case SUN
    }
    

    通过查看变量的内存可知:Swift枚举成员case是一个整型值即它所在的index,且只占1个字节(UInt8)。

    若标明类型String,则表示rawValueString类型,而case成员的类型。

    enum WEEK : String {
       case MON = "MON"
       case TUE = "TUE"
       case WED = "WED"
       case THU = "THU"
       case FRI = "FRI"
       case STA = "STA"
       case SUN = "SUN"
    }
    

    case后面的"MON"就是枚举值"="后面的"MON"rawValue(原始值 )
    Swift隐式rawValue分配:不写"="及后面的字符串,即:

    enum WEEK : String {
       case MON 
       case TUE 
       case WED 
       case THU 
       case FRI
       case STA 
       case SUN 
    }
    
    • 定义枚举变量

    var w = WEEK.MON
    

    一旦w的类型被声明为WEEK,则可以使用一个缩写语法(.)将其设置为WEEK的值,即:

    var w : WEEK = .TUE
    

    标明类型后,可以通过w.MON.rawValue获取原生值rawValue,本质是调用rawValueget();未标明类型则不能获取rawValue。

    // 前提是标明了rawValue的类型,才能获取rawValue
    var w = WEEK.MON.rawValue
    

    通过SIL看一下区别:这里标明的类型是Int

    未标明类型的SIL

    可知标明类型能调用rawValue的本质是:rawValue是一个计算属性,通过调用其get()获取该属性的;而在rawValueget()中,rawValueInt类型时默认从0开始,是String类型时默认是枚举成员本身的字符串:

    rawValue类型为Int的get()方法实现.png rawValue类型为String的get()方法实现.png

    那么当类型为String时,get()中构建的字符串存放在哪里呢?

    隐式rawValue构建的字符串在MachOView中的存放位置

    可知:隐式rawValue字符串编译过程中就已经存放在MachOView文件中的__Text,__cstring中,且通过字符串的地址可知占用的是一段连续的内存空间

    • 枚举变量rawValue的区别

    print(WEEK.THU) //THU
    print(WEEK.THU.rawValue) //THU
    

    在这里,虽然打印出来的都是THU,但需要区分的是WEEK.THUWEEK类型的,而WEEK.THU.rawValueString类型的。

    • 枚举中init?( )

    1. 先来看看什么情况下才会调用枚举的初始化方法?

    可知只有通过WEEK(rawValue:)这种方式才会调用到init()方法。

    1. 分析枚举的初始化方法:

    _allocateUninitializedArray:创建一个元组
    第一个元素:与枚举个数大小一样的数组,用于存储枚举中的rawValue,在本例中是StaticString--->Array<StaticString>
    第二个元素:数组的首地址--->BuildIn.RawPointer

    _findStringSwitchCase:查找指定枚举值在数组中的位置,返回index

    0count-1依次与index作比较:

    • 如果相等构建对应的枚举
    • 如果不相等则构建一个Optional.none!enumelt的枚举
    • 关联值

    enum Shape {
        case circle(radius:Double)
        case rectangle(length:Int, width:Double, height:Int)
    }
    
    // 创建枚举的关联值
    var shape = Shape.circle(radius: 3.00)
    // 重新分配关联值
    shape = Shape.circle(radius: 2.0)
    

    此时的枚举值不再有rawValue,也没必要(rawValue是单个值),而关联值可以是一组值
    通过SIL也可以看出:enum再有初始化方法以及计算属性rawValue

    SIL中的有关联值的enum

    关联值的标签可省略,只写类型(不推荐,可读性差),如:

    enum Shape {
        case circle(Double)
        case rectangle(Int, Double, Int)
    }
    
    • 枚举的用法:模式匹配

    1. 简单用法
    enum WEEK : String {
        case MON
        case TUE
        case WED
        case THU
        case FRI
        case STA
        case SUN
    }
    
    var currentDay : WEEK = WEEK.SUN
    var str : String
    switch currentDay {
        case .STA, .SUN:
            str = "Happy Day"
        default:
            str = "Sad Day"
    }
    

    SIL的实现:通过匹配case来做相应的代码跳转。

    2.复杂用法:匹配关联值

    可以通过SIL文件知道:

    • case let .circle(radius):中的 let 表示关联值是一个常量,不可被修改;也可换一种写法case .circle(let radius):
    • case .rectangle(let length, var width) :var width表示 width 是一个变量,可以被修改;如果关联值width用不到则可以用_代替,即case .rectangle(let length, _) :
    • 若只想匹配单个case可以:
    if case let Shape.circle(radius) = shape {
        tempValue = radius
    }
    
    • 如果只关心不同的case相同关联值,可以这样写:
    enum Shape {
        case circle(radius:Double)
        case rectangle(length:Double, width:Double)
        case square(length:Double, width:Double)
    }
    
    var rectangle = Shape.rectangle(length:5, width: 3)
    var square = Shape.square(length: 9, width: 5)
    var tempValue : Double
    
    switch square {
    case let .rectangle (5, x), let .square(x, 5):
        tempValue = x
    default :
        tempValue = 0.0
    } 
    // 若switch rectangle {..}则tempValue=3
    // 若switch square {..}则tempValue=9
    

    还可以使用通配符的方式:

    case let .rectangle (_, x), let .square(x, _):
    print(x)
    // 若switch rectangle {..}则 3
    // 若switch square {..}则 9
    //或者
    case let .rectangle (x, y), let .square(y, x):
    print(x,y)
    // 若switch rectangle {..}则 (5,3)
    // 若switch square {..}则 (5,9)
    

    底层实现还是通过匹配case,匹配成功后取出元组中x所在的元素,将该元素赋值给x,再将元素值赋值给tempValue

    • 枚举的嵌套

    1. 枚举中嵌套枚举
    enum CombineDirect {
        enum BaseDirect {
            case up
            case down
            case left
            case right
        }
        case leftUp(direct1:BaseDirect, direct2:BaseDirect)
        case rightUp(direct1:BaseDirect, direct2:BaseDirect)
        case leftDown(direct1:BaseDirect, direct2:BaseDirect)
        case rightDown(direct1:BaseDirect, direct2:BaseDirect)
    }
    
    var direct = CombineDirect.leftDown(direct1: .left, direct2: .right)
    
    1. 结构体中嵌套枚举
    struct Skill {
        enum Direct {
            case up
            case down
            case left
            case right
        }
        
        var direct : Direct
        func launchSkill() {
            switch direct {
                case .left, .right:
                    print("控制方向")
                case .up, .down:
                    print("移动距离")
            }
        }
    }
    
    • 枚举中包含属性

    枚举本身是类型,只能包含计算属性(只有方法,不存储在enum中)和类型属性(也不存储在enum中),能包含存储属性。
    注意:结构体可以有存储属性,结构体大小就是存储属性的大小。

    • 枚举中包含方法

    enum WEEK : Int {
        case MON
        case TUE
        case WED
        case THU
        case FRI
        case STA
        case SUN
        
        func nextDay()-> WEEK{
            switch self {
            case .SUN:
                return .MON
            default:
                return WEEK(rawValue: self.rawValue + 1) ?? self
            }
        }
    }
    
    • 迭代枚举

    如果需要迭代枚举中的所有值时,需要自定义的枚举遵守CaseIterable协议,通过获取enum的allCases属性获取所有值:

    enum FlowerType: CaseIterable {
        case Rose
        case Orchid
        case Peony
    }
    
    let numOfFlowerType = FlowerType.allCases.count
    for flower in FlowerType.allCases {
        print(flower)
    }
    //Rose
    //Orchid
    //Peony
    
    • 枚举中可使用协议

    protocol CustomDesc {
        var description : String { get }
    }
    
    enum FlowerType: CustomDesc {
        case Rose
        case Orchid
        case Peony
        
        var description: String {
            switch self {
            case .Rose:
                return "Rose"
            case .Orchid:
                return "Orchid"
            case .Peony:
                return "Peony"
            }
        }
    }
    
    • 枚举可扩展

    枚举通过扩展case(放在枚举中)和方法(放在枚举的扩展中)分离

    enum FlowerType {
        case Rose
        case Orchid
        case Peony
    }
    
    extension FlowerType {
        func introduced() -> String {
            switch self {
            case .Rose:
                return "Rose"
            case .Orchid:
                return "Orchid"
            case .Peony:
                return "Peony"
            }
        }
    }
    
    • 枚举可使用泛型

    枚举通过泛型参数定义以适应枚举中的关联值(可有多个泛型参数),拿Swift标准库中的Optional类型为例:

    enum Optional<Wrapped> {
        case none
        case some(Wrapped)
    }
    
    let aValue = Optional<Int>.some(5)
    let noValue = Optional<Int>.none
    if noValue == Optional.none { print("No value")
    
    • 枚举的大小

    1.只有一个case的简单枚举(其实无意义):

    enum test {
      case a
    }
    print(MemoryLayout<test>.size) //0
    print(MemoryLayout<test>.stride) // 1
    
    1. 有多个case的
    enum test {
      case a
      case b
      case c
      case d
    }
    print(MemoryLayout<test>.size) // 1
    print(MemoryLayout<test>.stride) // 1
    

    enum其实就是以1字节的长度存储在内存中即UInt8,当case个数超过UInt8的最大值255个时,编译器自动将enum的内存大小UInt16。也可在汇编模式下看出:每次移动1个字节。

    var tmp = test.a时 var tmp = test.b时

    总结:原始值的enum大小取决于case的数量,与rawValue类型无关,case超不过255个就是 UInt8--->1字节,如果超过则自动升级成UInt16

    1. 关联值的枚举:大小取决于case中最大内存的大小以及字节对齐

    PS:是否+1还没弄透彻,下次弄明白了再补上...,这里先记录一下自测总结出来的结果,可能有误...(可略过)

    猜想1: enum去占最大内存的case外:
    有占大于等于最大关联值类型a(>8视为8,<8就是a本身)的case的话,size就需要在对齐基础上+1(case的大小);
    小于最大关联值类型a字节则需要+1

     enum Shape {
         case circle(radius:UInt8) //1
         case rectangle(length:UInt8, width:Int, height:UInt16) // 1   8   2
         case square(width:Int32, height:Int32) // 4  4
     }  // size : 19
    
     enum Shape {
         case circle(radius:UInt8)
         case rectangle(length:UInt8, width:Int, height:UInt16)
         case square(width:UInt8, height:UInt16)
     }  // size : 18
    

    size:除去最大的rectangle(字节对齐的原则上其大小为8 + 8 + 2 = 18),circlesquare中只要任一个所占内存大于等于enum中的最大类型(这里所有的case中最大的是Int为8字节),该enum的size大小都需要在对齐基础+ 1(case的大小);
    这里circle只占了1个字节,而在上面的enum中square占4 + 4 等于8字节,所以需要+1 = 19,下面的enum中square占1 + 2 = 3小于8字节,不需要+1。

    猜想2: case数量>1的前提下, 最大case只有一个关联值时始终+1。

    stride:为了字节对齐(空间换取时间,提高效率),需要将实际大小补齐到最大所占内存大小(这里是Int-->8)的倍数,所以19补齐成8的倍数即3 * 8 = 24

    1. 枚举嵌套枚举的大小
      比较特殊:取决于内层枚举的大小字节对齐

    5.嵌套枚举的结构体的大小 :取决于存储属性的内存大小(有没有方法都一样,因为方法存在结构体中)
    有存储属性时size为1、stride为1
    无存储属性时size为0、stride为1 (空结构体也是size为0、stride为1 )

    • 区分内存对齐字节对齐
      内存对齐:64位下8字节对齐,分配对象时用到内存对齐。
      字节对齐:对齐的目的就是地址要从偶地址开始,成员变量的起始位要从该变量内存大小的倍数开始。

    下面两个结构体,不同类型的成员变量交换位置都会导致结构体的内存大小不一致

    struct A {
        var height : Double  //8
        var count : UInt16   //2
        var age : UInt8        //1
    }
    
    print(MemoryLayout<A>.size) //11
    print(MemoryLayout<A>.stride)  //16
    

    struct B {
        var height : Double  //8
        var age : UInt8        //1
        var count : UInt16   //2
    }
    
    print(MemoryLayout<B>.size) //12
    print(MemoryLayout<B>.stride)  //16
    
    结构体A的内存存放 结构体B的内存存放

    OC的结构体内存对齐规则如下:

    • struct的第一个数据成员要从偏移量offset为0的位置开始,后面的其他成员的起始的位置要从该成员类型的字节大小(Swift中>8的视为8)的整数倍开始

    • 步长是最大成员类型(Swift中>8的视为8)的整数倍

    • 若一个结构体包含另一个结构体的成员,结构体成员要从自身结构体中最大类型(Swift中>8的视为8)的整数倍开始

    • indirect关键字

    1. 使用场景:用递归的方式表达想要的数据结构
    使用`嵌套枚举`表达`链表`的结构

    上面图片中是一个递归枚举,若不用indirect关键字声明编译器会报错,所以下面case需要用indirect关键字声明:

    递归枚举

    原因:普通枚举大小在编译时期就确定好的,而这里的递归枚举大小在编译时是未知的,所以需要用indirect关键字来说明需要在堆上分配一块内存空间来存放,当前case会使用引用类型存储。

    1. 本质:在上分配一块内存,存储一个指向case的值地址
    递归枚举的内存结构

    定义一个简单的递归枚举变量,分析SILindirect关键字到底做了些什么事情?

    var list = List<Int>.node(4, List<Int>.end)
    
    // main
    sil @main : $@convention(c) (Int32, UnsafeMutablePointer<Optional<UnsafeMutablePointer<Int8>>>) -> Int32 {
    bb0(%0 : $Int32, %1 : $UnsafeMutablePointer<Optional<UnsafeMutablePointer<Int8>>>):
      alloc_global @main.list : main.List<Swift.Int>        // id: %2
      %3 = global_addr @main.list : main.List<Swift.Int> : $*List<Int> // user: %16
      %4 = metatype $@thin List<Int>.Type
      // 构建Int类型的值4
      %5 = integer_literal $Builtin.Int64, 4          // user: %6
      %6 = struct $Int (%5 : $Builtin.Int64)          // user: %13
      %7 = metatype $@thin List<Int>.Type
      %8 = enum $List<Int>, #List.end!enumelt         // user: %14
      // 分配堆空间存储metadata、refCount、value(List<Int>.node(T, List<Int>)这个case的值(是一个元组))
      %9 = alloc_box $<τ_0_0> { var (τ_0_0, List<τ_0_0>) } <Int> // users: %15, %10
      // 取出%9(类似对象)里面value的地址 *(Int, List<Int>)
      %10 = project_box %9 : $<τ_0_0> { var (τ_0_0, List<τ_0_0>) } <Int>, 0 // users: %12, %11
      // 第一个元素的地址 *Int
      %11 = tuple_element_addr %10 : $*(Int, List<Int>), 0 // user: %13
      // 第二个元素的地址 *List<Int>
      %12 = tuple_element_addr %10 : $*(Int, List<Int>), 1 // user: %14
      // 将4存入元组中第一个元素的地址里
      store %6 to %11 : $*Int                         // id: %13
      // 将List<Int>.end存入元组中第二个元素的地址里
      store %8 to %12 : $*List<Int>                   // id: %14
      // 构建一个枚举  List<Int>.node %9--->List<Int>.node(4, List<Int>.end)
      %15 = enum $List<Int>, #List.node!enumelt, %9 : $<τ_0_0> { var (τ_0_0, List<τ_0_0>) } <Int> // user: %16
      store %15 to %3 : $*List<Int>                   // id: %16
      %17 = integer_literal $Builtin.Int32, 0         // user: %18
      %18 = struct $Int32 (%17 : $Builtin.Int32)      // user: %19
      return %18 : $Int32                             // id: %19
    } // end sil function 'main'
    

    alloc_box:在空间上分配内存区域存放T类型的value,box相当于在value外面包裹了一层;project_box:取出来的是value的地址;可通过断点看到调用了swift_allocObject

    通过上面SIL分析可知:indirect关键字的本质就是在上分配一块内存来存储一个引用地址,该地址中存放的是被indirect修饰的case

    1. indirect关键字修饰enum:表明整个enum类型都是以引用类型来存储。

    先看看indirect修饰enum时,其case的内存结构:case里面直接存放的是关联值9;

    下面是加indirect修饰enum时,其case的内存结构:case里面存放的是一个引用地址

    • Swift和OC枚举混编

    • OC访问Swift中的枚举
    1. enum要用@objc修饰
    2. 必须将rawValue的类型声明成Int类型(因为OC中的枚举就是的整型值)
    @objc enum Week : Int {
        case MON
        case TUE
        case WED
        case THU
        case FRI
        case STA
        case SUN
    }
    

    然后在OC-Swift.h桥接文件中,该enum就已存在:

    typedef SWIFT_ENUM(NSInteger, Week, closed) {
      WeekMON = 0,
      WeekTUE = 1,
      WeekWED = 2,
      WeekTHU = 3,
      WeekFRI = 4,
      WeekSTA = 5,
      WeekSUN = 6,
    };
    

    OC文件中就可以直接使用

    OC中使用Swift的enum
    1. OC访问String类型的enum
    // .swift文件中封装String类型的enum
    @objc class Week : NSObject {
        @objc enum WeekInt : Int {
            case MON, TUE, WED, THU, FRI, STA ,SUN
            
            var string : String {
                return Week.getName(weekValue: self)
            }
        }
    
       @objc class func getName(weekValue:WeekInt)->String {
            switch weekValue {
                case .MON: return "MON"
                case .TUE: return "TUE"
                case .WED: return "WED"
                case .THU: return "THU"
                case .FRI: return "FRI"
                case .STA: return "STA"
                case .SUN: return "SUN"
            }
        }
    }
    
    // OC-Swift.h桥接文件
    enum WeekInt : NSInteger;
    @class NSString;
    
    SWIFT_CLASS("_TtC8YYOCTest4Week")
    @interface Week : NSObject
    + (NSString * _Nonnull)getNameWithWeekValue:(enum WeekInt)weekValue SWIFT_WARN_UNUSED_RESULT;
    - (nonnull instancetype)init OBJC_DESIGNATED_INITIALIZER;
    @end
    
    typedef SWIFT_ENUM(NSInteger, WeekInt, closed) {
      WeekIntMON = 0,
      WeekIntTUE = 1,
      WeekIntWED = 2,
      WeekIntTHU = 3,
      WeekIntFRI = 4,
      WeekIntSTA = 5,
      WeekIntSUN = 6,
    };
    
    // OC中调用
    NSString *weekStr = [Week getNameWithWeekValue:WeekIntFRI];
            NSLog(@"%@", weekStr); // FRI
    
    // Swift中调用
    var weekStr = Week.WeekInt.STA.string
    print(weekStr) //STA
    
    • Swift访问OC中的枚举
      OC中的枚举会被自动转换成Swift的enum。

    1.typedef NS_ENUM方式声明的枚举

    //OC .h文件中定义的枚举
    typedef NS_ENUM(NSInteger, YYSTATE){
        Invalid = -1,
        Failed,
        Success
    };
    
    // 自动转换成Swift的枚举:(在系统自动生成的Swift文件中)
    public enum YYSTATE : Int {
    
        
        case Invalid = -1
    
        case Failed = 0
    
        case Success = 1
    }
    
    // .swift文件中使用enum:
    var state = YYSTATE.Success
    print(state.rawValue) // 1
    
    //转换后的enum的大小与步长:
    print(MemoryLayout<YYSTATE>.size) //8
    print(MemoryLayout<YYSTATE>.stride)  //8
    
    查看系统生成的Swfit文件
    1. NS_ENUM方式声明的枚举
    // OC .h文件中定义枚举
    NS_ENUM(NSInteger, YYSTATE){
        Invalid = -1,
        Failed,
        Success
    };
    
    //自动转换后的枚举:
    public var YYSTATE: YYSTATE
    
    public enum YYSTATE : Int {
    
        
        case Invalid = -1
    
        case Failed = 0
    
        case Success = 1
    }
    
    // .swift中使用枚举:
    var state = YYSTATE.Success
    print(state.rawValue) // 1
    
    //转换后的enum的大小与步长:
    print(MemoryLayout<YYSTATE>.size) //8
    print(MemoryLayout<YYSTATE>.stride)  //8
    
    1. typedef enum方式声明的枚举
    // .h中定义枚举
    typedef enum {
        YYSTATEInvalid = -1,
        YYSTATEFailed,
        YYSTATESuccess
    }YYSTATE;
    
    //自动转换后的枚举:
    public struct YYSTATE : Equatable, RawRepresentable {
    
        public init(_ rawValue: Int32)
    
        public init(rawValue: Int32)
    
        public var rawValue: Int32
    }
    
    //.swift中使用枚举:
    var state = YYSTATESuccess
    print(state.rawValue) //1
    
    //转换后的enum的大小与步长:
    print(MemoryLayout<YYSTATE>.size) //4
    print(MemoryLayout<YYSTATE>.stride)  //4
    

    相关文章

      网友评论

          本文标题:Swift枚举

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