【iOS】Swift系列二十一 - 高级运算符

如果对C++有了解的话,理解运算符重载(Operator Overload)就很简单。OC不支持运算符重载,但Swift支持。

一、溢出运算符(Overflow Operator)

Swift的算数运算符出现溢出时会抛出运行时错误。

示例代码一:

1
2
3
4
5
6
7
8
print(Int8.min) // 输出:-128
print(Int8.max) // 输出:127
print(UInt8.min) // 输出:0
print(UInt8.max) // 输出:255

var a = UInt8.max
a += 1
print(a)

Int8的可表示数范围是-128~127UInt8可表示数范围是0~255。当超出可表示数范围时运行时就会报错。

Swift有溢出运算符用来支持溢出运算。
常见的溢出运算符:&+&-&*

示例代码二:

1
2
3
4
5
6
7
var a = UInt8.max
a = a &+ 1
print(a) // 输出:0

var b = UInt8.min
b = b &- 2
print(b) // 输出:254

当数据溢出时,溢出运算符会自动循环可数范围。

官方图例

二、运算符重载

类、结构体、枚举可以为现有的运算符提供自定义的实现,这个操作叫做运算符重载

正常加法运算:

1
2
3
let v1 = 10
let v2 = 20
let v3 = v1 + v2

如果换成非基本数值计算:

1
2
3
4
5
6
struct Point {
var x = 0, y = 0
}
let p1 = Point(x: 10, y: 20)
let p2 = Point(x: 20, y: 30)
let p3 = p1 + p2

编译器不支持这样的写法,这时候就需要用到运算符重载。

2.1. 运算符:+

1
2
3
4
5
6
7
8
9
10
11
12
struct Point {
var x = 0, y = 0
// +号运算符重载
static func +(p1: Point, p2: Point) -> Point {
Point(x: p1.x + p2.x, y: p1.y + p2.y)
}
}

let p1 = Point(x: 10, y: 20)
let p2 = Point(x: 20, y: 30)
let p3 = p1 + p2
print(p3) // 输出:Point(x: 30, y: 50)

+号运算符重载代码写到结构体外部也可以,只是平时建议写在内部(高内聚)。

2.2. 运算符:减号前缀(运算符放到前面)

1
2
3
4
5
6
static prefix func -(p: Point) -> Point {
Point(x: -p.x, y: -p.y)
}
let p1 = Point(x: 10, y: 20)
let p2 = -p1
print(p2) // 输出:Point(x: -10, y: -20)

2.3. 运算符:+=

1
2
3
4
5
6
7
static func +=(p1: inout Point, p2: Point) {
p1 = Point(x: p1.x + p2.x, y: p1.y + p2.y)
}
var p1 = Point(x: 10, y: 20)
let p2 = Point(x: 20, y: 30)
p1 += p2
print(p1) // 输出:Point(x: 30, y: 50)

+=运算符重载时,重载函数左边变量一定要用inout修饰,因为要修改外部变量的内存。并且外部变量使用var声明。由于只需要修改第一个参数的内存,所以函数不需要返回值。

2.4. 运算符:前置++

1
2
3
4
5
6
7
static prefix func ++(p: inout Point) -> Point {
p += Point(x: 1, y: 1)
return p
}
var p1 = Point(x: 10, y: 20)
let p2 = ++p1
print(p2) // 输出:Point(x: 11, y: 21)

++写在变量前面:先加后用

2.5. 运算符:后置++

1
2
3
4
5
6
7
8
static postfix func ++(p: inout Point) -> Point {
let tmp = p
p += Point(x: 1, y: 1)
return tmp
}
var p1 = Point(x: 10, y: 20)
let p2 = p1++
print(p2) // 输出:Point(x: 10, y: 20)

++写在变量后面:先用后加

2.6. 运算符:==

1
2
3
4
5
6
7
8
9
10
11
static func ==(p1: Point, p2: Point) -> Bool {
(p1.x == p2.x) && (p1.y == p2.y)
}
let p1 = Point(x: 10, y: 20)
let p2 = Point(x: 20, y: 30)
let isTrue1 = p1 == p2
print(isTrue1) // 输出:false

let p3 = Point(x: 20, y: 30)
let isTrue2 = p2 == p3
print(isTrue2) // 输出:true

要想得知2个实例是否等价,一般做法是遵守Equatable协议,重载==运算符。

2.7. Equatable协议

1
2
3
public protocol Equatable {
static func == (lhs: Self, rhs: Self) -> Bool
}

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
struct Point: Equatable {
var x = 0, y = 0
static func == (lhs: Self, rhs: Self) -> Bool {
// 普通写法
if lhs.x == rhs.x && lhs.y == rhs.y {
return true
}
return false
// 简写
// lhs.x == rhs.x && lhs.y == rhs.y
}
}
let p1 = Point(x: 10, y: 20)
let p2 = Point(x: 20, y: 30)
let isTrue1 = p1 == p2
print(isTrue1) // 输出:false

let p3 = Point(x: 20, y: 30)
let isTrue2 = p2 == p3
print(isTrue2) // 输出:true

无论是否遵守Equatable协议,都可以重载==运算符,为什么还要遵守协议呢?因为遵守协议就可以直接告诉其他人该类/结构体/枚举是支持==运算符比较的。还有一个很重要的区别是:遵守Equatable协议,默认重载!=运算符,但是自定义==运算符不会重载!=运算符。

Swift为以下类型提供默认的Equatable实现:

  • 没有关联类型的枚举

    1
    2
    3
    4
    5
    6
    7
    enum Answer {
    case wrong
    case right
    }
    var s1 = Answer.wrong
    var s2 = Answer.right
    print(s1 == s2) // 输出:false
  • 只拥有遵守Equatable协议关联类型的枚举

    1
    2
    3
    4
    5
    6
    7
    enum Answer: Equatable {
    case wrong(Int)
    case right
    }
    var s1 = Answer.wrong(10)
    var s2 = Answer.wrong(10)
    print(s1 == s2) // 输出:true

如果不遵守Equatable协议关联类型的枚举:

  • 只拥有遵守Equatable协议存储属性的结构体
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    struct Point: Equatable {
    var x = 0, y = 0
    }
    let p1 = Point(x: 10, y: 20)
    let p2 = Point(x: 20, y: 30)
    let isTrue1 = p1 == p2
    print(isTrue1) // 输出:false

    let p3 = Point(x: 20, y: 30)
    let isTrue2 = p2 == p3
    print(isTrue2) // 输出:true

2.8. 恒等运算符===!==

引用类型比较存储的地址值是否相等(是否引用着同一个对象),使用恒等运算符===,!==(仅限引用类型)。

1
2
3
4
5
class Person { }
var p1 = Person()
var p2 = Person()
print(p1 === p2) // 输出:false
print(p1 !== p2) // 输出:true

2.9. Comparable协议

要想比较2个实例的大小,一般做法是:

  • 遵守Comparable协议
  • 重载相应的运算符

官方定义的Comparable协议:

1
2
3
4
5
6
public protocol Comparable : Equatable {
static func < (lhs: Self, rhs: Self) -> Bool
static func <= (lhs: Self, rhs: Self) -> Bool
static func >= (lhs: Self, rhs: Self) -> Bool
static func > (lhs: Self, rhs: Self) -> Bool
}

示例代码:

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
// score大的比较大,若score相等,age小的比较大
struct Student : Comparable {
var age: Int
var score: Int
init(age: Int, score: Int) {
self.age = age
self.score = score
}
static func < (lhs: Student, rhs: Student) -> Bool {
(lhs.score < rhs.score) || (lhs.score == rhs.score && lhs.age > rhs.age)
}
static func > (lhs: Student, rhs: Student) -> Bool {
(lhs.score > rhs.score) || (lhs.score == rhs.score && lhs.age < rhs.age)
}
static func <= (lhs: Student, rhs: Student) -> Bool {
!(lhs > rhs)
}
static func >= (lhs: Student, rhs: Student) -> Bool {
!(lhs < rhs)
}
}

var stu1 = Student(age: 20, score: 100)
var stu2 = Student(age: 18, score: 98)
var stu3 = Student(age: 20, score: 100)
print(stu1 > stu2) // 输出:true
print(stu1 >= stu2) // 输出:true
print(stu1 >= stu3) // 输出:true
print(stu2 < stu1) // 输出:true
print(stu2 <= stu1) // 输出:true
print(stu1 <= stu3) // 输出:false

三、自定义运算符(Custom Operator)

上面的都是为已经存在的运算符进行重载,而自定义运算符是定义一个原本不存在的运算符。

自定义新的运算符在全局作用域使用operator进行声明。

格式:

1
2
3
4
5
6
7
8
9
10
11
prefix operator 前缀运算符

postfix operator 后缀运算符

infix operator 中缀运算符 : 优先级组
precedencegroup 优先级组 {
associativity: 结合性(left/right/none)
higherThan: 比谁的优先级高
lowerThan: 比谁的优先级低
assignment: true代表在可选链操作中拥有跟赋值运算符一样的优先级
}

示例代码(定义前缀运算符+++):

1
2
3
4
5
6
7
prefix operator +++
prefix func +++ (_ i: inout Int) {
i += 2
}
var age = 10
+++age
print(age) // 输出:12

示例代码(定义+-运算符并设置运算符优先级):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
infix operator +- : PlusMinusPrecedence
precedencegroup PlusMinusPrecedence {
associativity: none
higherThan: AdditionPrecedence
lowerThan: MultiplicationPrecedence
assignment: true
}
struct Point {
var x = 0, y = 0
static func +- (p1: Point, p2: Point) -> Point {
Point(x: p1.x + p2.x, y: p1.y - p2.y)
}
}
var p1 = Point(x: 10, y: 20)
var p2 = Point(x: 5, y: 15)
var p3 = p1 +- p2
print(p3) // 输出:Point(x: 15, y: 5)

如果设置associativity: none,并且使用了两个及以上运算符就会报错:

解决报错:

  1. associativity取值leftright
  2. 使用一个运算符

assignment示例:

1
2
3
4
5
6
class Person {
var point: Point = Point()
}
var p: Person? = Person()
let result = p?.point +- Point(x: 10, y: 20)
print(result!) // 输出:Point(x: 10, y: -20)

如果变量pnil,不会继续往右执行(+-运算符不会执行);如果不为nil,则正常顺序执行代码(+-运算符正常执行)。

优先级组参数说明:

  • associativity结合性有三个取值:
    • left:从左往右开始结合计算
    • right:从右往左开始结合计算
    • none:仅限一个运算符,多个运算符会报错(例:a1 + a2 + a3,有2个运算符编译报错)
  • higherThan:哪个运算符优先级比当前定义的运算符优先级高
  • lowerThan:哪个运算符优先级比当前定义的运算符优先级低
  • assignmenttrue代表在可选链操作中拥有跟赋值运算符一样的优先级

运算符优先级组描述:

参考官方文档:
1. 运算符优先级组描述:https://developer.apple.com/documentation/swift/swift_standard_library/operator_declarations

2. 高级运算符:https://docs.swift.org/swift-book/LanguageGuide/AdvancedOperators.html