C#怎么使用事件?C#定义事件和事件处理完整教程

来源:3D模型作者:USDT程序员头衔:程序员
导读:本期聚焦于USDT程序员创作的《C#怎么使用事件?C#定义事件和事件处理完整教程》,敬请观看详情。事件的定义和订阅总是让不少初学者犯迷糊:publisher和subscriber到底是怎么关联的?事件的本质是什么?这篇文章从委托的基础概念讲起,详细拆解C#中event关键字的底层机制,带你理解事件发布者和订阅者之间的关系。文中通过完整的代码示例演示如何声明事件、如何注册和取消事件处理方法,并进一步讲解EventHandler标准模式、自定义事件参数EventArgs的写法。还会分析事件与普通委托的区别、常见的内存泄漏陷阱以及多线程场景下的注意事项,帮助你真正掌握C#事件编程,写出解耦、易维护的代码。

C#里的事件机制是构建松耦合程序的重要工具。无论是WinForms里的按钮点击,还是WPF中的数据变更通知,背后都离不开事件。这篇文章会从委托讲起,一步步带你搞清楚事件的定义、订阅、触发,以及在实际项目中应该遵循的标准写法。

C#怎么使用事件?C#定义事件和事件处理完整教程

事件的底层原理:为什么事件离不开委托

要理解事件,必须先理解委托。委托本质上是一种类型安全的函数指针,它可以把方法当作参数传递,也可以把多个方法存到一个调用列表里。当你调用一个委托变量时,它会依次执行列表中的所有方法。

而事件,本质上就是对委托变量的一层封装。用event关键字修饰一个委托类型的字段后,这个字段在外部类中就只能出现在+=-=的左侧,无法被外部直接调用或赋值。这个限制就是事件机制的核心价值:发布者保留唯一的触发权,订阅者只能决定自己要不要参与。

如果没有这层封装,直接暴露一个公开的委托字段会带来两个严重问题。第一,外部代码可以把委托变量赋值为null,把其他订阅者全部清掉;第二,外部代码可以直接调用这个委托,冒充发布者触发事件。事件关键字正是为了堵住这两个漏洞而存在的。

定义和订阅事件:从最简单的例子开始

下面用一个经典的“煮咖啡通知”例子演示完整流程。先定义一个委托类型,再在发布者类中声明事件,最后在订阅方注册处理方法。

using System;

namespace EventDemo
{
    // 第一步:定义委托类型,约定事件处理方法的签名
    public delegate void BoiledEventHandler(object sender, EventArgs e);

    // 发布者:热水器
    public class Heater
    {
        private int temperature;

        // 第二步:声明事件
        public event BoiledEventHandler Boiled;

        // 第三步:提供触发事件的方法,通常声明为 protected virtual 以便子类重写
        protected virtual void OnBoiled(EventArgs e)
        {
            // 判空后再触发,防止没有订阅者时抛出空引用异常
            Boiled?.Invoke(this, e);
        }

        public void BoilWater()
        {
            for (int i = 0; i <= 100; i += 20)
            {
                temperature = i;
                if (temperature >= 80)
                {
                    // 达到条件,触发事件
                    OnBoiled(EventArgs.Empty);
                }
            }
        }
    }

    // 订阅者:警报器
    public class Alarm
    {
        public void MakeAlert(object sender, EventArgs e)
        {
            Console.WriteLine("警报:水快烧开了!");
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            Heater heater = new Heater();
            Alarm alarm = new Alarm();

            // 第四步:订阅事件,把处理方法挂到事件上
            heater.Boiled += alarm.MakeAlert;
            heater.Boiled += (s, e) => Console.WriteLine("匿名方法也可以订阅");

            heater.BoilWater();

            // 取消订阅,防止对象生命周期结束后仍被引用
            heater.Boiled -= alarm.MakeAlert;
            Console.ReadKey();
        }
    }
}

这段代码里有几个细节值得注意。触发事件时使用了?.Invoke()的写法,这是C# 6之后推荐的方式,比先判断null再调用更简洁且线程安全。OnBoiled方法被声明为protected virtual,这是.NET的设计惯例,目的是让继承的子类也能重写事件的触发逻辑。

订阅时用+=,取消订阅用-=。如果同一个方法被订阅了两次,触发时会被执行两次,取消一次订阅只移除一个引用,这些行为要记清楚,不然排查重复执行问题时会很头疼。

EventHandler标准模式与自定义EventArgs

实际项目中并不需要每次都自定义委托类型。.NET框架提供了泛型委托EventHandler<TEventArgs>,它的签名是void Handler(object sender, TEventArgs e),已经成为事件处理的事实标准。如果需要给订阅者传递数据,就继承EventArgs写一个自定义参数类。

using System;

// 自定义事件参数,携带温度数据
public class TemperatureEventArgs : EventArgs
{
    public int Temperature { get; }

    public TemperatureEventArgs(int temperature)
    {
        Temperature = temperature;
    }
}

public class Heater
{
    // 使用框架自带的泛型委托,无需自己声明委托类型
    public event EventHandler<TemperatureEventArgs> Boiled;

    protected virtual void OnBoiled(int temperature)
    {
        Boiled?.Invoke(this, new TemperatureEventArgs(temperature));
    }

    public void BoilWater()
    {
        OnBoiled(98);
    }
}

class Program
{
    static void Main()
    {
        var heater = new Heater();

        // 使用Lambda表达式订阅
        heater.Boiled += (sender, e) =>
        {
            Console.WriteLine($"当前温度:{e.Temperature} 度");
        };

        heater.BoilWater();
    }
}

这种写法的好处很明显:sender参数让订阅者知道事件来自哪个对象,继承EventArgs的参数类可以携带任意业务数据,而且整个签名符合.NET生态的统一约定,别的开发者一看就懂。如果你的类库会被外部使用,强烈建议遵循这套模式,而不是随意定义签名。

另外提醒一点,从C# 9开始EventArgs的构造不再强制要求继承,任何类型都可以直接作为泛型参数使用,但为了保持代码的一致性和兼容性,老项目里继续继承EventArgs依然是更稳妥的选择。

事件使用中的常见坑

第一个坑是内存泄漏。事件发布者会持有订阅者的强引用,只要发布者还活着,订阅者就无法被垃圾回收。这在WinForms开发中尤其常见:一个长生命周期的全局对象订阅了某个短生命周期的窗体事件,窗体关闭后内存迟迟不释放。解决办法是在窗体关闭时显式执行-=取消订阅。

第二个坑是事件处理方法的异常传播。一个事件有多个订阅者时,如果前面的订阅者抛出异常且未捕获,后面的订阅者将不会收到通知。Invoke是同步顺序调用的,这一点和多播委托的行为一致。如果需要保证每个订阅者都被执行,就要手动遍历委托的调用列表,逐个用try-catch包裹。

第三个坑涉及多线程。在触发事件前判断了null,真正调用前另一个线程恰好取消了订阅,这时仍可能抛出空引用异常。稳妥的做法是把事件变量先复制到局部变量再判断调用,形如var handler = Boiled; handler?.Invoke(this, e);,而Boiled?.Invoke()这种写法在编译后实际上就是这样的逻辑,所以直接用条件调用运算符即可。

掌握这些细节后,事件就不再神秘。它不过是封装过的多播委托,配合合理的命名规范和生命周期管理,能帮你写出结构清晰、模块之间互不依赖具体实现的代码,这也是观察者模式在C#中最直接的落地方式。

C#事件event关键字委托订阅修改时间:2026-09-07 16:31:02

免责声明:已尽一切努力确保本网站所含信息的准确性。网站作品多为原创整理与精心创作,观点力求客观中立。本站旨在免费分享,内容仅供个人学习、研究或参考使用。若引用了第三方作品,版权归原作者所有。如内容涉及您的权益,请联系我们进行处理Email:chomcom@qq.com。
引用或转载本作品时,请注明当前出处:https://www.ipipp.com/html/20260907/52316.html,基于非商业用途的前提下,欢迎转载或二创本作品。
内容垂直聚焦
专注技术核心技术栏目,确保每篇文章深度聚焦于实用技能。从代码技巧到架构设计,为用户提供无干扰的纯技术知识沉淀,精准满足专业提升需求。
知识结构清晰
覆盖从开发到部署的全链路。AI、前端、编程、数据库、服务器、建站、系统层层递进,构建清晰学习路径,帮助用户系统化掌握开发与运维所需的核心技术。
深度技术解析
拒绝泛泛而谈,深入技术细节与实践难点。无论是数据库优化还是服务器配置,均结合真实场景与代码示例进行剖析,致力于提供可直接应用于工作的解决方案。
专业领域覆盖
精准对应开发生命周期。从前端界面到后端编程,从数据库操作到服务器运维,形成完整闭环,一站式满足全栈工程师和运维人员的技术需求。
即学即用高效
内容强调实操性,步骤清晰、代码完整。用户可根据教程直接复现和应用于自身项目,显著缩短从学习到实践的距离,快速解决开发中的具体问题。
持续更新保障
专注既定技术方向进行长期、稳定的内容输出。确保各栏目技术文章持续更新迭代,紧跟主流技术发展趋势,为用户提供经久不衰的学习价值。