C# 语法速查表 - C# 常用语法与 .NET 开发大全
这套速查给用 .NET 写 Web API、服务或桌面应用的开发者,聚焦每天都会写的 C# 语法:带访问修饰符的类与接口、取代手写循环的 LINQ 查询、处理 I/O 密集的 async/await、泛型与委托,以及属性与事件模型。与平铺的 C# 特性表不同,条目按语言构造来分,并点出 C# 和 Java/JS 不一样的地方。读完应能用一条组合 LINQ 查询代替嵌套循环、写出不会死锁的异步方法,并用属性、事件和委托把类型设计清楚。
典型使用场景
面向用 C# 写服务端与桌面应用的开发者。本表把语法表面最容易忘的部分集中在一起:类型声明从 class/interface/sealed/abstract 到 record 的选型、属性的 get/set/init/表达式体三种写法、委托(delegate/Action/Func/Predicate)与事件的分工、LINQ 的过滤到聚合链路、以及 async/await 与 CancellationToken 的协作方式。适合刚接手的项目要快速判断「这里该用 record 还是 class」「这个回调该声明成 Func 还是事件」时按需回查。
类与接口 7
public class MyClass { }public interface IMyInterface { void Method(); }public class Derived : Base, IMyInterface { }public abstract class AbstractClass { }public sealed class SealedClass { }public static class Utils { }public record Person(string Name, int Age);属性与方法 6
public int Age { get; set; }public string Name { get; init; }public int Age => _age;public void Method() { }public int Add(int a, int b) => a + b;public event EventHandler MyEvent;LINQ 查询 8
var result = list.Where(x => x > 5);var result = list.Select(x => x * 2);var result = list.OrderBy(x => x.Age);var result = list.GroupBy(x => x.Category);var result = list.First(x => x.Id == 1);var result = list.Any(x => x > 0);var result = list.Sum(x => x.Price);var result = list.Aggregate((a, b) => a + b);异步编程 6
public async Task<int> GetDataAsync() { }await Task.Delay(1000);var result = await httpClient.GetStringAsync(url);Task.Run(() => { });await Task.WhenAll(task1, task2);CancellationTokenSource cts = new();泛型与委托 6
public class Stack<T> { }public T Max<T>(T a, T b) where T : IComparable<T> { }public delegate void MyDelegate(string msg);public event Action<string> OnMessage;public Func<int, int, int> add = (a, b) => a + b;public Predicate<int> isPositive = x => x > 0;集合与字典 6
List<int> list = new() { 1, 2, 3 };Dictionary<string, int> dict = new();HashSet<int> set = new() { 1, 2, 3 };Queue<int> queue = new();Stack<int> stack = new();var tuple = (1, "hello");参数矩阵
| 参数 | 作用 | 示例 |
|---|---|---|
record / init | 不可变数据类型:位置参数构造 + 只初始化属性,适合传输对象 | public record Person(string Name, int Age); |
Func<T, TResult> / Predicate<T> | 有返回值 / 返回 bool 的委托,分别对应映射与条件判断 | Func<int, int, int> add = (a, b) => a + b; |
CancellationTokenSource cts = new() | 取消令牌源:把 cts.Token 传入异步方法,配合 cts.Cancel() 中断长任务 | CancellationTokenSource cts = new(); |
async / await / Task.WhenAll | 非阻塞并发:WhenAll 并行等待多个任务,比逐个 await 快 | await Task.WhenAll(task1, task2); |
易错点与避坑指南
现象对象创建后想改属性,编译器报「只能赋值一次」的错误。
原因record 与 init 属性在初始化后不可再赋值,这是设计意图而非缺陷。
处置确实需要可变状态时改用 class + get/set 属性;只在传输层用 record 固化数据形状。
现象在 async 方法里忘了 await,异常没抛出,任务静默失败。
原因async 方法返回 Task,不 await 就等于没执行;异常被包进 Task 里无人观察。
处置每个 async 调用都加 await,或把返回的 Task 交给 await Task.WhenAll(...) 统一等待。
现象循环里请求接口越跑越慢,界面卡住。
原因在 async 方法里直接调用 Task.Run 或同步阻塞的 I/O,线程被占住。
处置I/O 直接 await(await httpClient.GetStringAsync(url)),并发交给 Task.WhenAll,而不是 Task.Run。
现象声明了 public event 却没人能赋值,外部无法触发。
原因事件默认只能在声明它的类内部触发,外部只能 += / -= 订阅。
处置若外部也要触发,改用 Action<string> 这类普通委托字段并提供 Raise 方法。
排障路径
1把一组异步调用改成真正的并发
await Task.WhenAll(task1, task2);逐个 await 是串行的;WhenAll 让两个请求同时在飞,耗时接近较慢的那个。
2给长任务加上可取消能力
CancellationTokenSource cts = new();创建源后把 cts.Token 传给 async 方法,调用方在 cts.Cancel() 时让方法体退出。
3确认集合里有元素再取值,避免空引用
var result = list.Any(x => x > 0);Any 先做存在性判断,再配合 First / Aggregate 取值,比直接 FirstOrDefault 更安全。
命令示例
record + init 表达不可变 DTO
public record Person(string Name, int Age);
var p = new Person("Ada", 36) with { Age = 37 };record 自动生成值相等、ToString 与解构,with 表达式基于原值复制并覆盖指定字段——这是「改一个字段,其它全保留」的惯用法。属性写成 init 后只能在构造或 with 里赋值,误改会直接编译失败,而不是在运行期污染对象。
LINQ 链:先过滤再排序求和
var total = orders
.Where(x => x.Status == "paid")
.OrderByDescending(x => x.Age)
.Select(x => x.Price)
.Sum();LINQ 方法返回的是 IEnumerable,链式调用不会立刻计算数据,写完链才执行。因此把 Where 放在 Sum 之前能显著减少参与运算的元素;而对 IQueryable(EF Core)来说,链的顺序同时决定生成的 SQL,顺序错了性能会差一个数量级。
带约束的泛型方法
public T Max<T>(T a, T b) where T : IComparable<T>
{
return a.CompareTo(b) >= 0 ? a : b;
}where T : IComparable<T> 约束让方法体内可以安全调用 CompareTo。没有约束时 T 只能做 == 这类无意义比较,想排序就得先强转装箱。约束是编译期检查,调用方传入不满足的类型会直接编译不过,而不是运行期抛异常。
Task.WhenAll 并行 + CancellationTokenSource
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10));
var t1 = httpClient.GetStringAsync(url1, cts.Token);
var t2 = httpClient.GetStringAsync(url2, cts.Token);
string[] results = await Task.WhenAll(t1, t2);await Task.WhenAll(...) 一次性等待多个任务,并发度真正提上去;using 保证 cts 被释放。传入 CancellationToken 后超时由调用方控制,避免一个慢请求把整批任务挂死——这是让并行调用可取消的关键,漏掉就只能等默认超时。
用泛型集合替代无类型容器
List<int> list = new() { 1, 2, 3 };
Dictionary<string, int> dict = new() { ["id"] = 1 };
HashSet<int> seen = new() { 1, 2, 3 };
Queue<int> queue = new();
Stack<int> stack = new();用 List<T> / Dictionary<TKey,TValue> 代替 ArrayList / Hashtable:编译期就能发现类型错误,泛型集合不装箱,值类型性能差距往往在一到两个数量级。Hashtable 允许 key 为 null 且比较用 Equals,Dictionary 两者都禁止——迁移时最容易出行为差异。
常见坑与注意事项
- async/await 方法里出现裸的 .Result 或 .Wait(),在同步上下文(WinForms/WPF/ASP.NET 经典管道)里会死锁:UI 线程等结果,被等的方法又要回 UI 线程继续。统一用 await,这是最难排查的一类挂起。
- async Task 方法没有以 await 结尾时编译器只发警告,异常会被静默吞掉,任务变成「已完成但什么都没做」。别用空 Task.FromResult 掩盖,也不要靠 try/catch 捕获一个永远不会抛的 await。
- LINQ 链里的每一步都是延迟执行。把它赋给变量却忘了终结操作(Count/ToList/foreach)时,查询一次都不执行,排查时表现为「代码明明写了却没效果」。
- var 只在右侧类型明确时才安全:var d = GetData(); 若返回类型被改动(如 IEnumerable 变 List),编译通过但运行期行为或性能悄悄变化。跨版本演进的数据结构不要用 var 承接。
- Task.Run 里访问 UI 控件或 HttpClient 之外的非线程安全对象会抛异常。CPU 密集计算才用 Task.Run;I/O 密集应直接 await,否则会白白占用线程池并引入竞态。
- 开启 C# 8+ 的可空引用类型后,GetValueOrDefault / 显式 null 检查漏写只是警告不是错误,最终变成运行期 NullReferenceException。升级项目时把警告级别提到 error 再逐个修,否则是埋雷。
提示
- LINQ 查询延迟执行,只在枚举结果时才真正执行。
- async/await 是异步编程的核心,避免阻塞 UI 线程。
- C# 9 引入 record 类型,适合不可变数据模型。
常见问题
Func、Action 和 Predicate 三种委托该怎么选?
看返回值和参数列表。Func<int, int, int> 表示有入参也有返回值,最后一个类型是返回值类型,本例中 add = (a, b) => a + b 是一个两参一返回的函数。Action<string> 只有入参没有返回值,适合事件回调这种「通知一声就完事」的场景,本表里 public event Action<string> OnMessage 就是这个用法。Predicate<int> 专门接收一个参数并返回 bool,isPositive = x => x > 0 属于这一类,通常用来把判断逻辑当值传递,例如交给 Where 之类的过滤入口,而不是写成完整的条件方法。
sealed、abstract 和 static 三个修饰符分别解决什么问题?
三个都是在收窄或固定类型的能力边界。public sealed class SealedClass 表示不可被继承,适合那些行为已经完整、不打算留给子类扩展的类型,密封后编译器还能对被调用的成员做更好的优化。public abstract class AbstractClass 表示只能被继承、不能直接实例化,用来承载一组子类必须实现的契约。public static class Utils 则完全相反——不可继承也不可实例化,只能直接拿它的静态成员,用作无状态的工具集合。选型时先问这个类型是「给别人实现」「封死不让扩展」还是「只提供一组函数」,答案直接对应其中一个修饰符。
where T : IComparable<T> 这类泛型约束什么时候必须加?
约束用来向编译器保证类型参数具备某些能力,编译器据此才能接受泛型体内的操作。本表的 public T Max<T>(T a, T b) where T : IComparable<T> 就是典型:如果方法体里要比较 a 和 b,就必须要求 T 实现 IComparable<T>,否则编译器无法确定 > 运算符是否可用。不写约束时编译器只能把 T 当成 object,比较、算术这类操作一律报错。加约束换来的是类型安全与更好的性能——编译器有机会为具体的值类型生成特化代码,而不是把一切都装箱成 object。
几个异步请求想同时跑,怎么写才不是串行等待?
await Task.WhenAll(task1, task2)。逐个 await 会让第二个请求等第一个返回才发出,网络往返时间被串起来;WhenAll 传入多个已启动的 Task,一次性等待它们全部完成,总耗时接近其中最慢的那个。参数可以多于两个,也可以直接传入 Task.WhenAll(tasks) 处理一个集合。前提是这些 Task 必须在调用 WhenAll 之前就已经启动——async 方法在第一次 await 处就返回控制权,所以先拿到几个 Task 变量、再统一 WhenAll,才是真正的并发。
Dictionary、HashSet、Queue、Stack 和元组分别适合什么数据?
按「要做什么操作」选。Dictionary<string, int> 存键值对,适合按 key 查找、修改与去重。HashSet<int> 只保留唯一元素,判断某值是否已存在是 O(1),适合过滤重复。Queue<int> 是先进先出,用于按到达顺序处理任务。Stack<int> 后进先出,用于回溯、撤销这类需要回到最近一步的场景。var tuple = (1, "hello") 则不是容器:它把少量强关联的值轻量绑定在一起,适合从一个方法一次返回两个结果,省去定义一个只为传值的类。
由 巧匠 维护
公开更新于 2026年7月21日,内容持续校对官方文档。