调用特定事件的处理程序的顺序取决于该特定事件的实现。例如,使用多案例委托的默认后备存储,将按照它们注册的顺序调用处理程序。但是类设计者/实现者可能已经使用add
andremove
关键字来为事件访问器提供不同的后备存储,因此调用顺序也会不同。
.NET 框架基础库本身是否存在事件文档精确描述其调用顺序的情况?无论是否存在,依赖这种记录在案的命令是否被认为是可接受的做法(例如,对于我自己实施和记录的事件)?为什么或者为什么不?
调用特定事件的处理程序的顺序取决于该特定事件的实现。例如,使用多案例委托的默认后备存储,将按照它们注册的顺序调用处理程序。但是类设计者/实现者可能已经使用add
andremove
关键字来为事件访问器提供不同的后备存储,因此调用顺序也会不同。
.NET 框架基础库本身是否存在事件文档精确描述其调用顺序的情况?无论是否存在,依赖这种记录在案的命令是否被认为是可接受的做法(例如,对于我自己实施和记录的事件)?为什么或者为什么不?
从来没听说过。这几乎总是以先进先出的方式结束,因为不使用列表并不是很有效。MulticastDelegate 和 EventHandlerList 就是这样工作的。
依赖秩序是有风险的。在很多情况下,程序员取消订阅事件以防止重入问题,在方法退出时再次订阅它。由于顺序会发生变化,这是不可避免的副作用,他的事件处理程序现在将被最后调用。如果这导致程序失败,那么那个程序员将会困惑很长一段时间。他只是看不到代码更改和错误行为之间的联系,这是一个很难修复的错误。尽管如此,让代码以意想不到的方式交互永远是一个陷阱,只有好的设计才能避免它,只有好的调试器才能诊断它。
我发布这个问题的那天,我专注于一个注册顺序明显且稳定的特定案例。但是第二天我已经注意到这是例外而不是规则。
处理程序可以通过任何代码路径添加到任何地方,因此通常不可能使用该顺序执行任何有用的操作。因此,一般规则是忽略注册顺序并尝试使您的代码工作,无论处理程序添加/调用的顺序是什么。这有时需要采取预防措施,即使您有工作代码。在我目前关注的情况下,这需要创建一个伴随Changing
事件,与事件配对Changed
,这样必须首先发生的事情将进入Changing
事件的处理程序。
我想您可以在注册明显且稳定的罕见情况下记录事件的顺序调用顺序。但是对于依赖于顺序的每个注册,您还需要记录其顺序重要性,随着代码的发展,您必须牢记这一点,否则某些事情会中断。听起来工作量很大,所以坚持上一段中提到的一般规则可能会更容易!
我可以想到一种可靠地控制调用顺序的潜在可行方法。您可以将优先级作为处理程序注册的一部分传递。这样您就不会依赖于顺序注册顺序。但是您正在控制相对调用顺序。但是,这样的实现更重且不标准,因此在大多数情况下可能并不理想。
我刚刚遇到了一个我认为在编写测试时完全可以接受的场景。考虑这个辅助类,用于异步加载某些内容并在操作完成时通过事件通知:
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 编写一个测试,它依赖于在我们调用 StartLoadingUser 之后的某个时间点 HandleUserLoaded 所做的任何事情,例如检查对模拟依赖项的某些方法的调用。
在检查模拟方法的调用之前,我们如何确定地等待 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();
}
}