在设置事件处理程序时使用空方法是不好的做法

时间:2012-07-31 14:50:57

标签: c# events

我有一个包含许多类的类库,在每个类中我都会引发一些事件。

每个事件都有自己的一组事件参数,因此我将这些参数作为自动属性存储在继承自EventArgs的类中。然后,我可以通过简单地调用Invoke并传入我的EventArgs继承类的新实例来引发相关事件。这就是我的意思:

using System;

//My Class Library
namespace MyClassLibrary
{
    //A class
    public class MyClass
    {
        //My event is a field
        public event EventHandler<MyEventHandler> myEvent;

        //I raise my event in this method
        public void InvokeMyEvent()
        {
            //Do some stuff

            //I raise my event here
            myEvent.Invoke(this, new MyEventHandler("The quick brown fox jumps over the lazy dog"));

            //Do some more stuff
        }
    }

    //An event handler, containing some interesting data about the event
    public class MyEventHandler : EventArgs
    {
        //Some interesting data as an automatic property
        public string MyInterestingData { get; private set; }

        //I assign the value of my intersting data in my constructor
        public MyEventHandler(string FooBar)
        {
            MyInterestingData = FooBar;
        }
    }
}

所以这就是我想要做的。现在,在我的应用程序中,我可以添加对类库的引用,实例化MyClass并订阅我的事件,就像这样。关于这一点的好处(使用通用事件处理程序)是Intellisense允许我在订阅我的事件时使用Tab键,并为我设置方法,默认情况下传入MyEventHandler。没有必要的铸造。

using System;
using MyClassLibrary;

namespace ConsoleApplication33
{
    class Program
    {
        static void Main(string[] args)
        {
            MyClass x = new MyClass();
            x.myEvent += new EventHandler<MyEventHandler>(x_myEvent);
        }

        static void x_myEvent(object sender, MyEventHandler e)
        {
            //Doing lots of important stuff

            //Able to access my interesting data in my event handler without casting
            var y = e.MyInterestingData;
        }
    }
}

这一切都很好,但是类库的要点肯定是有一堆可重用的代码,这当然是这里的意图。所以我想将我的类库添加到许多项目中。在其中一些项目中,我需要订阅myEvent,在其他项目中它不会,但我仍然希望在这些项目中使用该类的其他功能,并且可以选择订阅将来myEvent

但是,如果我在我没有订阅myEvent的项目中使用我的类库,那么每当myEvent被引发时我都会收到运行时错误。

我通过在myEvent的构造函数中使用一个空方法订阅MyClass来解决这个问题:

public MyClass()
{
    myEvent += new EventHandler<MyEventHandler>(MyClass_myEvent);
}


void MyClass_myEvent(object sender, MyEventHandler e)
{

}

这意味着我可以将我的类库添加到任何项目中,实例化MyClass并使用它提供的其他功能,并在需要时订阅myEvent,同时忽略{{1}如果我不这样做。

这个问题是我在MyClass中有一个空方法。想象一下这个场景,但有大约30个事件,因此有30个空方法。

几个问题

  1. 我有道理吗?就像你在做什么,至少你明白我想要做什么,即使你认为我这样做都错了?
  2. 这里确实存在问题,或者这是否是实现我想要实现的功能的一种相当标准的方式?
  3. 由于

7 个答案:

答案 0 :(得分:4)

事件MyEvent的典型实现如下:

protected virtual void OnMyEvent(MyEventArgs eventArgs) {
    var handler = MyEvent;
    if (handler != null) {
        handler(this, eventArgs);
    }
}

然后,只要您想触发事件,就可以调用OnMyEvent而不是MyEvent。

答案 1 :(得分:3)

这通常是您看到以下带有事件的模式的原因:

private void OnMyEvent(object sender, MyEventArgs args)
{
    var ev = myEvent;

    if (ev != null)
        ev(sender, args);
}

OnMyEvent(this, new MyEventArgs("The quick brown fox jumps over the lazy dog"));

如果没有订阅者,该事件将为null。此代码采用本地副本以确保检查点的事件不参与竞争条件。然后调用该副本。即使这样可以防止一种形式的竞争条件,原始订户列表仍然可以进行更改,这些更改在您的副本中不可见,因此它不完全是线程安全的。

说实话,我从未考虑过,甚至没有看到过这样做的方式。我说最好坚持使用空检查而不是空方法订阅者,人们期待前者,而不是后者。

此外,空方法路由会花费内存/对象,其中空路由只需要检查。

除此之外,MyEventHandler参数类通常称为MyEventArgs

答案 2 :(得分:2)

值得注意的是,虽然多播委托可以用于具有任意数量订阅者的事件,但Delegate.Combine的性能针对结果将等于传入的委托之一,Delegate.Remove的性能进行了优化。 {1}}针对结果为null的情况进行了优化,Delegate.Invoke针对仅调用一个委托(以及进行空检查的OnMyEvent的情况进行了优化在没有代表的情况下会更快。)因此,添加空事件处理程序将使代码的效率低于其他情况。

尚未提及的方法是创建静态do-nothing委托,然后根据需要更改一个添加/删除处理程序:添加订阅时,如果旧的subscription-list委托与静态do-nothing委托匹配,使用Interlocked.CompareExchange来存储新代表;否则计算Delegate.Combine以构建新的委托和CompareExchange(在任何一种情况下,如果CompareExchange失败,则重试add-subscription方法)。删除订阅时,请使用Delegate.Remove来确定新列表;如果为null,则替换静态do-nothing委托。然后使用CompareExchange更新订阅列表委托。

这种方法会稍微降低AddRemove方法的速度,但不会像在列表中留下do-nothing委托一样多;零订户情况下的委托调用将比空检查略慢,但单订户情况会稍快一些。

答案 3 :(得分:1)

处理此问题的规范方法是不引发没有订阅者的事件:

var handler = myEvent;
if (handler != null)
{
    handler(sender, new MyEventArgs());
}

如果在多线程场景中,单个订阅者在handler检查和调用之间取消订阅,则分配给中间null变量可避免出现异常。

答案 4 :(得分:0)

通常的做法是在举起活动前检查null。在你的情况下:

if (myEvent != null)
  myEvent.Invoke(this, new MyEventHandler("The quick brown fox jumps over the lazy dog")); 

答案 5 :(得分:0)

使用空处理程序初始化事件没有任何问题。我一直这样做。如果您将该事件召唤数千次,则可能比您想要的慢(测试,测量,决定)......

设置空处理程序时,您不必如此罗嗦,您可以这样做:

myEvent = (sender, args) => { };

并避免创建方法...

答案 6 :(得分:0)

我希望我能看到你的成就。我想,你可以为自己的空方法省去工作。只需按以下方式调用您的活动:

 //I raise my event in this method
    public void InvokeMyEvent()
    {
        //Do some stuff

        //Check if there are subscribers!!
        if (myEvent != null)
          myEvent.Invoke(this, new MyEventHandler("The quick brown fox jumps over the lazy dog"));

        //Do some more stuff
    }