【iOS】Swift系列二十六 - 模式匹配

什么是模式?

一、字面量(Literal)

了解模式之前,我们先看下什么是字面量。

1
2
3
var age = 10
var isShow = false
var name = "Jack"

上面代码中的10false"Jack"就是字面量。

1.1. 字面量类型

常见字面量的默认类型:

1
2
3
4
public typealias IntegerLiteralType = Int
public typealias FloatLiteralType = Double
public typealias BooleanLiteralType = Bool
public typealias StringLiteralType = String

可以通过typealias修改字面量的默认类型(一般也没必要修改):

1
2
3
4
public typealias FloatLiteralType = Float
public typealias IntegerLiteralType = UInt8
var age = 10 // UInt8类型
var height = 20.0 // Float类型

Swift自带的绝大部分类型,都支持直接通过字面量进行初始化(不需要直接调用初始化器):

1
BoolIntFloatDoubleStringArrayDictionarySetOptional等

1.2. 字面量协议

Swift自带类型之所以能够通过字面量初始化,是因为它们遵守了对应的协议。

1
2
3
4
5
6
7
Bool : ExpressibleByBooleanLiteral
Int : ExpressibleByIntegerLiteral
FloatDouble : ExpressibleByIntegerLiteralExpressibleByFloatLiteral
Dictionary : ExpressibleByDictionaryLiteral
String : ExpressibleByStringLiteral
ArraySet : ExpressibleByArrayLiteral
Optinal : ExpressibleByNilLiteral

示例代码:

1
2
3
4
5
6
7
8
9
10
11
var b: Bool = false // ExpressibleByBooleanLiteral
var i: Int = 10 // ExpressibleByIntegerLiteral
var f0: Float = 10 // ExpressibleByIntegerLiteral
var f1: Float = 10.0 // ExpressibleByFloatLiteral
var d0: Double = 10 // ExpressibleByIntegerLiteral
var d1: Double = 10.0 // ExpressibleByFloatLiteral
var s: String = "idbeny" // ExpressibleByStringLiteral
var arr: Array = [1, 2, 3] // ExpressibleByArrayLiteral
var set: Set = [1, 2, 3] // ExpressibleByArrayLiteral
var dict: Dictionary = ["name" : "daben"] // ExpressibleByDictionaryLiteral
var o: Optional<Int> = nil // ExpressibleByNilLiteral

1.3. 字面量协议应用

示例代码一:

Bool类型直接赋值给Int类型的变量,会直接报错:

如果要不报错,只需要给Int添加一个遵守协议的扩展即可:

1
2
3
4
5
6
7
extension Int : ExpressibleByBooleanLiteral {
public init(booleanLiteral value: Bool) {
self = value ? 1 : 0
}
}
var num: Int = true
print(num) // 输出:1

示例代码二:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
class Student: ExpressibleByIntegerLiteral, ExpressibleByFloatLiteral, ExpressibleByStringLiteral, CustomStringConvertible {
var name: String = ""
var score: Double = 0
required init(floatLiteral value: Double) {
self.score = value
}
required init(integerLiteral value: Int) {
self.score = Double(value)
}
required init(stringLiteral value: String) {
self.name = value
}
// 支持Unicode和特殊字符
required init(unicodeScalarLiteral value: String) {
self.name = value
}
required init(extendedGraphemeClusterLiteral value: String) {
self.name = value
}
var description: String {
"name=\(name), score=\(score)"
}
}
var stu: Student = 90
print(stu) // 输出:name=, score=90.0

stu = 98.5
print(stu) // 输出:name=, score=98.5

stu = "idbeny"
print(stu) // 输出:name=idbeny, score=0.0

示例代码三:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
struct Point {
var x = 0.0, y = 0.0
}
extension Point : ExpressibleByArrayLiteral, ExpressibleByDictionaryLiteral {
init(arrayLiteral elements: Double...) {
guard elements.count > 0 else {
return
}
self.x = elements[0]
guard elements.count > 1 else {
return
}
self.y = elements[1]
}
init(dictionaryLiteral elements: (String, Double)...) {
for (k, v) in elements {
if k == "x" {
self.x = v
} else if k == "y" {
self.y = v
}
}
}
}
var p: Point = [5.12, 10.24]
print(p) // 输出:Point(x: 5.12, y: 10.24)

p = ["x" : 11, "y" : 22]
print(p) // 输出:Point(x: 11.0, y: 22.0)

二、模式(Pattern)

模式是用于匹配的规则,比如switchcase、捕捉错误的catchif/guard/while/for语句的条件等。

Swift中的模式有:

  • 通配符模式(Wildcard Pattern)
  • 标识符模式(Identifier Pattern)
  • 值绑定模式(Value-Binding Pattern)
  • 元组模式(Tuple Pattern)
  • 枚举Case模式(Enumeration Pattern)
  • 可选模式(Optional Pattern)
  • 类型转换模式(Type-Casting Pattern)
  • 表达式模式(Expression Pattern)

2.1. 通配符模式

  • _匹配任何值
  • _?匹配非nil

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
enum Life {
case human(name: String, age: Int?)
case animal(name: String, age: Int?)
}
func check(_ life: Life) {
switch life {
case .human(let name, _):
print("human", name)
case .animal(let name, _?):
print("animal", name)
default:
print("other")
}
}
check(.human(name: "Rose", age: 20)) // 输出:human Rose
check(.human(name: "Jack", age: nil)) // 输出:human Jack
check(.animal(name: "Dog", age: 5)) // 输出:animal Dog
check(.animal(name: "Cat", age: nil)) // 输出:other

case .human(let name, _):中的_就是匹配任意值。

case .animal(let name, _?):中的_?就是匹配非nil值。

2.2. 标识符模式

给对应的变量、常量名赋值。

1
2
var age = 10
let name = "idbeny"

2.3. 值绑定模式

把对应位置的值绑定到变量/常量上。

1
2
3
4
5
6
let point = (3, 2)
switch point {
case let (x, y):
print("x:\(x), y:\(y)")
}
// 输出:x:3, y:2

2.4. 元组模式

本质也是值绑定和通配符模式。

示例代码一(数组):

1
2
3
4
5
6
7
8
9
10
let points = [(0, 0), (1, 0), (2, 0)]
for (x, _) in points {
print(x)
}
/*
输出 :
0
1
2
*/

示例代码二(case):

1
2
3
4
5
6
7
8
9
10
let name: String? = "idbeny"
let age = 18
let info: Any = [1, 2]
switch (name, age, info) {
case (_?, _, _ as String):
print("case")
default:
print("default")
}
// 输出:default

示例代码三(字典):

1
2
3
4
5
6
7
8
9
10
var scores = ["jack" : 98, "rose" : 100, "kate" : 86]
for (name, score) in scores {
print(name, score)
}
/*
输出:
jack 98
kate 86
rose 100
*/

2.5. 枚举Case模式

if case语句等价于只有1个caseswitch语句。

示例代码一:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
let age = 2
func test() {
// 原来的写法
if age >= 0 && age <= 9 {
print("[0, 9]")
}
// 枚举case模式
if case 0...9 = age {
print("[0, 9]")
}

guard case 0...9 = age else { return }
print("[0, 9]")
}
test()
/*
输出:
[0, 9]
[0, 9]
[0, 9]
*/

if case 0...9 = age可以理解为是拿出age的值和case后面的条件进行匹配。

下面的代码完全等价上面示例代码的if case

1
2
3
4
5
6
7
switch age {
case 0...9:
print("[0, 9]")
default:
break
}
// 输出:[0, 9]

示例代码二:

1
2
3
4
5
6
7
let age = 2
let ages: [Int?] = [2, 3, nil, 5]
for case nil in ages {
print("有nil值")
break
}
// 输出:有nil值

示例代码三:

1
2
3
4
5
6
7
8
9
10
let points = [(1, 0), (2, 1), (3, 0)]
for case let (x, 0) in points {
print(x)
}
// 输出:1 3

// 错误写法:
// for (x, 0) in points {
// print(x)
// }

2.6. 可选模式

示例代码一:

1
2
3
4
5
6
7
8
9
10
11
let age: Int? = 42
if case .some(let x) = age {
print(x)
}
// 输出:42

// x?代表非空
if case let x? = age {
print(x)
}
// 输出:42

示例代码二:

1
2
3
4
5
6
7
8
9
10
let ages: [Int?] = [nil, 2, 3, nil, 5]
for case let age? in ages {
print(age)
}
/*
输出:
2
3
5
*/

上面示例二的代码和下面的代码等效:

1
2
3
4
5
6
7
8
9
10
11
12
for item in ages {
// 可选项绑定
if let age = item {
print(age)
}
}
/*
输出:
2
3
5
*/

示例代码三:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
func check(_ num: Int?) {
switch num {
case 2?:
print("2")
case 4?:
print("4")
case 6?:
print("6")
case _?:
print("other")
case _:
print("nil")
}
}
check(4) // 输出:4
check(8) // 输出:other
check(nil) // 输出:nil

2.7. 类型转换模式

主要是isas的用法。

示例代码一:

1
2
3
4
5
6
7
8
let num: Any = 6
switch num {
case is Int:
print("is Int", num)
default:
break
}
// 输出:is Int 6

case is Int仅仅是判断num是否为Int类型,编译器不会自动强转,依然认为numAny类型。

如果需要强转并且判断类型,可以使用as

1
2
3
4
5
6
7
switch num {
case let n as Int:
print("as Int", n)
default:
break
}
// 输出:as Int 6

此时的nInt类型,但是num依然是Any类型。如果num不是Int类型就会跳过当前case,匹配下一个case

示例代码二:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
class Animal {
func eat() {
print(type(of: self), "eat")
}
}
class Dog : Animal {
func run() {
print(type(of: self), "run")
}
}
class Cat : Animal {
func jump() {
print(type(of: self), "jump")
}
}
func check(_ animal: Animal) {
switch animal {
case let dog as Dog:
dog.eat()
dog.run()
case is Cat:
animal.eat()
default:
break
}
}
check(Dog())
/*
输出:
Dog eat
Dog run
*/

check(Cat())
/*
输出:
Cat eat
*/

上面的示例中,如果匹配case is Cat时,怎样才能执行jump方法?因为animalAnimal类型。

可以对animal进行强制转换:

1
(animal as? Cat)?.jump()

2.8. 表达式模式

表达式模式用在case中:

1
2
3
4
5
6
7
8
9
10
let point = (1, 2)
switch point {
case (0, 0):
print("(0, 0) is at the origin.")
case (-2...2, -2...2):
print("(\(point.0), \(point.1)) is near the origin.")
default:
print("The point is at (\(point.0), \(point.1)).")
}
// 输出:(1, 2) is near the origin.

通过汇编查看上面示例代码,发现示例程序是用~=运算符做匹配:

其实,在Swift中,一些复杂switch匹配会用到~=运算符,但并不是所有的switch都是用到该运算符。

可以通过重载运算符,自定义表达式模式的匹配规则。

2.8.1. 自定义表达式模式

示例代码一:

1
2
3
4
5
6
7
8
9
10
11
12
struct Student {
var score = 0, name = ""
static func ~= (pattern: Int, value: Student) -> Bool {
value.score >= pattern
}
static func ~= (pattern: ClosedRange<Int>, value: Student) -> Bool {
pattern.contains(value.score)
}
static func ~= (pattern: Range<Int>, value: Student) -> Bool {
pattern.contains(value.score)
}
}

使用示例代码一:

1
2
3
4
5
6
7
8
9
10
var stu = Student(score: 72, name: "idbeny")
switch stu {
case 100: print(">=100")
case 90: print(">=90")
case 80..<90: print("[80, 90)")
case 60...79: print("[60, 79]")
case 0: print(">=0")
default: break
}
// 输出:[60, 79]

stu是怎么和IntRang进行匹配的呢?重写~=运算符。

基本上是固定写法(返回值必须是Bool):

1
2
3
4
5
// pattern: case后面的类型
// value: switch后面的类型
static func ~= (pattern: Int, value: Student) -> Bool {

}

示例代码二(if case本质就是switch):

1
2
3
4
if case 60 = stu {
print(">=60")
}
// 输出:>=60

示例代码三:

1
2
3
4
5
6
7
8
var info = (Student(score: 70, name: "daben"), "及格")
switch info {
case let (60, text):
print(text)
default:
break
}
// 输出:及格

示例代码四:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
extension String {
static func ~= (pattern: (String) -> Bool, value: String) -> Bool {
pattern(value)
}
}

func hasPrefix(_ prefix: String) -> ((String) -> Bool) {
{ $0.hasPrefix(prefix) }
}
func hasSuffix(_ suffix: String) -> ((String) -> Bool) {
{ $0.hasSuffix(suffix) }
}

var str = "idbeny"
switch str {
case hasPrefix("i"), hasSuffix("y"):
print("以i开头 或 以y结尾")
default:
break
}
// 输出:以i开头 或 以y结尾

示例代码五:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
func isEven(_ i: Int) -> Bool {
i % 2 == 0
}
func isOdd(_ i: Int) -> Bool {
i % 2 != 0
}

extension Int {
static func ~= (pattern: (Int) -> Bool, value: Int) -> Bool {
pattern(value)
}
}

var age = 10
switch age {
case isEven:
print("偶数")
case isOdd:
print("奇数")
default:
print("其他")
}
// 输出:偶数

还可以定义更多自定义操作符:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
prefix operator ~>
prefix operator ~>=
prefix operator ~<
prefix operator ~<=
prefix func ~> (_ i: Int) -> ((Int) -> Bool) { { $0 > i } }
prefix func ~>= (_ i: Int) -> ((Int) -> Bool) { { $0 >= i } }
prefix func ~< (_ i: Int) -> ((Int) -> Bool) { { $0 < i } }
prefix func ~<= (_ i: Int) -> ((Int) -> Bool) { { $0 <= i } }

switch age {
case ~>=0, ~<=10:
print("1")
case ~>10, ~<20:
print("2")
default:
break
}
// 输出:1

>, =, >=, <, <=运算符都是中缀运算符,为了不影响原有的运算符特性,在原有运算符前面加一个~符号成为一个新的运算符。

2.8.2. where

可以使用where为模式匹配增加匹配条件。

示例代码一(case):

1
2
3
4
5
6
7
8
9
10
var data = (10, "Jack")
switch data {
case let (age, _) where age > 10:
print(data.1, "age>10")
case let (age, _) where age > 0:
print(data.1, "age>0")
default:
break
}
// 输出:Jack age>0

示例代码二(for):

1
2
3
4
5
var ages = [10, 20, 12, 55, 30]
for age in ages where age > 20 {
print(age)
}
// 输出:55 30

示例代码三(protocol):

1
2
3
4
5
6
protocol Stackable {
associatedtype Element
}
protocol Container {
associatedtype Stack : Stackable where Stack.Element : Equatable
}

示例代码四(func):

1
2
3
func equal<S1: Stackable, S2: Stackable>(_ s1: S1, _ s2: S2) -> Bool where S1.Element == S2.Element, S1.Element : Hashable {
return false
}

示例代码五(extension):

1
extension Container where Self.Stack.Element : Hashable { }