运算符接收一个、两个或三个操作数并产生一个值:a + b、!done、x ?? fallback。C# 的运算符和 C、Java、JavaScript 的很接近,但有几个的行为会让新手吃惊,最主要的就是整数除法和 % 的符号。
算术运算符
+、-、*、/ 和 % 适用于所有数值类型。结果的类型取决于操作数:两个 int 值得到 int,一个 int 和一个 double 得到 double。
输出:
22
12
85
3
2
3.4
3.4
3
3.4
整数除法向零截断。 17 / 5 是 3,-17 / 5 是 -3。这会导致一个非常常见的 bug:只要 done < total,int percent = done / total * 100; 的结果就是 0,因为 done / total 已经是 0 了。先做乘法(done * 100 / total),或者按 double 做除法。
整数或 decimal 除以零会抛出 DivideByZeroException。double 除以零不会抛出异常;它得到无穷大,而 0.0 / 0.0 得到 NaN:
输出:
True
True
False
DivideByZeroException
取模运算符与负数
% 返回除法的余数,结果的符号与左操作数相同。这一点与 Python 以及数学上的“取模”不同,后两者在除数为正时结果总是非负的:
输出:
1
-1
1
2
even
62 min 5 s
Thu
1.5
只要左边可能为负、而结果又要用作索引,就用 Mod 辅助方法:当和为负数时,days[(today + offset) % 7] 会抛出 IndexOutOfRangeException。判断奇数也有同样的陷阱:对于负奇数,n % 2 == 1 是 false;应该判断 n % 2 != 0。
赋值、复合赋值与自增
= 用于赋值。复合形式 +=、-=、*=、/=、%=、&=、|=、^=、<<=、>>= 把一次运算和赋值合在一起。++ 和 -- 用于加 1 或减 1,它们有前缀和后缀两种形式,区别在于返回的值:
输出:
14
start, step
a=5 b=7 i=7
4
byte 的例子展示了一个怪癖:level = level + 10; 无法通过编译,因为 byte + int 的结果是 int;但 level += 10; 可以,因为复合赋值隐含了一次转换回变量类型的强制转换。这次转换在溢出时会回绕(250 + 10 变成 4)。
比较运算符与逻辑运算符
比较运算符(==、!=、<、>、<=、>=)返回一个 bool。对于字符串,== 比较的是文本,而不是引用。逻辑运算符有 !(非)、&&(与)、||(或)和 ^(异或)。
输出:
True
no active user
with ||:
evaluated A
with |:
evaluated A
evaluated B
True
&& 和 || 会短路:只有当右操作数可能改变结果时,它才会被执行。正因如此,user != null && user.IsActive 才是安全的。用在布尔值上的单个 & 和 | 总是对两边都求值,这在条件判断中很少是你想要的。
位运算符与移位运算符
在整数类型上,&、|、^ 和 ~ 作用于单个的位,<< 和 >> 则对位进行移动。它们用于标志位、掩码、哈希和底层协议:
输出:
3
True
False
101
1024
125
5
-1
对于有名字的一组标志,带 [Flags] 特性的枚举比整数常量更清晰,HasFlag 也比掩码更好读。C# 11 新增了 >>>,即无符号右移,即使对负数也会用零填充。
空值运算符
有三个运算符专门用于处理可能为 null 的值:
输出:
Guest
0
5
a ?? b 在 a 不为 null 时返回 a,否则返回 b。a?.Member 在 a 为 null 时返回 null,而不是抛出异常。C# 8 新增了 a ??= b,它只在 a 为 null 时才把 b 赋给 a。可空类型页面对这些运算符有深入介绍。
运算符优先级
当一个表达式混用多个运算符时,表中越靠上的结合得越紧。同一行的运算符从左到右结合,但赋值、?? 和 ?: 例外,它们从右到左结合。(操作数本身总是从左到右求值。)
| 类别 | 运算符 |
|---|---|
| 基本 | x.y、x?.y、f(x)、a[i]、x++、x--、new、typeof、nameof |
| 一元 | +x、-x、!x、~x、++x、--x、(T)x、await |
| 乘法类 | *、/、% |
| 加法类 | +、- |
| 移位 | <<、>>、>>> |
| 关系与类型检测 | <、>、<=、>=、is、as |
| 相等 | ==、!= |
| 按位与 | & |
| 按位异或 | ^ |
| 按位或 | | |
| 条件与 | && |
| 条件或 | || |
| 空合并 | ?? |
| 条件 | c ? a : b |
| 赋值与 lambda | =、+=、-=、??=、=>,以及其他复合形式 |
有两个后果值得了解:a + b * c 先算乘法;x & mask == 0 先做比较(== 比 & 结合得更紧),所以它的意思是 x & (mask == 0),对整数来说无法通过编译。应该写成 (x & mask) == 0。括号不花任何代价,还能让意图一目了然。
运算符重载
你自己的类型可以定义运算符的含义。decimal、DateTime 和 TimeSpan 就是这样支持 + 和 < 的:
输出:
33.49
True
运算符必须是 public static 的,而且有些必须成对定义:定义了 < 就必须定义 >,定义了 == 就必须定义 !=。定义了 == 之后,编译器还会一直发出警告,直到你相应地重写 Equals 和 GetHashCode。只在含义一目了然的地方重载运算符,比如数字、向量和金额。
常见问题
C# 中 % 的作用是什么?
% 是求余运算符:17 % 5 的结果是 2。结果的符号与左操作数相同,所以 -7 % 3 是 -1,而不是像 Python 那样得到 2。如果需要一个总是非负的结果(比如让索引循环时),写成 ((a % n) + n) % n。它也适用于 double 和 decimal:7.5 % 2 是 1.5。
为什么在 C# 中 7 / 2 等于 3?
当两个操作数都是整数时,/ 执行整数除法,丢掉小数部分,向零截断。把其中一个操作数变成浮点数或 decimal 值,就能得到带小数的结果:7 / 2.0 是 3.5,(double)a / b 会在除法之前先转换,7m / 2 则得到 decimal 类型的 3.5。
C# 中 && 和 & 有什么区别?
&& 是条件与:如果左边是 false,右边就不会被求值。用在布尔值上的 & 总是会对两边都求值,用在整数上时它是按位与。条件判断用 && 和 ||,尤其是像 user != null && user.IsActive 这样的保护性检查,换成 & 就会抛出 NullReferenceException。
C# 中 i++ 和 ++i 有什么区别?
两者都给 i 加 1。表达式 i++ 的值是旧值,++i 的值是新值。单独写成一行(i++;)时没有区别。只有用到表达式的结果时才有区别,比如 int a = i++;,这也是许多代码风格指南要求把自增单独写一行的原因。
C# 中运算符的优先级是怎样的?
从高到低依次是:基本运算符(x.y、f(x)、a[i]、x++)、一元运算符(!、-、++x、强制转换)、乘法类(* / %)、加法类(+ -)、移位、关系运算符(< > <= >= is as)、相等运算符(== !=)、&、^、|、&&、||、??、条件运算符 ?:,最后是赋值。拿不准时,就加上括号。