第7章 运算符与表达式:从取余、位运算到自定义运算符

第七章:运算符与表达式:从取余、位运算到自定义运算符

大部分运算符每个语言都差不多,Swift 在这里的特别之处是两件:它允许你定义自己的运算符并声明优先级(写 DSL 和数值库时很爽),以及**if 和 switch 现在可以直接当表达式用**——从此不用为了给常量赋个值而写一坨 let x: Int 再在分支里赋值。

7.1 算术运算符

+、-、*、/、% 五个,整数除法和求余的规矩在第 4 章讲过(向零截断、余数跟着被除数走)。这里补两条:

1
2
3
4
print(7 / 2, 7.0 / 2, 7 / 2.0)
// prints: 3 3.5 3.5
print(-7 % 3, 7 % -3)
// prints: -1 1

只要有一边是浮点,除法就按浮点走——前提是两边类型一致(Swift 没有隐式转换,必要时手写 Double(7) / 2)。

7.2 比较运算符

==、!=、<、<=、>、>=,作用在可比较的类型上。它们有一个容易被忽略的性质:同一组里的比较运算符不能连写。

1
print(1 < 2 == true)
1
error: adjacent operators are in non-associative precedence group 'ComparisonPrecedence'

编译器怕的是 a < b == c 这种"你到底想比什么"的糊涂账,于是直接不许写。加括号明确意图即可:

1
2
print((1 < 2) == true)
// prints: true

7.3 恒等运算符:=== 和 !==

它们问的不是"值相等吗",而是"是不是同一个对象",只对类(引用类型)有意义:

1
2
3
4
5
6
7
8
final class Box { var value = 0 }

let a = Box()
let b = a        // 同一个对象,多了一个名字
let c = Box()    // 另一个对象

print(a === b, a === c, b !== c)
// prints: true false true

要比较"内容"是否相等,需要类型遵循 Equatable(第 22 章)。很多初学者在这里栽跟头:给类写了 == 就以为 === 也能用,其实两者是完全不同的两件事。

7.4 逻辑运算符:会"偷懒"的 && 和 ||

下面的例子故意在每个操作数里都打一行日志,好让你看清楚谁真的被求值了:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
func check(_ name: String, _ result: Bool) -> Bool {
    print("求值:\(name)")
    return result
}

print("--- 短路与 && ---")
_ = check("左", false) && check("右", true)
// prints: --- 短路与 && ---
// prints: 求值:左

print("--- 短路或 || ---")
_ = check("左", true) || check("右", false)
// prints: --- 短路或 || ---
// prints: 求值:左

注意输出:右边根本没有被求值。这就是短路——&& 左边为假就整体为假,|| 左边为真就整体为真,右边直接跳过。

这不是性能优化的小技巧,而是写代码的常规武器:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
struct User {
    var isActive: Bool
}

let active: User? = User(isActive: true)
let missing: User? = nil

if active != nil && active!.isActive {
    print("active 可以开干")
}
// prints: active 可以开干

if missing != nil && missing!.isActive {   // 左边为假,右边不会被求值
    print("这一行永远不会执行")
}

左边挡住条件,右边才敢解包。反过来把顺序写错,就会在运行时炸掉。

7.5 位运算

把整数当成一排二进制位来操作,在协议解析、权限位、哈希这些场景里很常见:

1
2
3
4
5
6
7
let a: UInt8 = 0b1010_1100      // 172
let b: UInt8 = 0b0110_0110      // 102

print(a & b, a | b, a ^ b, ~a)
// prints: 36 238 202 83
print(a << 1, a >> 2)
// prints: 88 43
运算符含义
&按位与
``
^按位异或
~按位取反(前缀运算符)
<<左移,溢出位直接丢弃,不会崩
>>右移

<< 这个"丢弃而不崩"的行为值得单独记:它和 + 的"溢出就崩"不一样。移出的位直接丢掉,移位数达到或超过位宽时,结果就是 0:

1
2
3
let a: UInt8 = 172
print(a << 1, a << 8, a << 9)
// prints: 88 0 0

而带 & 的掩码移位(&<<、&>>)才会先把移位数按位宽取模:

1
2
print(a &<< 8, a &<< 9, a &>> 9)
// prints: 172 88 86

&<< 9 等价于 a << 1(9 对 8 取模得 1)。掩码移位的结果永远有定义,所以散列、加密这类需要"确定行为"的算法里用的都是它。

顺带一提,UInt8 的位运算常见于解析二进制协议、处理颜色分量这类场景——普通的业务代码里,一年可能都用不上一次。

7.6 区间:... 与 ..<

一个"包含端点",一个"不包含",switch、切片和 ForEach 都会用到它们:

1
2
3
4
5
6
7
let closed = 1...5        // 闭区间:1, 2, 3, 4, 5
let halfOpen = 1..<5      // 半开区间:1, 2, 3, 4

print(closed.count, halfOpen.count)
// prints: 5 4
print(closed.contains(5), halfOpen.contains(5))
// prints: true false

区间不只是给 for 用的,它本身就是值,可以传递、判断包含关系,还能"单边":

1
2
3
let through = ...5        // 5 及以下
print(through.contains(4), through.contains(6))
// prints: true false
写法名字含义
1...5闭区间含两端
1..<5半开区间含左不含右
...5单边(到 5 为止)适合 switch 里做区间分档
1...单边(从 1 起)同上,方向相反
..<5单边(小于 5)同上

数组下标习惯用半开区间(0..<count),字符串切片也常配合单边区间使用(第 5 章)。

7.7 三元运算符与空合运算符

它们把"二选一"写进一行表达式里;用得好省事,用过头就没人读得懂:

1
2
3
4
5
6
7
let age = 20
print(age >= 18 ? "成年" : "未成年")
// prints: 成年

let nickname: String? = nil
print(nickname ?? "匿名")
// prints: 匿名

?? 是"左边有值就用左边,否则用右边",它是可选值最常用的搭档(第 9 章细讲)。注意它的优先级比比较运算符高:

1
2
3
let a: Int? = nil
print((a ?? 3) < 5)
// prints: true

7.8 赋值与复合赋值

+= 这类写法读起来更贴近"在原值上做变化",但有一个前提值得记住:左边必须是可以被赋值的:

1
2
3
4
5
var total = 10
total += 5      // 加减乘除取余都有对应的复合写法
total *= 2
print(total)
// prints: 30

可用的复合赋值:+=、-=、*=、/=、%=、&=、|=、^=、<<=、>>=,以及带回绕的 &+=、&-=、&*=。

一个坑:Swift 的赋值语句不返回值,所以 if (x = 1) 这种"故意写错一个等号"的经典 bug 在这里写不出来——编译器直接报错。

7.9 优先级与结合性

Swift 的运算符优先级分组(从高到低):

优先级组包含的运算符
位运算移位<<、>>
乘法*、/、%、&、&*
加法+、-、|(按位或)、^、&+、&-
区间...、..<
类型转换is、as
空合??
比较==、!=、<、<=、>、>=、===、!==
逻辑与&&
逻辑或||
三元? :
赋值= 及各种复合赋值

举两个实际结果:

1
2
3
4
5
6
print(1 + 2 * 3, (1 + 2) * 3)
// prints: 7 9
print(1 + 2 << 3)               // 移位比加法优先级高:1 + (2 << 3)
// prints: 17
print(true || false && false)   // && 比 || 高:true || (false && false)
// prints: true

记不住没关系,加括号永远是对的。真正需要记的只有两条:&& 比 || 高,?? 比比较运算符高。

7.10 自定义运算符

Swift 允许你定义新的运算符,并且声明它和已有运算符的相对优先级。写数学库、DSL 的时候很有用:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
infix operator **: PowerPrecedence

precedencegroup PowerPrecedence {
    higherThan: MultiplicationPrecedence
    associativity: right
}

func ** (base: Int, exp: Int) -> Int {
    var result = 1
    for _ in 0..<exp { result *= base }
    return result
}

print(2 * 3 ** 2)    // 因为 ** 优先级高于 *,等于 2 * (3 ** 2)
// prints: 18
print(2 ** 3 ** 2)   // 因为声明了右结合,等于 2 ** (3 ** 2) = 2 的 9 次方
// prints: 512

要点三条:

  1. 先声明运算符的"身份":infix(中缀)、prefix(前缀)、postfix(后缀)。
  2. precedencegroup 决定它和别的运算符谁先算、从左还是从右结合。
  3. 运算符只能由规定的符号字符组成(/ = - + ! * % < > & | ^ ~ ? 及其组合);已有运算符可以为新类型重载,但不要改变它原本的语义。

自定义运算符是"强力工具",也是"团队沟通成本"。在公共 API 里新增一个 **,意味着每个读代码的人都要多记一条规则——所以官方的态度是:能用普通函数名表达就别造运算符。

7.11 if 和 switch 也能当表达式

从 Swift 5.9 起,if 和 switch 可以直接出现在赋值号右边:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
let score = 85

let grade = if score >= 90 { "A" } else if score >= 60 { "B" } else { "C" }
print(grade)
// prints: B

let symbol = switch score {
case ..<60: "✗"
default: "✓"
}
print(symbol)
// prints: ✓

这解决了一个长期的小尴尬:以前给常量按条件赋值,要么写三元(只适合两个分支)、要么先 var 再赋(就为了绕过编译器)。现在可以直接"声明即赋值",而且照样享受 let 的不可变性。

用的时候有三个约束:每个分支都必须产生同一种类型的值;if 必须有 else(否则编译器不知道没有匹配时给什么);switch 必须穷尽所有情况。

7.12 本章小结

你想做的事写法
算术+、-、*、/、%
比较==、!=、<、<=、>、>=
判断同一个对象===、!==(只用于类)
逻辑(会短路)&&、||、!
位运算&、|、^、~、<<、>>
区间1...5、1..<5、...5
二选一条件 ? A : B
可选值兜底值 ?? 默认值
按条件给常量赋值let x = if ... { } else { }(5.9 起)
按模式给常量赋值let x = switch ... { }(5.9 起)
自定义运算符infix operator **: 组名 + precedencegroup

7.13 本章易错点速查

容易踩的地方正确认识
1 < 2 == true 应该能写不能。比较运算符同一组内不可连写,会报 non-associative 错误,加括号即可
=== 是比较内容相等它比较"是不是同一个对象",只对类有效;内容相等要靠 Equatable 的 ==
&& 和 || 只是语法糖它们会短路:右边可能压根不执行。这既影响性能,也影响副作用
a << 9 对 UInt8 会溢出崩溃不会。普通 << 在移位数超过位宽时结果为 0;&<< 才会按位宽取模
区间只能用在 for 里区间是值,能传参、能 contains、能做单边形式 ...5
?? 优先级很低,可以随便混它比比较运算符高:a ?? 3 < 5 会解析成 (a ?? 3) < 5,但这类写法最好加括号
x = 1 能当条件用不能,赋值不返回值。if x == 1 写错成 = 会直接编译报错
自定义运算符很方便,多造几个每个新运算符都是读者的记忆负担;公共 API 里优先用普通函数名
if/switch 表达式随便用需要所有分支类型一致、if 必须有 else、switch 必须穷尽;它是 5.9 起才有的语法

7.14 下章预告

下一章把“临时打包”的元组、给类型取别名和基础类型判断讲清楚:什么时候该用元组返回多个值、typealias 为什么不是新类型、is / as? / as! 又分别适合什么场景。

最后修改 September 19, 2026: 更新 (3489033b1)