用途
loom-csharp 是一个 C# 语言技能,目标是产出符合惯用法、达到生产质量的 C# 代码。它假定使用者已经熟悉这门语言,价值在于处理那些“能干净编译、却仍然会失败”的问题:运行时并不成立的 nullable 注解、sync-over-async 死锁、被重复执行或在客户端执行的延迟 LINQ 查询、DI 生命周期不匹配,以及什么都没选中的测试过滤器。
工具链
dotnet CLI 同时充当构建工具、包管理器、测试驱动和格式化前端。技能要求保留仓库的 SDK 固定版本(global.json)、解决方案布局和包管理风格。
常见任务与命令:
- 新建解决方案与项目:
dotnet new sln -n Spool、dotnet new classlib -o src/Spool、dotnet new xunit -o tests/Spool.Tests - 关联项目:
dotnet sln add src/Spool/Spool.csproj、dotnet add tests/Spool.Tests/Spool.Tests.csproj reference src/Spool/Spool.csproj - 添加包:
dotnet add src/Spool/Spool.csproj package Polly(不要手工编辑 PackageReference) - 构建 / 测试:
dotnet build、dotnet test(会先还原并构建) - 格式化:
dotnet format(由 .editorconfig 驱动空白、代码风格和分析器修复) - 审计依赖:
dotnet list package --vulnerable --include-transitive - 本地工具:
dotnet tool restore(版本固定在 .config/dotnet-tools.json) - 忽略构建输出:
dotnet new gitignore(覆盖 bin/ 和 obj/)
共享设置放在仓库根目录的 Directory.Build.props 中,其下每个项目都会继承,示例属性包括 Nullable、ImplicitUsings、TreatWarningsAsErrors、AnalysisLevel、EnforceCodeStyleInBuild、RestorePackagesWithLockFile。中央包管理使用 Directory.Packages.props 与 ManagePackageVersionsCentrally,集中保存所有 PackageVersion,项目文件只带无版本的 PackageReference。NuGet 审计在还原时运行,把已知有漏洞的包报告为 NU1901–NU1904 警告;配合 TreatWarningsAsErrors 会让构建失败。CI 使用 dotnet restore --locked-mode,使漂移的 packages.lock.json 失败。
质量门(警告即错误):dotnet format --verify-no-changes && dotnet build && dotnet test --no-build
主要能力
可空引用类型
可空注解是编译期分析,运行时不做检查:string 参数仍可能从反射、反序列化、旧程序集或写了 null! 的调用方收到 null。建议按项目启用 Nullable 并把警告变成错误。在公共边界检查:ArgumentNullException.ThrowIfNull、ArgumentException.ThrowIfNullOrWhiteSpace(.NET 8)、ArgumentOutOfRangeException.ThrowIfNegative(.NET 8)。!(null-forgiving)只是让编译器闭嘴,运行时不改变任何东西。可用 System.Diagnostics.CodeAnalysis 特性教会分析器,例如 [NotNullWhen(true)]、[MemberNotNull]、[return: NotNullIfNotNull]、[DoesNotReturn]。分析看不到的漏洞包括:new string[4] 含四个 null、default(SomeStruct) 的引用字段为 null、既未由构造函数初始化也非 required 的属性。System.Text.Json 会向非可空属性写入 null,除非另行设置:把必需成员标为 required,并设置 JsonSerializerOptions.RespectNullableAnnotations = true(.NET 9)。无约束泛型 T? 表示“可能为 default”。
记录、结构体与类
- 需要不可变数据与值相等:record(引用)或 readonly record struct(小值)
- 需要标识、行为、可变状态:sealed class(仅在为继承设计时才不 sealed)
- 小(约 16 字节或更少)、短生命周期、无装箱:readonly struct
- 必须在构造时设置的成员:required(C# 11)+ init(C# 9)
注意 record 相等性用 EqualityComparer<T>.Default 逐成员比较,List<T>、数组或 ImmutableArray<T> 成员按引用比较,内容相等的两个 record 并不相等。record struct 的位置属性可变,readonly record struct 使其为 init。with 是浅拷贝。类上的主构造函数(C# 12)把参数捕获为隐藏的可变字段,不是属性。sealed 让 JIT 去虚拟化。非穷尽 switch 表达式会警告 CS8509 并在运行时抛 SwitchExpressionException;枚举是开放集合,需要 _ 分支。集合表达式(C# 12):int[] ids = [1, 2, 3];。C# 14(.NET 10)新增 field 关键字、扩展块、a?.B = x。
异常与释放
对调用方无法预料的失败抛异常;对可预期结果(解析失败、缓存未命中)使用 TryX(out T) 模式或结果类型。用 throw; 重新抛出,throw ex; 会重置堆栈跟踪;在别处重新抛出捕获的异常用 ExceptionDispatchInfo.Capture(ex).Throw()。异常过滤器在堆栈展开前运行。catch (Exception) 属于进程和请求边界。用 using var 或 await using 拥有资源。终结器只用于包装原始 OS 句柄的类型,SafeHandle 通常可免除这一需要。
Async / Await
全程 async。在带 SynchronizationContext 的线程上使用 .Result、.Wait()、.GetAwaiter().GetResult() 会死锁;ASP.NET Core 没有上下文,同样代码会耗尽线程池。库代码中每个 await 使用 ConfigureAwait(false)(可启用 CA2007)。async void 只用于事件处理器。ValueTask<T> 用于通常同步完成的热路径,只能 await 一次。CancellationToken 是每个异步 API 的最后一个参数。await Task.WhenAll(tasks) 只重新抛出第一个异常。lock 内不能 await(CS1996),用 SemaphoreSlim(1, 1)。有界并发用 Parallel.ForEachAsync 与 MaxDegreeOfParallelism,或 Channel.CreateBounded<T>。IAsyncEnumerable<T> 生产者接收 [EnumeratorCancellation] CancellationToken。即发即弃会丢失异常,应通过 Channel<T> 排队到 BackgroundService。
LINQ 陷阱
延迟执行:查询是配方,每次枚举都会再跑一遍。多次枚举时用 ToList()/ToArray() 物化一次(CA1851 会标记)。IQueryable<T> 与 IEnumerable<T>:EF Core 把表达式树翻译成 SQL,直到第一个 AsEnumerable()/ToList(),之后的每个操作符都在内存中运行。N+1 来自延迟加载代理或循环内读取导航属性,用 Include 或 Select 投影,必要时加 AsSplitQuery()。Any() 在第一个元素停止,Count() > 0 会数完。Single 在多于一个匹配时抛异常,First 在无匹配时抛异常。OrderBy(a).OrderBy(b) 丢弃第一个排序,应链式 ThenBy。for 循环变量被循环内所有 lambda 共享,foreach 变量是每次迭代独立的。内置操作符:Chunk、DistinctBy、MaxBy/MinBy(.NET 6);CountBy、AggregateBy、Index(.NET 9)。
ASP.NET Core Minimal APIs
示例展示了 WebApplication.CreateBuilder、AddProblemDetails、AddDbContext、AddHttpClient、AddOptions 与 BindConfiguration/ValidateDataAnnotations/ValidateOnStart,以及 MapGroup、RequireAuthorization、TypedResults 与 Results<...> 返回类型。绑定:路由和查询参数按名称,复杂类型来自 JSON 正文,注册服务来自 DI,CancellationToken 来自 HttpContext.RequestAborted,[AsParameters] 可把它们归组为一个 record。验证:.NET 10 新增 builder.Services.AddValidation(),在 minimal API 参数上运行 DataAnnotations;更早版本需要端点过滤器或 FluentValidation。DI 生命周期:单例持有 scoped 服务(如 DbContext)会形成 captive dependency;开发构建默认验证作用域,测试中也应打开 ValidateOnBuild 和 ValidateScopes;BackgroundService 通过 IServiceScopeFactory 创建作用域。HttpClient:每次调用 new HttpClient() 会耗尽套接字;用 IHttpClientFactory(AddHttpClient<T> 类型化客户端)。
测试
xUnit 与 NUnit 差异最大的地方是实例生命周期:xUnit 每个测试新建类实例,NUnit 默认一个 fixture 实例被其测试共享(可用 [FixtureLifeCycle(LifeCycle.InstancePerTestCase)] 改变)。xUnit 用 [Fact]、[Theory] + [InlineData]/[MemberData],构造函数/Dispose(异步用 IAsyncLifetime)做 setup/teardown,IClassFixture<T>、ICollectionFixture<T> 共享昂贵状态,类(集合)并行、类内测试串行。NUnit 用 [Test]、[TestCase(...)]/[TestCaseSource],[SetUp]/[TearDown]、[OneTimeSetUp],[OneTimeSetUp] 字段、[SetUpFixture] 共享状态,默认不并行除非 [Parallelizable]。
适用场景
适合需要编写或审查生产级 C# 代码、配置 .NET 项目结构与质量门、处理可空性、异步、LINQ、DI 与测试问题的开发场景。
限制
资料未提供该技能的安装方式、收费模式、平台支持范围或具体使用步骤,这些信息暂未提供。