Menu

C# 运算符:算术、取模、逻辑、位运算与优先级

C# 运算符及其确切行为:整数除法、负数取模、自增与复合赋值、比较、短路逻辑、位运算符、空值运算符,以及一张运算符优先级表。

本页包含可运行的编辑器。编辑、运行,立即查看输出。

运算符接收一个、两个或三个操作数并产生一个值: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)、相等运算符(== !=)、&、^、|、&&、||、??、条件运算符 ?:,最后是赋值。拿不准时,就加上括号。

Coddy 编程语言插图

和 Coddy 一起学编程

开始