Swift

Swift 中 Enum 及 Optional 介绍

2022-01-17  本文已影响0人  晨曦的简书

Enum

枚举的基本用法

swift 中通过 enum 关键字来声明一个枚举

enum CXEnum { 
  case test_one
  case test_two
  case test_three 
}

而在 C 或者 OC 中默认受整数支持,也就意味着下面的例子中: ABC 分别代表 0、 1、2。

typedef NS_ENUM(NSUInteger, CXEnum) { 
  A,
  B,
  C, 
};

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

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

enum CXEnum: Double { 
  case a = 10.0
  case b= 20.0 
  case c = 30.0 
  case d = 40.0
}

这里其实是因为编译器做了很多的操作,第一个就是隐士 RawValue 分配,是建立在 Swift 的类型推断机制上的。

enum DayOfWeek: Int {
  case mon, tue, wed, thu, fri = 10, sat, sun
}

这里从 mon 开始对应的值是从 0 开始的,但是也可以自己指定枚举的值,当指定 fri = 10 之后,从 fri 开始 satsun 就分别对应 11、12。

对于 DayOfWeek,也可以指定为 String 类型,也可以指定枚举的值,如下所示:

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

这里输出枚举值的 RawValue,当没指定枚举成员的原始值的时候 RawValue 的值跟枚举成员值的字符串是一样的, 这是编译器默认分配的。

关联值

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

var circle = Shape.circle(radius: 15.0)
var rectangle = Shape.rectangle(width: 15, height: 7)

如果我们想用枚举类型来表示更复杂的类型,例如圆形跟矩形,就可以如上,通过关联值的形式进行实现,circle 代表圆形,可以为它关联一个 Double 类型的 radius 来代表半径。rectangle 代表矩形,可以为它关联 Int 类型的 widthheight 来代表宽高。

模式匹配

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 .MONDAY: print(Weak.MONDAY.rawValue)
    case .TUEDAY: print(Weak.TUEDAY.rawValue)
    case .WEDDAY: print(Weak.WEDDAY.rawValue)
    case .THUDAY: print(Weak.THUDAY.rawValue)
    case .FRIDAY: print(Weak.FRIDAY.rawValue)
    case .SUNDAY: print(Weak.SUNDAY.rawValue)
    case .SATDAY: print(Weak.SUNDAY.rawValue)
}

如上代码所示,是穷尽了所有的枚举成员值,如果不想匹配所有的 case,可以使用 defalut 关键字。

switch currentWeak{
    case .SATDAY, .SUNDAY: print("Happy Day")
    default : print("SAD DAY")
}

如果我们要匹配关联值的话,示例代码如下:

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

var shape = Shape.circle(radius: 15.0)

switch shape {
    case let .circle(radious):
        print("Circle radious:\(radious)")
    case let .rectangle(width, height):
        print("rectangle width:\(width),height\(height)")
}

当然还可以这么写:

switch shape{
    case .circle(let radious):
        print("Circle radious:\(radious)")
    case .rectangle(let width, var height):
        height += 10
        print("rectangle width:\(width),height\(height)")
}

跟类跟结构体一样,枚举中也可以添加异变方法(mutaing)、属性、扩展(extension),也可以遵循协议。枚举是值类型,存储在栈区。

枚举的大小

No-payload enums

接下来我们来讨论一下枚举占用的内存大小,这里我们区分几种不同的情况,首先第一种就是
No-payload enums,没有关联值。

enum Weak: String {
    case MONDAY
    case TUEDAY
    case WEDDAY
    case THUDAY
    case FRIDAY
    case SATDAY
    case SUNDAY
}

大家可以看到这种枚举类型类似我们在 C 语言中的枚举,当前类型默认是 Int 类型,那么对于这一类的枚举在内存中是如何布局?以及在内存中占用的大小是多少那?这里我们就可以直接使用 MemoryLayout 来测量一下当前枚举内存大小。

可以看到这里我们测试出来的不管是 size 还是 stride 都是 1 ,这个地方我们也很好理解,当前的 enum有几个 case? 是不是 8 个,在 Swift 中进行枚举布局的时候一直是尝试使用最少的空间来存储 enum,对于当前的 case 数量来说, UInt8 能够表示 256 个 case,也就意味着如果一个默认枚举类型且没有关联值的 case 少于 256 ,当前枚举类型的大小都是 1 字节,所以可以由 UInt8 的方式来存储当前枚举的值。当 case 大于 256 的时候存储方式就会由 UInt8 升级为 UInt16

通过上面的打印我们可以直观的看到,当前变量 abc 这三个变量存储的内容分别是 000102,分别相差 1 个字节, 这和我们上面说的布局理解是一致的。

Single-payload enums

No-payload enums 的布局比较简单,我们也比较好理解,接下来我们来理解一下 Single-payload enums 的内存布局, 字面意思就是有一个成员负载的 enum,比如下面这个例子:

这个时候可以看到枚举的大小是 1 字节,下面我们将关联值改为 Int 类型再来看一下:

这里我们可以看到,同样是一个负载,只是因为关联值类型不同,就会造成内存的大小不同。 这是因为在 Swift 中的 enum 中的 Single-payload enums 会使用负载类型中的额外空间来记录没有负载的 case 值。这句话该怎么理解?首先 Bool 类型是 1 字节,也就是 UInt8 ,所以当前能表达 256 个 case 的情况,对于布尔类型来说,只需要使用低位的 0, 1 这两种情况,其他 7 位剩余的空间就可以用来表示没有负载的 case 值。对于 Int 类型的负载来说,其实系统是没有办法推算当前的负载所要使用的位数,Int 类型就是 8 字节,也就意味着当前 Int 类型的负载是没有额外的剩余空间的,这个时候我们就需要额外开辟内存空间来去存储我们的 case 值,也就是 8+1 =9 字节。

Mutil-payload enums

上面说完了 Single-payload enums, 接下来我们说第三种情况 Mutil-payload enums, 有多个负载的情况产生时,当前的 enum 是如何进行布局的呢?

这里我们可以看到当前内存存储的分别是 00, 01, 40, 41, 80,81 , 这里在存储当前的 case 时候会使用到 common spare bits,什么意思?其实在上一个案例我们也讲过了,首先 bool 类型需要 1 字节,也就是 8 位。

对于 bool 类型来说,我们存储的无非就是 0 或 1 ,只需要用到 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 这两个文件,找到了验证大家可以一起交流。

当前一般来说,我们有多个负载的枚举时,当前枚举类型的大小取决于当前最大关联值的大小。
我们来看一个例子

enum CXEnum{
    case test_one(Bool)
    case test_two(Int)
    case test_three
    case test_four
}

当前 CXEnum 的大小就等于 sizeof(Int) + sizeof(rawVlaue) = 9,在比如下面这个例子:

enum CXEnum{
    case test_one(Bool)
    case test_two(Int, Int, Int)
    case test_three
    case test_four
}

当前大小就是 sizeof(Int) * 3 + sizeof(rawVlaue) = 25

最后这里有我们再来看下下面的案例:

enum CXEnum{
    case test_one
}

对于当前的 CXEnum 只有一个 case,我们不需要用任何东⻄来去区分当前的 case, 所以当我们打印当前的 CXEnum,可以发现大小是 0。

indirect 关键字

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

如上所示,我们也可以用枚举来表示树中的一个节点,不过需要用到 indirect 关键字,indirect 的意思是表示当前枚举是引用类型,分配在堆空间。

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

我们也可以修改 indirect 关键字的位置,当 indirect 关键字放在 case 的前面的时候,表示只有是 node 这种 case 的时候枚举才会是引用类型,为 empty 的时候依然是值类型。

Optional

认识可选值

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

class LGTeacher{
    var age: Int?
}

当前的 age 我们就称之为可选值,当然可选值的写法这两者是等同的

var age: Int? = var age: Optional<Int>

那对于 Optional 的本质是什么?我们直接跳转到源码,打开 Optional.swift 文件,可以看到源码中 Optional 的本质是枚举。

@frozen
public enum Optional<Wrapped>: ExpressibleByNilLiteral {
    case none
    case some(Wrapped)
}

既然 Optional 的本质是枚举,那么我们也可以仿照系统的实现制作一个自己的 Optional

enum MyOptional<Value> {
    case some(Value)
    case none
}

比如给定任意一个自然数,如果当前自然数是偶数返回,否则为 nil,我们应该怎么表达这个案 例

enum MyOptional<Value> {
    case some(Value)
    case none
}

func getOddValue(_ value: Int) -> MyOptional<Int> { 
    if value % 2 == 0 {
        return .some(value) }
    else{
        return .none
    } 
}

这个时候给定一个数组,我们想删除数组中所有的偶数

var array = [1, 2, 3, 4, 5, 6]
for element in array {
    let value = getOddValue(element)
    array.remove(at: array.firstIndex(of: value)) 
}

这个时候编译器就会检查我们当前的 value 会发现他的类型和系统编译器期望的类型不符,这个时候我们就能使用 MyOptional 来限制语法的安全性。

于此同时我们通过 enum 的模式匹配来取出对应的值

for element in array {
    let value = getOddValue(element) 
    switch value {
        case .some(let value):
            array.remove(at: array.firstIndex(of: value)!) 
        case .none:
            print("vlaue not exist") }
}

如果我们把上述的返回值更换一下,其实就和系统的 Optional 使用无异

func getOddValue(_ value: Int) -> Int? { 
    if value % 2 == 0 {
        return .some(value) 
    }else{
        return .none
    } 
}

这样我们其实是利用当前编译器的类型检查来达到语法书写层面的安全性。

当然如果每一个可选值都用模式匹配的方式来获取值在代码书写上就比较繁琐,我们还可以使 用 if let 的方式来进行可选值绑定

if let value = value{
    array.remove(at: array.firstIndex(of: value)!)
}

除了使用 if let 来处理可选值之外,我们还可以使用 gurad let 来简化我们的代码,我们来看一个具体的案例

gurad letif let 刚好相反,守护一定有值。如果没有,直接返回。通常判断是否有值之后,会做具体的逻辑实现,通常代码多,如果用 if let 凭空多了一层分支,guard let 是降低分支层次的办法。

可选链

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

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

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

let str: String? = "kody"
let upperStr = str?.uppercased().lowercased()

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

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

运算符重载

在源码中我们可以看到除了重载了 ?? 运算符,Optional 类型还重载了 == , ?= 等运算符,实际开发中我们可以通过重载运算符简化我们的表达式。

比如在开发中我们定义了一个二维向量,这个时候我们想对两个向量进行基本的操作,那么我们就可以通过重载运算符来达到我们的目的

struct Vector { 
    let x: Int 
    let y: Int
}

extension Vector {
    static func + (fistVector: Vector, secondVector: Vector) -> Vector {
        return Vector(x: fistVector.x + secondVector.x, y: fistVector.y + seco 
    }
    static prefix func - (vector: Vector) -> Vector { 
        return Vector(x: -vector.x, y: -vector.y)
    }
    static func - (fistVector: Vector, secondVector: Vector) -> Vector {
        return fistVector + -secondVector 
    }
}

通过以上代码我们可以对 Vector 进行相加、相减、或者取负。

更多关于自定义运算符的规则也可以看下官方文档 自定义运算符

隐士解析可选类型

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

var age: Int? 
var age1: Int!
age = nil 
age1 = nil

其实日常开发中我们比较常⻅这种隐士解析可选类型

IBOutlet 类型是 Xcode 强制为可选类型的,因为它不是在初始化时赋值的,而是在加载视图的时候。你可以把它设置为普通可选类型,但是如果这个视图加载正确,它是不会为空的。

与可选值有关的高阶函数

var dict = ["one": "1", "two": "2"] 
let result = dict["one"].map{ Int($0) } // Optional(Optional(1))

上面的代码中我们从字典中取出字符串 "1",并将其转换为 Int 类型,但因为 String 转换成 Int 不一定能成功,所以返回的是 Int? 类型,而且字典通过键不一定能取得到值,所以 map 返回的也是一个 Optional,所以最后上述代码 result 的类型为 Int?? 类型。

那么如何把我们的双重可选展平开来,这个时候我们就需要使用到 flatMap

var dict = ["one": "1", "two": "2"]
let result = dict["one"].flatMap{ Int($0) } // Optional(1)
let array = ["1", "2", "3", nil]
let result = array.compactMap{ $0 } // ["1", "2", "3"]
let array = ["1", "2", "3", "four"]
let result = array.compactMap{ Int($0) } // [1, 2, 3]
上一篇下一篇

猜你喜欢

热点阅读