.NET进阶——深入理解委托(1)委托入门

59 阅读8分钟

一、什么是委托

委托就相当于是一个可以存放方法的箱子,我们可以通过这个“箱子”调用里面的方法,比如我下面的代码:

// 0.背景:委托定义与方法定义
    // 创建了一个名为MyDelegate的委托,这个委托里面装的函数必须有两个int类型的参数,一个int类型的返回值!
    public delegate int MyDelegate(int a,int b);

    // 创建了一个Add方法,需要两个int参数,一个int返回值,与上面委托要求的相符!
    int Add(int a, int b)
    {
        return a + b;
    }
    
// 1.实例化类,
    MyClass myClass = new MyClass();
    
// 2.实例化委托并且将方法装载到委托中
    MyDelegate myDelegate1 = new MyDelegate(myClass.Add);
    
// 3.调用委托,即调用委托这个“箱子里的方法Add(1,2)”
    int result = myDelegate1.Invoke(1,2);
    

这里我演示了如何简单的使用委托,那么有的同学看到可能会疑惑:

为什么要使用委托?直接调用Add(1,2)函数不就好了吗?为什么要写这么麻烦?

这个问题困扰着每一个委托初学者,其实这个问题非常好,但是现在我们先往下学,在本文,我会告诉大家这个问题的答案。

1.1 创建委托的语法

对我来说,我知道创建委托的方法有以下几个

① 显示创建委托

这是 C# 1.0 引入的最基础方式,通过 new 关键字显式创建委托实例,并将目标方法作为构造函数参数。

委托类型 委托实例 = new 委托类型(目标方法);
// 1. 定义委托类型
public delegate void MyDelegate(string message);

// 2. 目标方法(静态或实例方法均可)
public static void ShowMessage(string msg)
{
    Console.WriteLine(msg);
}

// 3. 显式实例化委托
MyDelegate del = new MyDelegate(ShowMessage);
del("Hello, 显式实例化委托!"); // 调用委托

② 方法组转换(简化方式)

C# 2.0 引入了方法组转换,允许直接将方法名赋值给委托实例(编译器自动完成 new 操作)。这是最常用的方式之一。

委托类型 委托实例 = 目标方法;
// 复用上面的 MyDelegate 委托和 ShowMessage 方法
MyDelegate del = ShowMessage; // 方法组转换
del("Hello, 方法组转换!");

说明

  • 目标方法可以是静态方法(直接用类名调用)或实例方法(需先创建类实例)。
  • 方法签名(参数类型、返回值类型)必须与委托完全匹配。

③ 匿名方法(临时方法)

C# 2.0 引入了匿名方法,允许在创建委托时直接定义方法体,无需单独声明命名方法。适合临时使用的简单逻辑。

语法

委托类型 委托实例 = delegate(参数列表)
{
    // 方法体
};

示例

public delegate void MyDelegate(string message);

MyDelegate del = delegate(string msg)
{
    Console.WriteLine("匿名方法:" + msg);
};

del("Hello, 匿名方法!");

说明

  • 匿名方法可以访问外部变量(闭包),但需注意变量生命周期。
  • 若委托无参数,delegate() 中的参数列表可省略(仅匿名方法支持)。

④ Lambda 表达式(最简洁方式

C# 3.0 引入了Lambda 表达式,是匿名方法的更简洁语法,广泛用于 LINQ、事件处理等场景。

语法

  • 无参数:() => { 方法体 } 或 () => 表达式(单行可省略大括号)
  • 有参数:(参数1, 参数2) => { 方法体 } 或 参数 => 表达式(单参数可省略括号)

示例

// 1. 无参数委托
public delegate void NoParamDelegate();
NoParamDelegate del1 = () => Console.WriteLine("无参数 Lambda!");
del1();

// 2. 有参数委托(单行简化)
MyDelegate del2 = msg => Console.WriteLine("Lambda:" + msg);
del2("Hello, Lambda!");

// 3. 多行 Lambda(需大括号和 return)
public delegate int CalcDelegate(int a, int b);
CalcDelegate del3 = (a, b) =>
{
    int sum = a + b;
    return sum;
};
int result = del3(10, 20); // 结果:30

说明

  • Lambda 表达式同样支持闭包,访问外部变量。
  • 编译器会根据委托签名自动推断参数类型(若委托是泛型委托,如 Func/Action,则参数类型需显式或由上下文推断)。

1.2 调用委托的语法

① 基础调用:Invoke()方法

Invoke() 是委托类型的实例方法,用于显式调用委托所引用的方法。这是最明确的调用方式,适合强调 “通过委托调用” 的场景。

委托实例.Invoke(参数列表);
public delegate void MyDelegate(string message);

public static void ShowMessage(string msg)
{
    Console.WriteLine(msg);
}

// 创建委托实例
MyDelegate del = ShowMessage;

// 使用 Invoke() 调用
del.Invoke("Hello via Invoke!"); // 输出:Hello via Invoke!

2. 简化调用:直接调用(编译器语法糖)

C# 允许将委托实例直接作为方法名调用,编译器会自动将其转换为 Invoke() 调用。这是最常用的简化语法。

语法:

委托实例(参数列表);

示例:

// 复用上面的委托和方法
MyDelegate del = ShowMessage;

// 直接调用(等价于 del.Invoke(...))
del("Hello via direct call!"); // 输出:Hello via direct call!

1.2 委托的原理

谈到这里,我们就可以好好聊聊之前的问题了,委托到底是什么?

① 先想清楚:为什么需要委托?

假设你是一个房东,想把房子租出去,但你不想自己找租客、签合同、收房租(太麻烦了)。这时候你会找一个房产中介,把 “租房” 这件事委托给中介去做。

在编程里,方法就像 “租房” 这件事,而委托就是 “房产中介”—— 它帮你管理方法的调用,让你不用直接和方法打交道。

② 委托的本质:方法的 “代言人”

委托的核心作用是:把 “方法” 变成一个可以 “传递、存储、调用” 的 “对象”

用更通俗的话讲:

  • 委托是一个 “方法的代言人”:它知道 “要调用哪个方法”“怎么调用这个方法”。
  • 委托是一个 “类型安全的中介”:它只接受 “符合要求的方法”(比如参数、返回值要匹配,就像中介只接受 “合法的房东”)。
  • 委托是一个 “可以同时干多件事的中介”:它可以同时帮你调用多个方法(比如中介同时帮你找租客、修房子、收房租)。

③ 用 “租房” 例子拆解委托的核心功能

我们把 “租房” 的流程对应到委托的使用上,你就能完全理解了:

第一步:定义 “委托类型”(相当于 “中介的服务范围”)

房东找中介前,得先明确 “中介要做什么”(比如 “帮我找租客,签合同,收房租”)。在编程里,这就是定义委托类型,规定 “这个委托能代言什么样的方法”(比如参数类型、返回值类型)。

// 定义一个委托类型:"租房中介",负责"处理租房相关的事"
// (参数是租客名字,返回值是租金)
public delegate int RentHouseDelegate(string tenantName);
第二步:准备 “要委托的方法”(相当于 “房东的具体需求”)

房东需要明确 “具体要做哪些事”,比如:

  • 找租客:FindTenant 方法
  • 签合同:SignContract 方法
  • 收房租:CollectRent 方法
// 房东的具体方法(要委托给中介的事)
public class Landlord
{
    // 找租客:返回押金(比如1000元)
    public int FindTenant(string tenant)
    {
        Console.WriteLine($"帮房东找租客:{tenant}");
        return 1000; // 押金
    }

    // 签合同:返回第一个月租金(比如3000元)
    public int SignContract(string tenant)
    {
        Console.WriteLine($"帮房东和租客 {tenant} 签合同");
        return 3000; // 首月租金
    }
}
第三步:创建 “委托实例”(相当于 “雇佣中介”)

房东找到中介,把 “找租客”“签合同” 这些事委托给中介(绑定方法到委托实例)。

// 创建房东实例(具体的房东)
Landlord landlord = new Landlord();

// 雇佣中介:把"找租客"方法委托给中介
RentHouseDelegate agent = landlord.FindTenant;

// 还可以让中介同时干多件事:再委托"签合同"方法
agent += landlord.SignContract;

4. 第四步:调用 “委托”(相当于 “让中介干活”)

现在,房东只需要告诉中介 “租客名字” ,中介就会自动执行所有委托的方法(找租客 → 签合同)。

// 让中介干活:处理租客"张三"的租房事宜
int totalMoney = agent("张三");

Console.WriteLine($"总共收到钱:{totalMoney}元");

5. 执行结果(中介干的活)

帮房东找租客:张三
帮房东和租客 张三 签合同
总共收到钱:3000// 注意:多播委托只返回最后一个方法的结果(签合同的3000元)

④ 委托的本质总结(一句话)

委托就是一个 “方法的代言人”,它帮你管理方法的调用,让方法可以像变量一样 “传递、存储、批量调用”

⑤ 为什么不用直接调用方法?

你可能会问:“我直接调用 landlord.FindTenant("张三") 不行吗?为什么要找中介?”

这就涉及到委托的核心价值:解决 “方法不能直接传递” 的问题

比如:

  • 你写了一个 “按钮” 控件,点击按钮时要执行 “用户自定义的方法”,但你不知道用户会写什么方法(可能是 “打开文件”,也可能是 “保存数据”)。这时候,你就需要一个委托来 “接收” 用户的方法,点击时再调用。
  • 你写了一个 “排序” 方法,需要用户提供 “比较规则”(比如按年龄排序还是按姓名排序),但你不知道用户的比较规则是什么。这时候,你就需要一个委托来 “接收” 用户的比较方法,排序时再调用。

⑥ 再举一个更简单的例子:按钮点击事件

这是委托最常见的应用场景,你肯定见过:

// 1. 系统定义了一个委托类型:"点击事件的代言人"
public delegate void EventHandler(object sender, EventArgs e);

// 2. 按钮有一个"点击事件"(本质是委托实例)
button.Click += new EventHandler(OnButtonClick);

// 3. 你写的"点击后要执行的方法"
private void OnButtonClick(object sender, EventArgs e)
{
    Console.WriteLine("按钮被点击了!");
}

这里的 EventHandler 就是委托,它帮系统 “记住” 了你要执行的 OnButtonClick 方法,当按钮被点击时,系统就调用这个委托,从而执行你的方法。

核心结论

委托的本质其实很简单:它是一个 “方法的代言人”,让方法可以像变量一样被传递、存储和调用

就像你找中介租房,不用自己跑断腿;编程里用委托,不用直接调用复杂的方法,而是让委托帮你搞定。

二、系统内置的两个委托

在 .NET 框架中,Action 和 Func 是最常用的两个内置泛型委托,它们由 System 命名空间提供,无需手动定义,可直接用于几乎所有委托场景。这两个委托的设计目标是减少自定义委托的数量,提高代码复用性和可读性。

Action 委托:无返回值的方法引用

Action 委托用于封装无返回值的方法(即 void 返回类型)。它支持 0 到 16 个输入参数,通过泛型参数指定参数类型。

1. 基本定义

  • 无参数Action用于封装 “无参数、无返回值” 的方法。
  • 带参数Action<T1>Action<T1, T2>、...、Action<T1, T2, ..., T16>用于封装 “带 1~16 个参数、无返回值” 的方法,泛型参数 T1~T16 表示参数类型。

2. 示例:使用 Action 委托

using System;

class Program
{
    static void Main()
    {
        // 1. 无参数 Action:封装无参数方法
        Action printHello = () => Console.WriteLine("Hello, Action!");
        printHello(); // 输出:Hello, Action!

        // 2. 带 1 个参数 Action:封装带 1 个参数的方法
        Action<string> printMessage = msg => Console.WriteLine($"Message: {msg}");
        printMessage("你好,世界!"); // 输出:Message: 你好,世界!

        // 3. 带 2 个参数 Action:封装带 2 个参数的方法
        Action<string, int> printInfo = (name, age) => Console.WriteLine($"{name} 今年 {age} 岁");
        printInfo("张三", 25); // 输出:张三 今年 25 岁
    }
}

Func 委托:有返回值的方法引用

Func 委托用于封装有返回值的方法。它支持 0 到 16 个输入参数,最后一个泛型参数始终表示返回值类型

1. 基本定义

  • 无输入参数,仅返回值Func<TResult>用于封装 “无参数、返回值类型为 TResult” 的方法。
  • 带输入参数和返回值Func<T1, TResult>Func<T1, T2, TResult>、...、Func<T1, T2, ..., T16, TResult>用于封装 “带 1~16 个输入参数、返回值类型为 TResult” 的方法,前 N 个泛型参数表示输入参数类型,最后一个表示返回值类型。

2. 示例:使用 Func 委托

using System;

class Program
{
    static void Main()
    {
        // 1. 无输入参数,返回 int:封装无参数、返回 int 的方法
        Func<int> getRandomNumber = () => new Random().Next(1, 100);
        int randomNum = getRandomNumber();
        Console.WriteLine($"随机数:{randomNum}"); // 输出:随机数:[1-99的随机数]

        // 2. 带 1 个输入参数,返回 string:封装带 1 个参数、返回 string 的方法
        Func<int, string> numberToString = num => $"数字是:{num}";
        string result1 = numberToString(123);
        Console.WriteLine(result1); // 输出:数字是:123

        // 3. 带 2 个输入参数,返回 int:封装带 2 个参数、返回 int 的方法
        Func<int, int, int> add = (a, b) => a + b;
        int sum = add(10, 20);
        Console.WriteLine($"和:{sum}"); // 输出:和:30
    }
}

三、Action 与 Func 的核心区别

特性Action 委托Func 委托
返回值无返回值(void必须有返回值(最后一个泛型参数为返回值类型)
泛型参数仅表示输入参数(0~16 个)前 N 个为输入参数,最后一个为返回值类型(0~16 个输入参数)
适用场景用于 “执行操作”(如打印、修改状态)用于 “计算结果”(如求和、转换类型)

补充:Predicate 委托(特殊的 Func

除了 Action 和 Func,.NET 还提供了 Predicate<T>  委托,它是 Func<T, bool> 的 “语法糖”,专门用于返回布尔值的条件判断(如 “判断元素是否满足条件”)。

基本定义

  • Predicate<T>:封装 “输入参数为 T、返回值为 bool” 的方法,等价于 Func<T, bool>

示例:使用 Predicate 委托

using System;

class Program
{
    static void Main()
    {
        // 检查数字是否为偶数(等价于 Func<int, bool>)
        Predicate<int> isEven = num => num % 2 == 0;
        
        Console.WriteLine(isEven(4));  // 输出:True
        Console.WriteLine(isEven(5));  // 输出:False
    }
}

学完了这些,我们基本上算委托入门了,接下来我会带着大家做几个委托实战来巩固这些知识点。