关于 csharp-refactoring
面向行为保持型 C# 重构的技能,涵盖需求分类、基于绑定的编辑和按变更范围验证,并区分结构调整与升级、功能及行为或契约变更。
主要功能
- 01编辑前分类重构请求
- 02指导基于绑定的重命名和移动
- 03维护公共边界上的兼容性
- 04按变更范围安排构建和测试
- 05规定超出范围工作的交接方式
适用场景
- 01在不改变行为的情况下重命名 C# 符号
- 02整合重复实现
- 03提取逻辑并保留调用方输入
- 04区分重构与行为或 API 变更
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ dotnet/ skills --skill csharp-refactoring安装后在 Claude Code 中输入/csharp-refactoring确认已加载。
附带文件references/operation-catalog.md
详细介绍
C# 重构(保持行为不变)
重构改变的是结构,绝不改变可观察到的行为。使用能感知绑定关系的工具进行修改, 然后通过构建和相关测试确认行为保持不变。投入的工作应与变更规模相称: 单行局部重命名不需要像面向公共 API 的多目标变更那样繁琐。
强制关卡:在验证或编辑前进行分类
只阅读足以对每项请求的操作进行分类的仓库上下文。在还原、 构建或编辑之前完成此步骤。分类优先级如下:
- 如果请求明确要求至少一项可分离的行为保持型操作,完成 这项结构性工作,并仅暂缓会改变行为或契约的操作。不要编造或推断结构性工作来规避停止响应。
- 如果结构性部分与行为变更部分无法分离,使用针对整个请求的停止响应,并说明两者为何无法分离。
- 否则,仅当所有请求的操作都不属于行为保持型重构时,才使用针对整个请求的停止响应。
对于不属于行为保持型重构的整个请求:
- 声明:
这不是行为保持型重构:<具体原因>。 - 声明:
未更改任何文件。 - 声明:
下一步工作流:<工作流>。然后停止。不要补充手动实施步骤、替代方案、在没有该工作流的情况下继续处理的提议,或后续问题——即使该工作流不可用。
| 作为“重构”提出的请求 | 分类和处理方式 |
|---|---|
| 更改框架或 NuGet 版本 | 升级。 不要在此处编辑或验证升级;移交给 dotnet-upgrade。 |
| 新增功能、标志、端点、层级或行为 | 功能。 不要在此处实现;移交给仓库的功能工作流。 |
| 更改阈值、速率、输出或 Bug 结果 | 行为变更。 暂缓该操作,并移交给仓库的 Bug 修复或行为变更工作流;仍须完成任何明确可分离的结构性操作。 |
| 收紧或放宽已发布/公开的 nullable 注解 | 源代码契约变更。 保持声明和 API 记录不变;移交给仓库的 API 契约工作流。 |
对于混合请求,完成分类后绝不要停止。执行可分离的结构性操作,
明确暂缓行为/契约变更,并且在完成结构性工作后绝不要声明未更改任何文件。 永远不要修改测试,让未经授权的行为变更看起来像是保持了原有行为。
仅在当前工作区内操作
以当前 Agent 工作区为操作边界。如果它是 Git 检出目录,解析其仓库根目录
(git rev-parse --show-toplevel)并确保操作不超出该目录。如果不存在 Git 元数据,则将当前工作目录及其子目录视为边界,并使用其中请求指定的解决方案或项目;重构不以 Git 为前提。将提示中提供的相对路径解析到该边界内。
仅搜索和编辑该工作区。绝不要因为某个文件也存在于其他位置,就进行全文件系统搜索,或选择名称相似的克隆仓库、其他工作树、构建输出或无关临时目录。如果指定路径不存在,请停止并报告不匹配,而不是猜测另一个工作区。如果工具因路径形式不正确或工具根目录不受支持等机制性原因拒绝工作区内的路径,请改用工作区内的其他方式重试。如果拒绝原因是权限或策略限制,请如实报告,不要绕过。绝不要搜索边界之外的内容。
根据绑定重命名/移动,而非根据文本
“重命名”悄悄破坏代码的首要原因,是编辑文本匹配项(注释、字符串、不相关的重载),而不是实际的绑定。先找到每个绑定引用,再进行语义编辑。使用最强大的可用工具:IDE/Roslyn 工作区重构,然后是 dotnet 插件声明的 C# LSP(findReferences、goToDefinition、incomingCalls、rename 代码操作),接着是分析器代码修复/Roslynator,最后是经过编译器验证的编辑(编辑实际绑定,重新构建,让编译器指出遗漏)。仅当范围可明确证明极小且已核实每个匹配项时,才使用普通查找/替换。包括每个 partial 声明,并编辑生成器输入,绝不编辑生成的 (*.g.cs) 输出。
有关操作 → Roslyn 提供程序的对应关系及代表性 PR,请参阅 references/operation-catalog.md。
整合并遵循现有事实来源
进行去重时,遵循代码或请求所述的所有权方向。如果 B 重复了由 A 负责的实现,就保留 A 作为规范实现,并让 B 委托给它;不要仅仅因为任一方向都能编译,就反转依赖关系。如果重复的接口已发布,则保留公共兼容性包装器,并只迁移可安全移动的仓库内调用方。
会改变编辑方式的决策
使用第一个匹配的行,不要机械地执行请求的操作:
| 情况 | 应做 | 绝不要 |
|---|---|---|
| 内联未发布的内部透传包装器 | 将所有绑定引用迁移到目标,移除包装器,然后编译以发现遗漏。 | 在不存在兼容性边界时,以“兼容性”为由保留无用的间接层。 |
| 内联或移除已发布/公共包装器 | 迁移普通的仓库内调用方,但保留一个 [Obsolete] 转发入口,除非请求明确授权破坏性变更。 | 仅仅因为当前所有源代码调用方都已迁移,就删除已发布的 API。 |
| 重命名通过字符串、反射、DI 或配置访问的成员 | 重命名基于绑定的调用方;通过转发垫片或元数据保留外部可见名称,并测试旧名称路径。 | 仅为使新的源代码名称保持一致,就改写外部名称/配置名称。 |
| 提取调用方传入不同值的重复逻辑 | 提取算法,并让每个调用方原样传入其现有输入。 | 将不同输入、求值顺序、舍入或副作用合并为某个调用方的版本。 |
重命名在 #if 或多个 TFM 下编译的代码 | 更新每个源代码分支,并显式验证每个目标框架。 | 把默认目标的构建通过当作未构建分支也通过的证据。 |
| 合并近似相同的类型 | 仅将不同的值参数化,迁移每个构造位置,并精确保留每个旧值。如果旧类型是内部/未发布的,且请求要求合并为一个类型,则删除它们的声明。 | 保留多余的别名、静态持有类、工厂或包装器类型,使所请求的合并不完整;引入新的层次结构或行为。 |
保留 C# 调用点之外的契约
编译只能证明绑定兼容性,无法证明所有外部契约都得到保留。重命名或移动类型/成员之前,请检查其名称或元数据是否会被序列化、反射、依赖注入、配置绑定、源生成器、P/Invoke 或 dynamic 使用。
| 边界 | 必须做出的决定 |
|---|---|
| 序列化/配置名称 | 使用仓库已有机制(例如 JsonPropertyName)保留外部名称,同时迁移 C# 调用方;运行有针对性的往返测试或载荷测试。 |
| 公共可空性注解 | 必须执行分类关卡:保持不变,并作为源代码契约变更移交。 |
| 未覆盖的反射或运行时查找 | 不要臆测编译通过的重命名就是安全的。保留实际观察到的名称,或停止并报告未经验证的运行时边界。 |
按比例验证
编辑后确认行为得到保留——验证范围应与影响面相称,而不是遵循固定流程:
- 局部 / 私有(方法局部或
private成员、单个文件、单个目标框架、无公共接口、无partial/生成代码/#if):除非代码树本身已有问题,否则可跳过单独的基线检查。进行编辑,然后运行范围最窄的构建和相关测试各一次。让编译器发现遗漏的引用。 - 跨边界(公共/已发布符号、多目标项目、
#if/平台分支,或partial/生成代码):先建立基线,再在编辑后针对每个目标框架运行显式构建和相关测试,并运行下方的风险检查(测试命令隐式执行的构建不算独立构建证据;默认构建通过也可能掩盖其他 TFM 上的问题)。
如果仓库文档说明了自己的构建/测试流程(README/CONTRIBUTING、build.*、eng/、global.json、.github/workflows),请使用该流程;其中的说明优先于任何通用命令。
典型工作流(单项操作)
- 选择一项明确命名的重构操作,并确保此步骤只专注于该操作。
- 查找真正的绑定引用(
findReferences/goToDefinition/重命名),并包含所有partial声明。 - 仅当变更跨越边界,或代码树尚未确认通过时,才先建立基线。
- 使用可用的语义感知能力最强的工具应用变更;尽可能避免盲目查找/替换。
- 重新构建并运行相关测试。如果关卡未通过,报告失败并修复或重新评估自己的编辑;绝不要丢弃工作树中无关的变更。
否则:
dotnet build # 0 errors
dotnet test # stays green; same pass count as before
每个步骤只进行一项操作;绝不要在同一步骤中混合重构和行为变更。若验证未通过,请停止并报告失败;只修复自己的编辑,不要丢弃工作树中无关的变更。
最终回复约定
交接内容应简洁,并以证据为依据:
- **重构:**说明结构性操作,以及更改的符号/文件。
- **已保留:**说明保留的行为或兼容性边界,以及采用的保留机制。
- **验证:**报告确切的命令和观察到的结果;如果还原、构建、目标框架或测试运行失败,绝不要声称成功。
- **延期:**对于混合请求,说明有意未处理的行为/契约变更及其正确的后续流程。如果没有延期事项,则省略此行。
跨边界风险(仅在触及边界时)
当且仅当更改涉及公共符号、多目标项目或
partial/生成的代码时,某些破坏不会表现为测试失败。请在仓库中搜索管理该符号的机制(不要想当然):公共 API 门禁(PublicApiAnalyzers 的 PublicAPI.Shipped/Unshipped.txt,以及/或
ApiCompat/<EnablePackageValidation>——这些机制不能互换)、<TargetFrameworks>/#if 分支,以及 InternalsVisibleTo。将公共类型移至另一个程序集,需要在原程序集内添加 [TypeForwardedTo];在同一程序集内移动则不需要。公共重命名需要使用 [Obsolete] 垫片,而不是转发器。对于可以确认仅限本地/私有范围的更改,跳过这些检查。
以下情况应停止范围内的重构
- 基线已经失败(无法证明行为得以保留)。
- 所请求的结构性更改会改变公共/已发布 API,且无法通过兼容性垫片或转发器保留兼容性。报告此边界。未完成任何结构性工作时,使用门禁规定的交接格式;完成可分离的工作后,则将不兼容的操作写在
Deferred:行。不要请求进行破坏性更改。 - 等价性取决于测试未覆盖的运行时行为(反射、DI、序列化、
dynamic、P/Invoke)——请报告未经验证的边界,不要声称行为得以保留。