Swift-枚举&Optional

枚举为一组相关的值定义了一个共同的类型,使你可以在代码中以类型安全的方式来使用这些值。在C或者Objective-C语言中,枚举会为一组整型值分配相关联的名称。

枚举

枚举的基本用法

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

下面看个例子,Swift中通过enum关键字来声明一个枚举:

enum ATEnum {
    case one
    case two
    case three
}
复制代码

这个是最简单的枚举定义,再看一下Objective-C的枚举声明:

typedef NS_ENUM(NSInteger, ATEnum) {
    A,
    B,
    C
}
复制代码

这里的A、B、C分别默认代表0、1、2,而在Swift中,原始值可以声明成不同的类型。

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

// Double类型
enum LGEnum: 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
}
复制代码

从上面的原始值来看也是从012开始的,然后fri10sat11,这个也是和Objective-C是一样的,我们把上面的Int类型改成String类型,把fri改个String类型的值,然后输出原始值看一下。 01.png 从输出可以看到系统会自动枚举成员值输出对应的字符串,如果成员枚举被单独赋值了那就读取它的值(fri输出hello),我们把上面代码通过命令编译成SIL文件,来研究一下枚举是怎么读取原始值的。

SIL文件分析

访问枚举rawValue的代码:

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

var x = DayOfWeek.mon.rawValue
复制代码

通过生成sil文件的命令

// 生成sil命令
swiftc -emit-sil main.swift > ./main.sil
复制代码

编译命令生成的sil文件,看到枚举的定义:

enum DayOfWeek : String {
  case mon, tue, wed, thu, fri, sat, sun
  init?(rawValue: String) // 可失败初始化器
  typealias RawValue = String // 取别名
  var rawValue: String { get } // get rawValue函数
}
复制代码

我们找到sil文件中main函数代码如下:

扫描二维码关注公众号,回复: 13744917 查看本文章
// main
sil @main : $@convention(c) (Int32, UnsafeMutablePointer<Optional<UnsafeMutablePointer<Int8>>>) -> Int32 {
bb0(%0 : $Int32, %1 : $UnsafeMutablePointer<Optional<UnsafeMutablePointer<Int8>>>):
  alloc_global @$s4main1xSSvp                     // id: %2
  %3 = global_addr @$s4main1xSSvp : $*String      // user: %8
  %4 = metatype $@thin DayOfWeek.Type
  %5 = enum $DayOfWeek, #DayOfWeek.mon!enumelt    // user: %7
  
  // function_ref DayOfWeek.rawValue.getter (rawValuegetter方法)
  %6 = function_ref @$s4main9DayOfWeekO8rawValueSSvg : $@convention(method) (DayOfWeek) -> @owned String // user: %7
  %7 = apply %6(%5) : $@convention(method) (DayOfWeek) -> @owned String // user: %8
  store %7 to %3 : $*String                       // id: %8
  %9 = integer_literal $Builtin.Int32, 0          // user: %10
  %10 = struct $Int32 (%9 : $Builtin.Int32)       // user: %11
  return %10 : $Int32                             // id: %11
} // end sil function 'main'
复制代码

rawValuegetter方法里,通过获取mon的枚举值传入到getter方法(也就是上面的s4main9DayOfWeekO8rawValueSSvg),搜索s4main9DayOfWeekO8rawValueSSvg函数看一下它的定义: 02.png

  • self就是DayOfWeek
  • %0就是传进来的枚举成员值
  • 通过模式匹配走到对应的分支(bb1、bb2...)
  • 不同的代码分支里获取到对应的字符串给到不同分支的返回值

而上面的字符串其实就是从Mach-O文件的Section64(__TEXT,__cstring)里读取的。

枚举值&原始值

还是上面的代码,分别打印枚举值和原始值,看日志的输出: 03.png 结果发现输出的都是mon,打印的结果是一样的,但实际的类型是不一致的。我们不能把一个枚举值赋值给一个String类型的变量,也不能把一个String类型的值赋值给一个枚举变量04.png 从上面的sil文件在枚举定义中有个可失败初始化器,我们在代码中加个初始化器的符号断点。 05.png 然后运行,发现这里可失败初始化方法并没有调用。那可失败初始化器什么时候才会调用?当我们给定一个原始值希望得到它的枚举值,可以通过下面方法:

DayOfWeek.init(rawValue: "mon")
复制代码

为什么是可失败初始化器?我们在初始化过程中可能赋值一个不存在的枚举字符串,这样返回就是nil了。 06.png 这种可以通过String值获取它的枚举值,一般的使用场景可以在模式匹配通过switch case来做相应的操作。

关联值

有时候我们想通过枚举值来表达更复杂的案例,可以定义Swift枚举来存储任意类型的关联值,每个枚举成员的关联值类型可以各不相同。通过下面的案例说明一下:

// 通过给定关联值来表示具体的图形
enum Shape {
    case circle(radius: Double)
    case rectangle(width: Double, height: Double)
}
复制代码

上面的案例就是通过给定具体关联值来表示不同的形状,当给定关联值后对于枚举成员变量来说就没有原始值了。对于关联值来说可以让枚举成员携带更复杂的信息从而表达更复杂的案例。

关联值的使用

通过关联值定义的枚举,使用也很简单。

// 定义一个半径为10的圆形
var circle = Shape.circle(radius: 10.0)
// 定义一个宽高为10、20的矩形
var rectangle = Shape.rectangle(width: 10.0, height: 20.0)
复制代码
关联值注意事项

模式匹配通过switch关键字去匹配当前的枚举值,从而获取对应的分支。

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

let currentWeak: Weak = Weak.MONDAY

// 通过switch匹配具体的分支
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 .SATDAY:
    print(Weak.SATDAY.rawValue)
case .SUNDAY:
    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: Double, height: Double)
}

let shape = Shape.circle(radius: 10.0)

switch shape {
    case let .circle(radius):
        print("Circle radius: \(radius)")

    case let .rectangle(width, height):
        print("rectangle width:\(width),height\(height)")
}
复制代码

switch语句中可以从上面通过关联值拿到radius的值然后打印出来。case let就是模式匹配的写法,除了这种写法还可以把let放在参数里面,相当于声明了一个临时变量去接收radius的值,代码如下:

switch shape {
    case .circle(let radius):
        print("Circle radius: \(radius)")

    case .rectangle(let width, let height):
        print("rectangle width:\(width), height\(height)")
}
复制代码

枚举的大小

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

No-payload enums

没有负载的enum,也就是只有隐式值没有关联值。下面通过一个案例说输出一下它的大小。 07.png 通过MemoryLayout输出枚举的大小,这里可以看到输出1字节。计算枚举的大小其实就是计算枚举值的大小,枚举值默认以UInt8来存储的,UInt8就是1字节

下面通过断点运行看看在内存中枚举的存储信息。定义3个变量,把对应的枚举值赋值后,通过断点一步步打印。 08.png 当运行到给a赋值后,通过API输出a的地址,memory read读取出来的是0,然后再过一个断点,同样看看b的信息。 09.png 可以看到这里b输出的是1,再跳到下一个断点,查看c的信息。 10.png 这里看到c输出的是2。因此在默认没有关联值的枚举成员来说,它是以UInt8的方式来存取枚举值的。UInt8最多能表示256case,当case超过了256个后,就自动提升为UInt16,然后UInt32以此类推。

Single-payload enums

Single-payload enums的内存布局,字面的意思就是有一个负载的enum,比如下面的例子:

enum ATEnum {
    case test_one(Bool)
    case test_two
    case test_three 
    case test_four
}
复制代码

当前的案例enum的内存大小是多少呢?我们在项目中运行看一下。 11.png 可以看到输出1字节,我们把上面的Bool改成Int,再次运行: 12.png 发现输出的是9,单个负载枚举的大小系统并不是把关联值加上枚举值的1字节,这种类型枚举大小取决于关联类型是否有额外的空间来记录枚举的其他case值。

什么意思呢?还是用上面的Bool类型说明,Bool类占用1字节(8位),8位中只使用了1位标识,剩下的7位未使用就可以提供给其他的case

enum ATEnum {
    case test_one(Int)
    case test_two
    case test_three 
    case test_four
}
复制代码

而对于Int类型来说实际占用8字节,下面其他的case需要另外申请空间,增加1字节,因此枚举大小占用8字节 + 1字节 = 9字节

Mutil-payload enums

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

// 这里定义了2个Bool类型
enum ATEnum {
    case test_one(Bool)
    case test_two(Bool)
    case test_three 
    case test_four
}
复制代码

结合多个负载的定义,在项目中运行一下看看输出。 13.png 结果输出还是1字节。还是和之前分析的一样,这里的1字节Bool类型已经可以存放其他的case

再把上面的Bool类型改成Int,再看一下结果输出。 14.png 发现这里输出的是9,而不是8 + 8 + 1 = 17字节,因为枚举的case中只会计算一个相同的关联值大小,所以在同一个枚举类型中,如果多个相同关联值的枚举只会计算1个。因此就是8 + 1 = 9字节。

以上就是关于枚举不同payload情况大小的案例分析。

递归枚举

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

// 示例代码,二叉树
indirect enum BinaryTree<T> {
    case empty
    case node(left: BinaryTree, value: T, right: BinaryTree)
}
复制代码

枚举类型是值类型,在编译的时候大小已经确定,但上面的二叉树示例无法确定它的大小,因此需要用indirect关键字来修饰,告诉编译器需要在堆空间上申请内存。结合实际例子来看一下: 15.png 这里定义了个BinaryTree<Int>类型的node,通过格式化输出看内存地址很像个示例对象。再通过汇编的方式运行: 16.png 可以看到调用了swift_allocObject,也就知道确实是生成了实例对象,内存分配到堆空间上。

上面的indirect关键字放在了枚举的外面,当然也可以放在case的前面,两者有什么区别呢?

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

如果是放在枚举的外面,就相当于整个枚举大小都是引用类型存放在堆空间上,如果是放在case前面,就只有当前的case会以引用类型存放在堆空间上,接下来还是通过案例来证明一下。 17.png 这里定义empty变量读取二叉树的empty节点,通过打印可以看到输出的是0x0,所以它还是值类型,由栈分配空间。

Optional

认识可选值

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

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

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

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

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

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

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

enum MyOptional<Value> {
    case some(Value)
    case none
}
复制代码

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

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

这个时候给定一个数组,我们想删除数组中所有的偶数 18.png 这个时候编译器就会检查我们当前的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来简化我们的代码,看一下下面的案例:

  1. if let方式

19.png 2. guard let方式 20.png

  • guard letif let刚好相反,guard 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
复制代码

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

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存储值的类型保持一致

运算符重载

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

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

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
    }
}
复制代码

根据上面的重载,实际定义2个Vector来运行一下。 21.png 关于运算符重载可以参考官方说明文档,如果重载常用运算符可以直接重定义,如果定义自己的的运算符,需要按照文档的要求进行重载。

自定义运算符

运算符分为infix(中缀)、prefix(前缀)和postfix(后缀)运算符,这里我们就定义个前缀运算符

infix operator operator
复制代码

再指定运算符的优先级

// 优先级组声明有以下形式
precedencegroup precedence group name {
    higherThan: lower group names
    lowerThan: higher group names
    associativity: associativity
    assignment: assignment
}
复制代码

根据声明我们定义自己的运算符号,代码如下:

// 自定义乘法
infix operator &*: AdditionPrecedence

precedencegroup AtomPrecedence {
    lowerThan: AdditionPrecedence // 低于AdditionPrecedence优先级
    associativity: left // 左结合
}
复制代码

然后在定义一下&*运算符的方法。

extension Vector {
    static func &* (firstVector: Vector, secondVector: Vector) -> Vector {
        return Vector(x: firstVector.x * secondVector.x, y: firstVector.y * secondVector.y)
    }
}
复制代码

再运行输出一下。 22.png 可以看到输出自定义运算符的结果。在实际开发中可以根据自己的需求定义相关的运算符。

隐式解析可选类型

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

24.png 在日常开发中比较常⻅这种隐士解析可选类型 25.png IBOutlet类型是Xcode强制为可选类型的,因为它不是在初始化时赋值的,而是在加载视图的时候。你可以把它设置为普通可选类型,但是如果这个视图加载正确,它是不会为空的。

猜你喜欢

转载自juejin.im/post/7079342466758885412