【问题标题】:Why must someone be subscribed for an event to occur?为什么必须订阅某人才能发生事件?
【发布时间】:2008-10-16 16:56:26
【问题描述】:

代码前的一些文本,以便问题摘要不会被破坏。

class Tree
{
    public event EventHandler MadeSound;

    public void Fall() { MadeSound(this, new EventArgs()); }

    static void Main(string[] args)
    {
        Tree oaky = new Tree();
        oaky.Fall();
    }
}

我在 C# 中使用的事件不多,但这会导致 NullRefEx 的事实似乎很奇怪。 EventHandler 引用被认为是空的,因为它目前没有订阅者 - 但这并不意味着事件没有发生,不是吗?

EventHandler 与标准委托的区别在于 event 关键字。为什么语言设计者不让他们在没有订阅者的情况下默默地向虚空开火? (我认为您可以通过显式添加一个空委托手动执行此操作)。

【问题讨论】:

  • 这个例子也是 +1。我想在寂寞的森林中调用 Fall 不会触发 MadeSound。
  • 当一棵树倒在森林里而没有人在听时,它仍然会发出声音——它会说“哞”
  • 我在下面发布了一个使用扩展方法的示例,我相信它可以实现您想要的——将所有空检查隔离在一个位置。

标签: c# events


【解决方案1】:

嗯,规范的形式是:

void OnMadeSound()
{
    if (MadeSound != null)
    {
        MadeSound(this, new EventArgs());
    }
}

public void Fall() {  OnMadeSound(); }

这比调用空委托稍微快一点,所以速度胜过编程方便。

【讨论】:

  • 我认为建议对 On* 方法进行保护,以允许派生类对事件做出反应或更改调用事件的方式或时间。作为补充。
  • 其实最安全的形式是这样的:protected void OnMadeSound() { EventHandler tempHandler = MadeSound; if (tempHandler != null) { tempHandler(this, new EventArgs()); } }
  • 该方法应该受到保护,并且 tempHandler 可以防止在 null 检查和实际引发事件之间删除处理程序时发生 NullReferenceException 的可能性。
【解决方案2】:

我见过的另一种解决此问题的好方法,而不必记住检查 null:

class Tree
{
    public event EventHandler MadeSound = delegate {};

    public void Fall() { MadeSound(this, new EventArgs()); }

    static void Main(string[] args)
    {
        Tree oaky = new Tree();
        oaky.Fall();
    }
}

注意匿名委托 - 可能会对性能造成轻微影响,因此您必须确定哪种方法(检查 null 或空委托)最适合您的情况。

【讨论】:

  • 您的代码仍然可以将事件 MadeSound 设置为 null,因此您仍然可以获得 NullReferenceException,从而使委托{}毫无意义
  • 或者,您可以将其设置为委托 {},而不是将其设置为 null...您多久将事件设置为 null?我从来没有这样做过。
【解决方案3】:

推荐的模式是(.net 2.0+)

public class MyClass
{
    public event EventHandler<EventArgs> MyEvent; // the event

    // protected to allow subclasses to override what happens when event raised.
    protected virtual void OnMyEvent(object sender, EventArgs e)
    {
        // prevent race condition by copying reference locally
        EventHandler<EventArgs> localHandler = MyEvent;
        if (localHandler != null)
        {
            localHandler(sender, e);
        }
    }
    public void SomethingThatGeneratesEvent()
    {
        OnMyEvent(this, EventArgs.Empty);
    }
}

我在初始化程序中看到了很多关于空委托{}的建议,但我完全不同意。如果您遵循上述模式,您只需在一处检查event != null。空的委托初始化程序是一种浪费,因为它是每个事件的额外调用,它浪费内存,如果 MyEvent 在我的类的其他地方设置为 null,它仍然可能失败。

* 如果你的班级是密封的,你就不会将OnMyEvent() 设为虚拟。

【讨论】:

    【解决方案4】:

    您需要了解您的事件声明实际上在做什么。它同时声明了一个事件和一个变量,当您在类中引用它时,您只是在引用变量,当没有订阅者时,该变量将为空。

    【讨论】:

    • 我不认为它只是指变量,因为 C# 不允许一个类以它操纵变量的方式操纵它的事件。例如,C# 将允许“someEvent += someDeletate;”,但我认为它不会允许“someEvent = someEvent + someDelegate;”。鉴于此,我很困惑为什么以类似于调用委托的语法方式使用事件未编码为“获取委托,如果不为空则调用”。另一方面,如果我有我的 druthers,事件将使用与多播委托不同的机制。
    • @supercat 一个 c# 事件只是一个带有 event 关键字的委托。这提供了您描述的特殊访问限制,但它只是一个类
    • @Gusdor:不,它不是不是“只是一个代表”,正如一个属性是“只是一个字段”。一个事件实际上是一对方法:一个用于订阅,一个用于取消订阅。以什么方式“只是一堂课”?
    • @JonSkeet:你知道为什么 C# 没有让事件调用语法将空事件解释为无操作,因为空事件委托是合法的预期情况?要求程序员写 {var handler=SomethingHappened; if (handler!=null) handler(this,e);} 或较短但错误的 if (somethingHappened!=null) somethinghappened(this,e); 似乎没有多大帮助。
    • @supercat:你的意思是任何委托调用吗?您肯定不想将其限制为just 类似字段的事件吗?但无论哪种方式,不,我不知道为什么 C# 是这样设计的。我怀疑如果他们从头开始可能会有所不同。
    【解决方案5】:

    很有禅意,嗯?

    当你想引发一个事件时,你必须测试 null:

    protected void OnMyEvent()
    {
        if (this.MyEvent != null) this.MyEvent(this, EventArgs.Empty);
    }
    

    如果您不必为此烦恼,那就太好了,但它们是休息时间。

    【讨论】:

      【解决方案6】:

      James 提供了一个很好的技术推理,我还想补充一点,我已经看到人们利用这个优势,如果没有订阅者正在收听事件,他们会采取行动将其记录在代码或类似的东西中。一个简单的例子,但适合这种情况。

      【讨论】:

        【解决方案7】:

        如果没有人在听,那么引发事件有什么意义?从技术上讲,这正是 C# 选择实现它的方式。

        在 C# 中,事件是具有一些特殊功能的委托。在这种情况下,委托可以被视为函数指针的链接列表(指向订阅者的处理程序方法)。当您“触发事件”时,依次调用每个函数指针。最初,委托是一个空对象,就像其他任何东西一样。当您为第一个订阅操作执行 += 时,会调用 Delegate.Combine 来实例化列表。 (调用 null.Invoke() 会引发 null 异常 - 当事件被触发时。)

        如果您仍然觉得“不能这样”,请使用此处提到的辅助类 EventsHelper 以及旧的和改进的“防御性事件发布”http://weblogs.asp.net/rosherove/articles/DefensiveEventPublishing.aspx

        【讨论】:

          【解决方案8】:

          在这种情况下使用扩展方法会很有帮助。

          public static class EventExtension
          {
              public static void RaiseEvent<T>(this EventHandler<T> handler, object obj, T args) where T : EventArgs
              {
                  if (handler != null)
                  {
                      handler(obj, args);
                  }
              }
          }
          

          然后可以像下面这样使用它。

          public event EventHandler<YourEventArgs> YourEvent;
          ...
          YourEvent.RaiseEvent(this, new YourEventArgs());
          

          【讨论】:

          • 谢谢。实际上,我自己也采用了这种方法,并为普通的旧 EventArgs 制作了一种方法:)
          【解决方案9】:

          感谢您的回复。我明白为什么会发生 NullReferenceException 以及如何解决它。

          Gishu说

          如果没有人在听,那么引发事件有什么意义?

          嗯,也许这是一个术语。在我看来,“事件”系统的吸引力在于,所发生事件的所有后果都应该由观察者而不是表演者承担。


          也许更好的问题是:如果委托字段在其前面声明了 event 关键字,为什么编译器不翻译所有实例:

          MadeSound(this, EventArgs.Empty)
          

          if (MadeSound != null) { MadeSound(this, EventArgs.Empty); }
          

          在幕后以与其他语法快捷方式相同的方式?人们必须手动编写的样板 OnSomeEvent null 检查方法的数量一定是巨大的。

          【讨论】:

          • 编译器将它在这里所做的事情隔离为一个方面——创建一个同名的变量和一个事件。为了复杂性,我宁愿它没有做更多的事情。不过,避免空值检查很容易:public event EventHandler Click = delegate{};
          • 所以从类外部,MadeSound 指的是一个“事件”,它管理向共享相同标识符的 EventHandler 委托的实例添加/删除合适的委托并在内部隐藏“事件”?这意味着事件不是直接触发的,而是它产生的委托列表?
          • @Jon 我不同意。我非常尊重您,但不明白您为什么要提倡委托{}初始化模式。
          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2020-01-01
          • 1970-01-01
          • 1970-01-01
          • 2016-01-08
          • 1970-01-01
          相关资源
          最近更新 更多