要点
这是查的不是读的:按用途分六组收录 C# 高频条目——类型系统、集合与 LINQ、异步、record 与模式匹配、常用特性、常见报错。每条给签名、一句话语义、最小示例和坑。概念背景在《C# 语法要点》,本文只做速查。版本口径:.NET 10 / C# 14。
一、类型系统
组内共性:C# 类型分值类型(struct,栈语义)与引用类型(class,堆语义)两族,泛型约束决定类型参数能干什么。
string 与内插
var name = "go"; var n = 3;
var s = $"{name,-5}|{n:D3}"; // "go |003",逗号后是对齐与格式说明符
var raw = """C:\temp\{x}"""; // 原始字符串字面量,反斜杠不转义、单花括号不内插
string 不可变,所有”修改”方法都返回新串。坑:循环里 += 拼串是 O(n²),改用 StringBuilder。
struct / record struct:值语义
public readonly record struct Point(int X, int Y); // 值相等 + 不可变 + 无堆分配
var a = new Point(1, 2); var b = a; b.X = 9; // b 是副本,a 不变
16 字节以下、生命周期短的数据用值类型省 GC;坑:作为接口变量传递会装箱(boxing),性能优势瞬间反转。
泛型约束 where
T Create<T>() where T : class, new() => new(); // 引用类型 + 有无参构造
| 约束 | 含义 | 坑 |
|---|---|---|
| where T : class | 引用类型 | 还能传 null,加 notnull 才禁止 |
| where T : struct | 非空值类型 | 不能与 new() 同用(本身可 default) |
| where T : new() | 有无参构造 | 靠反射创建,热路径慎用 |
| where T : ISearch | 实现某接口 | 可同时约束多个接口 |
| where T : unmanaged | 非托管类型 | interop / unsafe 场景专用 |
二、集合与 LINQ 操作符
组内共性:集合类解决”怎么存”,LINQ 解决”怎么查”;两者都以 IEnumerable 为通用货币。
三件套怎么选
| 类型 | 查找复杂度 | 坑 |
|---|---|---|
| List | 按索引 O(1),按值 O(n) | 频繁 Contains 就是选错了容器 |
| Dictionary | O(1) | 键重复抛异常;遍历时改集合抛 InvalidOperationException |
| HashSet | O(1) | 只判存在不存值;容量预估不当会频繁扩容 |
Where / Select / SelectMany
var urls = links.Where(l => l.Clicks > 100).Select(l => l.Url).ToList();
var tags = posts.SelectMany(p => p.Tags).Distinct().ToList(); // 摊平嵌套,join 的反面
坑:不加 ToList 返回的是惰性 IEnumerable,每次遍历重新执行——见语法篇”延迟执行”。
First 家族:空序列行为是分水岭
var a = list.First(x => x.N > 0); // 空 → 抛 InvalidOperationException
var b = list.FirstOrDefault(x => x.N > 0); // 空 → 返回 default(引用类型为 null)
坑:FirstOrDefault 拿到 null 分不清”没有”还是”元素本身是 null”;值类型加 Second 检查或用 TryGet 模式。Single 用于”必须有且仅有一个”,查出两个也抛——用它表达唯一性约束。
ToDictionary / ToLookup / GroupBy
var byCode = links.ToDictionary(l => l.Code); // 键重复直接抛
var grouped = links.ToLookup(l => l.Domain); // 键重复不抛,多值成组
var q = links.GroupBy(l => l.Domain); // 惰性,遍历才分组
坑:源数据可能重键时 GroupBy/ToLookup 是安全牌,ToDictionary 是断言。
Any / All / Contains / Aggregate / Chunk
if (cart.Any(i => i.Price > 1000)) { ... } // 存在性判断,别用 Count() > 0(要数完)
var total = cart.Aggregate(0m, (sum, i) => sum + i.Price); // 折叠
foreach (var batch in ids.Chunk(500)) { ... } // .NET 6+,分批防 IN 子句过长
坑:Count() 谓词版与 Any 语义可互换时,Any 找到即停。
三、异步:Task、async 流、取消
组内共性:异步三问——等谁(Task)、怎么取消(CancellationToken)、怎么流式拿(IAsyncEnumerable)。
Task.WhenAll / WhenAny
var results = await Task.WhenAll(urls.Select(FetchAsync)); // 全部成功才返回,异常在 await 处抛
var firstDone = await Task.WhenAny(tasks); // 任一完成,常配超时
坑:WhenAll 一个失败不取消其余任务;要”失败即止损”用 WhenAny + 异常检查的组合或 CancellationTokenSource。
CancellationToken:取消是协作不是强杀
var cts = new CancellationTokenSource(TimeSpan.FromSeconds(3));
var page = await httpClient.GetStringAsync(url, cts.Token); // token 一路传到底
cts.Token.ThrowIfCancellationRequested(); // 长循环里主动检查点
坑:注册了 token 却不传给底层 IO,取消不会生效;ASP.NET Core 里 RequestAborted 就是客户端断连信号,长任务记得挂上。
IAsyncEnumerable 与 await foreach
async IAsyncEnumerable<Link> StreamLinksAsync(CancellationToken ct)
{
foreach (var l in await LoadPageAsync(ct))
{
await Task.Delay(10, ct); // 模拟流式产出
yield return l; // async 流用 yield,消费方按需拉
}
}
await foreach (var link in StreamLinksAsync(ct)) Process(link);
对应同步版的 IEnumerable + yield return;适合分页抓取、日志 tail。坑:不用 await foreach 而用普通 foreach 会编译错误——它只能被异步消费。
Task.Run 的适用边界
var data = await Task.Run(() => CpuHeavyParse(text)); // 把 CPU 密集活挪到线程池
坑:把纯 IO 包进 Task.Run 是反模式——不省任何东西还占线程;服务器上 Task.Run 做发后不理,进程退出任务即丢,需要可靠后台处理用 Channel + BackgroundService(实战篇有完整例子)。
四、record 与模式匹配
组内共性:record 管数据形状,模式匹配管形状分支,二者是同一套建模思路的两面。
record:三种形态
public record Link(string Code, string Url); // 位置参数:自动出 init 属性 + 解构
public record struct Size(int W, int H); // 值语义 record(C# 10+)
public record CacheEntry(string Url, DateTime At); // 引用语义,拷贝成本低
坑:record 的相等比较逐字段调用 Equals,含大型集合字段时比较成本可观。
with 与解构
var updated = entry with { At = DateTime.UtcNow }; // 浅拷贝改字段,集合字段仍是同一引用
var (code, url) = link; // 解构成元组
坑:with 是浅拷贝,嵌套可变引用类型不会被复制。
switch 表达式与四类模式
string Classify(object o) => o switch
{
Link { Clicks: >= 1000 } hot => $"热门:{hot.Code}", // 属性 + 关系模式
Link l => $"普通:{l.Code}", // 类型模式 + 变量捕获
int and < 0 => "负数", // 类型与关系模式用 and 组合
_ => "未知"
};
// 列表模式(C# 11+)要求输入是集合,别打在 object 上
string ParseCmd(string[] argv) => argv switch
{
["run", var target] => $"运行 {target}",
["help", ..] => "帮助",
_ => "未知"
};
坑一:switch 表达式必须穷尽或落 _,漏分支是编译错误不是运行时隐患——这正是它比 if-else 链值钱的地方,新增 record 成员时编译器会把漏掉的分支顶出来。坑二:关系与列表模式对输入的静态类型有要求,打在 object 上直接编译失败,先用类型模式收窄再套。
五、常用属性特性
组内共性:特性(attribute)是贴在代码元素上的元数据,由编译器、运行时或库在各自阶段读取。
| 特性 | 谁消费 | 典型用途 |
|---|---|---|
| [Required] / [Range] / [StringLength] | DataAnnotations 校验器 | 模型校验,API 入参自动 400 |
| [JsonPropertyName(“code”)] | System.Text.Json | 改 JSON 字段名而不改 C# 命名 |
| [Obsolete(“用 NewApi 代替”, error: true)] | 编译器 | 废弃 API,error: true 直接编译失败 |
| [DebuggerDisplay(“Code={Code}”)] | 调试器 | 断点悬停显示有意义的摘要 |
| [Fact] / [Theory](xUnit) | 测试框架 | 标记测试方法;Theory 配 [InlineData] |
| [HttpGet(“/s/{code}”)] | ASP.NET Core 路由 | 控制器场景声明路由模板 |
坑:[Required] 只在触发校验管线时生效(控制器模型绑定、Manual Validation),Minimal API 手写端点不会自动校验。
六、常见报错速查
| 报错/警告 | 含义 | 处置 |
|---|---|---|
| NullReferenceException | 运行时解引用了 null | NRT 只是编译期分析,JSON/HTTP 边界数据仍要运行时校验;栈里第一行用户代码就是案发现场 |
| CS8602 警告 | 可能为 null 的值被解引用 | 别用 null! 硬压:先判空或用 ?. / ?? 收敛类型 |
| CS8619 / CS8625 警告 | 可空性不匹配(把可空赋给非空等) | 接口边界改 T?,实现处补判空 |
| InvalidOperationException: Sequence contains no elements | 对空序列调 First / Single / Max | 可能为空就 FirstOrDefault,且检查 null |
| ArgumentException: same key has already been added | ToDictionary / Dictionary.Add 键重复 | indexer 赋值可覆盖,语义不同;重键数据用 ToLookup |
| CS4014 警告 | await 的 Task 没接住 | 要么 await,要么显式丢弃 _ = Task,别让异常无声消失 |
| CS1998 警告 | async 方法里没有 await | 去掉 async 直接返回 Task.FromResult,或补真 await |
| CS8805 | 顶级语句与显式 Main 共存 | 二选一,见第一个应用篇 |
| CS0246 类型找不到 | 缺 using 或缺包引用 | ImplicitUsings 不含第三方命名空间;先 dotnet add package 再 using |
全量速查索引
| 条目 | 一句话用途 | 最大的坑 |
|---|---|---|
| 字符串内插 / 原始字符串 | 格式化与免转义字面量 | 循环 += 拼串 O(n²) |
| record struct | 小数据值语义 | 装箱即失效 |
| where 约束 | 给类型参数立法 | class 约束仍可空 |
| Dictionary / HashSet | O(1) 键值 / 存在性 | 遍历中修改集合 |
| SelectMany | 摊平嵌套 | 语义是笛卡尔不是 join |
| FirstOrDefault | 安全取首个 | null 语义二义 |
| ToDictionary / ToLookup | 建索引 / 分组索引 | 前者重键抛异常 |
| Any | 存在性判断 | 用 Count() > 0 替代它要数全量 |
| Task.WhenAll | 并发聚合 | 失败不取消兄弟任务 |
| CancellationToken | 协作式取消 | 不传到底层就形同虚设 |
| IAsyncEnumerable | 异步流式产出 | 只能 await foreach |
| Task.Run | CPU 密集挪线程池 | 包 IO 是反模式 |
| with 表达式 | 非破坏更新 | 浅拷贝 |
| switch 表达式 | 形状分支 | 必须穷尽(这是优点) |
| [Required] 等 | 声明式校验 | 管线不触发就不生效 |
延伸阅读
概念不明白的回《C# 语法要点》;
版本核实(以官方文档为准):本文条目以 .NET 10(当前 LTS)/ C# 14 为基准;Chunk、DistinctBy 为 .NET 6+,record struct 为 C# 10+,列表模式与 required 成员为 C# 11+,原始字符串字面量为 C# 11+,field 关键字为 C# 14;可空引用类型相关警告码(CS86xx)自 .NET 6 起默认启用。具体以官方文档为准。