关于 dotnet-csharp-code-smells
为 C# 开发者和代码审查者提供代码异味与反模式检查指南。内容按资源管理、LINQ、事件处理和设计等领域分类,并列出危害、修正方向及相关 CA 规则。
主要功能
- 01识别资源释放问题
- 02检查 LINQ 反模式
- 03发现事件处理泄漏
- 04整理异常处理缺陷
- 05关联 CA 代码分析规则
适用场景
- 01审查 C# 代码中的常见异味
- 02排查资源泄漏和事件订阅问题
- 03依据 CA 规则改进代码质量
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ wshaddix/ dotnet-skills --skill dotnet-csharp-code-smells安装后在 Claude Code 中输入/dotnet-csharp-code-smells确认已加载。
附带文件details.md
详细介绍
dotnet-csharp-code-smells
主动检测 C# 代码中的代码异味和反模式。此技能会在所有工作流模式(规划、实现和审查)中触发。每个条目都会指出代码异味、说明其危害、给出正确的修复方式,并引用相关的 CA 规则或交叉参考。
交叉参考:[skill:dotnet-csharp-async-patterns],了解异步相关陷阱;[skill:dotnet-csharp-coding-standards],了解命名和风格;[skill:dotnet-csharp-dependency-injection],了解 DI 生命周期误用;[skill:dotnet-csharp-nullable-reference-types],了解 NRT 注解错误。
不在本技能范围内: LLM 特有的生成错误(错误的 NuGet 包、不佳的项目结构、MSBuild 错误)由 [skill:dotnet-agent-gotchas] 介绍。本技能介绍的是所有开发者(无论是人类还是 AI)都应避免的一般 .NET 代码异味。
1. 资源管理(误用 IDisposable)
| 代码异味 | 危害 | 修复方法 | 规则 |
|---|---|---|---|
可释放的局部变量缺少 using | 泄漏非托管句柄(套接字、文件、数据库连接) | 使用 using 声明或 using 代码块 | CA2000 |
IDisposable 字段未释放 | 类持有可释放资源,却从不释放 | 实现 IDisposable;在 Dispose() 中释放字段 | CA2213 |
| Dispose 模式错误(没有终结器保护) | 重复释放,或在 GC 终结时遗漏清理 | 遵循标准的 Dispose(bool) 模式;调用 GC.SuppressFinalize(this) | CA1816 |
| 在一个方法中创建可释放对象,并存入字段 | 所有权不明确;容易忘记释放 | 记录所有权;使包含该字段的类实现 IDisposable | CA2000 |
对非自有资源使用 using | 过早释放共享资源(例如,注入的 HttpClient) | 只释放自己创建的资源;让 DI 管理注入的服务 | -- |
有关每种模式的代码示例,请参阅 details.md。
2. 警告抑制技巧
| 代码异味 | 危害 | 修复方法 | 规则 |
|---|---|---|---|
通过以 null 调用事件来抑制 CS0067 | 造成误导性的运行时行为;掩盖真正的错误 | 使用 #pragma warning disable CS0067 或显式事件访问器 { add {} remove {} } | CS0067 |
| 通过虚设变量赋值来抑制 CS0219 | 死代码会让读者困惑 | 使用 _ = expression; 丢弃值,或使用 #pragma warning disable | CS0219 |
使用 #pragma warning disable 后未恢复 | 会抑制文件其余部分的所有警告 | 始终配对使用 #pragma warning restore;只抑制特定代码 | -- |
使用 [SuppressMessage] 时未提供理由 | 后续维护者无法判断抑制是否仍然有效 | 始终包含 Justification = "reason" | CA1303 |
有关 CS0067 的示例(从错误模式改为正确修复),请参阅 details.md。
3. LINQ 反模式
| 代码异味 | 危害 | 修复方法 | 规则 |
|---|---|---|---|
链式调用中途过早使用 .ToList() | 强制完全物化;浪费内存 | 保持链式调用为延迟执行;只在末尾物化 | CA1851 |
多次枚举 IEnumerable<T> | 每次枚举都会重新执行查询或数据库调用 | 使用 .ToList() 物化一次,然后重复使用 | CA1851 |
| 在 EF Core 中进行客户端求值 | 将整张表加载到内存;造成隐蔽的性能问题 | 将查询改写为可转换的 LINQ,或明确表达意图后使用 AsAsyncEnumerable() | -- |
使用 .Count() > 0 而不是 .Any() | 会枚举整个集合,而不是短路求值 | 检查是否存在元素时使用 .Any() | CA1827 |
使用嵌套 foreach,而不是 .Join() 或 .GroupJoin() | 本可达到 O(n+m),却变成 O(n*m) | 使用 LINQ join 操作或 Dictionary 查找 | -- |
使用 .Where().First(),而不是 .First(predicate) | 创建不必要的中间迭代器 | 将谓词直接传递给 .First() 或 .FirstOrDefault() | CA1826 |
4. 事件处理泄漏
| 异味 | 危害 | 修复方法 | 规则 |
|---|---|---|---|
| 未取消订阅事件 | 内存泄漏:发布者持有订阅者的引用 | 在 Dispose() 中取消订阅,或使用弱事件模式 | -- |
| 在构造函数中引发事件 | 订阅者可能尚未连接;派生类也尚未完全构造 | 仅从完全初始化的实例引发事件 | CA2214 |
误用 async void 事件处理程序 | async void 是事件处理程序唯一有效的签名,但异常无法观测 | 在主体中使用 try/catch;明确记录并处理异常 | -- |
| 事件处理程序未检查 null | 没有订阅者时会发生 NullReferenceException | 使用 event?.Invoke() 空条件模式 | -- |
| 静态事件未清理 | 根引用会阻止对象在应用程序生命周期内被 GC 回收 | 优先使用实例事件,或使用 WeakEventManager | -- |
交叉引用:[skill:dotnet-csharp-async-patterns] 深入介绍了 async void 即发即弃模式。
5. 设计异味
| 异味 | 阈值 | 危害 | 修复方法 |
|---|---|---|---|
| 上帝类 | >500 行 | 职责过多;难以测试和维护 | 运用 SRP 提取内聚的类 |
| 过长方法 | >30 行 | 难以理解、测试和审查 | 提取名称明确的辅助方法 |
| 参数列表过长 | >5 个参数 | 表明缺少抽象 | 引入参数对象或构建器 |
| 特性嫉妒 | 方法使用其他类的数据多于自身数据 | 职责错位;耦合紧密 | 将方法移至其所嫉妒的类中 |
| 基本类型偏执 | 用原始 string/int 表示领域概念 | 缺少类型安全;验证逻辑分散 | 引入值对象或强类型 ID |
| 嵌套过深 | >3 层缩进 | 控制流难以跟踪 | 使用守卫子句(提前返回)并提取方法 |
6. 异常处理缺漏
| 异味 | 危害 | 修复方法 | 规则 |
|---|---|---|---|
| 空 catch 块 | 静默吞掉错误;掩盖 bug | 至少记录异常;优先让异常继续传播 | CA1031 |
捕获基类 Exception | 会捕获 OutOfMemoryException、StackOverflowException 等 | 捕获特定异常类型 | CA1031 |
记录后吞掉(catch { log; }) | 调用方无法得知操作失败 | 记录后重新抛出,或返回错误结果 | -- |
在 finally 中抛出异常 | 新异常会掩盖原始异常 | 在 finally 内使用 try/catch;绝不从 finally 中抛出异常 | -- |
使用 throw ex; 而非 throw; | 重置堆栈跟踪;隐藏原始故障位置 | 使用裸 throw; 保留堆栈跟踪 | CA2200 |
| 未包含内部异常 | 包装异常时会丢失因果链 | 将原始异常作为 innerException 参数传入 | -- |
交叉引用:[skill:dotnet-csharp-async-patterns] 介绍了即发即弃和 async void 场景中的异常处理。
快速参考:CA 规则
| 规则 | 说明 |
|---|---|
| CA1031 | 不要捕获一般异常类型 |
| CA1816 | 正确调用 GC.SuppressFinalize |
| CA1826 | 不要对可按索引访问的集合使用 Enumerable 方法 |
| CA1827 | 可使用 Any() 时,不要使用 Count()/LongCount() |
| CA1851 | 可能多次枚举 IEnumerable 集合 |
| CA2000 | 在对象超出作用域前将其释放 |
| CA2200 | 重新抛出异常以保留堆栈详细信息 |
| CA2213 | 应释放可释放字段 |
| CA2214 | 不要在构造函数中调用可重写的方法 |
在项目中通过 <AnalysisLevel>latest-all</AnalysisLevel> 启用这些规则。分析器配置请参阅 [skill:dotnet-csharp-coding-standards]。