Ownership Inversion in Delegates
As you probably know, I’m currently busy rewriting the Cloning system in Duality. While you can read most of my experiences doing so in my last blog entry, there is a peculiar little problem case that revealed itself just recently. It should be solved by now, but I thought it might be interesting enough for its own followup entry. Let’s call it Ownership Inversion in Delegates.
In the last blog entry, I described the problems that arise when attempting to clone an object and my approach on solving them. However, there is a second use case that is just as vital, although admittedly a lot more obscure: Copying all data from a source object to an existing target object. It may sound like no big deal at first, because apparently this is just the same problem in a different shape, but there’s a catch. Since our target already existed before performing the operation, other objects might hold references to itself and its child objects, and those references need to remain intact. This means, we can’t create new objects whenever there is an already existing equivalent in the target object graph, which in turn makes the original problem quite a little more complicated to solve. For the sake of this blog posting, let’s pretend we’re already done and instead focus on a single specific use case. Consider the following method:
private void DoStuff(Bar source, Bar target, Bar other)
{
// Subscribe some event handlers to the source objects event
source.SomeEvent += source.Child.ReceiveEventA;
source.SomeEvent += other.Child.ReceiveEventA;
source.SomeEvent += StaticReceiveEventA;
// Subscribe some more event handlers to the target objects event
target.SomeEvent += target.Child.ReceiveEventB;
target.SomeEvent += other.Child.ReceiveEventB;
target.SomeEvent += StaticReceiveEventB;
// Now, let's copy data from source to target
source.DeepCopyTo(target);
// Fire the targets event
target.FireEvent();
}
This method takes three arguments: A source object, a target object and a completely unrelated object. It then proceeds to subscribe a lot of event handlers to various events and fires the event before returning. It is a completely fictional method that is likely to not have an equivalent in your own project, but that doesn’t matter – its purpose is to illustrate a question: Which event handlers will be receiving the event?
When assuming that it is the source object firing the event instead of the target, the answer is obvious: The first three event handlers will be invoked, because they have been subscribed to the source event. Since source.DeepCopyTo(target)
copies all data from source to target, the naïve assumption would be that firing the targets event will have the same effect and all of the prior event handler subscriptions to the target object are lost. A fully automated cloning algorithm might as well do this – but is it really the proper expected result?
First of all, there should be a proper mapping between objects from the source graph and objects from the target graph. The event handler source.Child.ReceiveEventA
is part of the source objects ownership graph, so it would be natural for the target object to subscribe target.Child.ReceiveEventA
to its event instead, just like target.Child
naturally won’t reference the same object as source.Child
. After understanding the ownership concept in an automated cloning operation, this conclusion appears trivial – but it’s also not the point of this posting.
Far more interesting is what happens to all the other events, and that depends on the question of ownership: Whether or not an object is deeply cloned or simply mapped during a copy operation depends on whether the operations root object owns this object or not. The source object owns its children, so the copy algorithm deeply investigates them instead of just assigning a reference and calling it a day. The source object also owns its event and thus the backing Delegate and the Delegates internal invocation list, along with all its internal entries. But on a conceptual level, an event doesn’t own its event handler subscriptions. Consider the following case:
public class SomeObject
{
// ...
public OnInit(Bar obj)
{
obj.SomeEvent += this.SomeEventHandler;
}
public OnShutdown(Bar obj)
{
obj.SomeEvent -= this.SomeEventHandler;
}
// ...
}
The above code is a common pattern: An object wants to be informed about the occurrence of an event and subscribes itself to another object which fires this event. In order to avoid memory leaks and dead code to be run, it needs to take care of unsubscribing itself when it’s done. Unsubscribing an event handler is the duty of the object that subscribed it, because no other object knows of the subscription or is likely to have the access capabilities to do so. This responsibility forms a conceptual ownership relation that is inverse to the one represented by a Delegates invocation list.
Think about it: When a view listens to the events of a model, you wouldn’t expect it to magically listen to the models clone as well, would you? And if it did, how would the view even know this and unsubscribe itself later? Well, you might write a lot of boilerplate code to prevent errors caused by this, but what if you’re dealing with a third-party class? It’s not going to work – no class expects to be subscribed to events it didn’t subscribe for. The same is true for being unsubscribed by overwriting a Delegates invocation list during a copy operation – it cannot be done based on data ownership alone.
All of this led to a complicated series of small design changes in the cloning system I’ve been writing for Duality, but let’s just skip ahead to the results and answer the question above: When the target event is fired after the copy operation, which event handlers will be invoked?
target.Child.ReceiveEventA
other.Child.ReceiveEventB
StaticReceiveEventB
It is the least intrusive behavior to copy event subscriptions: The first one is simply a mapped version of the internal event subscription source.Child.ReceiveEventA
, as described above. The others are not considered to be part of the copied conceptual ownership graph, so the sources subscriptions aren’t cloned and the targets subscriptions aren’t removed. And that’s how the algorithm deals with Ownership Inversion in Delegates.