C#进阶学习(十)更加安全的委托——事件以及匿名函数与Lambda表达式和闭包的介绍

目录

第一部分:事件

一、什么是事件?

关键点:

二、事件的作用

三、事件怎么写以及注意事项

[访问修饰符 event 委托类型 事件名;](#访问修饰符 event 委托类型 事件名;)

四、事件区别于委托的细节之处

第二部分:匿名函数

一、什么是匿名函数

二、匿名函数的基本申明规则以及使用示例

三、匿名函数的优缺点

优点:

缺点:

第三部分:Lambda表达式

[一、什么是 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. 闭包的常见应用场景)

5.闭包的注意事项

第四部分:委托,事件,匿名函数的总结


第一部分:事件

一、什么是事件?

事件(Event)是 C# 中一种特殊的委托(Delegate),用于实现发布-订阅模型 (Publish-Subscribe Pattern)。它允许一个对象(发布者)在特定动作发生时通知其他对象(订阅者)。事件的核心思想是封装委托,提供更安全的访问控制。

事件是基于委托的存在
事件是委托的安全包裹
让委托的使用更具有安全性
事件是一种特殊的变量类型

关键点:
  1. 本质 :事件是对委托的封装,确保只有声明事件的类才能触发(Invoke)它。

  2. 设计模式:事件是观察者模式(Observer Pattern)的具体实现。

  3. 语法 :事件基于委托类型定义,但通过 event 关键字声明。

二、事件的作用

  1. 解耦通信

    事件允许对象之间通过松耦合的方式通信。例如,GUI 中的按钮点击事件不需要知道具体哪个类会处理点击逻辑。

  2. 安全性

    通过限制外部对委托的直接访问(如触发或重置委托链),防止误操作。

  3. 多播支持

    事件天然支持多播(多个订阅者),例如多个方法可以同时订阅同一个事件。

  4. 标准化设计

    事件常用于框架设计(如 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"
    }
}

三、匿名函数的优缺点

优点
  1. 简化代码:无需单独定义方法,减少代码量。

  2. 灵活性强:可直接访问外层变量(闭包),适合快速实现临时逻辑。

  3. 减少类成员:避免因简单逻辑污染类的成员列表。

缺点
  1. 可读性差:复杂逻辑内联在匿名函数中会降低代码可读性。

  2. 难以重用:匿名函数无法被其他代码直接调用。

  3. 闭包陷阱:若匿名函数引用外部变量,可能导致变量生命周期延长(内存泄漏风险)。

  4. 调试困难 :匿名函数在堆栈跟踪中显示为不可见的方法名(如 <Main>b__0)。

  5. 添加到委托或者事件容器中 不记录 无法单独移除

第三部分:Lambda表达式

一、什么是 Lambda 表达式?

Lambda 表达式 是 C# 中一种更简洁的匿名函数写法,本质上仍是匿名函数,但语法更精简。它通过 => 符号(读作"goes to")连接参数列表和方法体,核心目的是简化委托和事件的代码

可以将Lambda表达式理解为一种匿名函数的简写
他除了写法不同以外
使用上和匿名函数一模一样
都是和委托或者事件 配合使用的

关键特性:
  1. 匿名性:无需显式定义方法名。

  2. 类型推断:参数类型可省略(由编译器自动推断)。

  3. 灵活性:支持表达式体(单行代码)和语句体(多行代码)。

二、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. 闭包的常见应用场景
  1. 事件处理:在事件回调中访问外部变量。

  2. 异步编程 :在 async/await 中捕获上下文变量。

  3. 延迟执行:将逻辑封装为闭包,延迟到特定时机执行。

  4. 工厂模式:生成具有独立状态的函数(如示例 1 的计数器)。

5.闭包的注意事项

1.避免循环引用:闭包引用外部对象可能导致内存泄漏。

2.谨慎使用闭包捕获可变变量:共享变量可能导致线程安全问题。

小结:

  1. 闭包的本质:Lambda/匿名函数捕获外部作用域的变量,延长其生命周期。

  2. 核心价值:简化代码,支持函数式编程范式。

  3. 核心风险:内存泄漏和逻辑陷阱(如循环变量共享)。

第四部分:委托,事件,匿名函数的总结

特性 委托(Delegate) 事件(Event) 匿名函数(Lambda/匿名方法)
本质 类型安全的函数指针,用于封装方法 对委托的封装,提供更安全的访问控制 无名称的内联函数,依赖委托或事件存在
主要作用 定义方法签名,实现回调机制 实现发布-订阅模型,解耦对象通信 简化委托/事件的代码,处理临时逻辑
访问权限 公共成员,外部可直接调用或赋值 外部只能通过 +=-= 订阅或取消订阅 仅能通过委托或事件间接使用
多播支持 支持(可链式调用多个方法) 支持(本质是多播委托) 依赖委托的多播能力
典型应用场景 回调方法、LINQ、异步编程 GUI 交互(如按钮点击)、消息通知系统 事件处理、简单逻辑封装(如排序规则)
相关推荐
"_rainbow_"3 小时前
C++常用函数合集
开发语言·c++·算法
满怀10154 小时前
【Python进阶】正则表达式实战指南:从基础到高阶应用
开发语言·python·正则表达式
加点油。。。。5 小时前
C语言高频面试题目——内联函数和普通函数的区别
c语言·开发语言·面试
muyouking115 小时前
5.Rust+Axum:打造高效错误处理与响应转换机制
开发语言·后端·rust
songroom5 小时前
Rust: 从内存地址信息看内存布局
开发语言·后端·rust
听雨·眠5 小时前
go中map和slice非线程安全
java·开发语言·golang
chenglin0165 小时前
.net core 中directory , directoryinfo ,file, fileinfo区别,联系,场景
c#
酷ku的森6 小时前
4.LinkedList的模拟实现:
java·开发语言
LVerrrr6 小时前
Missashe考研日记-day24
学习·考研