5、Enum枚举、Optional可选项、运算符重载

1,423 阅读9分钟

一、枚举

1、枚举的基本用法

通过enum关键字来声明

enum Season {
    case spring
    case summer
    case autumn
    case winter
}

等价于:

enum Season{
    case spring,summer,autumn,winter
}

在oc中默认只接受整数类型

//以下的值分别是0 1 2
typedef enum : NSUInteger {
    enum_A,
    enum_B,
    enum_C,
} MyEnum;

而在swift中更加灵活:

  • 原始值可以是字符串、字符、整数值、浮点值
enum Color:String {
    case red   = "Red"
    case amber = "Amber"
    case green = "Green"
}

enum MyEnum:Double {
    case enum_A = 10.0
    case enum_B = 20.0
    case enum_C = 30.0
}
  • 不需要给枚举中的每个成员都提供值 Swift隐式RawValue分配,它建立在swift的类型推断机制上,即使你没有给每个枚举成员提供值,它也会自动提供符合指定类型的值
//指定为Int类型
//前面是 0 1 2 3 从fri开始就是 10 11 12
enum EnumWeek:Int{
    case mon, tue, wed, thu, fri = 10, sat, sun
}
print(EnumWeek.mon)
print(EnumWeek.mon.rawValue)
print(EnumWeek.fri.rawValue)


//指定为String类型
enum EnumWeek:String{
    case mon, tue, wed, thu, fri = "Hello", sat, sun
}

print(EnumWeek.mon)
print(EnumWeek.mon.rawValue)//原始值
print(EnumWeek.fri.rawValue)//原始值

看一下打印结果: iShot2022-04-24_15.36.53.png iShot2022-04-24_15.42.09.png

当指定为String类型的时候,rowValue的值跟枚举成员的字符串一样,来看一下SIL,找找为什么:

enum EnumWeek:String{
    case mon, tue, wed, thu, fri = "Hello", sat, sun
}
var x = EnumWeek.mon.rawValue

iShot2022-04-24_15.55.50.png 从这个结构体可以看出,我们调用rawValue的时候,其实是调用的get方法,那就找一下rawValue的get方法: iShot2022-04-24_15.59.10.png mon被作为一个参数传入了get方法 iShot2022-04-24_16.02.46.png 通过switch模式匹配,最终返回utf8编码的“mon”,那么这个“mon”是存在于哪里呢?我们看一下mach-o: iShot2022-04-24_16.04.54.png 是存在于__Text,__cstring这个Section中。

1.2 枚举值与原始值

enum EnumWeek:String{
    case mon, tue, wed, thu, fri = "Hello", sat, sun
}
print(EnumWeek.mon)//枚举值
print(EnumWeek.mon.rawValue)//原始值

iShot2022-04-24_16.40.22.png 虽然它俩打印出来一样,但其实类型是不一样,枚举值它是EnumWeek类型,原始值是String类型 iShot2022-04-24_16.51.42.png

1.3 枚举的可失败初始化器

当我们给到一个原始值rawValue,希望得到其对应的枚举值case,就可以使用可失败初始化器 iShot2022-04-24_17.25.40.png 但给了一个枚举类型中不存在原始值rawValue的时候,会返回nil

2、枚举的关联值

简单的枚举只有一个定义的类型,而枚举关联值可以给每一个枚举值都关联一个类型,但它就没有原始值

enum Shap{
    case circle(radios:Double) //关联了double
    case rectangle(width:Double, height:Double)//关联了double,double
}

var circle = Shap.circle(radios: 10)
print(circle)

3、枚举模式匹配

在Swift中使用switch进行模式匹配的时候,有几个点要注意:

  • 要匹配所有的case【oc中可以不用】,否则会报错 iShot2022-04-25_11.44.34.png

不想匹配所有case的话,可以先初始化然后增加default关键字 iShot2022-04-25_11.54.33.png

匹配【枚举关联值】 写法一: iShot2022-04-25_14.47.28.png 写法二: var修饰的参数可以修改 iShot2022-04-25_14.48.27.png

4、枚举值的内存大小

枚举占用的内存大小,这里我们区分几种不同的情况

  1. No-payload enums 无关联值的枚举(无负载)
  2. Single-payload enums 只有一个关联值的枚举(一个负载)
  3. Mutil-payload enums 有多个关联值的枚举(多个负载)

4.1 无关联值的枚举 占用的内存大小

enum Season {
    case spring
    case summer
    case autumn
    case winter
}

var a = Season.spring
var b = Season.summer
var c = Season.autumn
var e = Season.winter

print(MemoryLayout<Season>.size)      // 内存大小 1
print(MemoryLayout<Season>.stride)    // 1
print(MemoryLayout<Season>.alignment) // 1

打断点,利用以下两个lldb命令打印,观察内存占用情况:

po withUnsafePointer(to: &a, {print($0)})

memory read 0x0000000100008068 iShot2022-04-25_16.02.13.png

可以看到,每个枚举值占用一个【十六进制中的一位】

对于无关联值的枚举来说,它有一个隐式值硬编码入mach-o,在内存中只需要存储枚举值,默认是以UInt8存储的,也就是1字节

  • UInt8 = 2^8 = 256 所以对于无关联值的枚举来说,一个字节最多能存256个枚举值,超过256,系统就升级成UInt16 -> UInt32 -> UInt64

4.1 只有一个关联值的枚举 占用的内存大小

4.1.1 当关联值是BOOL类型时:

看一下这个例子:

enum CTEnum {
    case enum_One(Bool)
    case enum_Two
    case enum_Three
    case enum_Four
}
print(MemoryLayout<CTEnum>.size)

iShot2022-04-25_16.31.54.png Bool值虽然需要占用一个字节的内存,但实际存储的是0x00,或者0x01,也就是这一个字节中BOOL值它没有完全利用,系统把剩余的未利用的空间,来存储枚举中其他枚举值,也就是余下的7位,此时这个枚举最多只能有 2^7 = 128 个枚举值。如果不够,会申请更多内存来存储。

4.1.2 当关联值是Int类型时:

把上面bool类型换成Int类型试一下:

enum CTEnum {
    case enum_One(Int)
    case enum_Two
    case enum_Three
    case enum_Four
}
print(MemoryLayout<CTEnum>.size)      //9
print(MemoryLayout<CTEnum>.stride)    // 16
print(MemoryLayout<CTEnum>.alignment) // 8

iShot2022-04-25_17.02.38.png Int本身已经占了8位(1个字节),它无法向Bool一样有剩余的空间来存储其他枚举值

所以再需要一个字节来存储其他枚举值,就有了 8(Int) + 1 = 9

总结一下:只有一个关联值的枚举,它占用的内存大小与它关联的那个类型有关,有没有剩余空间可以存储其他枚举值

4.2 有多个关联值的枚举 占用的内存大小

先测一下几个不同的枚举占用内存大小

//4个bool关联值
enum Season1 {
    case spring(Bool)
    case summer(Bool)
    case autumn(Bool)
    case winter(Bool)
}
//4个Int关联值
enum Season2 {
    case spring(Int)
    case summer(Int)
    case autumn(Int)
    case winter(Int)
}
//一个Bool 一个Int 两个无关联值的枚举值
enum Season3 {
    case spring(Bool)
    case summer(Int)
    case autumn
    case winter
}

//一个 4*int关联值 三个无关联值的枚举值
enum Season4 {
    case spring(Int, Int, Int, Int)
    case summer
    case autumn
    case winter
}

print(MemoryLayout<Season1>.size)   // 1
print(MemoryLayout<Season2>.size)   // 9
print(MemoryLayout<Season3>.size)   // 9
print(MemoryLayout<Season4>.size)   // 33

结论:

  • 有多个关联值的时候,如果关联值大小加起来都小于一字节,那么枚举的大小就是1字节。
  • 如果关联值大小大于1字节,枚举的大小为:最大关联值的大小 + 1。

4.3 特殊情况

enum Season {
    case season
}

print(MemoryLayout<Season>.size)   // 0

当枚举中只有一个枚举值的时候,不需要任何东西来区分case,所以大小是0

4.4 关联值内存分布

当枚举中只有一个关联值的时候,内存的排布式连续的 iShot2022-04-26_11.16.27.png 有两个以上的关联值的时候,内存则不是连续排布 iShot2022-04-26_11.19.53.png

5、indirect关键字

有的时候我们想把枚举的关联值定义成这个枚举本身的类型(递归),像这样:

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

iShot2022-04-26_16.06.02.png 默认情况下是不允许的,需要添加indirect关键字

5.1 在枚举类型前面增加indirect关键字

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

默认情况夏枚举是值类型,是分配在栈空间的,而增加了indirect关键字之后,这个枚举里有关联类型的枚举值会被分配在堆空间上。

下面通过几个角度来证明:

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

var node = BinaryTree<Int>.node(left: BinaryTree<Int>.empty, value: 10, right: BinaryTree<Int>.empty)

print("end")

1、从LLDB的角度: iShot2022-04-26_16.18.53.png 2、从Mach-o内存分配的角度: iShot2022-04-26_17.04.47.png 3、从SIL的角度: iShot2022-04-26_16.24.19.png 在main函数中,调用了alloc_box,从sil文档可以查阅到:alloc_box的作用就是在堆上分配空间。本质上就是调用swift_allocObjectSIL文档iShot2022-04-26_16.28.17.png 4、从汇编的角度: iShot2022-04-26_16.42.09.png 5、枚举中无关联类型 iShot2022-04-27_10.31.26.png

5.2 在case前面增加indirect关键字

如果只有某个枚举值增加了indirect关键字,那么就只有这个枚举值是引用类型,只有这个枚举值分配到堆空间上。

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

var node = BinaryTree<Int>.node(left: BinaryTree<Int>.empty, value: 10, right: BinaryTree<Int>.empty)
var empty = BinaryTree<Int>.empty

print("end")

总结一下:

  • 1、默认情况下枚举值是值类型,是分配在栈空间的,而增加了indirect关键字之后,这个枚举里有关联类型的枚举值会被分配在堆空间上。
  • 2、如果只有某个枚举值增加了indirect关键字,那么就只有这个枚举值是引用类型,只有这个枚举值分配到堆空间上。
  • 3、无关联类型的枚举值不能使用indirect关键字。

二、Optional可选值

1、可选值

var age: Int?
//等价于
var age:Optional<Int>

从源码的角度分析一下Optional(Optional.swift): iShot2022-04-27_11.09.59.png Optional本质是一个枚举,有两个参数,nonesomesome存着我们对应的类型

2、可选值绑定(解包)

由于Optionl是一个枚举,那我们就可以对一个optional的变量进行模式匹配,以保证我们使用这个变量时确保它有值。

var age:Int? = 10;

switch age {
    case .some(let value):
        print(value)
    case .none:
        print("not exits")
}

2.1 可选值绑定if let

但这种写法太过麻烦,可以采取if let可选值绑定的方法

Optionl可选值可以想象成一个盒子,if let相当于把盒子里的东西取出来,赋值给let,这个过程称为可选值绑定解包,如果盒子里没有东西,那就是nil,则if let判断不成立。

var age:Int? = 10

if let value = age {
    print(value)
}

2.2 guard let守护

还有一种方法是guard let守护

  • 这个let一定有值,否则return
  • 通常判断是否有值后,在内部写具体逻辑实现
func submit() {
    
    let age:String? = "18"
    
    guard let value = age else{
        print("age为空")
        return
    }
    
    print("age判断通过")
}

submit()

guard let内部还可以增加条件,比如: iShot2022-04-27_12.02.49.png

三、??运算符(空合并运算符)

(a ?? b)

var age:Double?

var x = age ?? 10.11

print(x)

可选类型a进行空判断,如果a有值,就进行解包,否则就返回默认值b

  • 表达式a必须是optional

  • 默认值b类型必须和a存储值类型保持一致 iShot2022-04-27_14.46.53.png iShot2022-04-27_14.48.53.png 看看源码:

其实就是重载运算符 iShot2022-04-27_15.15.26.png

四、运算符重载

4.1 运算符重载

Swift提供了一些方法,让我们也能自己来实现运算符重载

struct Vector{
    let x:Int
    let y:Int
}
extension Vector{
    static func + (firstVector:Vector, secondVector:Vector) -> Vector{
        return Vector(x: firstVector.x + secondVector.x, y: firstVector.y + secondVector.y)
    }
    static prefix func - (vector:Vector) -> Vector {
        return Vector(x: -vector.x, y: -vector.y)
    }
    static func - (firstVector:Vector, secondVector:Vector) -> Vector{
        return firstVector + (-secondVector)
    }
}

var x = Vector(x: 10, y: 20)
var y = Vector(x: 20, y: 30)

var z = x + y
print(z) //Vector(x: 30, y: 50)

var z1 = y - x
print(z1) //Vector(x: 10, y: 10)

var z2 = -x
print(z2) //Vector(x: -10, y: -20)

iShot2022-04-27_16.47.22.png

4.2 自定义运算符

操作符声明 文档

自定义操作符 文档

4.2.1 自定义运算符

我们可以自己创造一个符号,然后给它一个运算的实现

例子: infix operator >< iShot2022-04-27_17.43.11.png 关键字infix是定义中缀运算的,还有prefix前缀运算符postfix后缀运算符

4.2.1 指定运算符【优先级组】

优先级组

  • 同类型的操作符组成一个优先级组(precedence group)
  • 组与组之间、组内操作符成员 的运算优先级 从上到下表示优先级从高到低

例子:以下是BitwiseShiftPrecedence组 和 MultiplicationPrecedenceiShot2022-04-27_17.56.24.png

在自定义中缀操作符的时候,可以指定它属于哪个【优先级组】的。当不指定时,模式是DefaultPrecedence也就是BitwiseShiftPrecedence,默认的优先级挺高的,比加减乘除高。

infix operator ><:MultiplicationPrecedence

这个时候,><运算符的优先级就跟MultiplicationPrecedence这个组的优先级一样 iShot2022-04-28_11.02.49.png iShot2022-04-28_11.08.09.png

4.2.2 自定义优先级组

我们可以自己定义一个优先级组,然后再自定义的运算符中指定到这个优先级组

precedencegroup MyPrecedenceGroup {
    higherThan: AdditionPrecedence
    lowerThan: MultiplicationPrecedence
    associativity: left
    assignment: false
}

infix operator ><: MyPrecedenceGroup

higherThan:比某个优先级组的优先级高。

lowerThan:比某个优先级组的优先级底。

associativity:运算顺序,left从左到右 right从右到左 none不许多个同样的运算符出现。

assignment:是否允许可选值使用运算符,TRUE的时候同意,且当可选值为nil的时候,不执行运算。

associativity:运算顺序: iShot2022-04-28_11.34.46.png iShot2022-04-28_11.35.42.png assignment:是否允许可选值使用运算符 iShot2022-04-28_11.39.41.png

五、隐式解析可选类型

解析可选类型 可以是显式的,也可以说是手动解包:

var age:Int? //可选类型

//if let解包
if let value = age { 
    print(value)
}

//guard let守护
func MyFunc(){
    guard let value = age else {
        print("is nil")
        return
    }
    print(value)
}

也可以是隐式的:

var age:Int!
print(age)

隐式解析可选类型相当于你告诉对 Swift 编译器,我在运行时访问时,一定有值。那么编译器便不会去检查它,但如果不讲武德实际上里面是nil,那么就会崩溃。