Swift进阶(六)—— 枚举和可选类型

枚举

枚举的基本用法

在Swift中,通过enum关键字来声明一个枚举,和结构体struct一样,也是一个值类型,同时也可以添加方法、计算属性、也可以遵循协议(protocol),支持扩展(extension)。

Swift中枚举的基本用法如下:

enum FWJEnum {
    case Test_One
    case Test_Two
    case Test_Three
}
复制代码

你也可以在同一个case中完成多个枚举

enum FWJEnum {
    case Test_One,Test_Two,Test_Three
}
复制代码

枚举原始值

在OC和C中,枚举主要支持整数类型,在下面的例子中:A,B,C分别默认代表 0,1,2

typedef NS_ENUM(NSUInteger, LGEnum) { 
    A,
    B,
    C, 
};
复制代码

而在Swift中的枚举则更加灵活,并且不需给枚举中的每一个成员都提供值。如果一个值(所谓“原始”值)要被提供给每一个枚举成员,那么这个值可以是字符串、字符、任意的整数值,或者是浮点类型。

enum Color: String {
    case red = "Red" 
    case amber = "Amber" 
    case green = "Green"
}

enum FWJEnum: Double {
    case a = 10.0 
    case b= 20.0 
    case c = 30.0 
    case d = 40.0
}
复制代码

隐式RawValue

在swift中,枚举的原始值可以通过类型推动机制自动设置,这种就是隐式 RawValue。我们通过下面这个案例来解释。

enum DayOfWeek: Int {
  case mon, tue, wed, thu, fri = 10, sat, sun
}
print(DayOfWeek.mon.rawValue) // 0

print(DayOfWeek.tue.rawValue) //1

print(DayOfWeek.fri.rawValue)//10

print(DayOfWeek.sun.rawValue)// 12
复制代码

这里的mon值是从0开始,但是当我们设置fri指定为值10之后,后面的satsun的值就会在fri设定的值的基础上进行累加,也就是说,satsun的值分别为11,12。

如果把枚举的值设定成string类型时,那么当case没有设定值时,系统编译器会自动把与case同名的字符串当成这个case值的原始值。你也可以手动设置case原始值。

enum DayOfWeek: String {
    case mon, tue, wed, thu, fri = "Fri", sat, sun

}

print(DayOfWeek.mon.rawValue) //mon

print(DayOfWeek.fri.rawValue) //Fri

print(DayOfWeek.sat.rawValue) // sat

复制代码

那枚举原始值是如何获取这个字符串的呢?我们打开sil文件去看一下。

enum DayOfWeek: String {
    case mon, tue, wed, thu, fri = "Fri", sat, sun

}
let rawValue = DayOfWeek.mon.rawValue
复制代码

我们先看一下enum类型的声明 截屏2022-03-06 上午9.33.40.png 可以看到声明里面有一个可失败的初始化器init?(rawValue:String),以及一个只能get的计算属性rawValue

我们接下来查看rawValueget方法。

截屏2022-03-06 上午9.38.28.png 在上面的sil代码中,进行一次模式匹配swift_enum %0,这里%0传入的是当前的枚举值。此时我们传入的是mon,因此匹配到的代码就是case #DayOfWeek.mon!enumelt: bb1,然后我们就进入了bb1代码模块。

bb1代码块中,直接字符串创建了 "mon" ,sil中对于枚举编译器会自动生成rawValue计算属性,和初始化方法,计算属性本质是个方法,这一切在编译时就已经确定了每个枚举值对应的rawValue值,也不需要存储。

那么这个字符串mon 是从哪里得到的哪?这样的字符串其实就是一个字符串常量,而字符串常量存储在哪里哪,我们把Mach-o文件拖到MachoView应用来查看一下。 截屏2022-03-07 上午9.12.33.png

枚举可失败初始化器

枚举里面的枚举值和原始值是两个不同的类型,我们使用上面的DayOfWeek来做说明。比如枚举值mon,它的类型就是DayOfWeek。而原始值虽然默认值是mon,但是它的类型却是String类型。所以我们不能直接使用字符串去创建一个枚举值。在Swift中,提供了一个可失败初始化器init?(rawValue:String)来初始化枚举值,接下来我们从sil文件中去了解一下枚举值是怎么初始化的。 截屏2022-03-07 上午9.26.22.png 截屏2022-03-07 上午9.28.07.png 通过sil可以发现,在调用 init(rawValue: )时 ,首先会把所有的case对应的字符串存储进一个连续的内存空间。然后根据传入的rawValue去和这个空间里面的字符串进行匹配得到枚举值,通过原始值进行枚举实例的构造时,是有可能构造失败的,因为传入的原始值不一定会对应某一个枚举值。因此,这个方法实际上返回的是一个Optional类型的可选值,如果构造失败,则会返回nil。

关联值

在Swfit中,枚举不仅仅是用来描述一些简单的类型,您可以通过使用关联值和其它的数据类型进行关联,从而可以实现对一些复杂数据模型的描述。 截屏2022-03-07 上午9.37.12.png

模式匹配

截屏2022-03-07 上午10.11.40.png

Swift要求要匹配所有的case,如果你不想匹配所有的case,可以使用default代替。 截屏2022-03-07 上午10.14.32.png

枚举的内存大小

前面我们知道了枚举的基本用法,还知道了它和可以有原始值和关联值,原始值是计算属性不会存储在内存中,所以对枚举的内存信息不会有影响,难么关联值对枚举的存储有什么影响呢,下面来分下面几种情况探究一下枚举值占有内存的大小

枚举值无关联值

截屏2022-03-07 上午10.48.25.png 截屏2022-03-07 下午1.52.32.png 我们先看一下无关联值的sizestride,这两个值都是为1。这个很好理解,因为枚举的隐式rawValue作为硬编码已经存储在Mach-O里面了。因此现在枚举只需要存储枚举值就可以了。而对于当前的枚举值,Swift默认是以UInt8类型来存储的,也就是1字节。我们接下来来看枚举值具体是怎么存储的。 截屏2022-03-07 下午1.58.09.png 通过打印a、b、c三个变量的内存。可以看到每一个枚举值内存中存储都是只有一个字节,并且三个连续的枚举值内存地址相差了1个字节,而且他们内存里面的值是0x00x10x2这样累加起来存放到内存用来标识。也就是说一个枚举值大小为1字节,可以至少存储256个枚举值。

枚举值只有一个关联值

我们先来看一下只关联一个BOOL值的情况 截屏2022-03-07 下午2.25.54.png 截屏2022-03-07 下午2.31.50.png 可以看到,每个枚举的大小(size)和步长(stride)都为1。这是因为Bool类型是1字节,也就是UInt8,所以当前能表达256个case的情况,对于布尔类型来说,只需要使用低位的 0, 1 这两种情况,其 他剩余的空间就可以用来表示没有负载的case值。 截屏2022-03-07 下午2.44.10.png 截屏2022-03-07 下午2.44.15.png 通过内存地址打印,可以看到,不同的case值确实是按照我们在开始得出来的那个结论进行布局的。

接下来我们看一下只有一个Int关联值的情况 截屏2022-03-07 下午2.25.37.png 截屏2022-03-07 下午2.25.46.png 可以看到,和上面的BOOL关联值情况不一样,这回每个枚举的大小(size)为9,步长(stride)为16。这是因为对于Int类型的负载来说,其实系统是没有办法推算当前的负载所要使用的位数,也就意味着当前Int类型的负载是没有额外的剩余空间的,这个时候我们就需要额外开辟内存空间来去存储我们的case值,也就是 8 + 1 = 9 字节。 截屏2022-03-07 下午2.48.00.png 截屏2022-03-07 下午2.53.52.png

枚举值有多个关联值

我们先看一下这多个关联值都是同一个类型的情况 截屏2022-03-07 下午3.29.53.png 截屏2022-03-07 下午3.29.57.png

可以看到,每个枚举的大小(size)和步长(stride)都为1。这是因为里面的关联值类型都是BOOL类型,而BOOL类型的枚举大小我们上面已经说过了。

我们接下来看一下内存分布情况 截屏2022-03-07 下午3.35.13.png 截屏2022-03-07 下午3.41.21.png 这里我们可以看到当前内存存储的分别是 00, 01, 40, 41, 80,81,这是因为对于bool类型来说,我们存储的无非就是01,只需要用到1位,所以剩余的 7 位这里我们都统称为common spare bits,对于当前的 case 数量来说我们完全可以把所有的情况放到 common spare bits 中,所以这里我们只需要 1 字节就可以存储所有的内容了。

接下来我们来看一下 00, 01, 40, 41, 80,81 分别代表的是什么?首先 0,4,8 这里我们叫做tag value0,1这里我们就做tag index,至于这个tag value怎么来的,如果感兴趣的通过可以去阅读一下源码中的Enum.cppGenEnum.cpp这两个文件。

我们再看一下多个关联值是不同类型的情况下是什么样的 截屏2022-03-07 下午3.56.40.png 截屏2022-03-07 下午3.56.46.png

此时枚举的大小是9,步长是16。这是因为我们有多个关联值的枚举时,当前枚举类型的大小取决于当前最大关联值的大小。因此当前枚举的大小就等于sizeof(Int) + sizeof(rawVlaue) = 9,步长大小根据内存对齐补到16。

如果是下面这种情况 截屏2022-03-07 下午4.05.41.png 那么当前枚举大小是sizeof(Int) * 3 + sizeof(rawVlaue) = 25

我们再看另外一个例子 截屏2022-03-07 下午4.09.14.png 可以看到,当关联值的类型顺序发生变化后,枚举的大小也会不一样。第一个LGEnum的大小为25,而LGEnum1的大小变成了32。这是因为bool类型的位置在中间时,枚举内部位了能够迅速读取和存储每个关联值,进行了内存对齐,每个关联值都是8个字节,bool类型会被补齐到8个字节, bool类型的位置在末尾时,系统计算内存时,会明确知道最后一位是bool类型是1位,并不会因为没有对齐需要去做指针的移动,所以就是25个字节,但是根据内存对齐的原则,分配内存时会去给当前枚举变量补齐内存。

枚举内存大小总结

当枚举值有关联值时,它的存储大小和关联值类型和数量有关,当关联值类型有额外空间可以的值时,系统会把额外的空间利用起来存储 枚举值,当没有额外空间时,系统会按照关联值类型所需的大小 + 存储枚举值的一个字节 作为实际需要的长度,然后根据对齐原则做内存补齐。

单个枚举值

截屏2022-03-07 下午4.16.01.png 当一个枚举只有一个枚举值时,我们不需要用任何东西来去区分当前的枚举值,所以当我们打印当前的枚举大小你会发现是0。

递归枚举

递归枚举是拥有另一个枚举作为枚举成员关联值的枚举。当编译器操作递归枚举时必须插入间接寻址层。你可以在声明枚举成员之前使用indirect关键字来明确它是递归的。

enum ArithmeticExpression {
    case number(Int)
    indirect case addition(ArithmeticExpression, ArithmeticExpression)
    indirect case multiplication(ArithmeticExpression, ArithmeticExpression)
}
复制代码

你同样可以在枚举之前写indirect来让整个枚举成员在需要时可以递归:

indirect enum ArithmeticExpression {
    case number(Int)
    case addition(ArithmeticExpression, ArithmeticExpression)
    case multiplication(ArithmeticExpression, ArithmeticExpression)
}
复制代码

接下来我们就可以通过上面定义的递归枚举完成一个表达式:(5 + 4) * 2

let five = ArithmeticExpression.number(5)
let four = ArithmeticExpression.number(4)
let sum = ArithmeticExpression.addition(five, four)
let product = ArithmeticExpression.multiplication(sum, ArithmeticExpression.number(2)) 

func evaluate(_ expression: ArithmeticExpression) -> Int {

    switch expression {
        case let .number(value):
            return value
        case let .addition(left, right):
            return evaluate(left) + evaluate(right)
        case let .multiplication(left, right):
            return evaluate(left) * evaluate(right)
    }
}

print(evaluate(product))

// Prints "18"
复制代码

可选值

之前我们在写代码的过程中早就接触过可选值,比如我们在代码这样定义:

class LGTeacher {
    var age: Int?
}
复制代码

当前的age我们就称之为可选值。

我们也可以这样写可选值。

var age: Int? = var age: Optional<Int>
复制代码

那对于Optional的本质是什么?我们直接跳转到源码,打开Optional.swift文件

@frozen
public enum Optional<Wrapped> {
    case none
    case some(Wrapped)
}
复制代码

可以看到,可选值Option实际是一个枚举类型,当为空的时候,返回一个none,当有值的时候,返回当前值。

可选值强制解绑

当你确定你定义的可选值有值时,你可以使用来对这个可选值进行强制解绑。

var age: Int? = 10
let age2 = age!
print(age2)
复制代码

可选值绑定

我们可以通过 if 语句来判断这个可选项是否有值,如下:

var age: Int?
if age == nil {
print("age is nil")
}else {
print("age is (age!)")
}
复制代码

除了通过这种方式,我们还可以通过可选项绑定来判断可选项是否有值,并且取出来。如果可选项包含有值,会自动解包,把值赋给一个临时的常量(let)或者变量(var),并返回一个 Bool 类型

代码如下:

var age: Int? = 10
if let age = age {
print("age is (age)")
}else {
print("age is nil")
}
复制代码

可选链

我们都知道再OC中我们给一个nil对象发送消息什么也不会发生,Swift中我们是没有办法向一个 nil 对象直接发送消息,但是借助可选链可以达到类似的效果。我们看下面两段代码

let str: String? = "abc" 
let upperStr = str?.uppercased() // Optional<"ABC"> 

var str1: Stringlet upperStr1 = str?.uppercased() // nil 
复制代码

可以看到,当str有初始值时,它就执行大写操作,返回一个Optional<"ABC"> ,而当没有值时,就返回一个nil

我们再来看下面这段代码输出什么

let str: String? = "abc"
let upperStr = str?.uppercased().lowercased() // Optional<"abc">
复制代码

同样的可选链对于下标和函数调用也适用

var closure: ((Int) -> ())? 
closure?(1) // closure 为 nil 不执行
let dict = ["one": 1, "two": 2] 
dict?["one"] // Optional(1) 
dict?["three"] // nil
复制代码

??运算符

( a ?? b ) 将对可选类型a进行空判断,如果a包含一个值就进行解包,否则就返回一个默认值b

  • 表达式 a 必须是Optional类型。
  • 默认值 b 的类型必须要和 a 存储值的类型保持一致。

隐士解析可选类型

隐式解析可选类型是可选类型的一种,使用的过程中和非可选类型无异。它们之间唯一的区别是,隐式解析可选类型是你告诉对 Swift 编译器,我在运行时访问时,值不会为nil

var age: Int? 
var age1: Int!//隐士解析可选类型
age = nil 
age1 = nil
复制代码

16426549656300.jpeg

猜你喜欢

转载自juejin.im/post/7072287470791966750
今日推荐