一篇博客彻底弄清楚C#中的事件

一篇博客彻底弄清楚C#中的事件

链接: 源码

1. 什么是事件

C#中的事件就像一个广播电台,当某个对象(比如按钮)发生了特定的事情(比如被点击),它就会“播出”一条消息。其他对象(比如窗体)如果对这条消息感兴趣,就可以“订阅”这个事件,就像收听某个电台一样。一旦事件发生,所有订阅了该事件的对象都会收到通知,并执行预先准备好的应对动作(比如弹出确认对话框)。这样,对象之间通过事件就能互相沟通,却不直接干涉彼此的内部运作,保持了代码的模块化和独立性。

C# 事件是面向对象编程中的一个重要特性,用于实现对象之间的通信和协作。事件允许一个对象(称为“发布者”或“事件源”)通知其他对象(称为“订阅者”或“事件处理器”)关于特定情况的发生。这种设计模式使得代码更具模块化和松耦合性,因为发布者不需要直接了解订阅者的具体实现细节。
事件是带有约束的委托实例.

2. 委托和事件的关系

事件和委托在C#中密切相关,它们共同构成了事件驱动编程的基础。下面详细阐述它们的联系与区别:

联系

  1. 基于委托:事件是基于委托的一种实现。每个事件都关联一个特定的委托类型,这个委托定义了事件处理程序的签名(返回类型、方法名、参数列表)。事件处理程序必须与该委托兼容才能被正确订阅。

    public delegate void MyDelegate(string message);
    
    public class EventPublisher
    {
        public event MyDelegate MyEvent;
    }
    

    在上述例子中,MyEvent 事件基于 MyDelegate 委托类型。

  2. 共享相同语法:事件的订阅(+=)和取消订阅(-=)操作与委托的组合(+=)和解除组合(-=)操作语法相同。

    // 订阅事件
    publisher.MyEvent += HandleMyEvent;
    
    // 取消订阅事件
    publisher.MyEvent -= HandleMyEvent;
    
    // 相同语法用于委托
    myDelegate += AnotherMethod;
    myDelegate -= AnotherMethod;
    
  3. 共同实现观察者模式:事件和委托一起实现了观察者模式,其中委托充当了“消息”接口,事件则提供了订阅和发布机制。发布者通过触发事件来通知订阅者,订阅者通过提供符合委托签名的方法作为事件处理程序来响应事件。

区别

  1. 语义和用途

    • 委托:是一种类型安全的函数指针,用于封装方法引用。它可以单独使用,支持多播(即一个委托实例可以包含多个方法),常用于回调、策略模式、函数式编程等场景,允许在运行时动态地改变行为。
    • 事件:是一种特殊的成员,设计用于实现发布/订阅(Observer)模式,提供了一种安全、可控的方式让一个对象通知其他对象其状态的改变。事件通常在类的外部只能订阅和取消订阅,而触发(引发)事件的权限仅限于事件所在的类内部
  2. 访问控制

    • 委托:作为一个类型或字段,其访问修饰符可以自由设定(如 publicprivate 等),可以在类的内外部被赋值、调用或测试是否为 null
    • 事件:虽然在语法上事件看起来像是一个字段,但实际上编译器会生成专用的访问器方法(addremove)来控制对事件的订阅和取消订阅。这些方法默认为 public,但事件本身通常声明为 publicprotectedinternal,以控制外部访问。外部代码不能直接访问或触发事件,只能通过 +=-= 操作符订阅和取消订阅。
  3. 触发机制

    • 委托可以直接调用,无论在类的内部还是外部。
    • 事件只能在类的内部通过 eventVariable?.Invoke(args) 或类似的语法触发。外部代码无法直接触发事件,这是为了确保事件的触发逻辑完全由事件拥有者控制。
  4. 安全性与封装

    • 委托:更灵活但也更暴露。如果一个公共字段是委托类型,任何代码都可以直接赋值、清空或调用它,可能导致意外的行为或安全问题。
    • 事件:提供了更高的封装性和安全性。外部代码只能通过 +=-= 操作符添加或移除事件处理程序,不能直接访问或修改事件背后的委托实例,也不能直接触发事件。
  5. 编译器支持

    • 委托:编译器不会为普通委托生成额外的辅助方法。
    • 事件:编译器会为事件生成私有委托字段(存储实际的事件订阅者列表)以及 addremove 访问器方法,这些方法负责维护订阅者列表的安全访问。

总结来说,事件和委托都是C#中用于处理方法调用的机制,它们紧密相关且都服务于事件驱动编程。委托提供了方法封装和多播能力,而事件在此基础上增加了专门的访问控制、触发规则和编译器支持,形成了一个更适合实现对象间事件通知的高级抽象。事件确保了发布者与订阅者之间的解耦,以及对事件触发逻辑的严格控制。

3. 如何定义事件

1. 使用 event 关键字结合自定义委托类型

这是最标准的定义事件的方式,先定义一个符合需求的自定义委托类型,然后在类中使用 event 关键字声明一个基于该委托类型的事件。

// 定义一个自定义委托类型
public delegate void CustomEventHandler(string message, bool isImportant);
//public event Action MyEvent;//用系统自带的Action委托也可以

public class MyClass
{
    // 使用自定义委托类型定义事件
    public event CustomEventHandler ImportantMessageReceived;
}

2. 使用 event 关键字结合预定义的泛型委托

C#提供了几个预定义的泛型委托,如 EventHandlerEventHandler<TEventArgs> 等,它们非常适合用于定义通用事件。使用这些预定义的委托可以简化事件定义,并遵循.NET框架的标准约定。

// 定义一个事件参数类,继承自 EventArgs
public class CustomEventArgs : EventArgs
{
    public string Message { get; set; }
    public bool IsImportant { get; set; }
}

public class MyClass
{
    // 使用预定义的泛型委托EventHandler<TEventArgs>定义事件
    public event EventHandler<CustomEventArgs> ImportantMessageReceived;
}

3. 使用 event 关键字结合匿名方法或Lambda表达式(仅限事件订阅者)

虽然不能直接使用匿名方法或Lambda表达式来定义事件本身(因为事件是类的成员,需要一个明确的类型),但可以在订阅事件时使用它们来创建事件处理程序。这样可以在订阅事件时简洁地定义处理逻辑。

public class MyClass
{
    public event EventHandler<CustomEventArgs> ImportantMessageReceived;
}

var myObject = new MyClass();
myObject.ImportantMessageReceived += (sender, e) =>
{
    Console.WriteLine($"Received message: {e.Message}, Importance: {e.IsImportant}");
};

总结起来,定义事件的主要方法包括使用 event 关键字结合自定义委托类型、预定义的泛型委托,以及在订阅事件时使用匿名方法或Lambda表达式来定义处理逻辑。在特殊情况下,还可以使用属性样式的事件定义来实现更复杂的控制逻辑。在大多数常规应用场景中,前两种方法(自定义委托类型或预定义泛型委托)是最常用的选择。

4. 事件的触发(引发)

事件由事件源类在其内部逻辑中通过调用 EventHandler.Invoke() 方法或使用 += 运算符来触发。通常,事件的触发封装在一个受保护的(protected)方法内,以便派生类可以访问。这个方法被称为“** raiser**”方法,通常以 On 开头。

public class PublisherClass
{
    // ...

    protected virtual void OnCustomEvent(MyEventArgs e)
    {
        CustomEvent?.Invoke(this, e);
    }

    private void SomeInternalMethod()
    {
        // 当满足特定条件时,触发事件
        var eventArgs = new MyEventArgs(...);
        OnCustomEvent(eventArgs);
    }
}

5. 事件订阅与取消订阅

所谓事件订阅,简单来说,就是事件绑定某个方法,我们就说该方法订阅了该事件.

订阅者通过将一个符合委托签名的方法(事件处理程序)赋值给事件来注册对事件的监听。这通常使用 += 运算符完成。同样,使用 -= 运算符可以取消订阅事件。

public class SubscriberClass
{
    private PublisherClass _publisher;

    public SubscriberClass(PublisherClass publisher)
    {
        _publisher = publisher;
        // 订阅事件
        _publisher.CustomEvent += HandleCustomEvent;
    }

    private void HandleCustomEvent(object sender, MyEventArgs e)
    {
        // 在这里处理事件逻辑
    }

    // 取消订阅事件
    public void UnsubscribeFromEvent()
    {
        _publisher.CustomEvent -= HandleCustomEvent;
    }
}

6. 事件的使用场景

事件常用于以下场景:

  • 用户界面编程:如按钮点击、文本框文本变化、窗口关闭等。
  • 系统级通知:如文件系统监控、网络连接状态变更、定时任务完成等。
  • 组件间通信:在模块化设计中,不同组件可以通过事件来交换信息而不直接依赖对方的实现细节。

7. 最佳实践

  • 避免空引用异常:在触发事件时,通常使用 EventHandler?.Invoke() 的形式,以防止在没有订阅者时引发 NullReferenceException
  • 使用有意义的事件参数:通过自定义 EventArgs 类传递与事件相关的详细信息。
  • 保持事件处理程序简洁:避免在事件处理程序中执行耗时操作,尤其是对于UI线程上的事件。考虑使用异步处理或任务调度。
  • 遵循约定:如事件名一般采用过去分词形式(如 ClickedChanged), raiser 方法以 On 开头等。

8. 综合案例

下面是一个综合案例, 小伙伴对着Program.cs 敲一遍代码,C#中的事件相关的知识应该差不多了. 代码源码链接在文章开头.

 public class MyEventClass
 {
     public event Action MyEvent;//声明事件

     protected virtual void OnMyEvent() //触发事件的方法
     {
         MyEvent?.Invoke();  //如果事件有订阅者(挂载了方法),通知订阅者(执行挂载的方法)
     }

     public void DoSomething() //模拟事件发布的方法
     {
         Console.WriteLine("Doing Something...");
         OnMyEvent();
     }
 }
    public class MyEventArgs : EventArgs
    {
        public string CustomData;
        public MyEventArgs(string customData)
        {
            CustomData = customData;
        }
    }
    public class MyEventListener
    {
        public void HandleMyEvent()
        {
            Console.WriteLine("MyEvent was raised!");
        }
    }
public class Subscribe
{
    public static void OnSubscribe(object sender, EventArgs e)
    {
        Console.WriteLine("订阅者收到消息");
        Console.WriteLine(sender.GetType().Name);
        Console.WriteLine(e.ToString());
    }
}
public class MyClass
{
    public event EventHandler MyEvent;

    public void EventSignal()
    {
        Console.WriteLine("触发事件的信号");
        MyEvent?.Invoke(this, null);
    }
    public void SubscribeEvent(object? sender, EventArgs e)
    {
        Console.WriteLine("事件被订阅");
    }
}

/// <summary>
/// 自己写委托,自己定义类,委托带参数
/// </summary>
public class MyClass2
{
    public delegate void MyDelgate(object? sender, EventArgs e);

    public event MyDelgate MyEvent;

    public void MyFunc(object? sender, EventArgs e)
    {
        Console.WriteLine($"发送者是{sender}");
        Console.WriteLine($"发送的参数是{e}");
    }
    public void EventSignal()
    {
        Console.WriteLine("触发事件的信号发射了->");
        MyEvent?.Invoke(this, null);
    }

}

/// <summary>
/// 自己写委托,自己定义类,委托带不参数
/// </summary>
public class MyClass3
{
    public delegate void MyDelgate();

    public event MyDelgate MyEvent;

    public void MyFunc()
    {
        Console.WriteLine("我是订阅者中的方法");
    }
    public void EventSignal()
    {
        Console.WriteLine("触发事件的信号发射了->");
        MyEvent?.Invoke();
    }

}
//步骤:
//1.事件声明
//2.事件绑定订阅者
//3.事件触发
//
//注意事项:
//1.事件绑定的方法,需要和事件的委托方法严格一致
//2.通过EventArgs可以携带参数
public class GenericEventHandler
{
    public event EventHandler<MyEventArgs> MyEvent;//声明事件
    public virtual void OnMyEvent(object sender, MyEventArgs e)
    {
        //事件处理逻辑
        Console.WriteLine("doing something...");
        Console.WriteLine(e.CustomData);
        Console.WriteLine("done");
        MyEvent?.Invoke(this, e);//如果事件有订阅者,则激活事件,将当前对象和事件参数e作为参数传递
      
    }
}
//Program.cs
using System.Reflection;
using System.Security.Cryptography.X509Certificates;
using 事件;


{//用系统自带的委托Action声明事件
    MyEventClass eventSource = new MyEventClass();//创建事件源对象
    MyEventListener listener = new MyEventListener();//创建事件监听器对象
    eventSource.MyEvent += listener.HandleMyEvent; //订阅事件(事件绑定某个方法)

    eventSource.DoSomething();

    eventSource.MyEvent -= listener.HandleMyEvent;
}

{//用关键字EventHandler<T>声明泛型事件
    GenericEventHandler eventHandlerTest = new GenericEventHandler();
    eventHandlerTest.MyEvent += Subscribe.OnSubscribe;
    eventHandlerTest.OnMyEvent(null, new MyEventArgs("hello,keson"));
}

{//用关键字EventHandler声明事件
    MyClass myClass = new MyClass();
    myClass.MyEvent += myClass.SubscribeEvent;
    myClass.EventSignal();
}

{//通过自定义委托,声明带参数事件
    MyClass2 myClass = new MyClass2();

    myClass.MyEvent += myClass.MyFunc;
    myClass.EventSignal();
}

{//通过自定义委托,声明不带参数事件
    MyClass3 myClass = new MyClass3();    
    myClass.MyEvent += myClass.MyFunc;
    myClass.EventSignal();
}
  • 15
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值