news 2026/9/9 11:20:47

C#中if/else的正确写法与重构思路

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
C#中if/else的正确写法与重构思路

很多人觉得 if/else 是编程入门第一课的内容,简单到没什么好聊的。但我在做代码评审、带新人、以及面试候选人的过程中,几乎每周都能看到把简单条件分支写成一团浆糊的程序:三层嵌套起步、条件表达式写成天书、能用 if 走天下绝不换姿势。C# 这么多年演进下来,语法层面早就提供了很多更安全、更易读的写法,但很多人还在用老一套的习惯硬写。这篇文章想跟你聊聊,C# 里 if/else 的正确写法和那些我见过的高频反例,顺便结合上位机、Socket 通讯这类实际场景,看看一个“最简单的语法”在工程里到底该怎么用。

先说明一下,这篇文章不是给刚学会变量赋值的纯新手准备的语法教程,而是给那些已经写了几个月甚至几年 C#,却总觉得自己代码里的 if/else “哪里不对”的程序员。文中会讲清楚背后的判断逻辑、常见反例,以及我自己的实操心得。看完之后,你再回头翻自己的代码,大概率会有一种“当时怎么这么写”的感慨。

1. 基础语法:if / else 看起来简单,坑其实不少

1.1 else 的匹配规则与省略大括号的代价

C# 里 if/else 的语法本身不复杂,但正因为简单,很多人会不自觉地玩“省略”。最常见的就是不写大括号,比如:

if (age >= 18) Console.WriteLine("成年"); else Console.WriteLine("未成年");

这种写法 C# 支持,但我不建议。因为一旦后面有人想加一行日志,很容易写成这样:

if (age >= 18) Console.WriteLine("成年"); LogHelper.Write("用户已成年"); else Console.WriteLine("未成年");

这段代码编译会直接报错,因为else前面有两行语句,编译器不知道它该跟哪个if配对。哪怕你侥幸没写else,只是往if里加一行日志,也会出现严重的逻辑错误——日志变成无条件执行了。

C# 里else if其实不是一个独立的关键字,它的本质是else { if (...) }的简写。所以配对规则遵循“就近匹配”原则。举个容易踩坑的例子:

if (a) if (b) Console.WriteLine("A and B"); else Console.WriteLine("not B");

你心里可能想的是else和第一个if配对,但编译器会把它和最近的if (b)配对。这个规则很多老手都容易记混,我给出的建议很简单:无论ifelseelse if后面是几条语句,一律写大括号,哪怕只有一行也写。别嫌麻烦,少写的代码不是节省,而是给未来的自己和同事埋雷。代码格式化工具如果开了dotnet format的规则检查,也会把它标成 warning。

1.2 条件表达式的类型陷阱与可空布尔

C 和 C++ 程序员转 C# 的时候有个经典疑惑:为什么if (x = 1)编译器直接报错?因为 C# 的语法规定 if 的条件必须是 bool 类型,所以if (x = 1)这种把赋值当判断的写法,在 C# 里根本编译不过。这是语言设计上的进步,但不代表没有类似的坑。

真正需要小心的,是bool?也就是可空布尔类型的判断。比如你从数据库读出一个字段,或者从配置中心拿一个开关:

bool? isEnabled = config.GetValue<bool?>("FeatureSwitch"); if (isEnabled == true) { // 打开状态 } else if (isEnabled == false) { // 明确关闭 } else { // 没有配置,默认处理 }

这种三段式判断是bool?的标准打开方式。但反例也很常见,有人会图省事写if (isEnabled.HasValue && isEnabled.Value),代码能跑,但读起来费劲。还有人会写if (isEnabled == true)时不加空值判断,直接用if (isEnabled),这在 C# 里编译不过,因为bool?不能隐式转成bool

我自己的习惯是:对于bool?,优先用isEnabled == true表示“明确为真”,用isEnabled is true也可以,后者是 C# 9 之后更推荐的模式匹配写法。它读起来更接近自然语言,而且不会出现误用=的风险。

1.3 优先级与括号:别让编译器替你猜

C# 的运算符优先级里有几个容易记混的位置,比如&&||==!=的相对优先级。==的优先级高于&&||,所以a == b && c == d会按预期解析成(a == b) && (c == d)。但一旦条件多起来,光靠优先级去背就很痛苦,比如:

if (status == Status.Running || status == Status.Paused && !isMaintenance)

这行代码实际含义是status == Status.Running || (status == Status.Paused && !isMaintenance),因为&&优先级高于||。但读代码的人第一眼未必能看出来,可能以为它是(status == Running || status == Paused) && !isMaintenance。这种歧义极其危险。

我的做法是:只要条件里同时出现&&||,就果断用括号把逻辑组圈起来。不需要知道优先级,也不需要让读代码的人去回忆优先级表。括号多一点不丢人,逻辑错了才是真丢人。

2. 条件表达式怎么写,才算“值得读”

2.1 正面表达:避免双重否定

代码评审的时候,我经常看到这样的条件:

if (!string.IsNullOrEmpty(userName) == false) { // 用户名不为空的时候做什么 }

这个!== false的组合看得人血压飙升。先不说逻辑对不对,这行代码本身就是一种双重否定的经典反例。它的实际含义是“用户名为空”,但写出来却绕了两个弯。

正确写法无非两种:

if (string.IsNullOrEmpty(userName)) { // 用户名为空的处理 } // 或者反过来 if (!string.IsNullOrEmpty(userName)) { // 用户名非空的处理 }

如果你要处理的是“非空”分支,完全可以直接写if (!string.IsNullOrEmpty(userName)),没有必要再加一个== false或者== true。对于布尔判断,直接使用变量本身:if (isSuccess)而不是if (isSuccess == true)if (!isSuccess)而不是if (isSuccess == false)

不过这里也有一个例外:如果变量名本身带有负面含义,比如isNotFound,那if (!isNotFound)就是一种双重否定。最好的办法是换个正面的变量名,比如isFound。变量命名直接影响条件表达式的可读性,这一点在 C# 里尤其重要,因为 C# 的语义本身还算直白,如果变量名绕来绕去,阅读成本就全堆在条件判断上了。

2.2 卫语句:让嵌套少一层

我见过太多“箭头形”代码,一层 if 套一层 if,最里面才是真正的业务逻辑:

if (user != null) { if (user.IsActive) { if (user.Role == Role.Admin) { // 真正的管理后台逻辑,几十行 } else { // 普通用户的逻辑 } } else { LogHelper.Write("用户未激活"); } } else { LogHelper.Write("用户不存在"); }

这种写法在逻辑上没错,但可读性很差。阅读者需要一直记着当前处于第几层嵌套,才能搞明白每个分支的运行条件。更推荐的做法是先把不满足条件的场景“挡在门外”,用卫语句提前返回:

if (user == null) { LogHelper.Write("用户不存在"); return; } if (!user.IsActive) { LogHelper.Write("用户未激活"); return; } if (user.Role == Role.Admin) { // 管理后台逻辑 } else { // 普通用户逻辑 }

卫语句的核心思想是:先处理异常、边界、不合法的场景,让正常流程继续往下走。这样后面“幸存下来”的代码,前提条件都非常清晰,不需要一层一层去回溯。我在上位机开发里也经常用这个模式,比如接收扫码枪的数据时,先判断数据长度是否合法,再判断是否包含结束符,最后才做解析。每一步都提前 return,代码结构非常清爽。

2.3 边界判断:空值、范围、字符串空格一个都不能漏

实际项目中,if/else 的很多 bug 不是逻辑不对,而是边界条件没考虑全。最常见的三个:空值、范围、字符串里的可见字符。

空值判断的典型反例是只判断了 null,没判断空数组或空字符串。比如获取一个订单列表,然后判断“有订单就处理”:

if (orderList != null) { foreach (var order in orderList) { // 处理 } }

这里的问题在于,orderList不为 null 但Count == 0时,foreach根本不会进入循环体,逻辑上倒没什么大错,但如果后面还有“没有订单时给默认值”的需求,这种判断就不够严谨。更稳妥的写法是用orderList is { Count: > 0 }这种属性模式,或者用orderList?.Count > 0。C# 里属性模式很强大,能一次性判断 null 和数量:

if (orderList is { Count: > 0 }) { // 有订单 }

字符串的边界就更多了。判断“用户输入了有效信息”时,如果直接用if (input.Length > 0),那输入一个空格也会被当成有效信息。正确做法是用string.IsNullOrWhiteSpace(input),它把 null、空串、全空格都覆盖了。还有一个经常被忽略的条件是字符串前后有空格导致比较失败,比如读取配置文件里的开关值," true""true"就不相等。所以在关键的比较场景里,先Trim()再比较,能省去很多查不出来的诡异问题。

范围判断方面,C# 里比较常见的是“开区间/闭区间”搞混。比如分数在 90 到 100 之间时逻辑不同,有人写if (score > 90 && score <= 100),如果业务要求是“90 分(含)以上”,那这个条件就错了,正确应该是if (score >= 90 && score <= 100)。这种半开半闭的边界问题,在数值判断里几乎是 bug 高发区。我的习惯是写条件时把所有等于号都用文字注释标出来,比如“包含 90 和不包含 100”,然后再翻译成代码。

2.4 把复杂条件拆成命名方法

当一个 if 条件写了三四行,或者包含多个逻辑运算符时,即使加上括号也很难读。这个时候最好的做法不是尝试精简表达式,而是把整个条件抽成一个方法,给这个“判断动作”取一个清晰的名字。

举个例子,一个上位机程序里要判断一条指令是否是合法的移动指令,条件可能包含设备状态、当前模式、指令类型、超时标记等。如果直接塞进 if:

if (deviceStatus == DeviceStatus.Ready && currentMode == Mode.Manual && cmdType == CmdType.Move && !isTimeout) { // 执行移动 }

这个条件不复杂,但已经需要读几秒了。抽成方法之后:

if (CanExecuteMoveCmd(deviceStatus, currentMode, cmdType, isTimeout)) { // 执行移动 } private static bool CanExecuteMoveCmd(DeviceStatus status, Mode mode, CmdType cmdType, bool isTimeout) { return status == DeviceStatus.Ready && mode == Mode.Manual && cmdType == CmdType.Move && !isTimeout; }

这样读代码的人一眼就能看出意图,“能执行移动指令吗”,而不用去理解四个条件各自是什么含义。这就是命名带来的价值。抽方法的另一个好处是,如果将来判断逻辑变得更复杂,比如新增一个“重复运行保护”的条件,只需要在方法内部加一行,不会污染调用方的阅读体验。

3. 不一定非要用 if / else:替代方案怎么选

3.1 简单赋值用三元运算符,但记得适可而止

二选一的情况下,三元运算符确实比 if/else 更紧凑。典型的场景是根据条件赋一个变量:

var level = score >= 60 ? "Pass" : "Fail";

这行代码把 if/else 写四行才能完成的事情,压缩成了一行。我也经常在组装报文、拼接字符串时用它,比如:

var result = string.Format("{0}:{1}", deviceId, isOnline ? "1" : "0");

但三元运算符的底线是:只用于这种简单的单一赋值。一旦出现嵌套,比如a ? b : (c ? d : e),可读性直线下降,我见过有人写出三层三元嵌套,那完全是给自己和后人添堵。我的建议是,当三元运算符开始要跨行书写,或者需要加注释才能看懂的时候,就应该老老实实改写 if/else。

3.2 switch 表达式与模式匹配:C# 8 之后的新选择

很多从 C# 6、C# 7 时代过来的人对 switch 的印象还停留在“只能判断整数和字符串常量”的旧语法。其实从 C# 8 开始,switch 表达式和模式匹配已经非常能打了,很多场景下,它比链式 if/else 更直观。

举个例子,根据指令类型做不同处理:

var handleResult = cmdType switch { CmdType.Move => HandleMove(), CmdType.Stop => HandleStop(), CmdType.Home => HandleHome(), _ => HandleUnknown() };

这段代码的功能等同于一大堆 if/else 或者旧式 switch,但结构清晰得多。C# 9 之后还支持属性模式,可以直接判断对象的状态:

var statusInfo = deviceStatus switch { { IsReady: true, IsTimeout: false } => "设备就绪", { IsReady: false } => "设备未就绪", _ => "未知状态" };

这种写法在判断同一个对象的多个属性时特别好用。我在处理海康相机或者 HALCON 返回的状态信息时,就经常用属性模式把“成功但结果为空”“失败但错误码正常”之类的组合状态一次性表达清楚。

但要注意一点:switch 表达式更适合“每个分支之间互斥、逻辑独立”的场景。如果分支之间存在复杂的业务依赖,或者每个分支内部的代码超过十几行,还是老老实实写 if/else,或者抽方法,不要硬套新语法。

3.3 用字典映射和委托消除重复判断

在热词搜索里出现了很多“C#上位机”“C# Socket”“扫码枪触发事件”这类场景,这些场景里最常见的 if/else 问题,就是根据不同指令码分发处理逻辑。很多人会用一长串 if/else 去判断指令码,比如:

if (msg.StartsWith("MOVE:")) { // 处理移动指令 } else if (msg.StartsWith("STOP:")) { // 处理停止指令 } else if (msg.StartsWith("HOME:")) { // 处理回零指令 } // 还有十几个...

每来一个新指令,就要往这个函数里再加一个 else if。日子久了,这个函数能长到几百行,改一个分支还会担心影响其他分支。这种情况下,用字典映射加委托来处理会优雅得多。

private readonly Dictionary<string, Action<string>> _commandHandlers = new() { ["MOVE"] = HandleMove, ["STOP"] = HandleStop, ["HOME"] = HandleHome, }; private void DispatchCommand(string rawMsg) { var prefix = rawMsg[..rawMsg.IndexOf(':')]; if (_commandHandlers.TryGetValue(prefix, out var handler)) { handler(rawMsg); } else { LogHelper.Write($"未知指令:{prefix}"); } }

这样每新增一种指令,只需要注册一个新的处理方法,而不会改动 DispatchCommand 本身,符合开闭原则。阅读代码的时候,整个“指令路由”逻辑也变成了一张表,非常直观。需要注意的坑是,字典的键区分大小写,如果你要兼容大小写混用的输入,可以在构造字典时用StringComparer.OrdinalIgnoreCase

3.4 引入设计模式的时机:别为了去掉 if 而过度设计

有些同学看到“策略模式”“状态模式”能减少 if/else,就像拿到新玩具一样,不管三七二十一,把所有条件分支都改成设计模式。结果是代码类是变短了,但整个项目多了十几个类、几十个接口,读代码要到处跳文件。

我的观点是,设计模式是用来管理复杂度的,不是用来消灭 if/else 的。当你只有三五种分支,每种分支也就一两行逻辑时,if/else 就是最清晰的表达方式,硬上策略模式反而制造复杂度。可一旦分支数量开始膨胀,而且每个分支的处理逻辑都有独立演进的趋势时,才应该考虑用接口抽象、字典映射或者策略模式。判断标准很简单:你在频繁改动这个 if/else 链吗?你每次改动都要小心翼翼吗?如果是,那该重构的信号已经出现。

4. 真实案例:上位机指令处理的 if / else 重构

4.1 接手前的代码长什么样

去年我接手维护一个工业设备的上位机程序,里面有一段处理 TCP 报文的函数,核心思路是根据设备发来的 ASCII 指令执行不同动作。原代码大概是这样的:

private void ProcessMessage(string msg) { if (msg.StartsWith("GET_STATUS")) { // 查询状态,拼接返回报文 var status = GetDeviceStatus(); SendResponse($"STATUS:{status}"); } else if (msg.StartsWith("SET_SPEED")) { var speed = ExtractParam(msg); if (speed >= 0 && speed <= 1000) { SetSpeed(speed); SendResponse("OK"); } else { SendResponse("ERR:SPEED_RANGE"); } } else if (msg.StartsWith("SET_ACC")) { // 类似的逻辑 } // 后面还跟着十几个 else if }

这段代码大概有三百多行,最明显的两个问题:一是指令越来越多,这个函数越来越长;二是嵌套层级越来越深,尤其是一旦涉及参数校验,里面又套了一层 if/else。每次新接一台设备要加指令,大家都得在这坨代码里找插入点,改一次怕一次。

4.2 第一步:抽方法 + 卫语句

我第一次重构,没急着马上改造成什么花哨结构,而是先做两件小事:把每个分支里的处理逻辑抽成独立方法,同时用卫语句把参数校验提前。

改造之后,主函数的逻辑变成了这样:

private void ProcessMessage(string msg) { if (string.IsNullOrWhiteSpace(msg)) { return; } if (msg.StartsWith("SET_SPEED")) { HandleSetSpeed(msg); } else if (msg.StartsWith("GET_STATUS")) { HandleGetStatus(); } // ... } private void HandleSetSpeed(string msg) { if (!TryExtractParam(msg, out int speed)) { SendResponse("ERR:PARSE"); return; } if (speed < 0 || speed > 1000) { SendResponse("ERR:SPEED_RANGE"); return; } SetSpeed(speed); SendResponse("OK"); }

这一步做完,至少消除了三层嵌套的问题。每个处理方法内部都是“先校验,再执行,最后返回”的顺序,读起来比原来的“there 套 if、else 套 if”清晰太多了。但我还没有解决“指令越来越多,路由代码越来越长”的核心矛盾,所以继续走第二步。

4.3 第二步:用字典映射代替 if / else 指令分发

第一轮重构后,路由函数里依然是一长串else if (msg.StartsWith(...))。为了让新增指令不再改这个主函数,我把处理逻辑统一成了委托,然后用字典把“指令前缀”和“处理方法”映射起来。

这里要处理一个问题:有些指令需要带参数,有些不需要。所以我定义了一个统一的委托签名:

private delegate void MessageHandler(string msg); private readonly Dictionary<string, MessageHandler> _handlers = new() { ["GET_STATUS"] = HandleGetStatus, ["SET_SPEED"] = HandleSetSpeed, ["SET_ACC"] = HandleSetAcc, ["HOME"] = HandleHome, };

路由函数瘦身成这个样子:

private void ProcessMessage(string msg) { if (string.IsNullOrWhiteSpace(msg)) { return; } var prefix = msg.Split(':')[0]; if (_handlers.TryGetValue(prefix, out var handler)) { handler(msg); } else { SendResponse("ERR:UNKNOWN_CMD"); LogHelper.Write($"未知指令:{msg}"); } }

这里有个小细节:原来的StartsWith是不带分隔符的模糊匹配,可能会有SET_SPEEDSET_SPEED_1这种前缀冲突。我改成Split(':')[0]之后,强制约定协议里指令前缀后面必须跟冒号,这样路由更精确,也避免了很多带有相似前缀的指令互相干扰。

4.4 重构之后的收获

这轮重构之后,最直观的感受是:新增一条指令只需要写一个新的处理方法,然后在字典里注册一下。主路由函数不再需要改动,测试也容易写多了。我可以把ProcessMessage当做一个纯分发器来测,单独测试每个 Handler 也只需要关注自己的输入输出,不用把整个消息链路的上下文都模拟出来。

另一个收获是“未知指令”的处理。原来的 if/else 链如果收到一个不认识的指令,最后会静默忽略或者走到一个莫名其妙的默认分支。现在字典的TryGetValue天然就带了一个清晰的找不到分支,我顺手加了统计日志,排查问题的时候方便多了。这个过程也让我更坚定了一个想法:if/else 本身不是罪魁祸首,真正的问题是不加节制地使用,并且没有及时识别出“这其实是一张指令映射表”的抽象。

5. 性能与工程实践:if / else 会拖慢程序吗

5.1 分支判断的底层成本

很多人写代码时会有一种迷思:if/else 是不是很慢?要不要用位运算或者什么骚操作去避免分支?实际上,现代 CPU 对分支的处理已经非常成熟,普通 if/else 在绝大多数业务代码里,性能开销小到可以忽略不计。真正影响性能的是分支预测失败,也就是 CPU 事先预测了某条分支会执行,结果预测错了,需要清空流水线重新执行。但在 C# 这种托管语言里,常规的业务逻辑很难精确控制到这一步。

如果你处理的场景是几百毫秒级的网络通讯、数据库读写、文件操作,那 if/else 那点判断成本根本不值一提。把时间花在优化 if/else 的微性能上,不如把代码可读性提上来。性能优化有一条很实际的原则:先测量,再优化。你连瓶颈在哪都不知道,就先别动 if/else 的脑筋。

5.2 switch 的跳转表优化真相

有些读者可能听过一个说法:switch 比 if/else 快,因为编译器会把它编译成跳转表。这个说法在 C/C++ 里有一定道理,但在 C# 里要分情况看。JIT 编译器会对密集的整数 switch 生成高效的跳转逻辑,对于字符串 switch,可能在内部会先算哈希再做比较。如果分支数量不多,if/else 和 switch 的差异几乎看不出来。

所以我的建议是:性能差异不应该是你选择 switch 还是 if/else 的主要理由,可读性和可维护性才是。在条件分支固定的场景下,switch 表达式的模式匹配能提升代码表达力,这就足够成为用它替代 if/else 的理由了。至于跳转表优化,只是附带红利,不要盯着它做决策。

5.3 循环里的 if:把不变量提出来

如果说 if/else 在性能上真有什么值得注意的点,那就是循环体里的条件判断。假设你有一个高频执行的循环,循环内部每次都要判断一个配置开关是否打开:

for (int i = 0; i < data.Length; i++) { if (_config.IsDebugMode) { LogHelper.Write(data[i]); } Process(data[i]); }

如果IsDebugMode在循环期间不会变化,理论上这个if每次都要重新判断,是一种浪费。直接把常量条件提到循环外会更清晰:

if (_config.IsDebugMode) { for (int i = 0; i < data.Length; i++) { LogHelper.Write(data[i]); Process(data[i]); } } else { for (int i = 0; i < data.Length; i++) { Process(data[i]); } }

但这么改有代码重复的问题,也不一定值得。实战中,JIT 往往能识别这类循环不变量优化,把它自动提出去。所以除非你用 profiler 实测出这里确实是热点,否则不要为了所谓性能牺牲代码结构。真正值得关注的是,不要在循环里做一些可以提前聚类的判断,比如“每一帧都判断对象类型”,这种情况考虑用多态替代反而更合理。

5.4 别用 if / else 控制异常流程

工程实践里还有一种 if/else 的误用,就是用条件判断去模拟异常处理。比如:

if (File.Exists(path) == false) { ShowError("文件不存在"); return; } var content = File.ReadAllText(path);

这段代码看起来没什么问题,但在并发环境下,文件可能在File.Exists之后、ReadAllText之前被删掉,依然会抛出FileNotFoundException。更合理的做法是直接尝试读文件,捕获特定异常,再做降级处理:

try { var content = File.ReadAllText(path); return content; } catch (FileNotFoundException) { ShowError("文件不存在或已被删除"); }

所以判断“是否存在”“是否可读”“是否有权限”这类场景,预检查不是完全没用,但要注意它只是减少异常概率,不能完全替代异常处理。正确姿势是:能用 if/else 表达的业务状态,就用 if/else;凡是可能因为外部环境变化而出现的不确定性错误,用 try/catch 兜底。两者不是替代关系,而是配合关系。

6. 常见问题速查:从语法到风格一脸看明白

6.1 if、else if、多个 if 的区别

这个问题面试里经常被问,代码评审中也经常看到有人混淆。直接说结论:else if是短路逻辑,只要前面的条件为真,后面的所有条件都不会再判断;而多个独立的if,每个条件都会依次判断。看代码最直观:

int score = 85; string level; if (score >= 90) level = "A"; else if (score >= 60) level = "B"; else level = "C"; // 如果写成三个独立 if if (score >= 90) level = "A"; if (score >= 60) level = "B"; if (score < 60) level = "C";

两种写法在特定输入下结果不同。当score = 85时,第一种得到"B",第二种先得到"A"又被第二次 if 覆盖成"B",最后因为不满足第三个条件,结果还是"B"。如果条件之间有重叠,或某个变量会在分支内被修改,多个 if 的覆盖效应就会导致诡异 bug。实际编码时,如果业务上几个条件是互斥区间,优先用else if;如果几个判断是互不关联的独立事件,才用多个 if。

场景使用建议风险点
互斥的多分支用 if/else if/else条件顺序影响结果
互不相关的判断用多个独立 if注意条件重叠导致的值覆盖
映射关系明确用 switch 表达式或字典分支过多时维护成本高

6.2 字符串比较:== 与 Equals 到底怎么选

C# 里字符串比较有个经典盲区。==对于字符串类型,实际调用的是字符串的相等性比较,而不是引用比较,这一点和 Java 不同。所以在大多数场景下:

if (cmd == "STOP") { // 没问题 }

是完全可以的。但有几个细节要注意:大小写敏感。如果协议里对方可能发stopSTOP,你直接==会判断不相等。这时用string.Equals(cmd, "STOP", StringComparison.OrdinalIgnoreCase)更稳妥。还有一个坑是字符串前后空格,用Trim()或者比较时使用StringComparison.OrdinalIgnoreCase的同时先处理空白,具体看业务语义。

在需要大量比较、且这些比较发生在热路径时,可以考虑用switch表达式,编译器会对字符串 switch 做哈希或跳转优化;但日常业务里,==已经完全够用,不必刻意替换成Equals

6.3 可空值类型的判断与模式匹配

C# 8 之后,is nullis not null逐渐成为推荐写法。和== null相比,is null有几个好处:当对象重载了==运算符时,is null不会受运算符重载影响,语义更安全;同时它在模式匹配的语境下读起来也更自然。

if (obj is null) { // 空值处理 } if (obj is not null) { // 非空处理 }

对于可空值类型的判断,C# 9 之后bool?也可以配合模式匹配写:

if (flag is true) { // 明确为 true } else if (flag is false) { // 明确为 false } else { // null }

这种写法相比flag == true最大的好处是,编译器能更好地推断出分支里的类型信息,配合属性模式还能做更复杂的解构判断。如果你还在用HasValue加双重判断,建议试试新的模式匹配写法,会让条件分支清晰不少。

6.4 关于缩进、大括号和格式化的统一建议

最后聊一个工程层面的小话题:代码风格。if/else 本身不是性能杀手,但“一种代码一种风格”绝对是维护成本的隐形杀手。见过有的项目里一半人把左大括号放在行尾,一半人放在下一行,每次合并代码都是格式大战;还有的人用 tab,有的人用空格。这类问题不该靠人的自觉去约束,直接用.editorconfigdotnet format在 CI 里强制统一就行。

缩进方面,我的习惯是 4 空格,配合 IDE 的自动格式化。大括号只要在配置里设成统一规则,全团队保持一致即可。真正的重点是:绝不省略大括号、条件表达式尽量加括号、if 和 else 分支如果都有代码,尽量让两条路径的代码长度接近,避免一行分支、几十行分支的失衡感。这些细节单独看每一条都很小,但放在一起,就是代码可读性的分水岭。

实际写代码的时候,我们经常在“快速实现”和“优雅设计”之间摇摆。我个人的经验是:if/else 这种基础语法,真正体现功力的地方不在于会不会写,而在于能不能在各种场景下选择最合适的表达方式,以及能不能让读代码的人不费脑子就理解逻辑。哪怕只是把嵌套改平、把条件抽成方法、把简单映射从 if/else 换成字典,代码的维护体验都会有质的提升。

最后再补充一个我踩过不少次的坑:改动 if/else 分支时,永远记得检查反向分支。很多 bug 不是因为在某个分支里写错了什么,而是因为新增了一个if却没有配套处理它的else场景,导致边界输入掉进了老逻辑。写条件语句时多问自己一句“不满足这个条件时,程序该走哪条路”,很多线上问题都能提前拦截在编码阶段。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/9 11:20:35

树莓派Pico调试工具横评:mpremote、Putty与MobaXterm怎么选?

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/9 11:20:23

技术博客创作复盘:从灵感到发布的全流程方法论与数据驱动迭代

不知不觉&#xff0c;又到了“我的创作纪念日”。说实话&#xff0c;以前我对这种日子没什么感觉&#xff0c;觉得它不过是一个时间节点&#xff0c;像生日一样&#xff0c;过完就完了。但今年不一样&#xff0c;我翻了一下后台的累计数据&#xff0c;突然想认真聊聊“创作”这…

作者头像 李华
网站建设 2026/9/9 11:20:16

风险IP定位实战:从日志分析到威胁情报与自动化封禁

上个月我处理一起异常流量的时候&#xff0c;客户把一堆日志导出给我&#xff0c;让我看看到底是谁在打他的接口。日志长什么样&#xff1f;几千条恶意请求&#xff0c;几十个IP&#xff0c;密密麻麻的4xx、5xx&#xff0c;还有几个触发了WAF规则。我知道很多人这时候的操作是打…

作者头像 李华
网站建设 2026/9/9 11:20:05

2026游戏主板选购指南:芯片组、供电与避坑全解析

1. 游戏主板选购&#xff0c;先搞懂这件事比品牌更重要 我每年都要帮朋友装好几台游戏主机&#xff0c;被问得最多的一句话就是“玩大型游戏用什么主板好”。说实话&#xff0c;这个问题看着简单&#xff0c;但要讲透并不容易。很多人一上来就盯着品牌和价格&#xff0c;结果要…

作者头像 李华
网站建设 2026/9/9 11:19:27

Docker镜像加速实战:从原理到配置,彻底解决docker pull慢的问题

1. 一条 docker pull 命令&#xff0c;为什么会让人等到怀疑人生半夜两点&#xff0c;线上服务要发新版本&#xff0c;docker pull 一个基础镜像&#xff0c;进度条卡在 83% 一动不动。这种场景你有没有经历过&#xff1f;反正我经历过不止一次&#xff0c;而且每次都让我对&qu…

作者头像 李华
网站建设 2026/9/9 11:18:01

ECharts中国地图JSON文件实战指南:从获取注册到避坑

简介&#xff1a;面向Web前端与数据可视化开发者的ECharts中国地图JSON数据包&#xff0c;包含全国及各省、地市级行政区划的边界坐标、地区编码及嵌套子区域信息&#xff0c;可直接用于地图注册、数据绑定与区域着色&#xff0c;解决ECharts地图开发中地理数据获取与格式匹配的…

作者头像 李华