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

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

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

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

十二、参考链接

相关推荐
X在敲AI代码2 小时前
推荐系统学习 D1推荐系统核心概述
学习·推荐算法
我的xiaodoujiao2 小时前
API接口自动化测试详细图文教程学习系列1--序章
python·学习·pytest
圆弧YH2 小时前
服务器及网站操作
学习
小杍随笔2 小时前
【Rust 语言编程知识与应用:基础数据类型详解】
开发语言·后端·rust
Yupureki2 小时前
《MySQL数据库基础》1. 数据库基础
c语言·开发语言·数据库·c++·mysql·oracle·github
Alphapeople2 小时前
具身智能学习路线
学习
enmouhuadou3 小时前
快速运行matlab仿真方法
开发语言·matlab
m0_706653233 小时前
使用C-Free进行浮点变量的四则运算指南
c语言·开发语言
肖恭伟3 小时前
VScode入门学习
ide·vscode·学习