VoociiBlog
HomeBlogAI TrendingPortfolioBooksLinksToolsAbout


Voocii© 2026. Built with Next.js & tRPC.
GitHubXEmailRSS

初步理解.NET 委托

dotnetrick-hayekrick-hayek2024年1月15日

先搞清楚一件事:委托到底是什么?

很多人一看到 delegate 这个词就觉得很抽象。其实说白了:

委托就是"能被当作变量传来传去的方法"。

对刚接触 C# 的开发者来说,可以先把普通方法理解为“直接调用的代码”;而委托允许我们把一个方法保存起来,再把它传给其他方法。委托本身是一个“类型”,描述的是一种方法的签名(参数和返回值长什么样)。

举个生活化的类比:

  • 普通方法 = 你雇的一个只会干一件事的员工,你只能直接喊他干活。
  • 委托 = 一张"工作说明书"(长啥样的活可以塞进来),你把符合说明书要求的员工的"电话号码"存起来,需要的时候打电话让他干活,甚至可以一次性打给好几个人。

一个最原始的自定义委托:

// 定义一个委托类型:描述"接受两个 int,返回 int"的方法长什么样
public delegate int MathOperation(int a, int b);

class Program
{
    static int Add(int a, int b) => a + b;
    static int Multiply(int a, int b) => a * b;

    static void Main()
    {
        // 把方法本身(不是调用结果)赋值给委托变量
        MathOperation op = Add;
        Console.WriteLine(op(3, 4)); // 输出 7

        op = Multiply;
        Console.WriteLine(op(3, 4)); // 输出 12
    }
}

看到了吗?op 这个变量里保存了一个可以被调用的方法。委托的核心作用就是:让方法可以被保存、传递,并在以后调用。

新的数据类型?

可以从另一个方面理解,C#里面有整型关键字int,字符串关键字string等数据类型。你定义了一个int变量之后,就可以把它传给函数了:

int count = 100;
Console.WriteLine(count);

而委托delegate的作用是,创建一个新的类型(类比int,string):

public delegate int MyNewType(int a);

现在,你可用的类型,除了int,string等C#语法里支持的之外,又多了个MyNewType。同样,你定义了一个MyNewType变量之后,就可以把它传给函数了:

// MyNewType 是类型;abs 是变量 - 只不过在使用abs时需要传一个参数
MyNewType abs = input => Math.Abs(input);
Console.WriteLine(abs(-1));

内置委托

如果你自己每次都要写 delegate int MathOperation(int a, int b) 这种声明,会比较啰嗦。所以 .NET 提供了几个常用的内置委托类型,最常见的是 Action、Func、Predicate;事件场景中还经常使用 EventHandler。


Action:没有返回值的委托

Action 用于"只干活,不汇报结果"的方法——也就是返回值是 void 的方法。

// Action 不带参数
Action sayHello = () => Console.WriteLine("Hello!");
sayHello();

// Action<T> 带一个参数
Action<string> greet = name => Console.WriteLine($"Hello, {name}!");
greet("Rick");

Action<string, int> printInfo = (name, age) =>
    Console.WriteLine($"{name} is {age} years old.");
printInfo("Rick", 30);

实际场景:日志记录、UI 按钮点击处理、遍历集合时对每个元素做点什么。

List<string> names = new() { "Alice", "Bob", "Charlie" };

// List.ForEach 接受一个 Action<T> 参数
names.ForEach(name => Console.WriteLine($"你好, {name}"));

一句话记忆:Action = "做点事,不用告诉我结果"。


Func:有返回值的委托

Func 用于"干完活要汇报结果"的方法。语法规则是:最后一个泛型参数是返回值类型,前面的都是入参。

// Func<TResult>:无参数,返回 int
Func<int> getRandomNumber = () => new Random().Next(100);
Console.WriteLine(getRandomNumber());

// Func<T, TResult>:一个参数,返回 bool
Func<int, bool> isEven = n => n % 2 == 0;
Console.WriteLine(isEven(4)); // true

// Func<T1, T2, TResult>:两个参数,返回 int
Func<int, int, int> add = (a, b) => a + b;
Console.WriteLine(add(3, 4)); // 7

实际场景:LINQ 几乎全靠 Func 驱动。

List<int> numbers = new() { 1, 2, 3, 4, 5, 6 };

// Select 需要一个 Func<int, TResult>:把每个元素"转换"成新值
var squared = numbers.Select(n => n * n);

// Where 需要一个 Func<int, bool>:判断每个元素是否保留
var evens = numbers.Where(n => n % 2 == 0);

Console.WriteLine(string.Join(", ", squared)); // 1, 4, 9, 16, 25, 36
Console.WriteLine(string.Join(", ", evens));   // 2, 4, 6

一句话记忆:Func = "做点事,然后把结果还给我"。


Predicate:专门用来"判断真假"的委托

Predicate<T> 其实就是 Func<T, bool> 的一个"专用马甲"——它只接受一个参数,永远返回 bool,专门用来表达"这个东西符不符合条件"。

Predicate<int> isNegative = n => n < 0;

List<int> numbers = new() { -3, -1, 0, 2, 5 };

// List<T>.Find 接受 Predicate<T>
int firstNegative = numbers.Find(isNegative);
Console.WriteLine(firstNegative); // -3

// FindAll、RemoveAll 也都用 Predicate<T>
List<int> negatives = numbers.FindAll(n => n < 0);
numbers.RemoveAll(n => n < 0);

为什么有了 Func<T, bool> 还要 Predicate<T>? 纯粹是历史原因——Predicate<T> 是 .NET Framework 早期(List<T> 的 Find/FindAll/RemoveAll 系列方法)就存在的类型,而 Func 家族是后来(.NET 3.5,配合 LINQ)才引入的。两者语义完全等价,能互相赋值:

Predicate<int> p = n => n > 0;
Func<int, bool> f = n => n > 0;

// 两者是不同的委托类型,需要通过 lambda 重新包装
Func<int, bool> converted = n => p(n);

现在写新代码时,大部分场景(尤其是 LINQ)直接用 Func<T, bool> 就够了;调用 List<T> 的 Find、FindAll、RemoveAll 等方法时,仍然会遇到 Predicate<T>。


EventHandler:为"事件"量身定做的委托

前面三种委托解决的是"把方法当参数传递"的问题,而 EventHandler 解决的是另一个问题:一个对象发生了某件事,通知所有关心这件事的人。这就是"发布-订阅"模式,在 .NET 里靠 event 关键字 + EventHandler 委托实现。

public class Button
{
    // 标准写法:EventHandler 委托 + event 关键字
    public event EventHandler? Clicked;

    public void SimulateClick()
    {
        Console.WriteLine("按钮被点击了...");
        // 触发事件:通知所有订阅者
        Clicked?.Invoke(this, EventArgs.Empty);
    }
}

class Program
{
    static void Main()
    {
        var button = new Button();

        // 订阅事件(多个订阅者都会被通知)
        button.Clicked += (sender, e) => Console.WriteLine("订阅者A:我知道按钮被点了!");
        button.Clicked += (sender, e) => Console.WriteLine("订阅者B:我也知道了!");

        button.SimulateClick();
    }
}

输出:

按钮被点击了...
订阅者A:我知道按钮被点了!
订阅者B:我也知道了!

EventHandler 的签名是固定的:void EventHandler(object? sender, EventArgs e)。如果你需要携带自定义数据,用泛型版本 EventHandler<TEventArgs>:

public class OrderEventArgs : EventArgs
{
    public string OrderId { get; init; } = "";
    public decimal Amount { get; init; }
}

public class OrderService
{
    public event EventHandler<OrderEventArgs>? OrderCreated;

    public void CreateOrder(string orderId, decimal amount)
    {
        // 处理订单逻辑...
        OrderCreated?.Invoke(this, new OrderEventArgs { OrderId = orderId, Amount = amount });
    }
}

// 订阅时能拿到强类型的数据
var service = new OrderService();
service.OrderCreated += (sender, e) =>
    Console.WriteLine($"新订单:{e.OrderId},金额:{e.Amount}");

service.CreateOrder("ORD-001", 99.9m);

为什么不直接用 Action<object, EventArgs> 代替 EventHandler? 技术上可以做到功能类似,但 event 关键字配合 EventHandler 是 .NET 里的约定俗成。WinForms、WPF 以及许多 .NET 类库中的事件都遵循 sender + EventArgs 的签名规范,用标准类型能让你的 API 一眼就能被其他 .NET 开发者理解。


四种委托怎么选?一张表记住

委托类型有无返回值典型场景
Action<T...>无(void)执行一个动作、回调、遍历处理
Func<T..., TResult>有计算、转换、LINQ 查询
Predicate<T>有(固定 bool)条件判断(多见于 List<T> 老方法)
EventHandler / EventHandler<T>无(void)对象内部状态变化,通知外部订阅者

扩展一:什么时候还需要自定义委托?

大部分时候内置的 Action/Func 就够用了。但如果你想让签名有明确的业务语义,自定义委托能提升可读性:

// 语义清晰:一眼就知道这是"验证逻辑",而不是随便一个 Func<string, bool>
public delegate bool Validator(string input);

public class FormValidator
{
    public List<Validator> Rules { get; } = new();

    public bool IsValid(string input) => Rules.All(rule => rule(input));
}

var validator = new FormValidator();
validator.Rules.Add(input => !string.IsNullOrEmpty(input));
validator.Rules.Add(input => input.Length <= 20);

Console.WriteLine(validator.IsValid("Rick")); // true

另外,委托还支持多播(multicast)——一个委托变量可以同时挂载多个方法,这也是 event 常见的底层实现方式。多播委托最适合返回值为 void 的场景;如果多个方法都有返回值,调用者通常只能得到最后一个方法的返回值。

Action pipeline = () => Console.WriteLine("Step 1");
pipeline += () => Console.WriteLine("Step 2");
pipeline += () => Console.WriteLine("Step 3");

pipeline(); // 依次输出 Step 1、Step 2、Step 3

扩展二:还有哪些地方会用到委托?

上面介绍的是最常见的几种委托类型。实际上,.NET 中还有很多 API 会把委托作为参数,用来表示“稍后要执行的代码”或“由调用方提供的处理逻辑”,例如:

  • Task.Run:接收需要执行的代码;
  • CancellationToken.Register:接收取消发生时要执行的回调;
  • List<T>.ForEach:接收遍历每个元素时要执行的操作;
  • LINQ 的 Where、Select:接收筛选条件或转换逻辑;
  • ThreadStart:表示线程启动时要执行的方法。

例如:

Task.Run(() => Console.WriteLine("在线程池中执行"));

cancellationToken.Register(() =>
    Console.WriteLine("操作被取消"));

这些 API 的共同点都是:把一段代码作为参数传给另一个方法。这里不需要先理解线程池或异步状态机,只要先记住“委托可以传递一段待执行的代码”即可。

如果想继续了解更多委托类型和具体 API,可以参考 .NET 委托官方文档。


小结

  • 委托 = 可以被当变量传递的方法。
  • Action 没有返回值;Func 有返回值;Predicate 是专门判断 true/false 的 Func<T, bool>。
  • EventHandler 是委托在"发布-订阅"模式下的标准化应用,配合 event 关键字使用。
  • 需要业务语义清晰时可以自定义委托;委托还支持多播。
  • 很多 .NET API(例如 Task.Run、CancellationToken.Register、List<T>.ForEach 和 LINQ)都会利用委托来传递一段待执行的代码。

把这几个类型吃透,再回头看 LINQ、事件驱动 UI、异步编程这些"看起来很高级"的 API,你会发现它们全都是同一套思路的不同应用而已。

Comments (0)

No comments yet. Be the first!

On this page
  • 先搞清楚一件事:委托到底是什么?
  • Action:没有返回值的委托
  • Func:有返回值的委托
  • Predicate:专门用来"判断真假"的委托
  • EventHandler:为"事件"量身定做的委托
  • 四种委托怎么选?一张表记住
  • 扩展一:什么时候还需要自定义委托?
  • 扩展二:还有哪些地方会用到委托?
  • 小结