【问题标题】:Documenting an event's invocation order记录事件的调用顺序
【发布时间】:2013-07-15 16:36:36
【问题描述】:

调用特定事件的处理程序的顺序取决于该特定事件的实现。例如,使用多案例委托的默认后备存储,将按照它们注册的顺序调用处理程序。但是类设计者/实现者可能使用了addremove 关键字来为事件访问器提供不同的后备存储,因此调用顺序也会不同。

.NET 框架基础库本身是否存在事件文档准确描述其调用顺序的情况?无论是否存在,依赖这种记录在案的订单是否被认为是可接受的做法(例如,对于我自己实施和记录的事件)?为什么或为什么不?

【问题讨论】:

    标签: .net event-handling


    【解决方案1】:

    我不知道。这几乎总是以先进先出的方式结束,因为不使用列表并不是很有效。 MulticastDelegate 和 EventHandlerList 就是这样工作的。

    依赖顺序是有风险的。在很多情况下,程序员取消订阅事件以防止重入问题,在方法退出时再次订阅它。由于顺序会发生变化,这是不可避免的副作用,他的事件处理程序现在将被最后调用。如果这导致程序失败,那么那个程序员将​​会困惑很长一段时间。他只是看不到代码更改和错误行为之间的联系,这是一个很难修复的错误。尽管如此,让代码以意想不到的方式交互永远是一个陷阱,只有好的设计才能避免它,也只有好的调试器才能诊断它。

    【讨论】:

    【解决方案2】:

    我发布这个问题的那天,我专注于一个注册顺序明显且稳定的特定案例。但是第二天我已经注意到这是例外而不是规则。

    可以通过任何代码路径在任何地方添加处理程序,因此通常不可能使用该顺序执行任何有用的操作。因此,一般规则是忽略注册顺序并尝试使您的代码工作,无论处理程序添加/调用的顺序是什么。这有时需要采取预防措施,即使您有工作代码。在我目前关注的情况下,这需要创建一个伴随的Changing 事件,与Changed 事件配对,这样必须首先发生的事情将进入Changing 事件的处理程序。

    我想您可以记录事件的顺序调用顺序,以应对注册明显且稳定的罕见情况。但是对于依赖于顺序的每个注册,您还需要记录其顺序重要性,随着代码的发展,您必须牢记这一点,否则某些事情会中断。听起来工作量很大,所以坚持上一段中提到的一般规则可能会更容易!

    我可以想到一种可靠地控制调用顺序的潜在可行方法。您可以将优先级作为处理程序注册的一部分传递。这样您就依赖于顺序注册顺序。但是您正在控制相对调用顺序。但是,这样的实现更繁重且不标准,因此在大多数情况下可能并不理想。

    【讨论】:

      【解决方案3】:

      我刚刚遇到了一个我认为在编写测试时完全可以接受的情况。考虑使用这个辅助类来异步加载某些内容并在操作完成时通过事件进行通知:

      public class AsyncItemLoader<T>
      {
          /// <summary>
          /// Handlers will be invoked in registration order, pinky-swear!
          /// Also, they are invoked on a thread from the ThreadPool.
          /// </summary>
          public EventHandler<T> ItemLoaded;
      
          public void LoadAsync()
          {
              // load the item asynchronously using Task.ContinueWith to fire 
              // ItemLoaded event to indicate that the item is now available
          }
      }
      

      现在假设我们在某个模型类中使用这个助手:

      public class MyAppModel
      {
          private readonly AsyncItemLoader<User> userLoader;
      
          public AsyncItemLoader<User> User { get { return userLoader; } }
      
          public MyAppModel()
          {
              this.userLoader = new AsyncItemLoader<User>(...);
              this.userLoader += HandleUserLoaded;
          }
      
          public void StartLoadingUser()
          {
              userLoader.LoadAsync();
          }
      
          private void HandleUserLoaded(object sender, User user)
          {
              // do something with the user here
          }
      }
      

      好的,现在我们要为 MyAppModel 编写一个测试,该测试依赖于 HandleUserLoaded 在我们调用 StartLoadingUser 之后的某个时间点所做的任何事情,例如检查对模拟依赖项的某些方法的调用。

      在我们检查模拟方法的调用之前,我们如何确定地等待 HandleUserLoaded 完成处理? 很容易,因为事件被记录为按注册顺序调用处理程序,并且我们知道 MyAppModel 在客户有机会注册他们的处理程序之前注册了自己的处理程序:

      public class MyAppModelTest
      {
          [Test]
          public void StartLoadingUserCausesMyAppModelToDoStuffWithOtherDep()
          {
              var model = new MyAppModel();
              var itemLoaded = new ManualResetEventSlim(initialState: false);
              model.User.ItemLoaded += (s, e) => itemLoaded.Set();
      
              model.StartLoadingUser();
              itemLoaded.Wait();
      
              // At this point we are guaranteed that MyAppModel.HandleUserLoaded 
              // has finished execution
              myServiceMock.Received().AmazingServiceCall();
          } 
      }
      

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2023-04-03
        • 2012-09-21
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多