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 复合赋值运算符:+= -= *= /= %=

将运算与赋值合并,属于语法糖,底层等效于「运算后重新赋值」,但存在一个极易踩坑的隐式强转特性。

经典坑:小整数类型的复合赋值

对于 byteshort 等小整型,直接运算会自动提升为 int,普通赋值会编译报错,但复合赋值会自动插入强制类型转换。
byte b = 10;
// b = b + 5; // 编译报错:b+5 结果为 int,不能隐式转换为 byte

b += 5; // 编译通过,编译器自动等效为:
        // b = (byte)(b + 5);

二、比较运算符

用于判断两个值的大小、相等关系,返回结果为 bool 类型。

2.1 基础比较运算符

==(等于)、!=(不等于)、>(大于)、<(小于)、>=(大于等于)、<=(小于等于)

相等性的核心差异

相等比较是 C# 最容易踩坑的点之一,不同类型的 == 行为完全不同:
  1. 值类型:默认比较「值」,值相同则返回 true
  2. 引用类型:默认比较「引用地址」,指向同一个堆对象才返回 true
  3. 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
最佳实践:
  1. 绝大多数场景优先使用 &&||,提升性能且符合常规预期
  2. 不要在右侧表达式中写有副作用的代码(如修改变量、方法调用),避免依赖短路逻辑导致隐蔽 bug
  3. && 优先级高于 ||,复杂逻辑建议加括号明确语义

四、位运算符

直接对整数的二进制位进行操作,性能极高,常用于 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,正常访问成员
  • 支持链式调用,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 typeofGetType()

  • 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 修饰
  • 至少有一个参数是当前类型
  • 比较运算符必须成对重载(==!=<> 等)
  • 重载 == 建议同时重写 EqualsGetHashCode,保持行为一致

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 模式匹配:兼顾安全与简洁,避免强转异常
  • 空值场景优先用 ?.??:减少冗余判空代码,降低空引用异常概率
  • 复杂表达式加括号:不要依赖优先级记忆,保证代码可读性
  • 运算符重载保持语义一致:重载 + 就应该做加法语义,不要实现无关逻辑
posted @ 2026-07-08 14:42  挺秃然的i  阅读(21)  评论(0)    收藏  举报