AGENTS.md
-
不要新增只被调用一次的小 helper;只有当它能命名清楚概念、隔离复杂逻辑、复用已有边界或改善测试时才抽函数。
-
作为对1的补充,如果同样的逻辑出现超过3次,就应考虑作为共享函数,并写入项目记忆以供继续复用(如果有项目记忆的话)。
-
新模块应围绕职责和变化原因命名,避免按“工具集合”“杂项”“common”堆放无关能力。
-
不要假设自己处在沙箱(sandbox)网络中,这会让你认为自己无法访问网络,干扰测试。
-
代码分析、修改和重构应使用Rider自带的mcp工具代替read/find/grep等默认工具。
-
建立sub agent进行任务时,务必撰写提示词,让sub agent也使用Rider mcp工具代替read/find/grep等默认工具。
-
除非用户明确要求,否则不得以任何理由逃避、跳过、结束调试,不得私自移除断点,一定要分析到断点的所有信息才能resume。
-
本仓库代码频繁改动,注释可能与实际代码逻辑不一致。判断函数行为时一律以代码实现为准,不得仅凭注释下结论;改动前必须读代码核实当前真实行为。
-
测试代码不得复制/重写业务逻辑。测试只负责组装输入(数据、文件路径、配置)和断言输出,完全复用业务逻辑,不重新实现业务逻辑,也不可自己绕过业务逻辑。
-
只为可独立重用或复杂到需要隔离的逻辑编写单元测试。对于其余逻辑,对整个功能进行集成测试或组件测试可以提供更好的覆盖率,且维护成本更低。
-
当需要验证某个函数的行为、排查bug、或确认代码路径时,优先用Rider自带的xdebug_*工具,不要使用
python或bash命令行启动。只有当MCP通讯受阻时,才应使用后台进程运行函数,以避免打断工作流。 -
创建 run configuration 的方法见
jetbrains-run-configskill。 -
如果用户的话里存在多个问题,一定要写 TODO 清单或并行开启sub agent,以防上下文固定于一个特定的问题而丢弃其他问题。
-
优化prompts应注意简短和通用性,不可将当前文档或问题限定的内容完全输入提示词。
-
编写测试代码时,禁止将文件输出到项目文件夹以外的路径。
-
编写总结、计划、方案、报告、备忘时,务必使用中文文件名并输出到项目路径下/plans文件夹,禁止输出到/tmp路径。
-
调试和分析问题时,应一次分析、修改、验证完毕,不要只分析然后说"仍需实施",或者只改代码就报称修复。每次修复后必须用单元测试/集成测试/组件测试/断点调试验证实际效果,确认问题真正解决才能报完成。
-
集成测试/组件测试必须全部通过才能运行主流程。
-
禁止使用环境变量作为配置项。
-
禁止修改代码格式(除非影响代码逻辑,例如不当的缩进导致编译错误或者if分支执行不完整),这会造成无谓的时间和token消耗;格式规约由dotnet format/csharpier钩子自动进行。
-
officecli是python-docx的超集,无论是检查还是处理docx文档信息,都应该使用officecli,禁止使用python-docx,也不许转成pdf或者用libreoffice解析,只是增加不必要实体。
-
除非用户要求,否则禁止使用任何打分机制和置信度机制,因为没有理论指导如何消费这些分数。
-
写提示词和注释的时候,一句话一行,例如:
你需要做如下工作,做完报告给我。禁止出现明明是一句话却要强行换行的情况:
你需要做如下工作, 做完报告给我。 -
确认废弃的代码一律删除,不要想着"保留工作"或者"兜底"。死代码不只是垃圾,还会吸附后续修复接错线。重构时先查调用方,确认零调用即删;测试、参数、兼容层随机制一并删除。
-
使用getter/setter操作属性时,应使用C# 14引入的
field语法,示例:// With C# 14: use the field keyword public int MaxLength { get; set { ArgumentOutOfRangeException.ThrowIfLessThan(value, 0); field = value; } } -
一个方法最好不要超过20条语句。
-
用插值字符串代替字符串拼接(因为C#编译器对此有优化)。示例:
// GOOD string result = $"Welcome, {firstName} {lastName}!"; // BAD,是字符串拼接 string result = string.Format("Welcome, {0} {1}!", firstName, lastName); // BAD,是字符串拼接 string result = string.Concat("Welcome, ", firstName, " ", lastName, "!"); -
对于小型、频繁传递的不可变数据,例如坐标、颜色或日期范围,使用struct/record struct代替class。
-
若初始化逻辑平凡(或者说仅起到字段赋值作用而无其他逻辑),优先使用主构造函数(primary constructor)代替一般构造函数:
// Before primary constructors public class OrderService { private readonly IOrderRepository repository; public OrderService(IOrderRepository repository) { this.repository = repository; } } // With primary constructors public class OrderService(IOrderRepository repository){ public async Task<Order> GetOrderAsync(Guid id) => await repository.GetByIdAsync(id); } -
对于无法直接编辑的第三方库,可以考虑使用扩展块(C# 14引入)和更早之前的扩展方法:
// C# 14 extension block extension(Order order) { public bool IsOverdue => order.DueDate < DateTimeOffset.UtcNow && !order.IsCompleted; public void MarkAsShipped(DateTimeOffset shippedAt) { order.Status = OrderStatus.Shipped; order.ShippedAt = shippedAt; } }但不要给你能直接修改的类添加扩展,这属于逻辑浪费。
-
在泛型类或方法中,不要与 object 类型进行来回转换,而应使用 where 约束或 as 运算符来指定泛型参数的确切特性。例如:
class SomeClass {} // Don't class MyClass { void SomeMethod(T t){ object temp = t; SomeClass obj = (SomeClass) temp; } } // Do class MyClass where T : SomeClass { void SomeMethod(T t) { SomeClass obj = t; } } -
LINQ表达式的返回结果应进行物化,例如:
var query = from customer in db.Customers where customer.Balance > GoldMemberThresholdInEuro select new GoldMember(customer.Name, customer.Balance); return query; // LINQ是延迟执行的,所以query实质上是表达式树,而非最终结果 return query.ToList(); //所以需要ToList()、ToArray()方法进行物化 -
何时使用IEnumerable
/Span (栈上)/Memory (堆上)? 需要处理数据吗? ├── 是连续内存块(数组、字符串、栈内存、非托管内存)? │ ├── 方法完全是同步的? │ │ ├── 需要写入?→ Span<T> │ │ └── 只读?→ ReadOnlySpan<T> │ └── 需要跨 await、存为字段、或传入异步流? │ ├── 需要写入?→ Memory<T> │ └── 只读?→ ReadOnlyMemory<T> └── 是离散/惰性序列(数据库行、文件行、无限流)? └── 用 IEnumerable<T> / IAsyncEnumerable<T>使用Span
/Memory 提升性能的典例: 对比维度 传统方式 Span / Memory 方式 典型提升 字符串切片 SubstringReadOnlySpan<char>.Slice4~7.5x 速度,零分配 字符串分割 string.SplitReadOnlySpan<char>.Split~38% 速度,零分配 数组子集 Array.Copy+new[]Span<T>.SliceO(1) 切片,零分配 数字解析 int.Parse(str.Substring(...))int.Parse(str.AsSpan(...))零子串分配 小缓冲区 new byte[256]stackalloc byte[256]+Span13x 速度,零堆分配 GUID 转换 Guid.Parse/ToString()Span解析/格式化40~50% 速度,减分配 一些普通操作的速查表:
场景 推荐类型 理由 同步字符串/数组处理 ReadOnlySpan<T>/Span<T>零分配切片,无 GC 压力 stackalloc缓冲区Span<T>唯一支持栈内存的类型 P/Invoke 同步调用 Span<T>可直接映射到指针 异步 I/O 缓冲区 Memory<T>可跨 await,再转Span处理类字段保存缓冲区 Memory<T>Span不能作为字段数据库/文件行遍历 IEnumerable<T>/IAsyncEnumerable<T>非连续,惰性求值 需要 LINQ 操作 IEnumerable<T>Span/Memory不支持原生 LINQ不确定数据是否连续 IEnumerable<T>最通用的抽象 -
代码中的字面量(魔数)应以常量声明封装(若用于日志记录和追踪除外),例如:
public class Whatever { public static readonly Color PapayaWhip = new Color(0xFFEFD5); //字面量0xFFEFD5用PapayaWhip声明 public const int MaxNumberOfWheels = 18; //字面量18用MaxNumberOfWheels声明 public const byte ReadCreateOverwriteMask = 0b0010_1100; //字面量0b0010_1100用ReadCreateOverwriteMask声明 }语义非常明确且不会发生变化的情况也不必封装,例如:
mean = (a + b) / 2; // 平均数自然是/2,声明成常量反倒是画蛇添足 WaitMilliseconds(waitTimeInSeconds * 1000); 秒换算成毫秒必须是*1000,声明成常量也是画蛇添足 -
避免使用嵌套的try catch块,这会减弱可读性。
-
不要使用ref/out参数,这会降低可读性,返回复合对象、结构体或元组。 例外情况:
bool success = int.TryParse(text, out int number); //使用了TryParse或者依赖库里本身就有ref/out参数,需要保证逻辑正确 -
若字符串含有大量转义字符,需要用原始字符串字面量代替:
string pattern = "^(https?:\\/\\/)(www\\.)?[a-zA-Z0-9]+\\.[a-z]+$"; //存在大量转义字符!需要放弃 string pattern = """^(https?:\/\/)(www\.)?[a-zA-Z0-9]+\.[a-z]+$"""; //原始字符串字面量用"""开始和结束,转义字符减少很多 -
async/await用于I/O密集型任务,Task.Run()/Task.StartNew()则用于CPU(计算)密集型任务。
-
建议直接await ValueTask或者ValueTask
,且只等待一次: // OK / GOOD int bytesRead = await stream.ReadAsync(buffer, cancellationToken); // OK / GOOD int bytesRead = await stream.ReadAsync(buffer, cancellationToken).ConfigureAwait(false); // OK / GOOD - Get task if you want to overcome the limitations exposed by ValueTask / ValueTask<T> Task<int> task = stream.ReadAsync(buffer, cancellationToken).AsTask(); -
应尽量使用C#新语法或者更规范的语法,包括且不限以下的内容:
ValueTuple<string, int> tuple = new ValueTuple<string, int>("", 1); //旧语法,不再使用 (string, int) tuple = ("", 1); //推荐的新语法Nullable<DateTime> startDate; //旧语法,不再使用 DateTime? startDate; //推荐的新语法if (startDate == null) ... //不规范,不推荐使用 if (startDate is null) ... //规范用法if (startDate.HasValue) ... //不规范,不推荐使用 if (startDate is not null) ... //规范用法if (startDate.HasValue && startDate.Value > DateTime.Now) ... // startDate.Value > DateTime.Now说明startData有值,没必要再判断一遍 if (startDate > DateTime.Now) ... // 正确的做法:只做必要的判断List<string> items = new List<string>(); List<string> items = []; // []比new List……更加简单直观if (list == null) list = []; // 旧语法,且易读性低,不再使用 list ??= []; //新语法,推荐使用 -
C#支持解构元组,错误和正确示例如下:
// 错误示例,写法繁冗 public record Point(int X, int Y); Point point = GetOrigin(); int x = point.X; int y = point.Y;// 正确示例,写法简单 (int x, int y) = GetOrigin(); // 也可以使用模式匹配解构数组 if (items is [int first, int second, ..]) { // use first and second directly } //对foreach也适用 foreach ((int key, int value) in dictionary){ Console.WriteLine($"{key}: {value}"); } -
不要使用#region标记。
-
写注释时禁止写入被否定的决策,除非它对应的代码曾在真实的运行中出现过bug。例如:
// 注释:白米粥里需要加入大便…… 用户驳回:白米粥里为什么要加入大便? // 注释:白米粥里不要加入大便,理由是大便污染了米粥…… ------> 错!白米粥一开始就不应该有大便! // 注释:白米粥的米和水体积比约为1:1.2…… --------> 对!说白米粥怎么做就可以了! // 注释:白米粥里之所以出现大便,是因为用户的狗在碗里拉屎,解决方法:把狗杀了。 ----------> 对!出现过真实的问题(狗在碗里拉屎),并给出解决方式(杀狗)! -
在写C#时,除非确实需要控制反转,否则不要总是试图用依赖注入去写逻辑。
-
禁止编写无行为的try catch块:
try{ doSomething(); }catch { //do Nothing! 不应该! }