问题
The order in which handlers for a particular event are invoked depends on the implementation of that particular event. For example, using the default backing store of a multi-case delegate, the handlers will be invoked in the order that they were registered. But the class designer/implementer may have used the add
and remove
keywords to provide an event accessor with a different backing store, and so the invocation order will also be different.
Are there any cases within the .NET framework base library itself that an event's documentation precisely describes its invocation order? Whether or not there are, would it be considered acceptable practice to depend on such a documented order (e.g. for an event I implemented and documented myself)? Why or why not?
回答1:
Not that I know of. This almost always ends up first-in first-out because not using a list just isn't very efficient. MulticastDelegate and EventHandlerList work that way.
Taking a dependency on order is risky. There are plenty of cases where a programmer unsubscribes an event to prevent re-entrancy problems, subscribing it again at method exit. With the inevitable side-effect that the order will change, his event handler will now be called last. If that causes program failure then that programmer is going to be puzzled for quite a while. He just won't see the connection between the code change and the mis-behavior, that's a pretty hard bug to fix. Still, having code interact in unexpected ways is forever a trap, only good design can avoid it and only a good debugger can diagnose it.
回答2:
The day that I posted this question, I was focused on a particular case where the registration order was obvious and stable. But the next day I already noticed that to be the exception rather than the rule.
Handlers can be added anywhere, via any code path, so it is normally impossible to do anything useful with that order. As a result, the general rule is to disregard the registration order and try to make your code work no matter what order the handlers are added/invoked in. This sometimes requires taking precautions even after you have working code. In the case I am currently focused on, this required creating a companion Changing
event, paired with the Changed
event, such that things which must occur first will go into the Changing
event's handler.
I suppose you could document an event's sequential invocation order for rare cases where registrations are obvious and stable. But then for each registration that depends on the order, you'd also need to document its sequential significance which you'd have to keep in mind as your code evolves else something will break. Sounds like a lot of work, so it'll probably be easier just to stick to the general rule mentioned in the above paragraph!
I can think of one potentially viable way of reliably controlling invocation order. You could pass in a priority as part of a handler's registration. This way you are not depending on the sequential registration order. But you are controlling the relative invocation order. Such an implementation is heavier and non-standard, though, and so probably not desirable in most cases.
回答3:
I just ran into a scenario where I consider it perfectly acceptable when writing a test. Consider this helper class for loading something asynchronously and notifying via events when the operation has completed:
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
}
}
Now assume we use this helper in some model class:
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
}
}
Okay, now we want to write a test for MyAppModel that depends on whatever HandleUserLoaded does at some point in time after we have called StartLoadingUser, like checking for the invocation of some method on a mocked dependency.
How can we deterministically wait for HandleUserLoaded to have finished processing before we check for invocation of our mocked method? Well easily, since the event was documented as invoking handlers in registration order and we know that MyAppModel registers its own handler before clients get a chance to register theirs:
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();
}
}
来源:https://stackoverflow.com/questions/17659253/documenting-an-events-invocation-order