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