委托是什么:把方法当参数传
普通函数只能传数据,而委托(delegate)让你能把一个方法本身当作参数传给另一个方法。你可以把委托理解成"装着方法的盒子"——谁往盒子里塞一个方法,将来调这个委托时就会执行它。
// 声明一个委托类型:签名是「吃一个 int,返回 int」
delegate int Calc(int x);
static int Double(int n) => n * 2;
static void Main() {
Calc c = Double; // 把方法装进委托
Console.WriteLine(c(5)); // 10
}
现实中你很少手写自定义委托,因为 .NET 已经给好了通用的 Func<T, TResult> 和 Action<T>:Func 有返回值,Action 没有。绝大多数"传个方法进去"的场景都用它们。
委托的意义:把"做什么"这件事从写死在代码里,变成可以运行时再决定。这是回调、事件、LINQ 的共同地基。
- delegate类型化的方法引用,能把方法当参数传递。
- event基于委托的发布订阅,外部只能订阅不能随意触发。
- LINQ用 where/select/orderby 链式查询集合,默认延迟执行。
- 表达式树把 lambda 变成可遍历的数据结构,供 EF 等翻译。
事件 event:发布与订阅
事件是对委托的一层封装:发布者在某个时机"触发"事件,所有订阅者的方法就被自动调用。用 event 关键字修饰后,外部只能用 += 订阅、-= 退订,不能从外部随意触发——这就把"谁能发、谁能收"分开了。
public class Door {
public event Action OnOpened; // 声明事件
public void Open() {
Console.WriteLine("门开了");
OnOpened?.Invoke(); // 触发事件
}
}
// 订阅方
var door = new Door();
door.OnOpened += () => Console.WriteLine("收到通知:响铃");
door.Open();
OnOpened?.Invoke() 里的问号表示:如果没人订阅,就别触发,避免空引用。事件是 GUI 里"点按钮触发回调"、消息组件里"发通知给多方"的标准实现方式。
LINQ 再深入:链式查询的本质
LINQ 让你用类似 SQL 的语气筛选、投影、排序集合。它的本质是一堆扩展方法,每个都接收一个 Func 委托,然后返回一个新的可枚举对象。方法可以一路点下去串成链。
var nums = new[] { 1, 2, 3, 4, 5, 6 };
var result = nums
.Where(n => n % 2 == 0) // 留下偶数
.Select(n => n * n) // 平方
.OrderBy(x => x);
foreach (var x in result) Console.WriteLine(x); // 4 16 36
每个 lambda(如 n => n % 2 == 0)其实就是传给 Where 的一个 Func 委托。理解了委托是地基,你就明白 LINQ 为什么能把"怎么筛、怎么投影"交给你自由定义。
延迟执行:where 不是马上跑
LINQ 一个关键且反直觉的点:Where、Select 这些操作默认不会立刻执行,只是记下"打算怎么处理"。真正开始跑,是在你遍历结果(foreach、ToArray)的那一刻。这叫延迟执行。
var query = nums.Where(n => {
Console.WriteLine("检查 " + n);
return n > 3;
});
// 上面这一行什么都没打印——还没执行
Console.WriteLine("现在开始遍历");
foreach (var x in query) Console.WriteLine(x); // 这里才逐个检查
延迟执行的好处:你可以先把查询"配方"组装好,最后才一次性执行;而且每次遍历都会重新跑一遍。坏处是同一个 query 被枚举两次就会算两遍,需要时用 ToList() 立刻落一份缓存。
表达式树:把代码翻译成数据
普通 lambda 编译成一段可执行的委托。但还有一种写法——放进 Expression<Func<T,bool>>——这时 C# 不会立刻执行它,而是把这段代码拆成一棵可以遍历的"语法树"数据。
using System.Linq.Expressions;
Expression<Func<int, bool>> pred = n => n > 3;
// pred.Body 就是「n > 3」这个结构,可以被读取、分析、改写
BinaryExpression body = (BinaryExpression)pred.Body;
Console.WriteLine(body.NodeType); // GreaterThan
这正是 EF Core 能把 where(x => x.Age > 18) 翻译成 SQL WHERE Age > 18 的原因:它拿到表达式树,逐节点翻译成 SQL 片段。你写的 C# 代码,在这里变成了可以被翻译的中间数据。
异步 async/await:C# 的招牌
C# 的 async/await 是它最广为人知的特性:写网络请求、读文件这类 I/O 操作时,用 async 标记方法、await 等待结果,主线程不必干等,界面也不卡。
async Task<string> FetchAsync(string url) {
using var http = new HttpClient();
string html = await http.GetStringAsync(url); // 不卡线程
return html;
}
// 调用处
string page = await FetchAsync("https://example.com");
关键心智模型:await 不是"阻塞等待",而是"把后面的工作登记成回调,等结果到了再接着跑"。这与下一篇 Rust 的 tokio 异步,本质是同一件事——只是 C# 把它做得对业务代码极其透明。
实战:用 LINQ 与委托处理数据
串起来:定义一组数据,用委托把"筛选条件"参数化,再用 LINQ 链一气呵成地过滤、分组、投影。
var students = new[] {
new { Name = "甲", Score = 91 },
new { Name = "乙", Score = 58 },
new { Name = "丙", Score = 76 }
};
// 筛选条件本身是一个委托
Func<dynamic, bool> pass = s => s.Score >= 60;
var names = students
.Where(pass)
.OrderByDescending(s => s.Score)
.Select(s => s.Name);
foreach (var n in names) Console.WriteLine(n); // 甲 丙
注意 pass 这个委托:你可以在运行时换一个不同的条件传进去,同一套 LINQ 链就筛出不同结果。委托让"规则"成了可替换的参数,LINQ 让"处理流程"成了可读的一条链。
高级特性检查清单
把 C# 这几样记成一条链:委托是地基,事件是封装,LINQ 是应用,表达式树是把代码变数据,async/await 是异步门面。提交代码前对照自查:
- 需要传方法时,优先用现成的 Func/Action,而不是自定义委托?
- 对外暴露的回调是不是用了 event,而不是直接暴露 public 委托字段?
- 清楚 LINQ 是延迟执行的吗?重复枚举时有没有必要 ToList 缓存?
- 需要把查询翻译成 SQL 时,用的是 Expression 而不是普通委托?
- async 方法有没有一路 await 到底,避免了 .Result 之类的阻塞死锁?
C# 的高级特性层层递进:懂了委托,事件、LINQ、async 都是它在不同方向上的延伸。把地基砸实,上面的花样就一通百通。
下一篇我们看看另一门把异步做到极致的语言:Rust 与 tokio 运行时,是怎么在没有 GC 的情况下高效跑成千上万任务的。