Swift 中的枚举及可选型

枚举可选型Swift 中两个很重要的概念,前者与 Objective-C 中的概念大不相同,后者完全不存在,因此需要详细介绍下这两个概念。

为什么将可选型枚举放在一起呢?因为可选型 Optional 本质就是枚举 Emun,接下来详细介绍。

枚举(Enum)

Enum 基本语法

Swift 中枚举的语法非常简洁:

// 定义一个表示季节的枚举
enum Season {
    case spring    // 春
    case summer    // 夏
    case autumn    // 秋
    case winter    // 冬
}

// 创建一个 Season 实例变量
var s = Season.spring
s = Season.summer // 重新赋值
s = .winter // 在确定 s 的类型为 Season 后可以直接使用.语法
print("\(s) is coming!") // winter is coming

// 在 switch 语句中使用 Enum
switch s {
case .spring:
    print("春天")
case .summer:
    print("夏天")
case .autumn:
    print("秋天")
case .winter:
    print("冬天")
}

如果让枚举遵循 CaseIterable 协议,即可获得一个 allCases 属性,用于表示一个包含枚举所有成员的集合:

// 枚举成员的遍历
enum Season: CaseIterable {
    case spring, summer, autumn, winter
}

// 通过 allCases 属性获得枚举成员的数量
let seasonNumber = Season.allCases.count

// 通过 allCases 属性在 for 循环中遍历枚举成员
for s in Season.allCases {
    print("Current season is \(s)")
}

关联值(Associated Values)

// 定义一个日期枚举,有两种 case : 1.数字表示的日期 2.字符串表示的日期
enum Date {
    case digit(year: Int, month: Int, dat: Int)
    case string(String)
}

var digitDate = Date.digit(year: 2019, month: 8, dat: 28)
var stringDate = Date.string("2019-08-28")

需要注意关联值是存储在枚举变量的内存里 ,比如上面的 2019828"2019-08-28" 都是存储在变量 digitDate

原始值(Raw Values)

可以指定 Enum 各个 case 的原始值类型

// 枚举 PokerSuit 原始值的类型为 Character
enum PokerSuit: Character {
    case spade = "♠"
    case heart = "♥"
    case diamond = "♦"
    case club = "♣"
}

let suit = PokerSuit.heart
print(suit)           // heart
print(suit.rawValue)  // ♥

需要注意原始值不存储在枚举变量的内存里 ,比如下面的枚举:

enum Level: String {
    case low = "low level"
    case middle = "middle level"
    case high = "high level"
}
print(MemoryLayout.stride)    // 1
print(MemoryLayout.size)      // 1
print(MemoryLayout.alignment) // 1

隐式原始值(Implicitly Assigned Raw Values)

如果指定的类型为 String 或者 IntSwift 编译器会隐式(自动)添加原始值:

enum Direction : String {
    case north
    case south
    case east
    case west
}
// 完全等价于
enum Direction : String {
    case north = "north"
    case south = "south"
    case east = "east"
    case west = "west"
}

enum Season : Int {
    case spring
    case summer
    case autumn
    case winter
}
// 也完全等价于
enum Season : Int {
    case spring = 0
    case summer = 1
    case autumn = 2
    case winter = 3
}

递归枚举(Recursive Enumeration)

递归枚举是一种特殊的枚举类型,它有一个或多个枚举成员使用该枚举类型的实例作为关联值。使用递归枚举时,编译器会插入一个间接层。你可以在枚举成员前加上 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)
}

可选型(Optional)

Swift 标准库源码 Optional 的定义:

public enum Optional: ExpressibleByNilLiteral {
  /// The absence of a value.
  ///
  /// In code, the absence of a value is typically written using the `nil`
  /// literal rather than the explicit `.none` enumeration case.
  case none

  /// The presence of a value, stored as `Wrapped`.
  case some(Wrapped)
  
  ...
}

很明显 Optional 就是一个 enum 类型,且有两个 casenonesome
其中 some 的关联值为泛型 < Wrapped >,即 Optional 内存储的数据类型为 Wrapped

从定义中不难理解可选型表示 一个类型即可能有值又可能为 nil

基本概念

定义一个普通的 String 类型变量:

var str: String = "hello"

然后将 str 变量清空:

str = nil

编译器会立马报错:

'nil' cannot be assigned to type 'String'

说明 Swift 不允许普通类型的变量赋值为 nil,要想 即可能有值又可能为空 ,需要使用可选类型 Optional,其基本语法如下:

var name: String?  // 默认为 nil
name = "CodingIran"
name = nil

var age: Int?  // 默认值为 nil
age = 30

可选型不仅可以表示这个类型的值可能为 nil,而且一旦声明为可选型之后, 其默认值即为 nil

另外需要注意:任何类型(如:IntDoubleStruct 等)的可选状态都可以被设置为 nil,不只是对象类型!

可选型的概念有点类似薛定谔的猫,可以将其想象成一个盒子,其中可能装着某个具体类型的数据,也有可能什么都没有。那如何才能打开盒子呢?

强制解包(Forced Unwrapping)

Swift 提供如下语法可以强制打开一个可选类型:

// 定一个可选 Int 类型的变量,表示网络请求的返回状态码
var responseCode: Int? = 404
print(responseCode!) // 404

responseCode = nil
print(responseCode!) // Fatal error: Unexpectedly found nil while unwrapping an Optional value

很显然是强制解包是不安全的, 除非能够确保一个可选类型一定有值否则不要轻易尝试强制解包 !在使用可选类型的值时应该先进行判断:

// 判断可选型是否为空
if responseCode != nil {
    print("responseCode's value is \(responseCode!)")
} else {
    print("responseCode has no value!")
}

这么写虽然解决了 nil 导致的错误,但显得很笨拙,对此 Swift 提供了一些更优雅的语法。

可选绑定(Optional Binding)

使用可选绑定来判断可选类型是否包含值,如果包含就把值赋给一个临时常量或者变量。

可选绑定可以用在 ifwhile 语句中,这条语句不仅可以用来判断可选类型中是否有值,同时可以将可选类型中的值赋给一个常量或者变量:

// 创建 unwrappedResponseCode 常量用来接受 responseCode 解包成功后结果
if let unwrappedResponseCode = responseCode {
    print("responseCode's value is \(unwrappedResponseCode)")
} else {
    // 解包失败(nil)会进入 else 分支
    print("responseCode has no value!")
}

这段代码可以理解为:

  • 如果 responseCode 返回的可选 Int 包含一个值,则创建一个叫做 unwrappedResponseCode 的新常量并将可选包含的值赋给它。

  • 如果解包成功,unwrappedResponseCode 常量可以在 if 语句的第一个分支中使用。它已经被可选类型包含的值初始化过,所以不需要再使用 ! 后缀来获取它的值。

  • 可以在可选绑定中使用常量变量。如果想在 if 语句的第一个分支中操作 unwrappedResponseCode 的值,可以改成

    if var unwrappedResponseCode...
    

凭空冒出一个新的变量(or 常量)名unwrappedResponseCode 显得有些奇怪,所以通常都让创建出的新名称与可选型的名称保持一致:

// 等号左右的名称一样
if let responseCode = responseCode {
    print("responseCode's value is \(responseCode)")
} else {
    print("responseCode has no value!")
}

第一次看到if let responseCode = responseCode可能会觉得『卧槽,还可以这样?』。

别急,你甚至还可以这样:

var responseCode: Int?
var responseData: Dictionary?

// do someting...

if let responseCode = responseCode, // 可选绑定 responseCode
   let responseData = responseData, // 可选绑定 responseData
   responseCode != 404,             // 判断 responseCode
   responseData.count != 0 {        // 判断 responseData
    // do someting...
}

guard 语句

在我们平时写代码的时候经常会遇到类型下面的代码:

/// 定义一个计算形状边数的函数
///
/// - Parameter shape: 形状字符串
/// - Returns: 形状的变数(由于传入的 shape 位置可能导致返回 nil,因此返回的是可选 Int)
func calculateNumberOfSides(shape: String) -> Int? {
    switch shape {
    case "Triangle":  // △
        return 3
    case "Square":    // □
        return 4
    case "Rectangle": // ▭
        return 4
    case "Pentagon":  // ⬠
        return 5
    case "Hexagon":
        return 6      // ⬡
    default:
        return nil
    }
}

func maybePrintSides(shape: String) {
    let sides = calculateNumberOfSides(shape: shape)
    
    if let sides = sides {
        print("A \(shape) has \(sides) sides.")
    } else {
        print("I don't know the number of sides for \(shape).")
    }
}

上面的代码没毛病,在 oc 时代我们几乎每天都在写这样的 if else 语句,但 Swift 给了一种新的条件判断的语句 guard

func maybePrintSides(shape: String) {
    guard let sides = calculateNumberOfSides(shape: shape) else {
        print("I don't know the number of sides for \(shape).")
        return
    }
    
    print("A \(shape) has \(sides) sides.")
}

有没有发现使用 guard 有什么好处?

当使用 guard 语句进行可选项绑定时,绑定的常量(let)、变量(var)也能 在外层作用域中使用

隐式解包(Implicitly Unwrapped Optional)

可选型的引入对于程序安全性有很大的提升,但即使使用可选绑定,有时依然觉得有些繁琐:

let number: Int? = 10

if let number = number {
    print("number is \(number)")
} else {
    print("number is non-existent!")
}

上面这段代码中定义了一个 Int 可选型的常量 number,并将 10 赋值给它。

既然是常量,那 number 永远都是 10,不可能出现 nil 的情况,这时候再去担心就显得很多余。对此 Swift 推出了隐式解包的概念:

let number: Int! = 10
let otherNumber: Int = number
print("otherNumber is \(otherNumber)")

需要注意:number 变量依然是一个可选 Int,只是在将它赋值给 otherNumber 时会 自动强制解包 !(因为你已经确保 number 不会为 nil 了)

空合并运算符(Nil Coalescing)

Swift 中还有一个更加方便的方法可以对可选类型进行解包。如果想获取一个可选类型的值,并且希望在它为空时 给它一个默认值 的话,空合并运算符是最好的方法:

var optionalInt: Int? = 10
var mustHaveResult = optionalInt ?? 0
// mustHaveResult 类型为 Int,值为 10

上面的代码optionalInt ?? 0表示如果 optionalInt 有值,则返回自动解包的 optionalInt10,如果为空则返回 0,其完全等价于下面的代码:

var optionalInt: Int? = 10
var mustHaveResult: Int
if let unwrapped = optionalInt {
  mustHaveResult = unwrapped
} else {
  mustHaveResult = 0
}

再看一种情况:

var a: Int? = 1
var b: Int? = 2

let c = a ?? b

此时 c 是什么类型?值又是多少?

普通青年这时候会开始瞎几把猜了,文艺青年已经打开 Xcode 进行实战操作,二逼青年直接去看 Swift 定义 ?? 运算符的源码。没错!我就是二逼青年:

// 可选型 ?? 非可选型
public func ?? (optional: T?, defaultValue: @autoclosure () throws -> T)
    rethrows -> T {
  switch optional {
  case .some(let value):
    return value
  case .none:
    return try defaultValue()
  }
}

// 可选型 ?? 可选型
public func ?? (optional: T?, defaultValue: @autoclosure () throws -> T?)
    rethrows -> T? {
  switch optional {
  case .some(let value):
    return value
  case .none:
    return try defaultValue()
  }
}

上面就是 ?? 的实现源码,可以在 Swift 源码文件夹(swift->stdlib->public->core->Optional.swift)查看。简简单单的10几行代码中有2个关键:

  • 空合并运算符 ?? 有两个函数,对比发现区别在于自动闭包内的返回值类型如果为 T ,整个 ?? 函数返回 T,如果自动闭包内的返回值类型如果为 T? ,整个 ?? 函数也返回 T?

  • ?? 函数体内使用 switch 语句对 optional 做了判断(如前面所说, 可选型的本质是枚举 ):如果有值则返回这个值,如果为空则返回默认值 defaultValue

因此对于 a ?? b 可以总结如下:

  • a 是必须是可选项(见源码函数的第一个参数)
  • b 可以是可选项也可以不是(源码的两个函数的第二个参数自动闭包的返回值类型,一个是可选型,另一个是非可选型)
  • ab 储存的类型必须相同(源码唯一的泛型 T
  • 如果 a 不为 nil,就返回 a(源码第一个 case 语句)
  • 如果 anil,就返回 b(源码第二个case 语句)
  • 如果 b 不是可选项,返回 a 时会自动解包(源码两个函数 ?? 函数的返回值一个为 T,另一个为 T?

你可能感兴趣的:(Swift 中的枚举及可选型)