Swift Enum & Optional

283 阅读8分钟

Swift相关:

1、Enum

Swift的枚举非常好用!

1.1 枚举的基本用法

swift 中通过 enum 关键字来声明一个枚举,它和类、结构体一样,枚举中也可以添加异变方法(mutaing)、计算属性、扩展(extension),遵循协议;枚举是值类型,存储在栈区。

enum Swagger {
    case Zed
    case Fiona
    case Jax
}

// 和这个写法一样
enum Swagger {
    case Zed, Fiona, Jax
}

1.2 关联值

在swift中,枚举不能定义存储属性,如果我们想用枚举类型来表示更复杂的类型,就需要通过关联值来实现,它可以为枚举值存储一些必要的数据:

enum Swagger {
    // zed的攻击,防御和能量
    case zed(attack: Int, defense: Int, power: Int)
    case jax(attack: Int, defense: Int)
}

模式匹配

var zed = Swagger.zed(attack: 100, defense: 60, power: 200)
var jax = Swagger.jax(attack: 120, defense: 100)

func league(sg: Swagger) {
    switch sg {
    case .zed(let attack, let defense, var power):
        if power > 80 { power -= 80 }
        print("\(attack) \(defense) \(power)")
    case let .jax(attack, defense):
        print("\(attack) \(defense)")
    }
}

func test(){
    league(sg: zed)
    league(sg: jax)
}

这种模式匹配的方式,可以把关联值或枚举值定义为 letvar,只是取决于后续是否需要修改,也可以使用 defalut 关键字

enum Weak: String {
    case MONDAY 
    case TUEDAY 
    case WEDDAY 
    case THUDAY 
    case FRIDAY
    case SATDAY
    case SUNDAY
}
let currentWeak: Weak = Weak.MONDAY
switch currentWeak{
  case .SATDAY, .SUNDAY: print("Happy day") 
  default : print("Sad day")
}

1.3 原始值

枚举成员使用相同类型的默认值,这个默认值叫做原始值

enum SwagNum: String {
    case one = "one"
    case two = "two"
    case three = "three"
}

var one = SwagNum.one
print(one.rawValue)  // one

在枚举 Season 的后面加上 : 并指定具体的类型,这个时候枚举的原始值默认就是这个类型。这个类型可以是字符串、字符、任意的整数值,或者是浮点类型。我们可以通过 rawValue 拿到枚举成员的原始值。

注意:原始值并不占用枚举变量的内存。

隐式原始值(Implicitly Assigned Raw Values)

如果枚举的原始值类型是 IntString,Swift 会自动分配原始值。如果枚举的原始值是 Int 类型的,自动分配的原始值从第一个成员开始,下标从 0 计算,依次 +1。代码如下

enum SwagNum: Int {
    case one, two, three, four
}

print(SwagNum.one.rawValue)    // 0
print(SwagNum.two.rawValue)    // 1
print(SwagNum.three.rawValue)  // 2
print(SwagNum.four.rawValue)   // 3

假设把 three 的原始值指定成 5,那么从 three 开始,下标从 5 开始计算,依次 +1。

enum SwagNum: Int {
    case one, two, three = 5, four
}

print(SwagNum.one.rawValue)    // 0
print(SwagNum.two.rawValue)    // 1
print(SwagNum.three.rawValue)  // 5
print(SwagNum.four.rawValue)   // 6

1.4 枚举的内存布局

接下来我们看一下枚举的大小,定义一个枚举 Password,如下:

enum Password {
    case number(Int, Int, Int, Int)
    case other
}

print(MemoryLayout<Password>.size)      // 33
print(MemoryLayout<Password>.stride)    // 40
print(MemoryLayout<Password>.alignment) // 8

通过打印发现,枚举占用 33 个字节,系统分配了 40 个字节,并且 8 字节对齐。那这是怎么一回事呢?

number 有四个 Int 类型的关联值,那一个 Int 类型占 8 个字节,四个 Int 类型占 32 个字节。成员 other 没有关联值,占 1 个字节。

那为什么占 33 个字节呢? 32 个字节已经可以存储 number 或者 other 了。

假设我定义一个枚举变量,每个枚举变量的值为 number,而 number 占 32 个字节。如果我用这 32 个字节来存储 other,没有任何问题。这个时候我反过来,假设这个枚举变量的值为 other,而 other 占 1 个字节。此时 1 个字节确实可以存储 other,但存储不了 number。因为系统不确定你这个枚举的变量将来存储的是 number 还是 other,所以,直接分配给这个枚举 33 个字节。而这个枚举是 8 字节对齐,所以系统最后分配了 40 个字节来存储枚举变量的值。

枚举也有一个比较特殊的地方,枚举占用的内存大小和分配的内存大小和关联值的位置也有关系:

enum Password {
    case number(Int, Int, Bool, Bool)
}

print(MemoryLayout<Password>.size)      // 18
print(MemoryLayout<Password>.stride)    // 24
print(MemoryLayout<Password>.alignment) // 8

enum Password {
    case number(Int, Bool, Int, Bool)
}

print(MemoryLayout<Password>.size)      // 25
print(MemoryLayout<Password>.stride)    // 32
print(MemoryLayout<Password>.alignment) // 8

注意,成员 number 的关联值,前两个为 Int 类型的时候,枚举占用 18 个字节,系统分配了 24 个字节。当我把第二个 Int 类型的关联值放到第三个位置的时候,枚举占用 25 个字节,系统分配了 32 个字节。

明明没有往枚举里添加新的成员,只是把位置互换,枚举的内存大小就发生了改变。这是为什么呢?

首先,Int 类型占 8 个字节,Bool 类型占 1 个字节。所以第一种情况前面两个关联值是 Int 类型的时候,枚举占用 18 个字节(8 + 8 + 1 + 1 = 18)。第二种情况,把 Int 类型放到了第三个位置,那应该也等于 18 个字节才对(8 + 1 + 8 + 1 = 18),但通过打印,实际占用的是 25 个字节。这个怎么解释呢?

以第二个为例,枚举在计算内存的大小时候是这样的:

  • 第一个关联值占 8 字节,这个时候内存对齐为 8 字节。
  • 到了第二个关联值的时候,8 + 1 = 9,因为当前的枚举已经是 8 字节对齐了,这个时候系统会进行内存对齐,所以当到了第二个的时候,此时应该枚举的内存应该是 16 字节对齐。
  • 接下来到第三个关联值,因为是 Int 类型,所以 16 + 8 = 24,到这里就算进行内存对齐也占用 24 个字节。
  • 最后一个关联值为 Bool 类型,占 1 个字节,所以 24 + 1 = 25,最终这个枚举的 number 成员占用 25 个字节,并且,它自身也是占用 25 个字节(因为只有一个成员 number)。

那为什么到最后一个的时候不进行内存对齐了呢,那是因为后面已经没有了关联值了,25 个字节的内存已经足够存储 number 的值,所以没有必要在这里进行内存对齐,在最后进行分配的时候,系统会整体的对枚举进行内存分配。

所以,枚举的内存大小和关联值的大小有关,并且还与关联值的存储位置有关系

1.5 关联值和原始值的区别

枚举的关联值和原始值在本质上的区别就是,关联值占用枚举的内存,而原始值不占用枚举的内存, 并且 rawValue 本质上是一个计算属性rawValue 的实现大概应该是这样:

enum SwagNum: Int {
    case one, two, three

    var rawValue: Int {
        get {
            switch self {
                case .one:
                    return 1
                case .two:
                    return 2
                case .three:
                    return 3
            }
        }
    }
}

print(SwagNum.one.rawValue)   // 1
print(SwagNum.two.rawValue)   // 2
print(SwagNum.three.rawValue)   // 3

枚举的原始值不占内存大小的原因是rawValue 是一个计算属性,而计算属性的本质就是方法,方法并不占用枚举的内存。

1.6 递归枚举

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

我们来看下面的代码:

indirect enum Swagger {
    case number(Int)
    case sum(Swagger, Swagger)
    case diff(Swagger, Swagger)
}

我们也可以单独对枚举的某个成员加上 indirect 修饰,代码如下:

enum Swagger {
    case number(Int)
    indirect case sum(Swagger, Swagger)
    indirect case diff(Swagger, Swagger)
}

在使用时可以定义一个方法,这个方法用来处理枚举的成员,如:

let one = Swagger.number(1)
let two = Swagger.number(2)
let three = Swagger.number(3)
let sum = Swagger.sum(one, two)
let diff = Swagger.diff(sum, three)

func calculate(_ num: Swagger) -> Int {
    switch num {
        case let .number(value):
            return value
        case let .sum(left, right):
            return calculate(left) + calculate(right)
        case let .diff(left, right):
            return calculate(left) - calculate(right)
    }
}

print(calculate(diff))

2、Optional

2.1 可选值

可选值,一般也叫可选类型,它允许将值设置为 nil。在类型后面添加一个问号(?)来定义一个可选值:

var name: String? = "Swagger"
name = nil

var age: Int? 
age = 10

除了可以用问号(?)表达之外,也可以使用 Optional 关键字来表示:

var name: Optional<String> = "Swagger"
name = nil

var age: Optional<Int> // age 默认就是 nil
age = 10

2.2 ?? 运算符 (空合并运算符)

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

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

2.3 可选链

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

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

2.4 隐式解析可选类型

隐式解析可选类型是可选类型的一种,使用的过程中和非可选类型一样。它们之间唯一的区别是,隐式解析可选类型其实就是告诉Swift编译器,在运行访问时,值不会为nil截屏2022-02-07 15.24.09.png