C# 初学者的 3 种重构模式

(Martin Fowler's Example)

1. 积极使用 Guard Clause(保护语句)

"如果条件不满足,立即返回。将核心逻辑放在最少缩进的地方。"

概念定义

Guard Clause(保护语句) 是一种在函数开头检查特定条件是否满足,如果不满足则立即退出(return) 的方法。
它的目的是减少不必要的 if 嵌套,使代码更加线性和平坦(flat)

Before(不好的示例)

if (user != null)
{if (user.IsActive){Process(user);}
}
  • 代码嵌套过多,核心逻辑 Process() 被隐藏在最内层。

  • 随着条件的增加,代码会形成“金字塔代码(Pyramid of Doom)”,变得越来越复杂。

  • 当测试条件增多时,代码覆盖率和调试的可读性都会变差。

After(好的示例)

if (user == null) { return; }
if (!user.IsActive) { return; }Process(user);
  • 在条件不满足的情况下,通过提前返回 快速整理流程。

  • 核心逻辑 Process() 位于最少缩进的中央部分 ,可读性更高。

  • 在测试/重构时,可以轻松追踪条件与结果

反对意见:“否定条件违背直觉”

一些开发者可能会提出以下观点:

“比起 if (!condition) return;if (condition) { ... } 更加直观。
否定条件会让代码难以理解。”

回应反对意见:
  • 关键在于条件的正/负,而不是‘意图’和‘重要性’的排序。

  • 如果“在这种情况下不应该执行操作”是明确的,那么否定条件更为清晰

指南:
  • 不重要的条件 = 否定形式 + 提前返回

  • 重要的条件 = 肯定形式 + 执行核心逻辑

补充提示:Guard 子句不仅可以使用 return,还可以使用 throwcontinue

if (value == null) { throw new ArgumentNullException(nameof(value)); }foreach (var item in collection)
{if (item.IsEmpty) { continue; }Process(item);
}

什么时候应该积极使用 Guard 子句?

情况

描述

包含大量验证的函数

通过提前返回来清理条件

包含许多状态分支的循环

使用continue作为 Guard 子句

存在未检查的异常风险

清理nullrangepermission等潜在问题

方法开始变长时

使用 Guard 子句整理条件过滤,简化代码

实战技巧

  • Guard Clause 通过去除不必要的嵌套 + 提前退出 ,使代码变得更加平坦(flat)

  • 这是一种设计策略,旨在将核心逻辑放在缩进最少、最显眼的位置。

  • 条件越多 失败案例越明确 测试越复杂 ,Guard Clause 的优势就越明显。

Dictionary<TKey, TValue> 在内部使用哈希表(Hash Table) 实现。

因此,键查找的平均时间复杂度为 O(1) ,这在很多情况下比 switch-case 更快

2. 战略性地将 Switch 转换为 Dictionary 映射

"条件分支越复杂,代码的责任应被分解得越清晰。"

概念定义

在 C# 中,switch-case 语句对简单的分支处理非常有用,但当分支数量增加时,在维护性和扩展性方面会暴露出局限性
在这种情况下,使用 Dictionary<Enum, Action>Dictionary<Enum, Func<T>> 构建显式映射 ,可以显著提升可读性和功能扩展性

Before(不好的示例)

switch (state)
{case UserState.Idle:HandleIdle();break;case UserState.Running:HandleRunning();break;case UserState.Dead:HandleDead();break;default:throw new InvalidOperationException();
}
  • 随着分支增多,代码变得冗长。

  • 如果在 switch 内部处理过多逻辑,容易违反单一职责原则(SRP)

  • 当新增状态时,需要找到对应的分支、添加逻辑并测试,修改分散且繁琐。

After(好的示例)

private static readonly Dictionary<UserState, Action> stateHandlers = new()
{{ UserState.Idle, HandleIdle },{ UserState.Running, HandleRunning },{ UserState.Dead, HandleDead }
};public void Handle(UserState state)
{if (stateHandlers.TryGetValue(state, out Action action)){action.Invoke();}else{throw new InvalidOperationException($"No handler for {state}");}
}
  • 状态与操作的关系通过映射明确管理

  • 新增状态时只需在字典中注册即可完成。

  • 测试时可以独立验证每个处理器。

使用场景示例

UI 状态机(State Machine)
Dictionary<GameState, Action> renderState = new()
{{ GameState.MainMenu, DrawMainMenu },{ GameState.InGame, DrawGame },{ GameState.Paused, DrawPauseScreen },
};

什么时候适合使用?

情况

描述

基于 Enum 的分支较多时

switch-case变得过于冗长时

命令/输入处理分支

键/按钮输入 → 动作映射

状态机

状态 → 行为对应结构

需要分离出可测试的逻辑时

switch单元测试困难 → 将处理器拆分为独立函数后易于测试

缺点及注意事项

缺点

应对措施

未注册的键没有对应处理

TryGetValue失败时明确抛出异常或执行空操作(No-op)

不保证顺序

使用OrderedDictionary或重构 Enum 以体现顺序

复杂条件分支难以处理

仅适用于简单条件逻辑,复杂逻辑仍需使用if/switch

实战技巧

  • 命令模式(Command Pattern)的简易实现
    可以像 Dictionary<string, ICommand> 一样,将命令和执行对象进行映射。

  • 向函数式编程(FP)靠拢的信号
    基于字典的映射实际上是将代码转换为数据驱动的表格式结构 ,这与 FP(函数式风格命令调度)的理念更为接近。

(MSDN Magazine Issues Volume 32 Number 3)

(Read only, frozen, and immutable collections - Developer Support)

3. 不可变数据(Immutable Data)习惯

“调试地狱从何开始?——正是从那些意外的值变更开始。”


概念定义

不可变(Immutable)数据 是指一旦定义后,其值不会改变的状态
在 C# 中,可以通过 readonlyconstrecord 等方式实现有意图的不可变设计

Before(不好的示例)

public class Player
{public int health;public void TakeDamage(int amount){health -= amount;}
}
  • 在这种结构中,很难追踪 health 是在哪里被修改的。
  • 特别是在多线程或事件驱动系统中,副作用 的累积会使调试难度急剧上升。

After(好的示例)

public class Player
{private readonly int maxHealth = 100;private int currentHealth;public int GetHealth() => currentHealth;public void SetHealth(int value){currentHealth = Math.Clamp(value, 0, maxHealth);}
}
  • maxHealth 是一个永远不会改变的常量 → 使用 readonly 声明。
  • 状态修改仅通过 SetHealth() 方法完成 → 访问控制与不可变性分离

为什么在游戏开发中很重要?

如果状态(State)变更以不透明的方式扩散
  • UI 更新延迟
  • Bug 不规则发生
  • 多人环境中同步问题

解决方法:将状态本身建模为不可变对象 进行管理。

public readonly struct PlayerState
{public readonly int health;public readonly bool isDead;public PlayerState(int health, bool isDead){this.health = health;this.isDead = isDead;}
}

状态不是用来修改的,而是‘重新创建’的。→ 函数式编程模式

Unity 开发者可以这样使用

  • 使用 ScriptableObject 存储配置数据时 → 只读结构化
  • 避免使用 public int value; 形式,改为没有 setter 的 SerializedField
  • 推荐将状态对象存储在不可变的 struct + Copy-on-Write 模式中,而不是放在可变的 MonoBehaviour 中。
[SerializeField] private int initialHealth = 100;public int InitialHealth => initialHealth; // 只允许读取

从 C# 9 开始引入了 record

  • record 默认是不可变对象
  • 使用 with 表达式进行修改时会创建新对象(immutable-safe)
var newStatus = status with { Health = status.Health - 10 };

高级技巧:并行处理与架构设计建议

readonly + volatile 组合
private volatile bool isGameOver;
private readonly object lock = new();public void SetGameOver()
{lock (lock){isGameOver = true;}
}
  • 将看似不可变的值在多线程环境中保持一致性保护
  • 这种组合也常用于双重检查模式。

注意:所有内容都必须不可变吗?

  • 如果在游戏循环中性能至关重要的结构 ,需要考虑 struct 不可变对象的创建成本。
  • 对于需要频繁状态变更的对象,可以采用“内部不可变性(internal immutability)”作为折衷方案。

什么时候适合使用不可变模式?

场景

描述

需要跟踪状态时

难以追踪状态变更的结构会导致调试噩梦

线程间共享数据

不可变性设计可以在无锁的情况下保证稳定性

可测试的状态建模

基于对象复制的测试和时间点比较更加容易

UI 状态更新

在 ViewModel 中便于变更跟踪和绑定

实战技巧

  • 不可变数据是降低调试成本的最佳结构
  • 状态不应修改,而应替换 :覆盖对象的方式对追踪和恢复更有利
  • 在 C# 中,可以通过 readonlyrecordScriptableObject + Getter 设计来实现。

结论:

我们了解了在 C# 中经常使用的基础重构方法。
实际上,详细撰写这种入门级别的文章对我也有帮助,因此我重新整理了一遍。
除此之外,还有很多其他模式,但我选出了三个我认为重要的基础概念。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.pswp.cn/diannao/84371.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

基于51单片机和8X8点阵屏、独立按键的滑动躲闪类小游戏

目录 系列文章目录前言一、效果展示二、原理分析三、各模块代码1、8X8点阵屏2、独立按键3、定时器04、定时器1 四、主函数总结 系列文章目录 前言 用的是普中A2开发板。 【单片机】STC89C52RC 【频率】12T11.0592MHz 【外设】8X8点阵屏、独立按键 效果查看/操作演示&#xff…

Java面向对象 一

系列文章目录 Java面向对象 二-CSDN博客 Java面向对象 三-CSDN博客 目录 系列文章目录 前言 一、初步认识面向对象 1.类和对象的简单理解 2.类的构成 二、类的实例化 1.对象的创建 2.对象的初始化 三、this引用的作用 四、构造方法 1.构造方法的提供 2.对象的构…

深度学习Y8周:yolov8.yaml文件解读

&#x1f368; 本文为&#x1f517;365天深度学习训练营中的学习记录博客&#x1f356; 原作者&#xff1a;K同学啊 本周任务&#xff1a;根据yolov8n、yolov8s模型的结构输出&#xff0c;手写出yolov8l的模型输出、 文件位置&#xff1a;./ultralytics/cfg/models/v8/yolov8.…

【RocketMQ 生产者和消费者】- 生产者启动源码 - MQClientInstance 定时任务(4)

文章目录 1. 前言2. startScheduledTask 启动定时任务2.1 fetchNameServerAddr 拉取名称服务地址2.2 updateTopicRouteInfoFromNameServer 更新 topic 路由信息2.2.1 topic 路由信息2.2.2 updateTopicRouteInfoFromNameServer 获取 topic2.2.3 updateTopicRouteInfoFromNameSer…

解决Docker容器内yum: not found、apt: not found、apk: command not found等命令找不到问题

Linux有很多发行版&#xff0c;各发行版的包管理工具不一定相同。 Alpine的包管理工具是 apk Debian/Ubuntu的包管理工具是 apt Centos/RHEL的包管理工具是 yum 在安装软件之前&#xff0c;需要先查看Docker容器内的Linux是什么发行版&#xff0c;可使用 cat /etc/os-rele…

每日c/c++题 备战蓝桥杯(修理牛棚 Barn Repair)

修理牛棚 Barn Repair 题解 问题背景与挑战 在一个暴风雨交加的夜晚&#xff0c;Farmer John 的牛棚遭受了严重的破坏。屋顶被掀飞&#xff0c;大门也不翼而飞。幸运的是&#xff0c;许多牛正在度假&#xff0c;牛棚并未住满。然而&#xff0c;为了保护那些还在牛棚里的牛&am…

鸿蒙版Flutter库torch_light手电筒功能深度适配

鸿蒙版Flutter库torch_light手电筒功能深度适配&#xff1a;跨平台开发者的光明之路 本项目作者&#xff1a;kirk/坚果 适配仓库地址 作者仓库&#xff1a;https://github.com/svprdga/torch_light# 在数字化浪潮的推动下&#xff0c;跨平台开发框架如 Flutter 凭借其高效、…

【信息系统项目管理师】一文掌握高项常考题型-项目进度类计算

更多内容请见: 备考信息系统项目管理师-专栏介绍和目录 文章目录 一、进度类计算的基本概念1.1 前导图法1.2 箭线图法1.3 时标网络图1.4 确定依赖关系1.5 提前量与滞后量1.6 关键路径法1.7 总浮动时间1.8 自由浮动时间1.9 关键链法1.10 资源优化技术1.11 进度压缩二、基本公式…

深入了解linux系统—— 操作系统的路径缓冲与链接机制

前言 在之前学习当中&#xff0c;我们了解了被打开的文件是如何管理的&#xff1b;磁盘&#xff0c;以及ext2文件系统是如何存储文件的。 那我们要打开一个文件&#xff0c;首先要先找到这个文件&#xff0c;操作系统又是如何去查找的呢&#xff1f; 理解操作系统搜索文件 …

Docker Hub仓库介绍

Docker Hub仓库全解析&#xff1a;从公共市场到私有化部署指南 一、Docker Hub公共镜像市场 1.1 核心功能解析 全球最大容器镜像库&#xff1a;累计托管超500万镜像核心服务矩阵&#xff1a; #mermaid-svg-CAMkhmtSWKEUw7z0 {font-family:"trebuchet ms",verdana,a…

redis使用RDB文件恢复数据

设置存盘间隔为120秒且10个key改变数据自动存盘使用RDB文件恢复数据 IP地址主机名192.168.10.170redis170 [rootredis170 ~]# yum install -y redis [rootredis170 ~]# systemctl start redis步骤一&#xff1a;设置存盘间隔为120秒且10个key改变自动存盘 [rootredis170 ~]#…

SpringBoot多环境配置文件切换

resources下application.yml、application-dev.yml、application-prod.yml多个配置文件。 spring:profiles:active: devspring:profiles:active: prod一般都是通过修改spring.profiles.active值来修改加载不同环境的配置信息&#xff0c;可以把切换的dev/prod放到pom.xml文件来…

Java 并发编程高级技巧:CyclicBarrier、CountDownLatch 和 Semaphore 的高级应用

Java 并发编程高级技巧&#xff1a;CyclicBarrier、CountDownLatch 和 Semaphore 的高级应用 一、引言 在 Java 并发编程中&#xff0c;CyclicBarrier、CountDownLatch 和 Semaphore 是三个常用且强大的并发工具类。它们在多线程场景下能够帮助我们实现复杂的线程协调与资源控…

【Java多线程】多线程状态下如何安全使用ArrayList以及哈希表

&#x1f50d; 开发者资源导航 &#x1f50d;&#x1f3f7;️ 博客主页&#xff1a; 个人主页&#x1f4da; 专栏订阅&#xff1a; JavaEE全栈专栏 多线程安全使用ArrayList 手动加锁 日常中最常用的方法&#xff0c;使用synchronized进行加锁&#xff0c;把代码打包成一份&a…

InnoDB引擎底层解析(二)之InnoDB的Buffer Pool(三)

Buffer Pool 实例 我们上边说过&#xff0c;Buffer Pool 本质是 InnoDB 向操作系统申请的一块连续的内存空间&#xff0c;在多线程环境下&#xff0c;访问 Buffer Pool 中的各种链表都需要加锁处理&#xff0c;在Buffer Pool特别大而且多线程并发访问特别高的情况下&#xff0…

Netty学习专栏(三):Netty重要组件详解(Future、ByteBuf、Bootstrap)

文章目录 前言一、Future & Promise&#xff1a;异步编程的救星1.1 传统NIO的问题1.2 Netty的解决方案1.3 代码示例&#xff1a;链式异步操作 二、ByteBuf&#xff1a;重新定义数据缓冲区2.1 传统NIO ByteBuffer的缺陷2.2 Netty ByteBuf的解决方案2.3 代码示例&#xff1a;…

Vue3逐步抛弃虚拟Dom,React如何抉择

虚拟DOM&#xff1a;前端界的替死鬼 这玩意儿就是个前端开发的充气娃娃&#xff01; 你以为它很牛逼&#xff1f;无非是给真DOM当替死鬼&#xff01; 每次数据变&#xff0c;虚拟DOM先搁内存里自嗨一顿&#xff0c;diff算法跟便秘似的算半天&#xff0c;最后才敢碰真DOM。 说白…

分布式锁总结

文章目录 分布式锁什么是分布式锁&#xff1f;分布式锁的实现方式基于数据库(mysql)实现基于缓存(redis)多实例并发访问问题演示项目代码(使用redis)配置nginx.confjmeter压测复现问题并发是1&#xff0c;即不产生并发问题并发30测试,产生并发问题(虽然单实例是synchronized&am…

解决自签名证书HTTPS告警:强制使用SHA-256算法生成证书

解决自签名证书HTTPS告警&#xff1a;强制使用SHA-256算法生成证书 一、问题场景 在使用OpenSSL生成和配置自签名证书时&#xff0c;常遇到以下现象&#xff1a; 浏览器已正确导入根证书&#xff08;.pem文件&#xff09;&#xff0c;但访问HTTPS站点时仍提示不安全连接或证…

线上 Linux 环境 MySQL 磁盘 IO 高负载深度排查与性能优化实战

目录 一、线上告警 二、问题诊断 1. 系统层面排查 2. 数据库层面分析 三、参数调优 1. sync_binlog 参数优化 2. innodb_flush_log_at_trx_commit 参数调整 四、其他优化建议 1. 日志文件位置调整 2. 生产环境核心参数配置模板 3. 突发 IO 高负载应急响应方案 五、…