刚接触 C# 时,面对各种运算符,你是否感觉概念都懂,但一到实际编码就卡壳?比如,++i和i++到底先执行谁?逻辑运算符&&和&在短路求值上有什么区别?三元运算符如何优雅地替代简单的if-else?这些看似基础的问题,恰恰是构建扎实编程功底的关键,也是面试和实际项目中高频出现的“考点”。
本文将以“运算符练习”为核心,带你从零开始,通过大量可运行的代码示例,系统性地掌握 C# 中的各类运算符。我们不仅会回顾算术、关系、逻辑等基础运算符,更会深入探讨赋值、条件、空值合并等进阶用法,并结合checked关键字、运算符重载等概念,帮你构建完整的运算符知识体系。无论你是正在学习 C# 语法的新手,还是需要巩固基础的开发者,都能通过本文的实战练习,将理论知识转化为解决实际问题的编码能力。
1. C# 运算符核心概念与分类
在编程中,运算符是用于执行程序代码运算的符号,它作用于一个或多个操作数,并产生一个结果。C# 提供了丰富的运算符,可以大致分为以下几类,理解这些分类是灵活运用的前提。
1.1 按操作数数量分类
这是最基础的分类方式,决定了运算符的使用形式。
- 一元运算符:只作用于一个操作数。例如,递增 (
++)、递减 (--)、逻辑非 (!)、取负 (-)。int x = 5; int y = -x; // 一元负号运算符,y = -5 bool flag = true; bool result = !flag; // 一元逻辑非运算符,result = false - 二元运算符:作用于两个操作数。这是最常见的类型,如算术运算符 (
+,-,*,/)、关系运算符 (>,<,==)、赋值运算符 (=)。int a = 10, b = 3; int sum = a + b; // 二元加法运算符,sum = 13 bool isGreater = a > b; // 二元大于运算符,isGreater = true - 三元运算符:C# 中只有一个三元运算符,即条件运算符 (
?:)。它作用于三个操作数。int score = 85; string grade = score >= 60 ? "及格" : "不及格"; // 三元条件运算符 Console.WriteLine(grade); // 输出:及格
1.2 按功能分类
这是更贴近实际使用的分类,帮助我们根据要执行的操作选择正确的运算符。
| 类别 | 运算符示例 | 描述 | 主要用途 |
|---|---|---|---|
| 算术运算符 | +,-,*,/,% | 执行基本数学运算。 | 数值计算。 |
| 关系运算符 | ==,!=,>,<,>=,<= | 比较两个操作数,返回布尔值。 | 条件判断、循环控制。 |
| 逻辑运算符 | &&, ` | ,!` | |
| 赋值运算符 | =,+=,-=,*=,/= | 将值赋给变量,常与算术运算结合。 | 变量赋值与更新。 |
| 位运算符 | &, ` | ,^,~,<<,>>` | 直接对整数的二进制位进行操作。 |
| 条件运算符 | ?: | 根据条件返回两个值中的一个。 | 简化简单的if-else语句。 |
| 空值相关运算符 | ??,?.,?[] | 简化对可能为null的对象的操作。 | 安全地处理空引用。 |
| 其他 | ++,--,is,as,sizeof等 | 包含递增/递减、类型检查等。 | 特定场景下的操作。 |
1.3 运算符优先级与结合性
当表达式中包含多个运算符时,优先级决定了谁先执行,结合性决定了相同优先级的运算符谁先执行(从左到右或从右到左)。忽略这一点是许多错误的根源。
常见优先级(从高到低):
- 括号
():永远拥有最高优先级,用于强制改变运算顺序。 - 一元运算符:如
++,--,!,+(正),-(负)。 - 乘除取余:
*,/,%。 - 加减:
+,-。 - 关系比较:
<,>,<=,>=。 - 相等比较:
==,!=。 - 逻辑与:
&&。 - 逻辑或:
||。 - 条件运算符:
?:。 - 赋值运算符:
=,+=等(结合性为从右到左)。
示例分析:
int result = 5 + 3 * 2; // 乘法优先级高, result = 5 + 6 = 11 bool logic = true || false && false; // && 优先级高于 ||, 相当于 true || (false && false) = true int x = 10; int y = x++ + ++x; // 复杂且不推荐,但用于理解:先计算 x++ (返回10,x变为11),再计算 ++x (x先加为12,返回12),最后 10+12=22 Console.WriteLine($"y={y}, x={x}"); // 输出:y=22, x=12最佳实践:在复杂的表达式中,积极使用括号()来明确你的意图,这能极大提高代码的可读性并避免优先级陷阱。
2. 开发环境准备与说明
在开始练习之前,确保你有一个可以编写和运行 C# 代码的环境。本文将使用最通用的方式进行演示。
2.1 环境与工具
- .NET SDK:这是核心。建议安装最新的.NET 8.0 LTS或.NET 9.0SDK。你可以从微软官网下载。安装后,在命令行输入
dotnet --version验证。 - 代码编辑器:
- Visual Studio 2022:功能强大的 IDE,社区版免费。非常适合 Windows 平台的 C# 开发。
- Visual Studio Code:轻量级跨平台编辑器,需安装C# 扩展。本文示例将主要基于 VS Code 和命令行,以保证通用性。
- Rider:JetBrains 出品的优秀跨平台 .NET IDE。
- 操作系统:Windows, macOS 或 Linux 均可,.NET 是跨平台的。
2.2 创建练习项目
我们创建一个控制台项目来容纳所有的练习代码。
- 打开终端(命令行)。
- 创建项目目录并进入:
mkdir CSharpOperatorsPractice cd CSharpOperatorsPractice - 创建新的控制台项目:
dotnet new console -n OperatorsDemo cd OperatorsDemo - 用编辑器打开项目。例如用 VS Code:
code .
现在,你的项目结构应类似于:
OperatorsDemo/ ├── OperatorsDemo.csproj ├── Program.cs └── obj/我们将主要在Program.cs文件中编写和修改代码。
3. 各类运算符详解与练习
本章节将逐一深入各类运算符,并提供可运行的练习代码。请务必在项目中亲自输入并运行这些代码,观察输出结果,理解其背后的逻辑。
3.1 算术运算符
用于基本的数学计算。
Console.WriteLine("=== 算术运算符练习 ==="); int a = 17, b = 5; // 加法 int sum = a + b; // 22 Console.WriteLine($"{a} + {b} = {sum}"); // 减法 int difference = a - b; // 12 Console.WriteLine($"{a} - {b} = {difference}"); // 乘法 int product = a * b; // 85 Console.WriteLine($"{a} * {b} = {product}"); // 除法 (整数除法) int quotient = a / b; // 3 (注意:不是3.4,整数除法会截断小数部分) Console.WriteLine($"{a} / {b} = {quotient}"); // 取余 (模运算) int remainder = a % b; // 2 (17除以5,商3余2) Console.WriteLine($"{a} % {b} = {remainder}"); // 浮点数除法 double x = 17.0, y = 5.0; double floatQuotient = x / y; // 3.4 Console.WriteLine($"{x} / {y} = {floatQuotient}"); // 自增/自减 int count = 10; Console.WriteLine($"初始 count = {count}"); // 10 int postIncrement = count++; // 后置递增:先使用count的值(10)赋给postIncrement,然后count加1 Console.WriteLine($"count++ 后,postIncrement = {postIncrement}, count = {count}"); // postIncrement=10, count=11 count = 10; // 重置 int preIncrement = ++count; // 前置递增:先将count加1(变为11),然后使用新值赋给preIncrement Console.WriteLine($"++count 后,preIncrement = {preIncrement}, count = {count}"); // preIncrement=11, count=11关键点:
- 整数除法 (
/) 会丢弃小数部分,结果仍是整数。 - 取余运算 (
%) 在判断奇偶、循环队列等场景非常有用。 - 分清
i++(后置) 和++i(前置) 是面试常见题。i++返回原值,++i返回加一后的值。
3.2 赋值运算符
将值赋给变量,复合赋值运算符可以简化代码。
Console.WriteLine("\n=== 赋值运算符练习 ==="); int num = 10; // 基本赋值 Console.WriteLine($"初始 num = {num}"); num += 5; // 等价于 num = num + 5; Console.WriteLine($"num += 5 后,num = {num}"); // 15 num -= 3; // 等价于 num = num - 3; Console.WriteLine($"num -= 3 后,num = {num}"); // 12 num *= 2; // 等价于 num = num * 2; Console.WriteLine($"num *= 2 后,num = {num}"); // 24 num /= 6; // 等价于 num = num / 6; Console.WriteLine($"num /= 6 后,num = {num}"); // 4 num %= 3; // 等价于 num = num % 3; Console.WriteLine($"num %= 3 后,num = {num}"); // 1 (4 % 3 = 1)注意:复合赋值运算符 (+=,-=等) 不仅使代码更简洁,对于某些复杂类型(如StringBuilder)还可能带来性能上的微小优势。
3.3 关系运算符与逻辑运算符
用于比较和逻辑判断,结果总是bool类型 (true或false)。
Console.WriteLine("\n=== 关系与逻辑运算符练习 ==="); int p = 8, q = 12; // 关系运算符 Console.WriteLine($"{p} == {q} : {p == q}"); // False Console.WriteLine($"{p} != {q} : {p != q}"); // True Console.WriteLine($"{p} > {q} : {p > q}"); // False Console.WriteLine($"{p} < {q} : {p < q}"); // True Console.WriteLine($"{p} >= 8 : {p >= 8}"); // True Console.WriteLine($"{q} <= 12 : {q <= 12}"); // True // 逻辑运算符 bool conditionA = true, conditionB = false; Console.WriteLine($"conditionA = {conditionA}, conditionB = {conditionB}"); // 逻辑与 (AND): 两者都为真,结果才为真 Console.WriteLine($"conditionA && conditionB : {conditionA && conditionB}"); // False // 逻辑或 (OR): 至少一个为真,结果就为真 Console.WriteLine($"conditionA || conditionB : {conditionA || conditionB}"); // True // 逻辑非 (NOT): 取反 Console.WriteLine($"!conditionA : {!conditionA}"); // False Console.WriteLine($"!conditionB : {!conditionB}"); // True // 短路求值 (Short-circuit Evaluation) - 关键知识点! int index = 0; bool result1 = (conditionB) && (++index > 0); // 因为conditionB为false,&&右侧的(++index)根本不会执行! Console.WriteLine($"短路 && 后, result1={result1}, index={index}"); // result1=False, index=0 index = 0; bool result2 = (conditionA) || (++index > 0); // 因为conditionA为true,||右侧的(++index)根本不会执行! Console.WriteLine($"短路 || 后, result2={result2}, index={index}"); // result2=True, index=0 // 非短路版本: & 和 | (也可用于布尔运算,但会计算两边) index = 0; bool result3 = (conditionB) & (++index > 0); // & 不是短路运算符,两边都会计算 Console.WriteLine($"非短路 & 后, result3={result3}, index={index}"); // result3=False, index=1核心区别:
&&和||是短路逻辑运算符。如果根据左侧操作数就能确定最终结果,右侧操作数将不会被计算。这可以提高效率,并常用于防止空引用异常(如if (obj != null && obj.Value > 10))。&和|当用于布尔运算时,是非短路的,它们总是会计算两边的操作数。它们更常用于位运算。
3.4 条件运算符 (三元运算符?:)
它是if-else语句的表达式版本,用于根据条件返回两个值中的一个。
Console.WriteLine("\n=== 条件运算符 (三元运算符) 练习 ==="); // 基本语法: condition ? expression_if_true : expression_if_false int temperature = 25; string weather = temperature > 20 ? "温暖" : "凉爽"; Console.WriteLine($"温度 {temperature}°C, 天气: {weather}"); // 温暖 // 嵌套使用 (可读性会下降,需谨慎) int score = 78; string grade = score >= 90 ? "优秀" : score >= 80 ? "良好" : score >= 60 ? "及格" : "不及格"; Console.WriteLine($"分数 {score}, 等级: {grade}"); // 及格 // 与 null 结合使用,提供默认值 (在C# 9.0之前是常见模式) string userName = null; string displayName = userName != null ? userName : "匿名用户"; Console.WriteLine($"显示名: {displayName}"); // 匿名用户 // 注意:现代C#更推荐使用空值合并运算符 `??`,见下文。最佳实践:三元运算符非常适合简单的、一目了然的条件赋值。避免过度嵌套,否则会严重损害代码可读性。
3.5 空值相关运算符
C# 提供了强大的运算符来安全、简洁地处理可能为null的引用类型。
Console.WriteLine("\n=== 空值相关运算符练习 ==="); // 1. 空值合并运算符 `??` // 如果左操作数不为 null,则返回左操作数;否则返回右操作数。 string possibleNullString = null; string sureNotNullString = possibleNullString ?? "默认字符串"; Console.WriteLine($"使用 ?? 后: {sureNotNullString}"); // 默认字符串 possibleNullString = "我有值"; sureNotNullString = possibleNullString ?? "默认字符串"; Console.WriteLine($"使用 ?? 后: {sureNotNullString}"); // 我有值 // 2. 空值合并赋值运算符 `??=` (C# 8.0+) // 如果左操作数为 null,则将右操作数的值赋给左操作数。 List<int> numbers = null; numbers ??= new List<int>(); // 如果numbers为null,则新建一个列表 numbers.Add(1); Console.WriteLine($"使用 ??= 后,列表元素数: {numbers.Count}"); // 1 // 3. 空条件运算符 `?.` 和 `?[]` // 如果左操作数为 null,则整个表达式结果为 null,不会抛出 NullReferenceException。 class Person { public string Name { get; set; } public Person Manager { get; set; } } Person employee = null; // 传统写法(易引发异常) // string managerName = employee.Manager.Name; // 如果employee为null,此处崩溃 // 安全写法(冗长) string managerNameOld = (employee != null && employee.Manager != null) ? employee.Manager.Name : null; // 使用 `?.` 和 `??` 的现代写法(简洁安全) string managerNameNew = employee?.Manager?.Name ?? "无经理信息"; Console.WriteLine($"经理姓名: {managerNameNew}"); // 无经理信息 employee = new Person { Name = "张三" }; managerNameNew = employee?.Manager?.Name ?? "无经理信息"; Console.WriteLine($"经理姓名: {managerNameNew}"); // 无经理信息 (因为Manager属性为null) employee.Manager = new Person { Name = "李四" }; managerNameNew = employee?.Manager?.Name ?? "无经理信息"; Console.WriteLine($"经理姓名: {managerNameNew}"); // 李四 // `?[]` 用于索引器 int[] array = null; int? firstElement = array?[0]; // 如果array为null,则firstElement为null,不会报错 Console.WriteLine($"数组第一个元素: {firstElement?.ToString() ?? "数组为空或索引越界"}");重要性:空值相关运算符是现代 C# 编写健壮、简洁代码的利器,能有效减少空引用异常。
3.6 位运算符
直接操作整数类型 (int,long,byte等) 的二进制位。在标志位、权限控制、底层优化等场景有用。
Console.WriteLine("\n=== 位运算符练习 ==="); byte flags = 0b_0000_0000; // 二进制表示,初始所有位为0 // 按位或 `|`:设置位(将指定位设为1) const byte READ = 0b_0000_0001; // 第0位:读权限 const byte WRITE = 0b_0000_0010; // 第1位:写权限 const byte EXECUTE = 0b_0000_0100;// 第2位:执行权限 flags |= READ; // 赋予读权限 flags |= WRITE; // 赋予写权限 Console.WriteLine($"赋予读写权限后,flags = {Convert.ToString(flags, 2).PadLeft(8, '0')}"); // 00000011 // 按位与 `&`:检查位(检查指定位是否为1) bool canWrite = (flags & WRITE) != 0; Console.WriteLine($"是否有写权限: {canWrite}"); // True bool canExecute = (flags & EXECUTE) != 0; Console.WriteLine($"是否有执行权限: {canExecute}"); // False // 按位异或 `^`:翻转位(相同为0,不同为1) flags ^= WRITE; // 翻转写权限位(原来是1,变为0) Console.WriteLine($"翻转写权限后,flags = {Convert.ToString(flags, 2).PadLeft(8, '0')}"); // 00000001 canWrite = (flags & WRITE) != 0; Console.WriteLine($"现在是否有写权限: {canWrite}"); // False // 按位取反 `~`:所有位取反 byte invertedFlags = (byte)~flags; Console.WriteLine($"flags取反后 = {Convert.ToString(invertedFlags, 2).PadLeft(8, '0')}"); // 11111110 // 移位运算符 `<<`, `>>` int value = 1; // 二进制: 0001 int leftShifted = value << 2; // 向左移2位: 0100 (十进制4) Console.WriteLine($"{value} << 2 = {leftShifted}"); // 4 value = 8; // 二进制: 1000 int rightShifted = value >> 2; // 向右移2位: 0010 (十进制2) Console.WriteLine($"{value} >> 2 = {rightShifted}"); // 2注意:>>对于有符号整数是算术右移(保留符号位),对于无符号整数是逻辑右移。
4. 综合实战:一个简易计算器
现在,我们将运用所学的运算符知识,构建一个支持加、减、乘、除、取余的命令行简易计算器。
4.1 项目结构与设计
我们将创建一个新的控制台应用。在Program.cs中,我们将实现以下功能:
- 显示操作菜单。
- 接收用户输入的两个数字和运算符。
- 根据运算符执行计算。
- 处理除零错误等异常。
- 循环运行,直到用户选择退出。
4.2 完整实现代码
将Program.cs文件内容替换为以下代码:
using System; namespace OperatorsDemo { class Program { static void Main(string[] args) { Console.WriteLine("=== C# 运算符实战:简易计算器 ==="); bool continueCalculating = true; while (continueCalculating) { try { // 1. 获取第一个操作数 Console.Write("\n请输入第一个数字: "); if (!double.TryParse(Console.ReadLine(), out double num1)) { Console.WriteLine("输入无效,请输入一个有效的数字。"); continue; } // 2. 获取运算符 Console.Write("请选择运算符 (+, -, *, /, %): "); string op = Console.ReadLine(); if (string.IsNullOrWhiteSpace(op) || op.Length != 1 || !"+-*/%".Contains(op)) { Console.WriteLine("运算符无效,请使用 +, -, *, /, % 中的一个。"); continue; } // 3. 获取第二个操作数 Console.Write("请输入第二个数字: "); if (!double.TryParse(Console.ReadLine(), out double num2)) { Console.WriteLine("输入无效,请输入一个有效的数字。"); continue; } // 4. 执行计算 double result = 0; bool calculationSuccessful = true; string operationDescription = ""; switch (op) { case "+": result = num1 + num2; operationDescription = "加法"; break; case "-": result = num1 - num2; operationDescription = "减法"; break; case "*": result = num1 * num2; operationDescription = "乘法"; break; case "/": if (num2 == 0) { Console.WriteLine("错误:除数不能为零!"); calculationSuccessful = false; } else { result = num1 / num2; operationDescription = "除法"; } break; case "%": if (num2 == 0) { Console.WriteLine("错误:取余运算的除数不能为零!"); calculationSuccessful = false; } else { // 注意:% 对double是浮点取余,与整数取余概念略有不同 result = num1 % num2; operationDescription = "取余"; } break; default: // 理论上不会走到这里,因为前面已校验 calculationSuccessful = false; break; } // 5. 输出结果 if (calculationSuccessful) { Console.WriteLine($"\n{operationDescription}结果: {num1} {op} {num2} = {result:F4}"); // F4表示保留4位小数 } } catch (Exception ex) { // 捕获其他未预料到的异常 Console.WriteLine($"计算过程中发生意外错误: {ex.Message}"); } // 6. 询问是否继续 Console.Write("\n是否继续计算?(输入 y 继续,其他任意键退出): "); string continueInput = Console.ReadLine()?.ToLower(); continueCalculating = (continueInput == "y" || continueInput == "yes"); } Console.WriteLine("\n感谢使用简易计算器,再见!"); } } }4.3 运行与验证
- 在项目根目录 (
OperatorsDemo) 下,运行命令:dotnet run - 按照程序提示输入数字和运算符,观察输出。
- 测试边界情况,如除数为零、输入非数字等。
示例交互:
=== C# 运算符实战:简易计算器 === 请输入第一个数字: 15.5 请选择运算符 (+, -, *, /, %): * 请输入第二个数字: 4 乘法结果: 15.5 * 4 = 62.0000 是否继续计算?(输入 y 继续,其他任意键退出): y 请输入第一个数字: 17 请选择运算符 (+, -, *, /, %): / 请输入第二个数字: 0 错误:除数不能为零! 是否继续计算?(输入 y 继续,其他任意键退出): n 感谢使用简易计算器,再见!4.4 代码要点解析
- 输入验证:使用
double.TryParse安全地转换用户输入,避免因格式错误导致程序崩溃。 - 运算符处理:使用
switch语句清晰地区分不同运算符的逻辑。 - 异常处理:对除法和取余运算进行了除零检查,这是使用算术运算符时必须考虑的边界条件。
- 循环结构:使用
while循环让计算器可以连续使用,提升了用户体验。 - 字符串插值:使用
$前缀的字符串插值语法,使输出格式更加清晰易读。
5. 进阶话题与常见陷阱
掌握了基础运算符后,了解一些进阶知识和常见陷阱能让你在开发中更加得心应手。
5.1 运算符重载
C# 允许为用户自定义的类型(类或结构)重新定义运算符的行为。这可以让你的类型用起来像内置类型一样自然。
Console.WriteLine("\n=== 运算符重载示例 ==="); public struct Vector2D { public double X { get; } public double Y { get; } public Vector2D(double x, double y) { X = x; Y = y; } // 重载加法运算符 `+` public static Vector2D operator +(Vector2D a, Vector2D b) { return new Vector2D(a.X + b.X, a.Y + b.Y); } // 重载相等运算符 `==` 和 `!=` (通常需要一起重写) public static bool operator ==(Vector2D a, Vector2D b) { return a.X == b.X && a.Y == b.Y; } public static bool operator !=(Vector2D a, Vector2D b) { return !(a == b); } // 重写 Equals 和 GetHashCode 是良好实践 public override bool Equals(object obj) => obj is Vector2D other && this == other; public override int GetHashCode() => HashCode.Combine(X, Y); public override string ToString() => $"({X}, {Y})"; } // 使用重载的运算符 Vector2D v1 = new Vector2D(1, 2); Vector2D v2 = new Vector2D(3, 4); Vector2D sum = v1 + v2; // 使用重载的 `+` Console.WriteLine($"{v1} + {v2} = {sum}"); // 输出: (1, 2) + (3, 4) = (4, 6) Console.WriteLine($"v1 == v2 ? {v1 == v2}"); // 输出: v1 == v2 ? False注意:运算符重载应谨慎使用,确保其行为符合直觉,不会让代码使用者感到困惑。
5.2checked与unchecked上下文
算术运算可能导致溢出(例如,两个很大的int相加,结果超出int范围)。C# 默认在未检查(unchecked) 上下文中执行运算,溢出时不会报错,但结果会被截断(可能不是你想要的结果)。使用checked关键字可以在溢出时抛出OverflowException。
Console.WriteLine("\n=== checked 与 unchecked 上下文 ==="); int maxInt = int.MaxValue; // 2147483647 int minInt = int.MinValue; // -2147483648 // 默认 unchecked 上下文 int uncheckedResult = maxInt + 1; // 发生溢出,结果回绕到 int.MinValue Console.WriteLine($"unchecked: {maxInt} + 1 = {uncheckedResult}"); // -2147483648 // 使用 checked 上下文 try { int checkedResult = checked(maxInt + 1); // 抛出 OverflowException Console.WriteLine($"这行不会被执行"); } catch (OverflowException) { Console.WriteLine("checked 上下文捕获到算术溢出异常!"); } // 也可以在代码块级别使用 checked { try { int anotherResult = maxInt * 2; } catch (OverflowException ex) { Console.WriteLine($"在checked块中捕获溢出: {ex.Message}"); } }建议:在涉及财务计算或对精度要求极高的场景,考虑使用checked上下文或直接使用decimal类型。对于性能关键且你确信不会溢出的循环,可以使用unchecked。
5.3 常见陷阱与避坑指南
整数除法陷阱:
int a = 5, b = 2; double wrong = a / b; // 错误!先进行整数除法得到2,再转换为double 2.0 double correct = (double)a / b; // 正确!先将a转为double,进行浮点除法得到2.5 Console.WriteLine($"错误示例: {wrong}"); // 2 Console.WriteLine($"正确示例: {correct}"); // 2.5避坑:确保至少有一个操作数是浮点类型,才能得到浮点结果。
浮点数比较陷阱:
double d1 = 0.1 + 0.2; double d2 = 0.3; Console.WriteLine($"d1 == d2 ? {d1 == d2}"); // False! 由于浮点数精度问题 Console.WriteLine($"d1 = {d1:R}, d2 = {d2:R}"); // 显示更多位数,可以看到差异避坑:不要直接用
==比较浮点数。应比较它们的差值是否在一个极小的误差范围内(epsilon)。double epsilon = 1e-10; bool isEqual = Math.Abs(d1 - d2) < epsilon; Console.WriteLine($"使用epsilon比较: {isEqual}"); // True对于货币等精确计算,使用
decimal类型。逻辑运算符的短路与非短路混淆:
// 错误示例:本想检查数组不为空且第一个元素大于0 int[] arr = null; // if (arr != null & arr[0] > 0) // 使用非短路的 &,即使arr为null,也会尝试访问arr[0],导致崩溃! if (arr != null && arr[0] > 0) // 使用短路的 &&,arr为null时直接返回false,不会执行后半部分 { // ... }避坑:在条件判断中,除非有特殊需求,否则总是使用
&&和||。赋值运算符
=与相等运算符==混淆: 这是经典错误,尤其在条件语句中。int x = 5; // if (x = 10) // C# 中这是编译错误,因为赋值表达式的结果是int,不是bool。但在某些语言或旧代码中可能是个bug。 if (x == 10) // 正确:比较是否相等 { // ... }避坑:C# 编译器对此有严格检查,但编写条件时仍需保持清醒。
6. 最佳实践与工程建议
- 追求清晰,而非炫技:运算符的优先级和结合性规则虽然强大,但过度依赖它们会写出难以理解的“聪明代码”。多用括号
()来明确表达你的意图,这不会影响性能,却能极大提升代码的可读性和可维护性。 - 善用复合赋值运算符:
+=,-=等不仅简洁,有时还能向阅读者传达“这是一个更新操作”的意图。 - 拥抱空值安全运算符:在现代 C# 开发中,积极使用
?.,??,??=来处理可能为null的场景。这能显著减少NullReferenceException,并使代码更加健壮。 - 理解并利用短路求值:
&&和||的短路特性不仅是性能优化,更是编写安全代码的工具。将最可能为false(对于&&) 或true(对于||) 的条件放在前面,并将可能引发异常的条件检查(如空值检查)放在前面。 - 为自定义类型重载运算符时要克制:只有当运算符的含义对你的类型来说非常自然和直观时才进行重载。例如,为
Money类型重载+和-是合理的,但为其重载*(乘法)可能就不那么清晰了(是乘利率还是乘数量?)。 - 注意数值运算的边界:思考你的数据范围。对于可能溢出的计算,考虑使用
checked关键字、更大的数据类型(如long代替int)或使用decimal进行高精度计算。 - 使用合适的类型进行比较:避免隐式类型转换带来的意外。比较
int和double时,明确你的比较逻辑。 - 注释复杂的表达式:如果一个表达式包含了超过三个运算符,或者逻辑不那么一目了然,添加一行简短的注释来解释其目的,这对未来的你和其他开发者都是巨大的帮助。
运算符是构成 C# 表达式的基石。通过本文从概念到分类,从单项练习到综合实战,再到陷阱规避和最佳实践的梳理,你应该已经对 C# 运算符有了系统而深入的理解。真正的掌握来自于实践,请务必运行文中的每一段代码,并尝试修改它们,观察不同的输出。接下来,你可以继续探索 C# 中的表达式树、LINQ 查询表达式等更高级的主题,它们都建立在扎实的运算符基础之上。