Swift枚举

作者: 正_文 | 来源:发表于2022-09-03 12:17 被阅读0次

枚举为一组相关的值定义了一个共同的类型,使你可以在你的代码中以类型安全的方式来使用这些值。
我们熟悉 的C 语言,枚举会为一组整型值分配相关联的名称。Swift 中的枚举更加灵活,不必给每一个枚举成员提供一个值。如果给枚举成员提供一个值(称为原始值),则该值的类型可以是字符串字符,或是一个整型值浮点数
此外,枚举成员可以指定任意类型的关联值存储到枚举成员中,就像其他语言中的联合体(unions)和变体(variants)。你可以在一个枚举中定义一组相关的枚举成员,每一个枚举成员都可以有适当类型的关联值
Swift 中,枚举类型是一等(first-class)类型。它们采用了很多在传统上只被类(class)所支持的特性,例如计算属性(computed properties),用于提供枚举值的附加信息;实例方法(instance methods),用于提供和枚举值相关联的功能;枚举也可以定义构造函数(initializers)来提供一个初始值;可以在原始实现的基础上扩展它们的功能;还可以遵循协议(protocols)来提供标准的功能。

一、枚举语法

Swift 中,枚举支持IntDoubleString等基础类型,也有默认枚举值String类型默认枚举值为casekey名称,IntDouble数值型默认枚举值为0开始,+1递增。
代码:

// 写法一
// 不需要逗号隔开
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


/*
String类型的enum
- =左边的值是枚举值,例如 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分配。

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

//打印结果:MON
1.2 枚举的访问

那么Swift是如何获取rawValue的值,我们可以通过SIL文件分析。
SIL命令:

swiftc -emit-sil main.swift |xcrun swift-demangle >> ./main.sil && open main.sil

SIL文件

enum Week : String {
  case MON, TUE, WED, THU, FRI, SAT, SUN
  init?(rawValue: String)      //默认添加了一个可选类型的init方法
  typealias RawValue = String  //给枚举值的类型,通过typealias取了一个别名RawValue
  var rawValue: String { get } //增加一个计算属性rawValue,用于获取枚举值的原始值
}

main函数流程:

image.png
rawValuegetter方法: image.png
bb8代码段: image.png
总结:rawValue的底层就是调用的getter方法,getter方法中构造了字符串,但是这个字符串的值(例如“MON”)从哪里取出的呢?他们在编译期已经确定了,我们查看Mach-O,在TEXT的__cstring段就能看到。
1.3 case枚举值 & rawValue原始值

代码:

//输出 case枚举值
print(Week.MON)
//输出 rawValue
print(Week.MON.rawValue)
//打印结果:MON  MON

从结果来看是没有什么区别的,输出值都一样,但他们本质是不一样的。
第一个,输出的case枚举值;第二个,是通过rawValue访问的rawValue的get方法
下面这种写法,编译器就会报错:

image.png
1.4 枚举的init

枚举的init会在什么时候调用,我们通过断点看一下:

image.png
①,不设置Condition,进不到方法里面;②原生代码第一个断点,进入的是getter方法,进不去init
即,enuminit方法的调用是通过枚举.init(rawValue:)或者枚举(rawValue:)触发的。

继续:

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方法进行匹配。
如下:

2251862-4a1f18106a9065f7_.jpg
index_addr:表示获取当前数组中的第n个元素值的地址,然后再把构建好的字符串放到当前地址中
struct_extract:表示取出当前的Int值,Int类型在系统中也是结构体
cond_br:表示比较的表达式,即分支条件跳转
    - 如果匹配成功,则构建一个 .some的Optional 返回
    - 如果匹配不成功,则继续匹配,知道最后还是没有匹配上,则构建一个.none的Optional返回

_findStringSwitchCaseswift-source中,接收两个参数,分别是 数组 + 需要匹配的String。①遍历数组,如果匹配则返回对应的index;②如果不匹配,则返回-1
所以,这也是为什么一个打印可选值,一个打印nil的原因。

1.5枚举的遍历

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

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

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

二、关联值枚举、模式匹配、属性方法

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

他与普通类型的枚举不同没有rawValue,没有rawValue的getter方法;没有初始化init方法

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

//创建
var circle = Shape.circle(radius: 10.0)

//重新分配
circle = Shape.rectangle(width: 10, height: 10)
模式匹配

enum中的模式匹配其实就是匹配case枚举值,根据枚举类型,分为2种:
1、简单类型的枚举的模式匹配;
2、自定义类型的枚举(关联值)的模式匹配。

简单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)
    default:print("unknow day")
}

//打印结果:unknow day
关联值类型的模式匹配

关联值类型模式匹配有两种方式:1、switch - case, 匹配所有case;2、if - case, 匹配单个case
switch - case
定义关联值枚举:

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

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

let shape = Shape.circle(radius: 10.0)
switch shape{
    //相当于将10.0赋值给了声明的radius常量
    case let .circle(radius):
        print("circle radius: \(radius)")
    case .rectangle(let width, var height):
        height += 1
        print("rectangle width: \(width) height: \(height)")
}
  • 通过if-case匹配单个case,如下:
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
}

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

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类型的枚举。

属性 & 函数

enum中可以包含计算属性类型属性不能包含存储属性

enum Direct: Int {
    case up
    case down
    case left
    case right
    
    // 计算型属性
    var description: String{
        switch self {
        case .up:
            return "这是上面"
        default:
            return "这是\(self)"
        }
    }
    
    //存储属性:编译器报错,Enums must not contain stored properties
    //var radius: Double
    
    //类型属性 - 是一个全局变量
    static let height = 20.0
    
    // 函数
    func printSelf() {
        print(description)
    }
    
    mutating func nextDay(){
        if self == .up{
            self = Direct(rawValue: 1)!
        }else{
            self = Direct(rawValue: self.rawValue+1)!
        }
    }
}

Direct.down.printSelf()
//打印结果:这是down

var direct = Direct.left;
direct.nextDay();
direct.printSelf()
//打印结果:这是right

为什么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)

三、枚举的嵌套

枚举的嵌套主要用于以下场景:
1、枚举嵌套枚举:一个复杂枚举是由一个或多个枚举组成;
2、结构体嵌套枚举:enum是不对外公开的,即是私有的。

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)
结构体嵌套枚举
//结构体嵌套枚举
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")
        }
    }
}
枚举的递归:indirect

递归枚举是一种枚举类型,它有一个或多个枚举成员使用该枚举类型的实例作为关联值。使用递归枚举时,编译器会插入一个间接层。你可以在枚举成员前加上 indirect 来表示该成员可递归。

//一、用枚举表示链表结构
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>)
}

第一种写法,如果没有关键字indirect,编译报错。原因是使用该枚举时,enum的大小需要case来确定,而case的大小又需要使用到enum大小。所以无法计算enmu的大小,于是报错!
根据编译器提示,需要使用关键字indirect,意思就是将该枚举标记位递归,同时也支持标记单个case。
enum内存大小:

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

print(MemoryLayout<Int>.size)
print(MemoryLayout<List<Int>>.size)
print(MemoryLayout<List<Int>>.stride)

print(MemoryLayout<String>.size)
print(MemoryLayout<List<String>>.size)
print(MemoryLayout<List<String>>.stride)

//打印结果:
//8  8  8
//16  8  8

发现IntString都是8。为什么?
下面通过LLDB分析查看一下:

image.png
如果是end,此时存储的是case值,为0,而case为node时存储的是引用地址
所以,indirect关键字其实就是通知编译器,我当前的enum是递归的,大小是不确定的,需要分配一块堆区的内存空间,用来存放enum。

四、swift和OC混编enum

在swift中,enum非常强大,而在OC中,enum仅仅只是一个整数值
因此,OC调用Swift枚举,必须具备2个条件:①、用@objc关键字标记enum;②、当前enum应该是Int类型

// Swift中定义枚举
@objc enum Weak: Int{
    case MON, TUE, WED, THU, FRI, SAT, SUN
}

// OC使用
- (void)test{
    Weak mon = WeakMON;
}

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

// OC定义1:NS_ENUM
NS_ENUM(NSInteger, ENUM_OC_TYPE){
    Value1,
    Value2
};

// OC定义2:typedef enum
typedef enum {
    Num1,
    Num2
}OCEnumType;

// swift使用
//1、将OC头文件导入桥接文件xxx-Bridging-Header.h
#import "xxx.h"
//2、使用
let ocEnum1 = ENUM_OC_TYPE.Value1
let ocEnum2 = OCEnumType.init(0)

print("\(ocEnum1)  +  \(ocEnum2.rawValue)")
//打印结果:ENUM_OC_TYPE  +  0
OC自动转换成swift方式: image.png image.png
上图可知,通过typedef enum定义的enum,在swift中变成了一个结构体,并遵循了两个协议:EquatableRawRepresentable
OC使用Swift中String类型的枚举方式
@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

即:swift中的enum成Int整型;enum再声明一个变量/方法,用于返回固定的字符串,用于在swift中使用。

五、Enum内存大小

我们主要分析两个函数的区别:

  1. size:实际占用内存大小;
  2. stride:系统分配的内存大小。
5.1 普通enum

一个case的情况:

enum Weak {
    case MON
    //case TUE
}

print(MemoryLayout<Weak>.size)
print(MemoryLayout<Weak>.stride)
//打印结果:0  1

再加一个case:

enum Weak {
    case MON
    case TUE
}

print(MemoryLayout<Weak>.size)
print(MemoryLayout<Weak>.stride)
//打印结果:1  1

继续增加多个case:

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

print(MemoryLayout<Weak>.size)
print(MemoryLayout<Weak>.stride)
//打印结果:1  1

以上可以看出,当case个数为1时,枚举size为0个数>=2时,size的大小始终是1,即,说明enum就是以1字节存储在内存中的。why?

image.png
读取内存可以看出,case都是1字节大小,1个字节是8个byte,那么有255种排列组合(0x00000000 - 0x11111111)。
所以,当case为1个的时候,size的大小是0(二进制是0x0)case数<=255时,size都是1,是UInt8类型
超过255个时,会自动扩容,size和stride都会增加

总结:
1、如果enum中有原始值,即rawValue,其大小取决于case的多少,如果没有超过UInt8即255,则就是1字节存储case,Int标识的其实就是 RawValue的值。
2、当只有一个case的情况下,size是0,表示这个enum是没有意义的。
3、当有两个及以上case时,如果没有超过255,则case的步长是1字节;如果超过,则UInt8->UInt16...,以此类推。

5.2 关联值的enum

自定义类型的枚举,即关联值类型,sizestride值的变化,如下:

enum Shape{
    case circle(radius: Double)
    case rectangle(width: Double, height: Double)
}
print(MemoryLayout<Shape>.size)
print(MemoryLayout<Shape>.stride)
//打印结果:17   24

分析:

  1. case(枚举值)size1
  2. 枚举是共用内存,有关联值时,取最大值
    2.1 circle的参数是double类型,内存size8
    2.2 rectangle的参数是两个double类型,内存size16
  3. 最大值,为16。即,内存size为:16(最大关联值大小) + 1(case枚举值)= 17
  4. 内存对齐,所以stride为24
    断点读取内存验证一下: image.png
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)
//打印结果:2  2

说明:enum嵌套enum同具有关联值的enum是一样的,同样取决于关联值的大小,其内存大小是最大关联值的大小
定义一个变量,观察一下内存:

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

总结,有待进一步验证:
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...以此类推。

5.4 结构体嵌套enum
struct Skill {
    enum KeyType{
        case up
        case down
        case left
        case right
    }
}
print(MemoryLayout<Skill>.size)
print(MemoryLayout<Skill>.stride)
//打印结果:0   1

如果只嵌套了enum,没有声明变量。size的大小取决于成员变量,但是struct中目前没有属性,所以size是1

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

结构体的大小计算,跟函数无关,所以只看成员变量key的大小,key是枚举Skill类型,大小为1,所以结构体大小为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
    }
    let key: KeyType //1字节
    var height: UInt8 //1字节
    var width: Int //8字节

    func launchSkill(){
        switch key {
        case .left, .right:
            print("left, right")
        case .up, .down:
            print("up, down")
        }
    }
}
print(MemoryLayout<Skill>.size)
print(MemoryLayout<Skill>.stride)
//打印结果:16   16
//打印结果2:如果把Int属性放在最前面,则为,10   16

为什么属性位置不一样,打印结果不一样,也是因为内存对齐。内存对齐规则如下:

/**
 数据成员的对齐规则可以理解为min(m, n) 的公式
 m,表示当前成员的开始位置;n,表示当前成员所需要的位数。
 如果满足条件 m 整除 n (即 m % n == 0), n 从 m 位置开始存储;
 反之继续检查 m+1 能否整除 n, 直到可以整除, 从而就确定了当前成员的开始位置。
 */
struct Mystruct4{
    int a;              //4字节 min(0,4)--- (0,1,2,3)
    struct Mystruct5{   //从4开始,存储开始位置必须是最大的整数倍(最大成员为8),min(4,8)不符合 4,5,6,7,8 -- min(8,8)满足,从8开始存储
        double b;       //8字节 min(8,8)  --- (8,9,10,11,12,13,14,15)
        short c;         //1字节,从16开始,min(16,1) -- (16,17)
    }Mystruct5;
}Mystruct4;
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        NSLog(@"%lu  %lu", sizeof(Mystruct4), sizeof(Mystruct4.Mystruct5));
        //打印结果:24  16
        
        NSLog(@"Hello, World!");
    }
    return 0;
}

结构体嵌套了enum总结:
1、如果没有声明变量,此时的size是0,stride是1
2、如果结构体中没有其他属性,只有枚举变量,那么结构体的大小就是枚举的大小,即size为1
3、如果结构体中还有其他属性,则按照OC中的结构体内存对齐原则进行分析。

内存对齐 & 字节对齐 区分
内存对齐:iOS中是8字节对齐,苹果实际分配采用16字节对齐,这种只会在分配对象时出现。
字节对齐:存储属性的位置必须是偶地址,即OC内存对齐中的min(m,n),其中m表示存储的位置n表示属性的大小,需要满足位置m整除n时,才能从该位置存放属性。简单来说,就是必须在自身的倍数位置开始。
外部调用对象时,对象是服从内存对齐
单纯从结构上说,结构内部服从最大字节对齐。即,枚举size为1的情况。

总结:
一、枚举定义:
1、 enum中使用rawValue的本质是调用get方法,即在get方法中从Mach-O对应地址中取出字符串并返回的操作;
2、 enuminit方法的调用是通过枚举.init(rawValue:)或者枚举(rawValue:)触发的;
3、没有关联值的enum,如果希望获取所有枚举值,需要遵循CaseIterable协议,然后通过枚举名.allCase的方式获取;
4、case枚举值rawValue原始值的关系:case 枚举值 = rawValue原始值
5、具有关联值的枚举,可以称为三无enum,因为没有别名RawValue、init、计算属性rawValue
6、enum模式匹配方式,主要有两种:switch-case / 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枚举

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