【问题标题】:Is it poor form for a C# class to subscribe to its own published events?C# 类订阅自己发布的事件是不是很糟糕?
【发布时间】:2010-10-27 08:17:07
【问题描述】:

我可能只是有点神经质,但我经常发现自己有一个发布事件的类,并且我发现从类本身(例如在构造函数中)订阅这个事件很方便,而不仅仅是订阅外部类。

这对我来说听起来很合理,但我不禁感到这是一种糟糕的做法,原因很简单,我总是面临这样一个问题:“为什么不执行你在触发事件的代码中的事件处理程序?”

public class Button
{
   public Button()
   {
      this.Click += someHandler; // bad practice?
   }

   public event EventHandler Click;

   public void HandleInput()
   {
      if (someInputCondition)
      {
         // Perform necessary actions here rather than 
         // subscribing in the constructor?
         this.Click(this, ...);
      }
   }
}

订阅自己的活动有什么缺点吗?

【问题讨论】:

    标签: c# events


    【解决方案1】:

    这对我来说听起来很合理,但我不禁感到这是一种糟糕的做法,原因很简单,我总是面临这样一个问题:“为什么不执行你在触发事件的代码中的事件处理程序?”

    要回答这个问题,请考虑部分类场景。假设您有一个基类型 B。您运行一个自动化工具,该工具通过将 B 扩展为派生类 D 来装饰 B。您的工具会生成一个分部类,以便使用 D 的开发人员可以根据自己的目的进一步自定义它。

    在这种情况下,当 B 声明的事件或 D 的机器生成端由 D 的机器生成端引发时,D 的用户编写端希望注册被调用似乎是完全合理的.

    这是我们多年前设计 VSTO 时遇到的场景。事实证明,在 C# 中做到这一点并不难,但在 VB 中让它全部工作却相当棘手。我相信 VB 已经对其事件订阅模型进行了一些调整,以使其更容易。

    也就是说:如果你能避免这种情况,我会的。如果您只是为内部订阅制作一个看起来很糟糕的代码气味的事件。 C# 3 中的部分方法在这方面有很大帮助,因为它们使机器生成端可以轻松且低成本地调用用户生成端的小通知函数,而不必费心发布事件。

    【讨论】:

    • 部分方法解决了生成代码的各种问题,遗憾的是它们没有更多地向后移植到数据集等。
    【解决方案2】:

    我认为这没有问题。但是如果你在同一个类中处理事件,你也可以重写事件方法:

    protected override void OnClick(Eventargs e)
    {
       base.OnClick(e);
    }
    

    这更有效,并且让您能够在必要时吞下事件(只需不调用 base.OnClick())。

    【讨论】:

    • 此外,它允许您指定您的代码是在任何其他事件处理程序之前还是之后(或两者兼而有之)运行。
    • 就我而言,这是我自己的自定义 Button 类——与该类名没有特定的相关性;它可以是任何东西。因此,没有可覆盖的 OnClick 方法(除非我的 C# 事件知识存在漏洞)。
    • @vargonian:那么我建议你制作一个 (protected virtual void OnLoad(EventArgs e);)。如果您没有设计自己的 Button 类来支持继承,那么请改用 private void。正如 Flagbug 指出的那样,这样做可以确保您控制外部事件的执行时间(即它们是在类的内部点击处理代码之前还是之后执行)。
    • OnLoad() 的问题是不能保证只被调用一次,并且你无法确切知道它何时被调用。
    • @codymanix:你刚才所说的有几个缺陷。 A) 使用简单的bool 可以很容易地防止OnLoad 处理多次。 B) OnLoad 将受到保护,因此多次调用它不太可能成为问题。如果您真的不信任您的用户,请将其处理部分设为私有,并在调用 OnLoad 之前或之后调用它。 C) 不能保证一个事件只被调用一次,你无法确切知道它何时被调用。
    【解决方案3】:

    由于内部优化,在执行此操作时有一个非常 exotic problem。由于优化添加/删除事件处理程序不是线程安全的。它仅适用于您的示例中声明类型使用的事件。

    幸运的是,4.0 对此进行了更改,但如果您使用的是以前的版本,您可以体验到这一点。

    【讨论】:

    • +1 很好,但在承包商中订阅或 UI 控件(也是单线程)时这不是问题
    • @Ian:我认为很少有人会遇到这个微妙的错误,但很少有人会遇到很多麻烦。
    • 如果你在你的类的ctor中订阅它自己的事件,那么就不可能遇到线程问题。
    【解决方案4】:

    问题是“someHandler”会改变你的对象的状态。您希望在事件运行任何“外部”代码之前或之后更改此状态吗?

    目前尚不清楚如果您订阅该事件,状态会在什么时候发生变化,但是在“HandleInput()”中调用它可以更清楚地了解何时调用它。

    (另外调用“HandleInput()”、“OnClick”并使其成为虚拟以便子类可以覆盖它更正常)

    说了这么多,一般情况下订阅自己的事件也没什么大不了的;在表示表单的 UI 类中,这很常见,否则它往往会让很多阅读您的代码的人“感到惊讶”。

    【讨论】:

      【解决方案5】:

      如果你的按钮类应该是第一个接收点击事件的,你应该在事件方法中编写你的代码,例如:

      protected virtual void OnClick(EventArgs e)
      {
          //insert your code here
      
          if(this.Click != null)
          {
              this.Click(this, e);
          }
      }
      

      但如果你的班级不是第一个接收者,你可以正常订阅事件。

      【讨论】:

      • +1:我在System.Windows.Forms 中注意到的一件事是,通常在 UI 类中找到的任何事件都具有匹​​配的受保护函数,例如protected virtual void OnLoad(EventArgs e);public event EventHandler Load; 。框架内的其他地方也使用了类似的技术。
      • 是的,这是因为否则无法确保派生类是事件的第一个接收者。使用这种技术,您可以简单地覆盖 OnLoad 方法,在其中编写自定义代码,然后调用 base.OnLoad 方法,以便触发事件。
      【解决方案6】:

      如果以普通的 System.Windows.Form 类为例,
      当您想处理 Form_Load 事件(使用 Visual Studio 设计器)时,它会被处理 在表单本身的类中!

      this.Load += new System.EventHandler(this.Form1_Load);
      
       private void Form1_Load(object sender, EventArgs e)
       {
       }
      

      所以我认为这根本不是问题!。

      【讨论】:

      • 但是当你创建一个 UI 表单时,你不希望你类之外的任何人来挂钩这些事件。所以这是一个非常常见的“特殊情况”。
      • 为什么人们将事件处理程序包装在new System.EventHandler中??简单的this.Load += this.Form1_Load 对我来说似乎更好......
      • 是的,您可以这样做。但是,我觉得只覆盖 OnLoad 函数就更干净了。
      猜你喜欢
      • 2010-11-06
      • 2011-10-06
      • 1970-01-01
      • 2015-10-29
      • 2010-10-30
      • 1970-01-01
      • 2014-08-20
      • 2019-06-09
      • 2021-06-29
      相关资源
      最近更新 更多