目 录CONTENT

文章目录

CSharp(四十) 委托与多播委托详解

C# 委托与多播委托详解


一、什么是委托?

1.1 先从一个生活场景说起

你有没有用过"外卖平台"?你下单后,外卖平台帮你找到骑手去送餐。

委托在 C# 里的角色,就像这个外卖平台

  • 你不需要自己跑到餐厅去取餐
  • 你只需要告诉平台"我要这个餐厅的饭"
  • 平台帮你找到对应的方法(骑手)去执行

委托就是用来"传递方法"的工具——把方法当作参数传给另一个方法。

1.2 为什么需要委托?

没有委托时,你有 3 个不同的操作:

// 三个不同的操作
void Add(int a, int b)      { Console.WriteLine(a + b); }
void Multiply(int a, int b) { Console.WriteLine(a * b); }
void Subtract(int a, int b) { Console.WriteLine(a - b); }

// ❌ 想统一调用它们,只能写死
Add(3, 5);
Multiply(3, 5);
Subtract(3, 5);

有个需求:用户选什么操作,就执行什么操作。传统的写法是这样:

string operation = "Add";  // 用户选择的

// ❌ 只能用 if-else 或者 switch,每加一个新操作都要改代码
if (operation == "Add")      Add(3, 5);
else if (operation == "Mul") Multiply(3, 5);
else if (operation == "Sub") Subtract(3, 5);

有了委托后,你可以把方法当成参数传进去

// ✅ 定义一个委托类型——它说的是"我的方法长什么样"
delegate void Calculate(int a, int b);

// 把方法当成参数
static void Execute(Calculate calc, int x, int y)
{
    calc(x, y);  // 执行传进来的方法
}

// 调用时,直接传方法名
Execute(Add, 3, 5);       // 输出: 8
Execute(Multiply, 3, 5);  // 输出: 15
Execute(Subtract, 3, 5);  // 输出: -2

一句话总结:委托 = 方法的"传送带",让你把方法像数据一样传来传去。


二、定义和使用委托

2.1 定义委托(四步走)

// 语法:delegate 返回值类型 委托名称(参数列表);

// 步骤1:定义一个委托类型(说清楚这个委托能装什么形状的方法)
delegate void MyDelegate(string message);

// 步骤2:写几个匹配的方法("形状"要一模一样:返回值 + 参数列表)
static void PrintEnglish(string msg) { Console.WriteLine($"英文: {msg}"); }
static void PrintChinese(string msg) { Console.WriteLine($"中文: {msg}"); }
static void PrintUppercase(string msg) { Console.WriteLine($"大写: {msg.ToUpper()}"); }

// 步骤3:创建委托实例,绑定方法
MyDelegate del1 = PrintEnglish;  // 新建语法
MyDelegate del2 = new MyDelegate(PrintChinese);  // 老式语法,也能用

// 步骤4:调用委托(就像直接调用方法一样)
del1("Hello");  // 输出: 英文: Hello
del2("你好");   // 输出: 中文: 你好

2.2 完整基础示例

using System;

// 1. 定义委托
delegate int MathOperation(int a, int b);

class Program
{
    // 2. 写几个匹配的方法
    static int Add(int a, int b)      => a + b;
    static int Subtract(int a, int b) => a - b;
    static int Multiply(int a, int b) => a * b;
    static int Divide(int a, int b)   => a / b;

    // 3. 用委托作为参数——"计算器核心"
    static int Calculator(MathOperation op, int x, int y)
    {
        return op(x, y);  // 调用传进来的方法
    }

    static void Main()
    {
        int result;

        result = Calculator(Add, 10, 3);       // 输出: 13
        Console.WriteLine($"10 + 3 = {result}");

        result = Calculator(Subtract, 10, 3);  // 输出: 7
        Console.WriteLine($"10 - 3 = {result}");

        result = Calculator(Multiply, 10, 3);  // 输出: 30
        Console.WriteLine($"10 * 3 = {result}");

        result = Calculator(Divide, 10, 3);    // 输出: 3
        Console.WriteLine($"10 / 3 = {result}");
    }
}

输出:

10 + 3 = 13
10 - 3 = 7
10 * 3 = 30
10 / 3 = 3

三、委托的本质理解(图文详解)

3.1 委托 = 方法的"容器"

委托就像一个方法签名模板,只有"形状匹配"的方法才能放进去。

委托类型:delegate int MathOperation(int a, int b);
         └──返回值──┘               └──参数──┘

能装的方法:
  ✅ int Add(int a, int b)      → 形状匹配
  ✅ int Subtract(int a, int b) → 形状匹配
  ✅ int Multiply(int a, int b) → 形状匹配
  ❌ void Print(string s)       → 形状不匹配(返回值是 void)
  ❌ int Square(int x)          → 只能算是匹配,但语义上没问题
  ❌ double Divide(int a, int b)→ 形状不匹配(返回值是 double)

3.2 委托的内部原理(简化理解)

MathOperation op = Add;

内存中的 op:
┌──────────────────────┐
│  op:委托实例         │
│  ┌────────────────┐  │
│  │ Target: null    │  │ ← 静态方法时是 null
│  │ Method: Add     │  │ ← 指向 Add 方法的地址
│  └────────────────┘  │
└──────────────────────┘

当你调用 op(10, 3) 时,等价于调用了 Add(10, 3)

四、委托的三种创建方式

delegate void MyDelegate(string msg);

static void Print(string msg) => Console.WriteLine(msg);

// 方式1:直接赋值(最常用)
MyDelegate d1 = Print;

// 方式2:完整 new 语法(老式写法)
MyDelegate d2 = new MyDelegate(Print);

// 方式3:匿名方法(C# 2.0)
MyDelegate d3 = delegate(string msg)
{
    Console.WriteLine($"匿名方法说: {msg}");
};

// 方式4:Lambda 表达式(C# 3.0+,最简洁)
MyDelegate d4 = msg => Console.WriteLine($"Lambda 说: {msg}");

// 方式5:已有方法的 Lambda
MyDelegate d5 = msg => Print(msg.ToUpper());

// 全部都能调
d1("Hello");  // Hello
d2("Hello");  // Hello
d3("Hello");  // 匿名方法说: Hello
d4("Hello");  // Lambda 说: Hello
d5("Hello");  // HELLO

五、多播委托

5.1 什么是多播委托?

多播委托 = 一个委托"链"上可以挂多个方法,调用一次,所有方法依次执行。

打个比喻:

普通委托像给一个人打电话——你说一句话,一个人听到。
多播委托像广播喇叭——你说一句话,全校人都能听到。

5.2 创建多播委托(用 + 号串联)

delegate void Notify(string msg);

static void SendEmail(string msg)    => Console.WriteLine($"📧 邮件通知: {msg}");
static void SendSMS(string msg)      => Console.WriteLine($"📱 短信通知: {msg}");
static void SendAppPush(string msg)  => Console.WriteLine($"🔔 APP推送: {msg}");
static void LogToFile(string msg)    => Console.WriteLine($"📝 日志记录: {msg}");

static void Main()
{
    // 一步步串联
    Notify notifier = SendEmail;       // 先挂上邮件
    notifier += SendSMS;              // 再挂上短信
    notifier += SendAppPush;          // 再挂上 APP 推送
    notifier += LogToFile;            // 再挂上日志

    // 调用一次,四个方法全部执行!
    Console.WriteLine("=== 多播委托调用 ===");
    notifier("您的订单已发货!");
}

输出:

=== 多播委托调用 ===
📧 邮件通知: 您的订单已发货!
📱 短信通知: 您的订单已发货!
🔔 APP推送: 您的订单已发货!
📝 日志记录: 您的订单已发货!

5.3 移除方法(用 - 号)

Notify notifier = SendEmail;
notifier += SendSMS;
notifier += SendAppPush;
notifier += LogToFile;

Console.WriteLine($"当前挂载了 {notifier.GetInvocationList().Length} 个方法");

// 移除短信通知
notifier -= SendSMS;
Console.WriteLine($"移除短信后,剩 {notifier.GetInvocationList().Length} 个方法");

Console.WriteLine("\n=== 移除后调用 ===");
notifier("系统维护中...");

输出:

当前挂载了 4 个方法
移除短信后,剩 3 个方法

=== 移除后调用 ===
📧 邮件通知: 系统维护中...
🔔 APP推送: 系统维护中...
📝 日志记录: 系统维护中...

5.4 多播委托的返回值(重要!)

如果委托有返回值,多播委托只返回最后一个方法的返回值

delegate int GetNumber();

static int GetOne()   { Console.WriteLine("GetOne 执行");   return 1; }
static int GetTwo()   { Console.WriteLine("GetTwo 执行");   return 2; }
static int GetThree() { Console.WriteLine("GetThree 执行"); return 3; }

static void Main()
{
    GetNumber chain = GetOne;
    chain += GetTwo;
    chain += GetThree;

    int result = chain();   // 三个方法都执行了!
    Console.WriteLine($"\n返回的值: {result}");  // 3 ← 只有最后一个的返回值!
}

输出:

GetOne 执行
GetTwo 执行
GetThree 执行

返回的值: 3

注意:中间方法的返回值全部被丢弃!如果每个返回值都重要,需要手动遍历。

5.5 手动获取每个方法的返回值

GetNumber chain = GetOne;
chain += GetTwo;
chain += GetThree;

// 手动遍历委托链中的每个方法
Delegate[] methods = chain.GetInvocationList();
foreach (GetNumber method in methods)
{
    int result = method();
    Console.WriteLine($"  → 返回: {result}");
}

输出:

GetOne 执行
  → 返回: 1
GetTwo 执行
  → 返回: 2
GetThree 执行
  → 返回: 3

5.6 多播委托的异常处理

如果一个方法抛异常,后面的方法不会执行:

delegate void Action();

static void Do1() { Console.WriteLine("步骤1 完成"); }
static void Do2() { Console.WriteLine("步骤2 完成"); }
static void Do3() { throw new Exception("步骤3 出错!"); }
static void Do4() { Console.WriteLine("步骤4 完成"); }

Action chain = Do1;
chain += Do2;
chain += Do3;
chain += Do4;

try
{
    chain();
}
catch (Exception ex)
{
    Console.WriteLine($"捕获异常: {ex.Message}");
    // Do4 没有执行!
}

输出:

步骤1 完成
步骤2 完成
捕获异常: 步骤3 出错!

安全的做法——手动遍历:

Action chain = Do1;
chain += Do2;
chain += Do3;
chain += Do4;

foreach (Action method in chain.GetInvocationList())
{
    try
    {
        method();
    }
    catch (Exception ex)
    {
        Console.WriteLine($"⚠️ 方法执行失败: {ex.Message}");
    }
}

输出:

步骤1 完成
步骤2 完成
⚠️ 方法执行失败: 步骤3 出错!
步骤4 完成          ← 继续执行了!

六、Action、Func、Predicate —— 内置委托(不用自己定义!)

C# 已经帮你定义好了常用的委托类型,绝大多数情况下不需要自己定义

6.1 Action —— 无返回值

// Action:可以有 0~16 个参数,无返回值

Action hello = () => Console.WriteLine("你好!");
Action<string> print = msg => Console.WriteLine(msg);
Action<string, int> repeat = (msg, n) =>
{
    for (int i = 0; i < n; i++)
        Console.Write(msg);
    Console.WriteLine();
};

hello();           // 你好!
print("C# 真好学"); // C# 真好学
repeat("⭐", 5);    // ⭐⭐⭐⭐⭐

6.2 Func —— 有返回值

// Func:最后一个类型参数是返回值类型,前面是参数类型

// Func<TResult>  —— 无参数,返回 TResult
Func<int> getRandom = () => new Random().Next(1, 100);
Console.WriteLine(getRandom());  // 比如: 42

// Func<T, TResult>  —— 一个参数,返回 TResult
Func<int, int> square = x => x * x;
Console.WriteLine(square(5));    // 25

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

// Func<T1, T2, T3, TResult>  —— 三个参数
Func<string, string, string> concat = (s1, s2) => s1 + s2;
Console.WriteLine(concat("Hello", "World")); // HelloWorld

6.3 Predicate —— 返回 bool

// Predicate<T>:接收一个 T 参数,返回 bool(等价于 Func<T, bool>)

Predicate<int> isPositive = n => n > 0;
Console.WriteLine(isPositive(5));   // True
Console.WriteLine(isPositive(-3));  // False

// 常用于集合的 Find、FindAll 等方法
List<int> nums = new List<int> { -3, -1, 0, 2, 5, 8 };
int firstPositive = nums.Find(isPositive);
Console.WriteLine(firstPositive);  // 2

6.4 内置委托速查表

委托类型 参数 返回值 示例用法
Action Action act = () => ...
Action<T> 1 个 Action<string> print = s => ...
Action<T1,T2> 2 个 Action<int,int> = (a,b) => ...
... 最多 16
Func<TResult> 1 个 Func<int> r = () => 42
Func<T,TResult> 1 个 1 个 Func<int,int> sq = x => x*x
Func<T1,T2,TResult> 2 个 1 个 Func<int,int,int> add
... 最多 16 1 个
Predicate<T> 1 个 bool Predicate<int> p = x => x>0

建议:尽量不要自己定义委托了,直接用 ActionFunc 就行。


七、重写上面的例子——用 Func 代替自定义委托

把之前的计算器改成用 Func:

using System;

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

    // ✅ 参数不用自定义 delegate,直接写 Func<int, int, int>
    static int Calculator(Func<int, int, int> operation, int x, int y)
    {
        return operation(x, y);
    }

    static void Main()
    {
        Console.WriteLine($"10 + 3 = {Calculator(Add, 10, 3)}");
        Console.WriteLine($"10 - 3 = {Calculator(Subtract, 10, 3)}");
        Console.WriteLine($"10 * 3 = {Calculator(Multiply, 10, 3)}");
        Console.WriteLine($"10 / 3 = {Calculator(Divide, 10, 3)}");

        // 甚至可以直接传 Lambda!
        Console.WriteLine($"取最大值: {Calculator((a, b) => a > b ? a : b, 10, 3)}");
    }
}

八、委托的实际应用场景

8.1 场景一:回调函数(异步操作完成后通知)

// 模拟下载文件,完成后回调
static void DownloadFile(string url, Action<string> onComplete)
{
    Console.WriteLine($"正在从 {url} 下载...");
    System.Threading.Thread.Sleep(1000);  // 模拟耗时
    string result = "文件内容xxx";
    onComplete(result);  // 下载完通知
}

// 使用
DownloadFile("https://example.com/file.txt", content =>
{
    Console.WriteLine($"下载完成!内容: {content}");
});

8.2 场景二:事件处理(事件的基础就是委托)

// 按钮点击就是典型的委托应用
button.Click += (sender, e) => Console.WriteLine("按钮被点了!");

8.3 场景三:过滤/筛选条件

// 一个通用的过滤方法
static List<T> Filter<T>(List<T> list, Predicate<T> condition)
{
    List<T> result = new List<T>();
    foreach (T item in list)
    {
        if (condition(item))
            result.Add(item);
    }
    return result;
}

// 使用
List<int> numbers = new List<int> { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };

var evenNumbers = Filter(numbers, n => n % 2 == 0);
var bigNumbers = Filter(numbers, n => n > 5);

Console.WriteLine($"偶数: {string.Join(", ", evenNumbers)}");  // 2, 4, 6, 8, 10
Console.WriteLine($"大于5: {string.Join(", ", bigNumbers)}");  // 6, 7, 8, 9, 10

8.4 场景四:策略模式(动态切换算法)

// 不同的排序策略
Func<int[], int[]> bubbleSort = arr =>
{
    Console.WriteLine("使用冒泡排序");
    // ... 实现略
    return arr;
};

Func<int[], int[]> quickSort = arr =>
{
    Console.WriteLine("使用快速排序");
    // ... 实现略
    return arr;
};

// 根据数组大小选择排序策略
int[] data = new int[1000];
Func<int[], int[]> chosenSort = data.Length < 100 ? bubbleSort : quickSort;
chosenSort(data);

8.5 完整实战示例:简易任务调度系统

using System;
using System.Collections.Generic;

class Program
{
    // 任务队列:存要执行的方法
    static List<Action> tasks = new List<Action>();

    static void AddTask(Action task)
    {
        tasks.Add(task);
        Console.WriteLine($"任务已添加,当前共 {tasks.Count} 个任务");
    }

    static void RunAllTasks()
    {
        Console.WriteLine($"\n===== 开始执行 {tasks.Count} 个任务 =====");
        for (int i = 0; i < tasks.Count; i++)
        {
            Console.Write($"任务 {i + 1}: ");
            tasks[i]();
        }
        tasks.Clear();
        Console.WriteLine("===== 全部完成 =====\n");
    }

    static void Main()
    {
        // 添加各种任务
        AddTask(() => Console.WriteLine("备份数据库...完成 ✅"));
        AddTask(() => Console.WriteLine("发送日报邮件...完成 ✅"));
        AddTask(() => Console.WriteLine("清理临时文件...完成 ✅"));
        AddTask(() =>
        {
            // 复杂任务
            Console.Write("生成月度报表...");
            System.Threading.Thread.Sleep(500);  // 模拟耗时
            Console.WriteLine("完成 ✅");
        });

        // 一键执行
        RunAllTasks();

        // 再添加一些任务
        AddTask(() => Console.WriteLine("系统健康检查...完成 ✅"));
        AddTask(() => Console.WriteLine("更新缓存...完成 ✅"));

        RunAllTasks();
    }
}

输出:

任务已添加,当前共 1 个任务
任务已添加,当前共 2 个任务
任务已添加,当前共 3 个任务
任务已添加,当前共 4 个任务

===== 开始执行 4 个任务 =====
任务 1: 备份数据库...完成 ✅
任务 2: 发送日报邮件...完成 ✅
任务 3: 清理临时文件...完成 ✅
任务 4: 生成月度报表...完成 ✅
===== 全部完成 =====

任务已添加,当前共 1 个任务
任务已添加,当前共 2 个任务

===== 开始执行 2 个任务 =====
任务 1: 系统健康检查...完成 ✅
任务 2: 更新缓存...完成 ✅
===== 全部完成 =====

九、匿名方法与 Lambda 表达式

委托的发展历程——越来越简洁:

// 1.0 时代:命名方法
delegate int Operation(int x);
static int DoubleIt(int x) { return x * 2; }
Operation op1 = DoubleIt;

// 2.0 时代:匿名方法
Operation op2 = delegate(int x) { return x * 2; };

// 3.0 时代:Lambda 表达式
Operation op3 = (int x) => { return x * 2; };

// 再简化:省略类型
Operation op4 = (x) => { return x * 2; };

// 最简:单表达式可以去掉花括号和 return
Operation op5 = x => x * 2;

// 全部等价,结果都是 20
Console.WriteLine(op1(10));  // 20
Console.WriteLine(op2(10));  // 20
Console.WriteLine(op3(10));  // 20
Console.WriteLine(op4(10));  // 20
Console.WriteLine(op5(10));  // 20

Lambda 表达式语法速记:

// 无参数
Func<int> f1 = () => 42;

// 一个参数(括号可省略)
Func<int, int> f2 = x => x * x;

// 多个参数
Func<int, int, int> f3 = (x, y) => x + y;

// 多行语句(需要花括号 + return)
Func<int, int> f4 = x =>
{
    int result = x * 2;
    Console.WriteLine($"计算中: {x} * 2 = {result}");
    return result;
};

// 无参数无返回值
Action act = () => Console.WriteLine("执行完毕");

十、协变与逆变(进阶,了解即可)

// 协变(返回值):委托可以返回比定义更"具体"的类型
class Animal { }
class Dog : Animal { }

delegate Animal AnimalFactory();  // 返回 Animal

static Dog CreateDog() => new Dog();

AnimalFactory factory = CreateDog;  // ✅ 返回 Dog 的也可以
Animal result = factory();          // 运行时返回的是 Dog

// 逆变(参数):委托可以接收比定义更"宽泛"的类型
delegate void DogHandler(Dog d);  // 接收 Dog 参数

static void HandleAnimal(Animal a) => Console.WriteLine("处理动物");

DogHandler handler = HandleAnimal;  // ✅ 接收 Animal 的也可以
handler(new Dog());                 // 传入 Dog,实际当作 Animal 处理

这部分初学者可以先跳过,理解委托基础后再回来看。


十一、常见易错点(避坑指南)

坑1:多播委托有返回值只取最后一个

Func<int> chain = () => 1;
chain += () => 2;
chain += () => 3;
Console.WriteLine(chain());  // 3 ← 不是 6!

坑2:多播委托中一个方法报错,后面都不执行

Action chain = () => Console.WriteLine("A");
chain += () => throw new Exception("炸了!");
chain += () => Console.WriteLine("B");  // ❌ 永远不会执行!

坑3:委托是 null 时调用会报错

Action act = null;
// act();  // ❌ NullReferenceException!

// ✅ 安全调用
act?.Invoke();            // C# 6.0+ 空条件运算符
// 或
if (act != null) act();

坑4:Lambda 中捕获的变量是引用

Action[] actions = new Action[3];

for (int i = 0; i < 3; i++)
{
    actions[i] = () => Console.WriteLine(i);
}

foreach (var act in actions)
    act();
// 输出: 3, 3, 3 ← 全是 3!因为 i 是同一个变量

正确做法——用局部变量捕获:

for (int i = 0; i < 3; i++)
{
    int copy = i;                // 每次循环都创建一个新变量
    actions[i] = () => Console.WriteLine(copy);
}

foreach (var act in actions)
    act();
// 输出: 0, 1, 2 ✅

坑5:-= 移除方法时注意事项

Action act = () => Console.WriteLine("A");
act += () => Console.WriteLine("B");

// ✅ 同一个 Lambda 不能用来移除,因为每次写 Lambda 都是新对象
act += () => Console.WriteLine("C");
act -= () => Console.WriteLine("C");  // ❌ 删不掉的!
// 这两个 () => Console.WriteLine("C") 是不同的对象

十二、总结

委托核心概念速查

概念 说明
委托是什么 方法的"容器"或"传送带",可以把方法当参数传递
定义 delegate 返回值 名称(参数列表);
内置委托 Action(无返回)、Func(有返回)、Predicate(返回 bool)
多播委托 + 串联多个方法,调用一次全部执行
移除方法 - 从链上移除
Lambda x => x * 2,创建委托的最简写法

委托 vs 多播委托

特点 普通委托 多播委托
挂载方式 del = method del += method1; del += method2
方法个数 1 个 多个
调用方式 del() del()(一模一样)
返回值 正常返回 只返回最后一个的
异常 方法报错→调用者捕获 一个报错→后面全部不执行
移除 del = null del -= method

记忆口诀

委托就是把方法当快递寄,
Action 不带回执 Func 有回执,
加号串联多播全执行,
Lambda 一行搞定最省事。

一句话总结:委托是 C# 中把方法当参数传递的核心机制,用 Action/Func 不用自己定义,用 + 号可以串联成多播委托让多个方法依次执行。它是一切事件、回调、LINQ 的基础。

0

评论区