C#基础(三):运算符
运算符是 C# 中执行数值计算、逻辑判断、类型转换、空值安全等操作的核心语法单元。C# 提供了非常丰富的运算符体系,同时有严格的优先级、结合性与重载规则。本文按功能分类深入讲解每个运算符的底层行为、适用场景、常见陷阱与最佳实践,配合完整代码示例。
一、算术运算符
用于数值类型的数学运算,是最基础的运算符类别。
1.1 基础算术运算符:+ - * / %
| 运算符 | 含义 | 说明 |
|---|---|---|
+ |
加法 | 也可用于字符串拼接、委托合并 |
- |
减法 | 也可作为一元负号 |
* |
乘法 | |
/ |
除法 | 整数相除默认截断小数,浮点数相除保留小数 |
% |
取余(取模) | 返回除法的余数,符号与被除数一致 |
核心坑点与代码示例
// 1. 整数除法截断(高频坑) int a = 5; int b = 2; Console.WriteLine(a / b); // 输出 2,而非 2.5,直接丢弃小数部分 // 正确写法:先转浮点再运算 Console.WriteLine((double)a / b); // 输出 2.5 // 2. 取余的符号规则:结果符号与被除数相同 Console.WriteLine(5 % 2); // 1 Console.WriteLine(-5 % 2); // -1 Console.WriteLine(5 % -2); // 1 Console.WriteLine(-5 % -2); // -1 // 3. 字符串拼接:+ 运算符被 string 重载 string s = "Hello" + " " + "World"; // 4. 数值溢出:默认不检查,可通过 checked 开启 int max = int.MaxValue; unchecked { int overflow = max + 1; // 不抛异常,结果溢出为 int.MinValue } checked { // int error = max + 1; // 抛出 OverflowException }
1.2 自增与自减:++ --
对变量执行 +1 /-1 操作,分为前缀和后缀两种写法,核心区别是返回值的时机不同。
执行原理
- 后缀
x++:先返回变量的原始值,再执行自增 - 前缀
++x:先执行自增,再返回自增后的新值
// 后缀自增:先取值,再加1 int num1 = 10; int result1 = num1++; Console.WriteLine($"result1={result1}, num1={num1}"); // result1=10, num1=11 // 前缀自增:先加1,再取值 int num2 = 10; int result2 = ++num2; Console.WriteLine($"result2={result2}, num2={num2}"); // result2=11, num2=11 // 等效拆解(后缀) int temp = num1; num1 = num1 + 1; int result = temp;
最佳实践:不要在复杂表达式中嵌套使用自增自减,严重降低可读性;单独一行使用时前缀后缀无差异。
1.3 复合赋值运算符:+= -= *= /= %=
将运算与赋值合并,属于语法糖,底层等效于「运算后重新赋值」,但存在一个极易踩坑的隐式强转特性。
经典坑:小整数类型的复合赋值
byte、short 等小整型,直接运算会自动提升为 int,普通赋值会编译报错,但复合赋值会自动插入强制类型转换。byte b = 10; // b = b + 5; // 编译报错:b+5 结果为 int,不能隐式转换为 byte b += 5; // 编译通过,编译器自动等效为: // b = (byte)(b + 5);
二、比较运算符
用于判断两个值的大小、相等关系,返回结果为 bool 类型。
2.1 基础比较运算符
==(等于)、!=(不等于)、>(大于)、<(小于)、>=(大于等于)、<=(小于等于)
相等性的核心差异
== 行为完全不同:- 值类型:默认比较「值」,值相同则返回 true
- 引用类型:默认比较「引用地址」,指向同一个堆对象才返回 true
- string 特殊:虽然是引用类型,但
==被重写为按值比较
// 值类型:按值比较 int x = 10, y = 10; Console.WriteLine(x == y); // true // 普通引用类型:按引用比较 Person p1 = new Person { Age = 20 }; Person p2 = new Person { Age = 20 }; Console.WriteLine(p1 == p2); // false,两个不同的堆对象 // string:按值比较(运算符重载) string s1 = new string(new char[] {'a','b','c'}); string s2 = new string(new char[] {'a','b','c'}); Console.WriteLine(s1 == s2); // true,重写为值比较
四种相等判断方式对比
| 判断方式 | 本质 | 适用场景 |
|---|---|---|
== |
运算符,可重载 | 数值、字符串等日常比较 |
Equals(obj) |
实例方法,可重写 | 通用值比较,支持多态 |
object.ReferenceEquals(a,b) |
静态方法,不可重写 | 严格判断是否为同一个引用 |
EqualityComparer<T>.Default |
泛型比较器 | 泛型场景下的标准相等比较 |
浮点数相等比较(高频坑)
由于二进制浮点存在精度丢失,绝对不能直接用 == 比较两个浮点数是否相等,应判断差值是否小于可接受的精度阈值。
double d1 = 0.1 + 0.2; double d2 = 0.3; Console.WriteLine(d1 == d2); // false,精度误差 // 正确写法:判断差值小于极小阈值 bool isEqual = Math.Abs(d1 - d2) < 1e-9; // true
三、逻辑与布尔运算符
用于布尔值的逻辑运算,分为「不短路逻辑运算符」和「短路条件运算符」两类。
3.1 不短路逻辑运算符:& | ^ !
&逻辑与:两边都为 true 结果才为 true|逻辑或:任意一边为 true 结果就为 true^逻辑异或:两边不同则为 true,相同则为 false!逻辑非:取反
核心特点:无论左边结果如何,右边表达式一定会执行,即不短路。
bool IsTrue() { Console.WriteLine("执行了"); return true; } // & 不短路:两边都执行 bool result1 = false & IsTrue(); // 控制台输出"执行了",result1=false // 异或妙用:不使用临时变量交换两个整数 int a = 3, b = 5; a ^= b; b ^= a; a ^= b; // 结果:a=5, b=3
3.2 短路条件运算符:&& ||
这是日常开发最常用的逻辑运算符,核心特性是短路求值:
&&:左边为 false 时,直接返回 false,右边不执行||:左边为 true 时,直接返回 true,右边不执行
bool IsTrue() { Console.WriteLine("执行了"); return true; } // && 短路:左边false,右边不执行 bool result1 = false && IsTrue(); // 无输出,result1=false // || 短路:左边true,右边不执行 bool result2 = true || IsTrue(); // 无输出,result2=true
- 绝大多数场景优先使用
&&和||,提升性能且符合常规预期 - 不要在右侧表达式中写有副作用的代码(如修改变量、方法调用),避免依赖短路逻辑导致隐蔽 bug
&&优先级高于||,复杂逻辑建议加括号明确语义
四、位运算符
直接对整数的二进制位进行操作,性能极高,常用于 Flags 枚举、状态标志、高性能底层开发。
4.1 核心位运算符
| 运算符 | 含义 | 运算规则 | |
|---|---|---|---|
& |
按位与 | 对应位都为 1 结果才为 1 | |
| ` | ` | 按位或 | 对应位有 1 结果就为 1 |
^ |
按位异或 | 对应位不同则为 1,相同则为 0 | |
~ |
按位取反 | 所有位 0 变 1、1 变 0 | |
<< |
左移 | 所有位向左移动,低位补 0,等价于乘以 2 的 n 次方 | |
>> |
有符号右移 | 所有位向右移动,高位补符号位(正数补 0,负数补 1) | |
>>> |
无符号右移(C# 11+) | 所有位向右移动,高位统一补 0 |
int a = 0b_0000_1100; // 十进制 12 int b = 0b_0000_1010; // 十进制 10 Console.WriteLine(a & b); // 8 (00001000) Console.WriteLine(a | b); // 14 (00001110) Console.WriteLine(a ^ b); // 6 (00000110) Console.WriteLine(a << 1); // 24,左移1位 = 乘以2 Console.WriteLine(a >> 1); // 6, 右移1位 = 除以2 // 负数右移区别 int negative = -1; Console.WriteLine(negative >> 1); // -1,有符号右移补1,始终为-1 // Console.WriteLine(negative >>> 1); // C# 11+ 结果为 2147483647(int最大值)
4.2 典型应用场景:Flags 枚举位运算
这是位运算最常用的业务场景,配合 [Flags] 枚举实现多选状态管理。
[Flags] public enum Permission { None = 0, Read = 1 << 0, // 1 Write = 1 << 1, // 2 Delete = 1 << 2,// 4 Admin = Read | Write | Delete } // 1. 添加权限:| Permission user = Permission.Read | Permission.Write; // 2. 判断是否包含权限:& bool canDelete = (user & Permission.Delete) == Permission.Delete; // false // 或使用 HasFlag 方法(语法糖,内部也是位运算) bool canRead = user.HasFlag(Permission.Read); // true // 3. 移除权限:& ~ user = user & ~Permission.Write; // 4. 切换权限:^ user = user ^ Permission.Delete;
五、空值安全运算符
C# 针对 null 场景提供了专属运算符,大幅简化空值判断代码,是现代 C# 的核心特性之一。
5.1 空条件运算符:?. ?[]
- 若对象为 null,直接返回 null,不执行后续成员访问,不会抛出空引用异常
- 若对象不为 null,正常访问成员
- 支持链式调用,null 会沿着调用链传播
User? user = GetUser(); // 传统写法:多层判空,代码臃肿 string? name1 = null; if (user != null && user.Profile != null) { name1 = user.Profile.NickName; } // 空条件运算符写法:简洁安全 string? name2 = user?.Profile?.NickName; // 索引器场景:?[] int? firstItem = user?.Tags?[0];
注意:空条件运算符是「一断全断」,链式调用中任意一环为 null,整个表达式结果即为 null,返回类型自动变为可空类型。
5.2 空合并运算符:??
如果左侧值为 null,则返回右侧值;否则返回左侧值,用于给可空变量提供默认值。
string? input = null; // 传统写法 string result1 = input != null ? input : "默认值"; // 空合并写法 string result2 = input ?? "默认值"; // 支持链式调用,取第一个非 null 值 string final = a ?? b ?? c ?? "兜底值";
5.3 空合并赋值运算符:??=(C# 8.0+)
如果左侧变量为 null,则将右侧值赋值给左侧变量;否则不做任何操作。最经典的用途是惰性初始化。
private List<int>? _list; // 传统懒加载写法 public List<int> GetList1() { if (_list == null) { _list = new List<int>(); } return _list; } // ??= 简洁写法 public List<int> GetList2() { return _list ??= new List<int>(); }
六、类型判断与转换运算符
6.1 is 运算符与模式匹配
is 不仅可以判断类型是否匹配,C# 7.0 之后演进为强大的模式匹配语法,支持类型转换、常量判断、属性匹配等多种能力。
1. 基础类型判断 + 变量声明
判断类型的同时直接声明转换后的变量,无需二次转换,是最常用写法。
object obj = "hello world"; // 传统写法 if (obj is string) { string s = (string)obj; // 二次转换 Console.WriteLine(s.Length); } // 推荐写法:类型模式 if (obj is string s) { Console.WriteLine(s.Length); // 直接使用 s }
2. 常量与否定模式(C# 9.0+)
// 判断非 null if (obj is not null) { } // 判断常量值 if (num is 0) { } if (str is "admin") { }
3. 属性与关系模式(C# 9.0+)
// 属性模式:判断类型+属性值 if (obj is Person { Age: > 18, Name: "Tom" } p) { // 年龄大于18且名字是Tom的Person对象 } // 关系模式:范围判断 if (age is >= 18 and <= 60) { } if (level is 1 or 2 or 3) { }
6.2 as 运算符
尝试将对象转换为指定类型,转换失败不抛异常,直接返回 null。
- 仅支持引用类型和可空值类型
- 不会执行自定义类型转换,仅支持引用转换、装箱拆箱
object obj = "hello"; string? s = obj as string; if (s != null) { // 转换成功 } // 转换失败返回 null int? i = obj as int?; // null
6.3 强制类型转换 (T)
显式将一种类型转换为另一种,失败时抛出 InvalidCastException。
- 数值类型:可能发生精度丢失或溢出
- 引用类型:仅支持有继承 / 实现关系的类型转换
| 特性 | 强转 (T) | as |
|---|---|---|
| 失败处理 | 抛异常 | 返回 null |
| 支持类型 | 值类型 + 引用类型 | 引用类型 + 可空值类型 |
| 适用场景 | 确定转换一定成功 | 不确定是否能转换,需判空 |
6.4 typeof 与 GetType()
typeof(T):编译时获取类型的Type对象,性能高obj.GetType():运行时获取对象的实际类型,支持多态
// 编译时确定 Type stringType = typeof(string); // 运行时确定实际类型 object obj = "hello"; Type actualType = obj.GetType(); // System.String
七、其他常用特殊运算符
7.1 三元条件运算符:?:
条件 ? 表达式1 : 表达式2,是 if-else 的简写形式,条件为 true 执行表达式 1,否则执行表达式 2。
int age = 20; string result = age >= 18 ? "成年人" : "未成年人"; // 右结合性:从右向左运算 int level = score >= 90 ? 1 : score >= 60 ? 2 : 3; // 等效于:score >= 90 ? 1 : (score >= 60 ? 2 : 3)
注意:两个分支的返回类型必须兼容,编译器才能推断结果类型。
7.2 null 包容运算符:!(C# 8.0+)
在可空引用类型上下文中,告诉编译器「该值一定不为 null」,抑制编译器的空警告。
- 仅作用于编译时,运行时无任何影响
- 滥用会掩盖真实的空引用风险,仅在你 100% 确定不为 null 时使用
#nullable enable string? maybeNull = GetString(); // 你通过业务逻辑确定一定不为 null,但编译器仍警告 Console.WriteLine(maybeNull!.Length); // ! 抑制警告
7.3 索引与范围运算符:^ ..(C# 8.0+)
^n:从序列末尾开始计数,^1表示最后一个元素[a..b]:取序列的切片,左闭右开区间
int[] arr = { 1, 2, 3, 4, 5 }; // 末尾索引 Console.WriteLine(arr[^1]); // 5,最后一个元素 Console.WriteLine(arr[^2]); // 4,倒数第二个元素 // 范围切片 int[] sub1 = arr[1..3]; // {2, 3},索引1到2 int[] sub2 = arr[2..^1]; // {3, 4},索引2到倒数第2个 int[] sub3 = arr[..2]; // {1, 2},开头到索引1 int[] sub4 = arr[2..]; // {3, 4, 5},索引2到结尾
7.4 nameof 运算符
编译时获取变量、类型、成员的字符串名称,重构代码时会自动同步,避免硬编码字符串错误。
// 参数校验场景 public void SetName(string name) { if (name == null) throw new ArgumentNullException(nameof(name)); // 重构修改参数名时,nameof 会自动同步 } // 属性通知、日志等场景广泛使用 Console.WriteLine(nameof(User.Name)); // "Name"
八、运算符优先级与结合性
表达式中多个运算符同时出现时,按优先级从高到低执行;优先级相同时,按结合性决定执行顺序。
8.1 优先级速查表(从高到低)
| 优先级 | 类别 | 运算符 | ||
|---|---|---|---|---|
| 最高 | 初级运算符 | . () [] x++ x-- new typeof sizeof checked unchecked |
||
| ↑ | 一元运算符 | + - ! ~ ++x --x (T)x |
||
| 乘除模 | * / % |
|||
| 加减 | + - |
|||
| 移位 | << >> >>> |
|||
| 关系与类型 | < > <= >= is as |
|||
| 相等 | == != |
|||
| 按位与 | & |
|||
| 按位异或 | ^ |
|||
| 按位或 | ` | ` | ||
| 条件与 | && |
|||
| 条件或 | ` | ` | ||
| 空合并 | ?? |
|||
| 三元条件 | ?: |
|||
| 最低 | 赋值与 Lambda | = += -= *= /= %= &= ` |
= ^= <<= >>= ??= =>` |
8.2 结合性
- 左结合:绝大多数二元运算符,从左向右执行,例如
a + b - c等效于(a + b) - c - 右结合:赋值运算符、三元条件、空合并,从右向左执行,例如
a = b = c等效于a = (b = c)
最佳实践:不要依赖优先级写复杂表达式,不确定就加括号,可读性永远优先于炫技。
九、运算符重载
C# 允许自定义类型重载运算符,让自定义类型支持加减、比较等运算,提升代码可读性。
9.1 重载规则
- 必须使用
public static修饰 - 至少有一个参数是当前类型
- 比较运算符必须成对重载(
==和!=、<和>等) - 重载
==建议同时重写Equals和GetHashCode,保持行为一致
9.2 代码示例:自定义 Point 结构体
public struct Point { public int X { get; set; } public int Y { get; set; } public Point(int x, int y) { X = x; Y = y; } // 重载 + 运算符 public static Point operator +(Point a, Point b) { return new Point(a.X + b.X, a.Y + b.Y); } // 重载 == 和 != public static bool operator ==(Point a, Point b) { return a.X == b.X && a.Y == b.Y; } public static bool operator !=(Point a, Point b) { return !(a == b); } // 配合重写 Equals 和 GetHashCode public override bool Equals(object? obj) { return obj is Point point && this == point; } public override int GetHashCode() { return HashCode.Combine(X, Y); } } // 使用 Point p1 = new Point(1, 2); Point p2 = new Point(3, 4); Point sum = p1 + p2; // {4, 6} bool equal = p1 == p2; // false
最佳实践:仅当运算符语义非常明确时才重载,不要为了炫技滥用运算符重载,避免降低代码可维护性。
十、常见坑点与最佳实践总结
- 整数除法自动截断:两个 int 相除会丢弃小数,需要浮点结果请先强转其中一个数为 double/decimal
- 浮点数禁止直接
==比较:通过差值小于精度阈值判断相等 - 逻辑运算优先用短路版:
&&||性能更好,不要在右侧写有副作用的表达式 - 字符串相等用
==即可:C# 已重写为值比较,无需刻意用Equals - 类型转换优先用
is模式匹配:兼顾安全与简洁,避免强转异常 - 空值场景优先用
?.和??:减少冗余判空代码,降低空引用异常概率 - 复杂表达式加括号:不要依赖优先级记忆,保证代码可读性
- 运算符重载保持语义一致:重载
+就应该做加法语义,不要实现无关逻辑

浙公网安备 33010602011771号