C#学习记录-事件

在学习C#的过程中,事件是一个非常重要的知识点。它经常和委托一起出现,因此很多初学者会觉得事件有些抽象,不太容易区分。实际上事件并没有脱离委托,它本质上是,基于委托实现的一种发布-订阅机制。简单来说,事件的作用就是:某个对象发生了一件事,其他对象如果对这件事感兴趣,就可以提前订阅,当事件真的发生时,系统会自动通知这些订阅者。

这套机制在实际开发中非常常见,例如:按钮被点击,文件下载完成,用户登录成功等,这些都可以用事件来实现。

一、什么是事件

事件可以理解为一种对象对外发出的通知 。当某个类中发生了某种情况时,它可以触发一个事件,通知所有已经订阅了这个事件的方法执行相应逻辑。你可以先把事件理解成这样一句话:事件就是"某件事发生了"的消息通知机制。

二、为什么需要事件

很多人学到这里会想,既然委托已经能调用方法了,为什么还需要事件?这是一个非常关键的问题。委托确实可以让我们保存方法并调用方,但如果直接把委托暴露出去,会有一些问题,外部可以直接调用这个委托,外部可以直接清空委托中的方法列表,外部可以随意覆盖原有的订阅关系。这样会导致封装性很差,也不安全。事件就是在委托的基础上增加了一层限制:外部只能订阅和取消订阅,不能随意触发事件。这样就让通知机制变的合理,也更符合面向对象设计思想。

三、事件的基本语法

在C#中,事件使用event关键字声明

语法格式

cs 复制代码
访问修饰符 event 委托类型 事件名;

例如

cs 复制代码
public event Action OnCompleted;

或者使用自定义委托:

cs 复制代码
public delegate void AlarmHandler();
public event AlarmHandler Alarm;

这里的意思是:先声明一个事件,这个事件基于某种委托类型,只有符合这个委托签名的方法才能订阅。

四、事件示例

下面来看一个简答的例子

cs 复制代码
using System;

public class Publisher
{
    public event Action SomethingHappened;

    public void DoSomething()
    {
        Console.WriteLine("正在执行某个操作...");
        SomethingHappened?.Invoke();
    }
}

public class Program
{
    public static void Main()
    {
        Publisher publisher = new Publisher();

        publisher.SomethingHappened += OnSomethingHappened;

        publisher.DoSomething();
    }

    public static void OnSomethingHappened()
    {
        Console.WriteLine("事件被触发了,收到通知。");
    }
}

在这个例子中:

首先声明了

cs 复制代码
public event Action SomethingHappened;

一个事件SomethingHappened,它是基于Action委托。然后

cs 复制代码
publisher.SomethingHappened += OnSomethingHappened;

表示把OnSomethingHappened方法注册到事件中。

cs 复制代码
SomethingHappened?.Invoke();

表示如果有人订阅了这个事件,就依次调用这些订阅的方法。这里的?.Invoke()是安全写法,意思是

  • 如果事件不为null,就执行
  • 如果没人订阅,事件为null,就不会报错。

五、事件为什么比委托更安全

假设我们这样写:

cs 复制代码
public Action Notify;

外部不仅可以订阅

cs 复制代码
obj.Notify += Handler;

还可以:

cs 复制代码
obj.Notify = null;      // 清空所有订阅
obj.Notify = Handler;   // 直接覆盖
obj.Notify();           // 外部直接触发

这样就会破坏类的封装性。

使用event后,我们这样改:

cs 复制代码
public event Action Notify;

那么外部就只能:

cs 复制代码
obj.Notify += Handler;
obj.Notify -= Handler;

不能直接覆盖赋值,直接清空,直接触发调用,这就保证了事件只能由声明它的类自己控制触发时机。这正是事件的核心价值之一。

六、标准事件模式

在实际的开发中,C#更推荐使用一种标准事件模式。这种模式通常会用到object sender和EventArgs e。

标准模式如下:

cs 复制代码
public event EventHandler EventName;

或者:

cs 复制代码
public event EventHandler<TEventArgs> EventName;

下面是一个具体的例子:

cs 复制代码
using System;

public class Button
{
    public event EventHandler Clicked;

    public void Click()
    {
        Console.WriteLine("按钮被点击了");
        Clicked?.Invoke(this, EventArgs.Empty);
    }
}

public class Program
{
    public static void Main()
    {
        Button button = new Button();
        button.Clicked += Button_Clicked;

        button.Click();
    }

    private static void Button_Clicked(object sender, EventArgs e)
    {
        Console.WriteLine("收到按钮点击事件");
    }
}

这里的Clicked是事件,EventHandler是.NET内置委托类型,this表示事件来源,EventArgs.Empty表示没有额外数据。

七、带事件数据的事件

很多时候,事件不仅仅是通知"某件事发生了",还需要携带一些额外信息。例如:

登陆事件需要带用户名,成绩变化事件需要带新的成绩,下载完成事件需要带文件名和大小等。这时就可以自定义事件参数类。

下面给出一个学生成绩的例子

7.1自定义EventArgs

cs 复制代码
using System;

public class ScoreChangedEventArgs : EventArgs
{
    public string StudentName { get; set; }
    public int NewScore { get; set; }

    public ScoreChangedEventArgs(string studentName, int newScore)
    {
        StudentName = studentName;
        NewScore = newScore;
    }
}

7.2使用EventHandler<TEventArgs>

cs 复制代码
using System;

public class Student
{
    public string Name { get; set; }
    private int score;

    public event EventHandler<ScoreChangedEventArgs> ScoreChanged;

    public Student(string name)
    {
        Name = name;
    }

    public void UpdateScore(int newScore)
    {
        score = newScore;
        Console.WriteLine($"{Name} 的成绩已更新为 {score}");

        ScoreChanged?.Invoke(this, new ScoreChangedEventArgs(Name, newScore));
    }
}

public class Program
{
    public static void Main()
    {
        Student student = new Student("张三");
        student.ScoreChanged += OnScoreChanged;

        student.UpdateScore(95);
    }

    public static void OnScoreChanged(object sender, ScoreChangedEventArgs e)
    {
        Console.WriteLine($"收到事件:学生 {e.StudentName} 的新成绩是 {e.NewScore}");
    }
}

这个例子就体现了标准事件模式的完整结构:

发布者:Student

事件:ScoreChanged

自定义事件数据:ScoreChangedEventArgs

订阅者:OnScoreChanged

八、事件的触发方式

事件通常在类内部某个操作完成后触发,例如状态改变后触发,数据更新后触发,用户操作发生后触发,某任务完成后触发。

8.1常见触发写法

cs 复制代码
MyEvent?.Invoke(this, EventArgs.Empty);

这是最常见的安全触发方式。

这里在讲下为啥要用?.Invoke()

因为如果没人订阅事件,那么事件变量就是null。直接调用会报错:

cs 复制代码
MyEvent(this, EventArgs.Empty); // 可能空引用异常

使用?.Invoke()可以避免这个问题。

九、匿名方法与Lambda订阅事件

在实际开发中,很多事件处理程序并不会单独写成命名方法,而是直接使用匿名方法或Lambda表达式。

9.1使用匿名方法

cs 复制代码
button.Clicked += delegate (object sender, EventArgs e)
{
    Console.WriteLine("匿名方法:按钮被点击");
};
cs 复制代码
button.Clicked += (sender, e) =>
{
    Console.WriteLine("Lambda:按钮被点击");
};

Lambda是现代C#中最常见的写法,简洁且直观。

十、完成案例:温度监控系统

下面是给出的一个完整的案例:温度监控系统,系统要实现监控当前温度,当温度超过警戒值时触发事件,订阅者收到事件后给出提示。

代码实现:

cs 复制代码
using System;

public class TemperatureExceededEventArgs : EventArgs
{
    public double Temperature { get; }

    public TemperatureExceededEventArgs(double temperature)
    {
        Temperature = temperature;
    }
}

public class Thermometer
{
    public double WarningTemperature { get; set; }

    public event EventHandler<TemperatureExceededEventArgs> TemperatureExceeded;

    public Thermometer(double warningTemperature)
    {
        WarningTemperature = warningTemperature;
    }

    public void CheckTemperature(double currentTemperature)
    {
        Console.WriteLine($"当前温度:{currentTemperature}℃");

        if (currentTemperature > WarningTemperature)
        {
            TemperatureExceeded?.Invoke(
                this,
                new TemperatureExceededEventArgs(currentTemperature)
            );
        }
    }
}

public class Alarm
{
    public void OnTemperatureExceeded(object sender, TemperatureExceededEventArgs e)
    {
        Console.WriteLine($"警报:温度超标,当前温度为 {e.Temperature}℃!");
    }
}

public class Program
{
    public static void Main()
    {
        Thermometer thermometer = new Thermometer(30);
        Alarm alarm = new Alarm();

        thermometer.TemperatureExceeded += alarm.OnTemperatureExceeded;

        thermometer.CheckTemperature(25);
        thermometer.CheckTemperature(32);
    }
}

这个案例中的:

发布者:Thermometer

事件:TemperatureExceeded

事件数据:TemperatureExceededEventArgs

订阅者:Alarm

这里Thermometer不需要知道谁来处理温度超标,它只负责在条件满足时触发事件。而处理逻辑由外部订阅者决定,这就实现了解耦。

十一、事件与委托的区别总结

|-----------|---------|-------------|
| 对比项 | 委托 | 事件 |
| 本质 | 引用方法的类型 | 基于委托的通知机制 |
| 外部是否可直接调用 | 可以 | 不可以 |
| 外部是否可覆盖 | 可以 | 不可以直接覆盖 |
| 主要用途 | 传递方法、回调 | 发布-订阅通知 |
| 使用方式 | 赋值、调用 | +=订阅、-=取消订阅 |

总结成一句话:委托偏重方法传递,事件偏重消息通知

十二、参考链接

相关推荐
Scott9999HH5 小时前
【IIoT流量实战】蒸汽管道阀门全关却仍有流量?用 Python 实现涡街信号 FFT 频谱分析与温压全补偿积算网关,深度拆解靠谱的涡街流量计厂家硬核技术标准
开发语言·python
EIP低代码平台5 小时前
EIP 低代码平台 - 角色维护
低代码·c#·权限·工作流·netcore
码智社6 小时前
AES加密原理详解及Java实现加解密实战
java·开发语言
AI云海6 小时前
python 列表、元组、集合和字典
开发语言·python
萧瑟余晖7 小时前
JDK 26 新特性详解
java·开发语言
马优晨7 小时前
Freemarker 完整讲解(后端 Java 模板引擎)
java·开发语言·freemarker·freemarker 完整讲解·freemarker模板引擎
a1117769 小时前
2FA 验证码生成器(github登录验证 app)
笔记·学习
我的xiaodoujiao9 小时前
API 接口自动化测试详细图文教程学习系列32--Allure测试报告2
python·学习·测试工具·pytest
人邮异步社区9 小时前
怎么把C语言学到精通?
c语言·开发语言
心平气和量大福大10 小时前
C#-WPF-控件-TextBox 数据绑定
开发语言·c#·wpf