C#进阶学习(十)更加安全的委托——事件以及匿名函数与Lambda表达式和闭包的介绍_c# 事件匿名方法不能移除吗
目录
第一部分:事件
一、什么是事件?
关键点:
二、事件的作用
三、事件怎么写以及注意事项
访问修饰符 event 委托类型 事件名;
四、事件区别于委托的细节之处
第二部分:匿名函数
一、什么是匿名函数
二、匿名函数的基本申明规则以及使用示例
三、匿名函数的优缺点
优点:
缺点:
第三部分:Lambda表达式
一、什么是 Lambda 表达式?
关键特性:
二、Lambda 表达式的语法
三、Lambda 表达式的使用示例
1. 有参有返回(显式类型)
2. 有参有返回(显式类型,语句体)
3. 无参有返回
4. 无参无返回
5. 不显式声明类型(类型推断)
四、什么是闭包
1. 闭包的基本原理
2. 闭包捕获的是变量,不是值
示例 2:闭包与循环陷阱
3. 如何避免循环陷阱?
修复示例:
4. 闭包的常见应用场景
5.闭包的注意事项
第四部分:委托,事件,匿名函数的总结
第一部分:事件
一、什么是事件?
事件(Event)是 C# 中一种特殊的委托(Delegate),用于实现发布-订阅模型(Publish-Subscribe Pattern)。它允许一个对象(发布者)在特定动作发生时通知其他对象(订阅者)。事件的核心思想是封装委托,提供更安全的访问控制。
事件是基于委托的存在
事件是委托的安全包裹
让委托的使用更具有安全性
事件是一种特殊的变量类型
关键点:
本质:事件是对委托的封装,确保只有声明事件的类才能触发(
Invoke
)它。设计模式:事件是观察者模式(Observer Pattern)的具体实现。
语法:事件基于委托类型定义,但通过
event
关键字声明。
二、事件的作用
解耦通信
事件允许对象之间通过松耦合的方式通信。例如,GUI 中的按钮点击事件不需要知道具体哪个类会处理点击逻辑。安全性
通过限制外部对委托的直接访问(如触发或重置委托链),防止误操作。多播支持
事件天然支持多播(多个订阅者),例如多个方法可以同时订阅同一个事件。标准化设计
事件常用于框架设计(如 WinForms、WPF、ASP.NET),提供统一的交互模式。
三、事件怎么写以及注意事项
事件的使用:
1.事件是作为 成员变量存在于类中
2.委托怎么用 事件就怎么用
事件相对于委托的区别:
1.不能在类外部 赋值
2.不能在类外部 调用
注意
他只能作为成员存在于类和接口以及结构体中
1. 事件的基本声明规则
事件的声明需遵循以下规则:
-
委托类型:事件必须基于一个已定义的委托类型(如
EventHandler
或自定义委托),不能直接指定返回值类型。 -
语法格式:
访问修饰符 event 委托类型 事件名;
-
例如:
public event Action Clicked;
(若委托类型为Action
),当然也可以自定义委托类型。 -
触发权限:只有声明事件的类可以触发(调用)事件。
-
订阅限制:外部代码只能通过
+=
订阅、-=
取消订阅,不能直接赋值(如= null
)。
2.事件的实际代码示例:
注意?.的用法,首先判断左边类型是否为空,不为空则唤醒即执行,为空则不执行右边。相当于是更加安全的使用了委托
using System;// 1. 定义委托类型(简短示例)public delegate void Notify(); // 无参数、无返回值的委托// 2. 声明包含事件的类public class EventDemo{ // 声明事件(基于 Notify 委托) public event Notify OnEvent; // 触发事件的方法(Trigger) public void Trigger() { OnEvent?.Invoke(); // 安全调用 }}// 3. 订阅事件的类public class Subscriber{ // 事件处理方法(简短方法名:Log) public void Log() { Console.WriteLine(\"事件已触发!\"); }}// 4. 主函数中的使用public class Program{ public static void Main() { EventDemo demo = new EventDemo(); Subscriber sub = new Subscriber(); // 订阅事件 demo.OnEvent += sub.Log; // 触发事件(由 EventDemo 类内部控制) demo.Trigger(); }}
四、事件区别于委托的细节之处
+=
和 -=
订阅= null
)对比示例:
// 委托public Action MyDelegate;MyDelegate = () => Console.WriteLine(\"Delegate called\"); // 外部可随意覆盖MyDelegate(); // 外部可触发// 事件public event Action MyEvent;MyEvent = () => Console.WriteLine(\"Error!\"); // 编译错误(外部不可赋值)MyEvent?.Invoke(); // 编译错误(外部不可触发)
第二部分:匿名函数
一、什么是匿名函数
所谓匿名函数,就是没有名字的函数,那他有啥用呢。他主要是和委托和事件一起玩儿,可以说离开了这两家伙,匿名函数根本就没任何用处。
匿名函数是 C# 中一种简化委托和事件使用的语法糖,它允许开发者直接内联定义函数逻辑,而无需显式声明方法名
匿名函数:没有名字的函数
匿名函数的作用主要是配合着委托和事件使用
脱离委托和事件,匿名函数没有意义
二、匿名函数的基本申明规则以及使用示例
申明规则:
delegate(参数列表)
{
函数体
};
何时使用?
1.函数中传递委托函数时
2.委托或事件赋值时
示例 1:匿名函数赋值给委托变量
using System;// 定义委托类型public delegate int MathOperation(int a, int b);public class Program{ public static void Main() { // 匿名函数实现加法 MathOperation add = delegate(int x, int y) { return x + y; }; Console.WriteLine(add(3, 5)); // 输出 8 }}
示例 2:匿名函数订阅事件
using System;public class Button{ public event Action Clicked; public void Press() { Clicked?.Invoke(); }}public class Program{ public static void Main() { Button button = new Button(); // 使用匿名函数订阅事件 button.Clicked += delegate { Console.WriteLine(\"按钮被点击了!\"); }; button.Press(); // 输出 \"按钮被点击了!\" }}
示例 3:匿名函数访问外部变量(闭包)这个闭包我们在下面的第三部分学习,这里先看事件的使用
using System;public class Program{ public static void Main() { int counter = 0; Action increment = delegate { counter++; // 访问外部变量 counter Console.WriteLine($\"当前值:{counter}\"); }; increment(); // 输出 \"当前值:1\" increment(); // 输出 \"当前值:2\" }}
三、匿名函数的优缺点
优点:
简化代码:无需单独定义方法,减少代码量。
灵活性强:可直接访问外层变量(闭包),适合快速实现临时逻辑。
减少类成员:避免因简单逻辑污染类的成员列表。
缺点:
可读性差:复杂逻辑内联在匿名函数中会降低代码可读性。
难以重用:匿名函数无法被其他代码直接调用。
闭包陷阱:若匿名函数引用外部变量,可能导致变量生命周期延长(内存泄漏风险)。
调试困难:匿名函数在堆栈跟踪中显示为不可见的方法名(如
b__0
)。添加到委托或者事件容器中 不记录 无法单独移除
第三部分:Lambda表达式
一、什么是 Lambda 表达式?
Lambda 表达式是 C# 中一种更简洁的匿名函数写法,本质上仍是匿名函数,但语法更精简。它通过 =>
符号(读作“goes to”)连接参数列表和方法体,核心目的是简化委托和事件的代码。
可以将Lambda表达式理解为一种匿名函数的简写
他除了写法不同以外
使用上和匿名函数一模一样
都是和委托或者事件 配合使用的
关键特性:
匿名性:无需显式定义方法名。
类型推断:参数类型可省略(由编译器自动推断)。
灵活性:支持表达式体(单行代码)和语句体(多行代码)。
二、Lambda 表达式的语法
Lambda 表达式的基本语法如下:
Lambda表达式
(参数列表) => { 函数体 }
参数列表:
无参数:
() => ...
单参数:
x => ...
(可省略括号)多参数:
(x, y) => ...
表达式体:单行代码,自动返回结果(无需
return
)。语句体:多行代码,需用
{ }
包裹,且需显式使用return
。
三、Lambda 表达式的使用示例
1. 有参有返回(显式类型)
// 显式声明参数类型Func add = (int x, int y) => x + y;Console.WriteLine(add(3, 5)); // 输出 8
2. 有参有返回(显式类型,语句体)
// 多行代码需用 { } 和 returnFunc multiply = (int a, int b) => { int result = a * b; return result;};Console.WriteLine(multiply(4, 5)); // 输出 20
3. 无参有返回
// 无参数时必须保留 ()Func getRandom = () => new Random().Next(1, 100);Console.WriteLine(getRandom()); // 输出随机数
4. 无参无返回
// Action 表示无返回值Action logMessage = () => Console.WriteLine(\"Hello, Lambda!\");logMessage(); // 输出 \"Hello, Lambda!\"
5. 不显式声明类型(类型推断)
// 参数类型由编译器推断Func subtract = (x, y) => x - y;Console.WriteLine(subtract(10, 3)); // 输出 7// 单参数可省略括号Action greet = name => Console.WriteLine($\"你好,{name}!\");greet(\"张三\"); // 输出 \"你好,张三!\"
四、什么是闭包
闭包是函数式编程中的一个核心概念,在 C# 中通过 Lambda 表达式或匿名函数实现。它的本质是:
一个函数(Lambda/匿名函数)可以捕获并访问其外部作用域中的变量,即使外部作用域已经退出。
闭包的核心特性是延长变量的生命周期,使得外部变量不会被垃圾回收(GC),直到闭包本身不再被引用。
简单地说就是改变了变量的生命周期,例如本来在一个函数里面的变量,结果在类中还可以修改,这就是闭包。
内层的函数可以引用包含在它外层的函数的变量
即使外层的函数的执行已经终止
注意;
该变量提供的值并非变量创建时的值,而是在父函数范围内的最终值
1. 闭包的基本原理
捕获外部变量:Lambda 表达式或匿名函数可以“记住”定义时所在作用域的变量。
变量的生命周期:被捕获的变量会一直存活,直到闭包不再被使用。
示例 1:简单闭包
using System;Func CreateCounter(){ int count = 0; // 外部变量 // 闭包捕获 count return () => ++count; // Lambda 表达式}public static void Main(){ var counter = CreateCounter(); Console.WriteLine(counter()); // 输出 1 Console.WriteLine(counter()); // 输出 2 Console.WriteLine(counter()); // 输出 3}
解释:
关键点:
CreateCounter
方法执行完毕后,局部变量count
本应被销毁,但由于闭包的存在,它的生命周期被延长。每次调用
counter()
时,闭包操作的count
是同一个变量。
2. 闭包捕获的是变量,不是值
闭包捕获的是变量的引用,而不是变量在某一时刻的值。这意味着如果外部变量后续被修改,闭包中看到的是修改后的值。
示例 2:闭包与循环陷阱
var actions = new List();for (int i = 0; i Console.WriteLine(i));}foreach (var action in actions){ action(); // 输出 3, 3, 3(而非 0, 1, 2)}
原因:
闭包捕获的是循环变量
i
的引用,而不是每次循环时的值。循环结束时,
i
的值为 3,所有闭包共享同一个i
。
3. 如何避免循环陷阱?
通过创建局部变量的副本,让闭包捕获独立的变量:
修复示例:
var actions = new List();for (int i = 0; i Console.WriteLine(temp)); // 捕获 temp}foreach (var action in actions){ action(); // 输出 0, 1, 2}
每次循环都会创建一个新的
temp
变量,闭包捕获的是不同的temp
。
4. 闭包的常见应用场景
事件处理:在事件回调中访问外部变量。
异步编程:在
async/await
中捕获上下文变量。延迟执行:将逻辑封装为闭包,延迟到特定时机执行。
工厂模式:生成具有独立状态的函数(如示例 1 的计数器)。
5.闭包的注意事项
1.避免循环引用:闭包引用外部对象可能导致内存泄漏。
2.谨慎使用闭包捕获可变变量:共享变量可能导致线程安全问题。
小结:
闭包的本质:Lambda/匿名函数捕获外部作用域的变量,延长其生命周期。
核心价值:简化代码,支持函数式编程范式。
核心风险:内存泄漏和逻辑陷阱(如循环变量共享)。
第四部分:委托,事件,匿名函数的总结
+=
和 -=
订阅或取消订阅