C# 委托、Action、Func、Event、Lambda 表达式详解(豆包总结)

在 C# 中,委托、Action、Func、Event、Lambda 表达式是 面向对象+函数式编程 的核心特性,它们共同解决了「方法作为参数传递」「解耦组件通信」等关键问题。本文将从「概念本质→核心用途→经典示例→关联区别→补充特性」逐步拆解,确保通俗易懂。

一、委托(Delegate):方法的"类型安全指针"

1. 核心概念

委托是一种 引用类型,它专门用来「封装具有特定签名(参数列表+返回值)的方法」—— 可以把委托理解为「方法的契约」:只有符合契约的方法(参数类型/数量、返回值一致)才能被委托引用。

委托的核心价值:将方法作为参数传递,实现代码的灵活性和解耦(比如回调函数场景)。

2. 定义与使用步骤

步骤1:定义委托(声明"方法契约")

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

步骤2:实现符合契约的方法

步骤3:创建委托实例并绑定方法

步骤4:调用委托(间接调用绑定的方法)

3. 经典示例:委托实现"计算回调"

需求:实现一个通用计算方法,支持动态传入「加法/减法/乘法」逻辑。

using System;

// 1. 定义委托(契约:接收两个int,返回int)
public delegate int CalculateDelegate(int a, int b);

class Program
{
    // 2. 实现符合契约的具体方法
    static int Add(int x, int y) => x + y;
    static int Subtract(int x, int y) => x - y;
    static int Multiply(int x, int y) => x * y;

    // 3. 通用计算方法(接收委托作为参数,实现逻辑解耦)
    static int Calculate(int a, int b, CalculateDelegate calculateFunc)
    {
        // 委托为null时调用会报错,需判空(C# 6+ 可简化为 calculateFunc?.Invoke(a, b))
        if (calculateFunc == null)
            throw new ArgumentNullException(nameof(calculateFunc));
        return calculateFunc.Invoke(a, b); // 调用委托绑定的方法
    }

    static void Main()
    {
        // 4. 创建委托实例并绑定方法(3种写法,效果一致)
        CalculateDelegate addDelegate = Add; // 直接绑定静态方法
        CalculateDelegate subtractDelegate = new CalculateDelegate(Subtract); // 显式实例化
        CalculateDelegate multiplyDelegate = (x, y) => x * y; // 用Lambda表达式绑定(简化)

        // 5. 调用通用计算方法,动态传入不同逻辑
        Console.WriteLine("3+5=" + Calculate(3, 5, addDelegate)); // 输出 8
        Console.WriteLine("10-4=" + Calculate(10, 4, subtractDelegate)); // 输出 6
        Console.WriteLine("6*7=" + Calculate(6, 7, multiplyDelegate)); // 输出 42
    }
}

4. 委托的关键特性

  • 类型安全:只能绑定与委托签名一致的方法(参数、返回值必须匹配),编译时会校验。
  • 多播委托:一个委托可以绑定多个方法(通过 += 添加,-= 移除),调用时按绑定顺序依次执行。
    // 多播委托示例
    CalculateDelegate multiDelegate = Add;
    multiDelegate += Multiply; // 绑定第二个方法
    multiDelegate(2, 3); // 先执行 Add(2,3)=5,再执行 Multiply(2,3)=6(返回值取最后一个方法的结果)
    multiDelegate -= Add; // 移除Add方法
    
  • 支持静态方法和实例方法:委托可绑定静态方法(如上述示例)或实例方法。
    class Calculator
    {
        public int Divide(int x, int y) => x / y; // 实例方法
    }
    
    // 绑定实例方法
    Calculator calc = new Calculator();
    CalculateDelegate divideDelegate = calc.Divide;
    Console.WriteLine("8/2=" + divideDelegate(8, 2)); // 输出 4
    

二、Action:无返回值的"简化委托"

1. 核心概念

Action 是 .NET 框架 预定义的委托,专门用于封装「无返回值(void)」的方法。它的本质是「系统帮你写好了的委托」,避免重复定义无返回值委托的冗余代码。

2. 重载形式(支持0~16个参数)

  • Action:无参数、无返回值
  • Action<T>:1个参数(类型T)、无返回值
  • Action<T1, T2>:2个参数(类型T1、T2)、无返回值
  • ... 最多支持 Action<T1, T2, ..., T16>(16个参数)

3. 经典示例:Action实现"任务执行器"

需求:实现一个通用方法,执行不同的"无返回值任务"(如打印日志、发送通知)。

using System;

class Program
{
    // 通用任务执行器(接收Action委托,无返回值)
    static void ExecuteTask(Action task)
    {
        Console.WriteLine("任务开始...");
        task?.Invoke(); // 执行委托绑定的任务
        Console.WriteLine("任务结束!\n");
    }

    // 具体任务方法(无返回值,符合Action契约)
    static void PrintLog() => Console.WriteLine("执行日志打印:操作成功");
    static void SendNotification(string msg) => Console.WriteLine($"发送通知:{msg}");

    static void Main()
    {
        // 1. 调用无参数Action
        ExecuteTask(PrintLog); // 绑定静态方法

        // 2. 调用带1个参数的Action<string>
        Action<string> notifyAction = SendNotification;
        notifyAction("用户登录成功"); // 直接调用:输出「发送通知:用户登录成功」

        // 3. 用Lambda表达式简化(无需定义单独方法)
        ExecuteTask(() => Console.WriteLine("执行临时任务:清理缓存"));
    }
}

输出结果:

任务开始...
执行日志打印:操作成功
任务结束!

发送通知:用户登录成功
任务开始...
执行临时任务:清理缓存
任务结束!

三、Func:有返回值的"简化委托"

1. 核心概念

Func 是 .NET 框架 预定义的委托,专门用于封装「有返回值」的方法。与 Action 互补,避免重复定义有返回值委托的冗余代码。

2. 重载形式(支持0~16个参数,最后一个类型是返回值类型)

  • Func<TResult>:无参数、返回值类型为TResult
  • Func<T, TResult>:1个参数(类型T)、返回值类型为TResult
  • Func<T1, T2, TResult>:2个参数(类型T1、T2)、返回值类型为TResult
  • ... 最多支持 Func<T1, T2, ..., T16, TResult>(16个参数+1个返回值)

3. 经典示例:Func实现"数据转换器"

需求:实现一个通用转换器,将int类型数据转换为不同类型(字符串、double、bool)。

using System;

class Program
{
    // 通用转换器(接收Func<int, TResult>,输入int,返回TResult)
    static TResult ConvertData<TResult>(int input, Func<int, TResult> converter)
    {
        Console.WriteLine($"转换输入:{input}");
        return converter?.Invoke(input) ?? default(TResult); // 执行转换并返回结果
    }

    static void Main()
    {
        // 1. int→string(用Lambda简化,无需定义单独方法)
        string strResult = ConvertData(123, num => num.ToString("D5")); // D5:补零到5位
        Console.WriteLine($"转换结果(string):{strResult}\n"); // 输出 00123

        // 2. int→double(绑定静态方法)
        Func<int, double> doubleConverter = NumToDouble;
        double doubleResult = ConvertData(45, doubleConverter);
        Console.WriteLine($"转换结果(double):{doubleResult}\n"); // 输出 45.5

        // 3. int→bool(判断是否为偶数)
        bool boolResult = ConvertData(7, num => num % 2 == 0);
        Console.WriteLine($"转换结果(bool,是否偶数):{boolResult}"); // 输出 False
    }

    // 静态转换方法(符合Func<int, double>契约)
    static double NumToDouble(int num) => num + 0.5;
}

4. Action vs Func:核心区别

特性 Action Func
返回值 无(void) 有(最后一个泛型是返回值)
适用场景 执行"操作"(如打印、通知) 执行"计算/转换"(如求和、类型转换)
泛型参数 仅参数类型(0~16个) 参数类型+返回值类型(0~16个参数)

四、Lambda 表达式:匿名方法的"语法糖"

1. 核心概念

Lambda 表达式是 匿名方法的简化语法,本质是「没有名称的方法」,可直接作为委托(Delegate/Action/Func)的参数或返回值。它的核心价值是 简化代码,避免定义冗余的命名方法

2. 语法格式

(参数列表) => 表达式/代码块

  • =>:Lambda 运算符,读作"goes to"
  • 若参数列表只有1个参数,可省略括号(如 num => num * 2
  • 若无参数,需写空括号(如 () => Console.WriteLine("Hello")
  • 若表达式是多行代码,需用大括号 {} 包裹,且需显式写 return(单行表达式可省略 return

3. 经典示例:Lambda 简化委托使用

结合前面的委托/Action/Func,看 Lambda 如何简化代码:

using System;

class Program
{
    static void Main()
    {
        // 1. 简化委托调用(无需定义Add方法)
        CalculateDelegate addDelegate = (x, y) => x + y;
        Console.WriteLine("3+5=" + addDelegate(3, 5)); // 输出 8

        // 2. 简化Action(无参数)
        Action printAction = () => Console.WriteLine("Hello Lambda!");
        printAction(); // 输出 Hello Lambda!

        // 3. 简化Func(带参数+返回值)
        Func<int, int, int> multiplyFunc = (a, b) => a * b;
        Console.WriteLine("4*6=" + multiplyFunc(4, 6)); // 输出 24

        // 4. 多行Lambda(需{}和return)
        Func<int, string> describeFunc = num =>
        {
            if (num > 0) return $"正数:{num}";
            else if (num < 0) return $"负数:{num}";
            else return "零";
        };
        Console.WriteLine(describeFunc(-3)); // 输出 负数:-3
    }
}

// 复用前面定义的委托
public delegate int CalculateDelegate(int a, int b);

4. Lambda 的关键特性

  • 类型推断:编译器会根据上下文(如委托类型)自动推断参数类型和返回值类型,无需显式声明(如 (x, y) => x + y 中,x和y的类型由委托决定)。
  • 捕获变量:Lambda 可以访问外层作用域的变量(值类型捕获副本,引用类型捕获引用)。
    int factor = 2;
    Func<int, int> multiplyByFactor = num => num * factor;
    Console.WriteLine(multiplyByFactor(5)); // 输出 10(捕获了factor变量)
    factor = 3;
    Console.WriteLine(multiplyByFactor(5)); // 输出 15(引用类型捕获,值会变化)
    
  • 仅能赋值给委托:Lambda 本身没有类型,必须赋值给具体的委托类型(Delegate/Action/Func)才能使用。

五、事件(Event):基于委托的"发布-订阅模式"

1. 核心概念

事件(Event)是 委托的"安全包装器",基于「发布-订阅(Publish-Subscribe)」模式,用于组件间的解耦通信(如按钮点击、消息通知)。

事件的核心价值:限制委托的访问权限—— 外部只能通过 += 订阅、-= 取消订阅,不能直接调用委托或覆盖绑定(避免恶意修改)。

2. 本质理解

事件 = 委托字段 + 访问控制(仅暴露订阅/取消订阅接口)。如果直接用委托字段,外部可以:

  • 直接调用委托(delegateInstance()),破坏封装;
  • = 覆盖已绑定的方法(而非 += 添加),导致之前的订阅失效。

事件解决了这些问题,是委托的「安全使用场景」。

3. 定义与使用步骤

步骤1:定义委托(事件的"通信契约")

步骤2:用 event 关键字声明事件(基于委托)

步骤3:发布者(Publisher):定义触发事件的方法(通常是 OnXXX 命名)

步骤4:订阅者(Subscriber):通过 += 订阅事件,-= 取消订阅

步骤5:发布者触发事件,所有订阅者的方法会被自动调用

4. 经典示例:按钮点击事件(WinForm核心场景)

模拟 WinForm 按钮点击的事件机制,理解发布-订阅模式:

using System;

// 1. 定义委托(事件契约:无参数、无返回值)
public delegate void ButtonClickHandler();

// 发布者(Publisher):按钮组件
public class Button
{
    // 2. 声明事件(基于委托,外部仅能订阅/取消订阅)
    public event ButtonClickHandler OnClick;

    // 3. 触发事件的方法(通常是protected virtual,允许子类重写)
    protected virtual void RaiseClick()
    {
        // 事件触发前需判空(无订阅者时为null)
        OnClick?.Invoke(); // 触发事件,所有订阅者的方法会执行
    }

    // 模拟用户点击按钮(外部调用此方法触发事件)
    public void Click()
    {
        Console.WriteLine("用户点击了按钮...");
        RaiseClick(); // 内部调用触发方法
    }
}

// 订阅者1(Subscriber):日志组件
public class LogManager
{
    // 订阅事件的方法(符合ButtonClickHandler契约)
    public void OnButtonClicked()
    {
        Console.WriteLine("日志组件:记录按钮点击事件\n");
    }
}

// 订阅者2(Subscriber):通知组件
public class NotificationManager
{
    public void OnButtonClicked()
    {
        Console.WriteLine("通知组件:弹出点击成功提示\n");
    }
}

class Program
{
    static void Main()
    {
        // 实例化发布者和订阅者
        Button btn = new Button();
        LogManager log = new LogManager();
        NotificationManager notify = new NotificationManager();

        // 4. 订阅事件(+= 绑定方法)
        btn.OnClick += log.OnButtonClicked;
        btn.OnClick += notify.OnButtonClicked;

        // 5. 模拟用户点击按钮(触发事件)
        btn.Click();

        // 6. 取消订阅(-= 移除方法)
        btn.OnClick -= notify.OnButtonClicked;
        Console.WriteLine("通知组件已取消订阅,再次点击按钮:");
        btn.Click();
    }
}

输出结果:

用户点击了按钮...
日志组件:记录按钮点击事件

通知组件:弹出点击成功提示

通知组件已取消订阅,再次点击按钮:
用户点击了按钮...
日志组件:记录按钮点击事件

5. 事件的关键特性

  • 访问权限控制:外部只能 +=/-=,不能直接调用 OnClick() 或用 = 覆盖。
  • 多订阅者支持:一个事件可以有多个订阅者,触发时按订阅顺序依次执行。
  • 标准事件模式:.NET 推荐使用 EventHandler 委托(自带 sender 和 EventArgs 参数),而非自定义委托:
    // 标准事件委托(sender:事件源,e:事件参数)
    public delegate void EventHandler(object sender, EventArgs e);
    
    // 声明标准事件
    public event EventHandler OnClick;
    
    // 触发时传递事件源和参数
    protected virtual void RaiseClick()
    {
        OnClick?.Invoke(this, EventArgs.Empty); // this:事件源(按钮本身)
    }
    
    优点:统一事件格式,支持传递事件上下文(如点击位置、消息内容)。

六、关联特性补充

1. 匿名方法(Anonymous Method)

Lambda 表达式的「前身」,语法更繁琐,现在已基本被 Lambda 替代。语法:delegate(参数列表) { 代码块; }

// 匿名方法示例(绑定到Action)
Action printAction = delegate() { Console.WriteLine("匿名方法"); };
printAction();

// 等价于 Lambda 表达式
Action lambdaAction = () => Console.WriteLine("Lambda 表达式");

适用场景:仅当需要显式忽略参数(如 delegate { ... } 无参数列表)时,匿名方法更简洁。

2. 泛型委托的协变与逆变

协变(out):允许返回值类型「从子类向父类转换」;
逆变(in):允许参数类型「从父类向子类转换」。
Action 和 Func 已内置协变/逆变支持:

  • Func<out TResult>:TResult 支持协变;
  • Action<in T>:T 支持逆变。

示例:

// 协变示例(返回值:Dog→Animal)
Func<Dog> getDog = () => new Dog();
Func<Animal> getAnimal = getDog; // 允许,因为 Dog 是 Animal 的子类

// 逆变示例(参数:Animal→Dog)
Action<Dog> feedDog = dog => Console.WriteLine("喂狗");
Action<Animal> feedAnimal = feedDog; // 允许,因为 Dog 是 Animal 的子类

public class Animal { }
public class Dog : Animal { }

3. 委托的 Combine 与 Remove 方法

手动操作多播委托的绑定/解绑(等价于 +=/-=):

CalculateDelegate add = Add;
CalculateDelegate multiply = Multiply;

// 组合委托(等价于 add += multiply)
CalculateDelegate combined = (CalculateDelegate)Delegate.Combine(add, multiply);
combined(2, 3); // 执行 Add 和 Multiply

// 移除委托(等价于 combined -= add)
CalculateDelegate removed = (CalculateDelegate)Delegate.Remove(combined, add);
removed(2, 3); // 仅执行 Multiply

4. 异步委托(BeginInvoke/EndInvoke)

早期用于异步执行方法的委托特性(现在推荐用 async/await),通过 IAsyncResult 获取异步执行结果:

// 异步执行委托
CalculateDelegate add = Add;
IAsyncResult result = add.BeginInvoke(3, 5, null, null); // 异步执行 Add(3,5)
// 做其他事情...
int sum = add.EndInvoke(result); // 等待异步完成并获取结果
Console.WriteLine(sum); // 输出 8

七、核心特性总结表

特性 本质 核心用途 关键语法/关键字
委托(Delegate) 方法的类型安全指针 封装方法,作为参数传递 delegate 关键字
Action 预定义无返回值委托 执行无返回值操作 Action<T1,...T16>
Func 预定义有返回值委托 执行计算/转换,返回结果 Func<T1,...T16,TResult>
Lambda 表达式 匿名方法的语法糖 简化委托绑定,避免冗余方法 (参数) => 表达式
事件(Event) 委托的安全包装器 组件间解耦通信(发布-订阅) event 关键字

八、最佳实践

  1. 优先使用 Action/Func 而非自定义委托(减少冗余代码);
  2. 事件优先使用标准 EventHandler 委托(统一格式,支持参数传递);
  3. Lambda 表达式仅用于简单逻辑(复杂逻辑建议用命名方法,提高可读性);
  4. 委托/事件调用前必须判空(?.Invoke()),避免空引用异常;
  5. 多播委托注意执行顺序(按绑定顺序执行),且返回值仅取最后一个方法的结果。

通过以上特性,C# 实现了「方法作为一等公民」的编程体验,既保留了面向对象的封装性,又融入了函数式编程的灵活性,是构建复杂应用(如 UI 交互、异步通信)的基础。

posted @ 2025-12-05 22:03  焦涛  阅读(1224)  评论(0)    收藏  举报