什么是委托

  • 定义:委托是一种类型安全的函数指针,封装了一个或多个具有相同签名(返回类型和参数列表相同)的方法引用。可以把委托看作是一个函数指针,但它比函数指针更安全、更灵活,因为它是面向对象的,并且 C# 的委托支持协变与逆变。
  • 作用:通过委托可以实现方法的动态调用、回调机制,支持多路广播(Multicast Delegate),一次调用可执行多方法,以及事件的发布/订阅模式。
  • 类型安全:在编译期就能保证所调用的方法签名(参数列表和返回类型)与委托类型一致。

声明格式:

public delegate TResult MyDelegate<T1,…,TResult>(T1 arg1, …);

示例

void PrintNumber(int n) {
    Console.WriteLine(n);
}

MyDelegate d = new MyDelegate(PrintNumber);
// 也可简写
MyDelegate d2 = PrintNumber;

// 调用
d(42);    // 输出:42
d.Invoke( 100 );  // 等同于 d(100)

// 声明一个返回 void、带一个 int 参数的委托
public delegate void MyDelegate(int x);

匿名方法与 Lambda 表达式

匿名方法

MyDelegate anon = delegate(int x) {
    Console.WriteLine("匿名:" + x);
};
anon(7);

// 声明一个返回 void、带一个 int 参数的委托
public delegate void MyDelegate(int x);

Lambda 表达式

MyDelegate lambda = x => Console.WriteLine("Lambda:" + x);
lambda(9);

// 声明一个返回 void、带一个 int 参数的委托
public delegate void MyDelegate(int x);

语法更为简介

多路委托

同一个委托实例可以同时封装多个方法,调用时会按注册顺序依次执行。
使用 +=-= 运算符进行注册和注销。

void PrintNumber(int n) {
    Console.WriteLine(n);
}

MyDelegate anon = delegate(int x) {
    Console.WriteLine("匿名:" + x);
};
MyDelegate lambda = x => Console.WriteLine("Lambda:" + x);


MyDelegate m = PrintNumber;
m += lambda;
m += anon;
m(1);
// 依次输出:
// 1
// Lambda:1
// 匿名:1

m -= lambda;
m(2);
// 输出:
// 2
// 匿名:2

// 声明一个返回 void、带一个 int 参数的委托
public delegate void MyDelegate(int x);

内置委托类型

.NET 提供了几种常用的泛型委托,无需自定义类型:

委托类型 签名 说明
Action 无返回值,0~16 个泛型参数 最常用的无返回值委托
Action void (T1,…,Tn)
Func TResult () 无入参,有返回值
Func TResult (T1,…,Tn) 最多支持 16 个入参
Predicate bool (T) 专用于判断,返回 bool
Action<string> printer = s => Console.WriteLine(s);
Func<int, int, int> adder = (a, b) => a + b;
Predicate<int> isEven = x => x % 2 == 0;

printer("Hello");
int sum = adder(3, 4);
bool even = isEven(10);

Console.WriteLine(sum);
Console.WriteLine(even);
// 输出:
// Hello
// 7
// True

什么是事件

  • 事件是基于委托的发布/订阅机制(publish–subscribe),用于解耦“发布者”(Publisher)与“订阅者”(Subscriber)。
  • 发布者定义事件,负责触发;订阅者注册处理函数,负责响应

声明与订阅

声明事件

// 1. 定义委托(可用内置类型)
public delegate void NotifyHandler(string message);

// 2. 在类中声明事件
public class Notifier
{
    // 外部只能 += 或 -=
    public event NotifyHandler Notify;
}

也可直接使用 EventHandlerEventHandler<TEventArgs>

public class Notifier
{
    // 标准无参事件
    public event EventHandler Initialized;

    // 带自定义参数的事件
    public event EventHandler<MyEventArgs> DataReceived;
}

public class MyEventArgs : EventArgs
{
    public string Data { get; }
    public MyEventArgs(string data) => Data = data;
}

订阅与退订

void TestHandler(string message)
{
    Console.WriteLine("收到通知:" + message);
}

var n = new Notifier();

// 订阅
n.Notify += TestHandler;

n.Invoke("Hello");
// 退订
n.Notify -= TestHandler;
n.Invoke("World");


public delegate void NotifyHandler(string message);

public class Notifier
{
    // event 外部只能 += 或 -=
    public event NotifyHandler? Notify;
  
    public void Invoke(string message) => Notify?.Invoke(message); 
}


// 输出:
// 收到通知:Hello

触发

public class Notifier
{
    public event EventHandler<MyEventArgs> DataReceived;

    // 安全触发模式
    protected virtual void OnDataReceived(MyEventArgs e)
        => DataReceived?.Invoke(this, e);

    public void Receive(string data)
    {
        // …处理逻辑…
        OnDataReceived(new MyEventArgs(data));
    }
}
  • 空检查DataReceived? 确保没有订阅者时不抛空引用。
  • 封装触发:把 Invoke 放在受保护的虚方法里,子类可重写。

示例场景

文件下载进度

// 使用者订阅
var downloader = new FileDownloader();
downloader.ProgressChanged += (_, e) =>
    Console.WriteLine($"下载 {e.Url}:{e.Progress}%{(e.Completed?" 完成":"")}");

await downloader.DownloadAsync("https://example.com/file.zip");


public class DownloadEventArgs : EventArgs
{
    public string Url { get; }
    public int Progress { get; }
    public bool Completed { get; }
    public DownloadEventArgs(string url, int progress, bool done)
        => (Url, Progress, Completed) = (url, progress, done);
}

public class FileDownloader
{
    public event EventHandler<DownloadEventArgs>? ProgressChanged;

    protected virtual void OnProgressChanged(DownloadEventArgs e)
        => ProgressChanged?.Invoke(this, e);

    public async Task DownloadAsync(string url)
    {
        for (int p = 0; p <= 100; p += 10)
        {
            await Task.Delay(100);
            OnProgressChanged(new DownloadEventArgs(url, p, p == 100));
        }
    }
}

// 输出
// 下载 https://example.com/file.zip:0%
// 下载 https://example.com/file.zip:10%
// 下载 https://example.com/file.zip:20%
// 下载 https://example.com/file.zip:30%
// 下载 https://example.com/file.zip:40%
// 下载 https://example.com/file.zip:50%
// 下载 https://example.com/file.zip:60%
// 下载 https://example.com/file.zip:70%
// 下载 https://example.com/file.zip:80%
// 下载 https://example.com/file.zip:90%
// 下载 https://example.com/file.zip:100% 完成
  • 职责分离FileDownloader 只负责下载并触发事件;
  • 订阅者:只需通过 += 获取进度更新,完全不关心内部实现。

窗体按钮点击事件处理

在 WinForms、WPF、ASP.NET 等界面和后台框架开发中,按钮点击等 UI 事件本质上就是基于委托+事件实现的。不用在按钮内部硬编码逻辑,而是把处理逻辑“注入”进去。

// 定义一个事件(基于 EventHandler 委托)
public event EventHandler<ButtonClickEventArgs> ButtonClicked;

// 在按钮控件内部触发事件
protected void OnClick()
{
    ButtonClicked?.Invoke(this, new ButtonClickEventArgs(/*…*/));
}

// 在使用者处订阅并提供回调
myButton.ButtonClicked += (s, e) =>
{
    // 这里就是响应按钮点击的逻辑
    MessageBox.Show("用户点击了按钮,参数:" + e.SomeValue);
};

自定义排序(Strategy 模式)

假设有一个产品列表,需要动态地按价格、按销量或按评分排序。可以定义一个接收比较函数的通用方法:

// 使用内置的 Comparison<T> 委托(签名:int (T,T))
void SortProducts(List<Product> list, Comparison<Product> comparison)
{
    list.Sort(comparison);
}

// 调用端按不同策略传入不同比较方法
SortProducts(products, (a, b) => a.Price.CompareTo(b.Price));      // 按价格升序
SortProducts(products, (a, b) => b.Sales.CompareTo(a.Sales));     // 按销量降序
SortProducts(products, (a, b) => b.Rating.CompareTo(a.Rating));   // 按评分降序

定时任务调度

使用 System.Timers.Timer 时,通过 Elapsed 事件来注册定时执行的回调:

var timer = new System.Timers.Timer(1000); // 每 1 秒触发一次
timer.Elapsed += (s, e) =>
{
    Console.WriteLine($"[{e.SignalTime}] 定时任务执行中…");
};
timer.Start();

与其他语言对比

Python

C# 多路委托

Action onSave = SaveToFile;
onSave += SaveToDatabase;
onSave += NotifyUser;
// 一次 onSave(),文件、数据库、通知同时触发
onSave();

Python 列表循环

callbacks = [save_to_file, save_to_db, notify_user]
for cb in callbacks:
    cb()
  • 得先维护一个列表,然后循环触发。
  • C# 委托像“电话会议”,拨一个会议号,所有人都自动接入;
  • Python 列表回调,像“一个人一个电话”,得一个个打电话通知。

Java

C#(基于 event + 委托):

var btn = new Button();
btn.Click += (_, _) => Console.WriteLine("C# Button clicked!");
btn.SimulateClick();


public delegate void ClickHandler(object sender, EventArgs e);

public class Button
{
    public event ClickHandler? Click; 

    public void SimulateClick()
        => Click?.Invoke(this, EventArgs.Empty);
}

Java(基于接口 + 监听器):

Button btn = new Button();
btn.addClickListener(sender -> System.out.println("Java Button clicked!"));
btn.simulateClick();



public interface ClickListener {
    void onClick(Object sender);
}

public class Button {
    private List<ClickListener> listeners = new ArrayList<>();

    public void addClickListener(ClickListener l) {
        listeners.add(l);
    }
    public void removeClickListener(ClickListener l) {
        listeners.remove(l);
    }
    public void simulateClick() {
        for (ClickListener l : listeners)
        l.onClick(this);
    }
}