委托是什么:把方法当参数传

普通函数只能传数据,而委托(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 等翻译。
C# 高级特性配图:蓝紫渐变背景上左侧多条彩色发光线条汇入中央一个发光的集线器方块,集线器再分叉连到右侧三个接收小方块,中间有漏斗状变换装置把一束线条整理成整齐输出
多条数据线汇入事件中心,再分发到各个接收方——这正是委托与事件的发布订阅拓扑。

事件 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# 把它做得对业务代码极其透明。