第7章 运算符与表达式:从取余、位运算到自定义运算符
8 分钟阅读
第七章:运算符与表达式:从取余、位运算到自定义运算符
大部分运算符每个语言都差不多,Swift 在这里的特别之处是两件:它允许你定义自己的运算符并声明优先级(写 DSL 和数值库时很爽),以及**
if和switch现在可以直接当表达式用**——从此不用为了给常量赋个值而写一坨let x: Int再在分支里赋值。
7.1 算术运算符
+、-、*、/、% 五个,整数除法和求余的规矩在第 4 章讲过(向零截断、余数跟着被除数走)。这里补两条:
| |
只要有一边是浮点,除法就按浮点走——前提是两边类型一致(Swift 没有隐式转换,必要时手写 Double(7) / 2)。
7.2 比较运算符
==、!=、<、<=、>、>=,作用在可比较的类型上。它们有一个容易被忽略的性质:同一组里的比较运算符不能连写。
| |
| |
编译器怕的是 a < b == c 这种"你到底想比什么"的糊涂账,于是直接不许写。加括号明确意图即可:
| |
7.3 恒等运算符:=== 和 !==
它们问的不是"值相等吗",而是"是不是同一个对象",只对类(引用类型)有意义:
| |
要比较"内容"是否相等,需要类型遵循 Equatable(第 22 章)。很多初学者在这里栽跟头:给类写了 == 就以为 === 也能用,其实两者是完全不同的两件事。
7.4 逻辑运算符:会"偷懒"的 && 和 ||
下面的例子故意在每个操作数里都打一行日志,好让你看清楚谁真的被求值了:
| |
注意输出:右边根本没有被求值。这就是短路——&& 左边为假就整体为假,|| 左边为真就整体为真,右边直接跳过。
这不是性能优化的小技巧,而是写代码的常规武器:
| |
左边挡住条件,右边才敢解包。反过来把顺序写错,就会在运行时炸掉。
7.5 位运算
把整数当成一排二进制位来操作,在协议解析、权限位、哈希这些场景里很常见:
| |
| 运算符 | 含义 |
|---|---|
& | 按位与 |
| ` | ` |
^ | 按位异或 |
~ | 按位取反(前缀运算符) |
<< | 左移,溢出位直接丢弃,不会崩 |
>> | 右移 |
<< 这个"丢弃而不崩"的行为值得单独记:它和 + 的"溢出就崩"不一样。移出的位直接丢掉,移位数达到或超过位宽时,结果就是 0:
| |
而带 & 的掩码移位(&<<、&>>)才会先把移位数按位宽取模:
| |
&<< 9 等价于 a << 1(9 对 8 取模得 1)。掩码移位的结果永远有定义,所以散列、加密这类需要"确定行为"的算法里用的都是它。
顺带一提,UInt8 的位运算常见于解析二进制协议、处理颜色分量这类场景——普通的业务代码里,一年可能都用不上一次。
7.6 区间:... 与 ..<
一个"包含端点",一个"不包含",switch、切片和 ForEach 都会用到它们:
| |
区间不只是给 for 用的,它本身就是值,可以传递、判断包含关系,还能"单边":
| |
| 写法 | 名字 | 含义 |
|---|---|---|
1...5 | 闭区间 | 含两端 |
1..<5 | 半开区间 | 含左不含右 |
...5 | 单边(到 5 为止) | 适合 switch 里做区间分档 |
1... | 单边(从 1 起) | 同上,方向相反 |
..<5 | 单边(小于 5) | 同上 |
数组下标习惯用半开区间(0..<count),字符串切片也常配合单边区间使用(第 5 章)。
7.7 三元运算符与空合运算符
它们把"二选一"写进一行表达式里;用得好省事,用过头就没人读得懂:
| |
?? 是"左边有值就用左边,否则用右边",它是可选值最常用的搭档(第 9 章细讲)。注意它的优先级比比较运算符高:
| |
7.8 赋值与复合赋值
+= 这类写法读起来更贴近"在原值上做变化",但有一个前提值得记住:左边必须是可以被赋值的:
| |
可用的复合赋值:+=、-=、*=、/=、%=、&=、|=、^=、<<=、>>=,以及带回绕的 &+=、&-=、&*=。
一个坑:Swift 的赋值语句不返回值,所以 if (x = 1) 这种"故意写错一个等号"的经典 bug 在这里写不出来——编译器直接报错。
7.9 优先级与结合性
Swift 的运算符优先级分组(从高到低):
| 优先级组 | 包含的运算符 |
|---|---|
| 位运算移位 | <<、>> |
| 乘法 | *、/、%、&、&* |
| 加法 | +、-、|(按位或)、^、&+、&- |
| 区间 | ...、..< |
| 类型转换 | is、as |
| 空合 | ?? |
| 比较 | ==、!=、<、<=、>、>=、===、!== |
| 逻辑与 | && |
| 逻辑或 | || |
| 三元 | ? : |
| 赋值 | = 及各种复合赋值 |
举两个实际结果:
| |
记不住没关系,加括号永远是对的。真正需要记的只有两条:&& 比 || 高,?? 比比较运算符高。
7.10 自定义运算符
Swift 允许你定义新的运算符,并且声明它和已有运算符的相对优先级。写数学库、DSL 的时候很有用:
| |
要点三条:
- 先声明运算符的"身份":
infix(中缀)、prefix(前缀)、postfix(后缀)。 precedencegroup决定它和别的运算符谁先算、从左还是从右结合。- 运算符只能由规定的符号字符组成(
/ = - + ! * % < > & | ^ ~ ?及其组合);已有运算符可以为新类型重载,但不要改变它原本的语义。
自定义运算符是"强力工具",也是"团队沟通成本"。在公共 API 里新增一个 **,意味着每个读代码的人都要多记一条规则——所以官方的态度是:能用普通函数名表达就别造运算符。
7.11 if 和 switch 也能当表达式
从 Swift 5.9 起,if 和 switch 可以直接出现在赋值号右边:
| |
这解决了一个长期的小尴尬:以前给常量按条件赋值,要么写三元(只适合两个分支)、要么先 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! 又分别适合什么场景。