The text in bold refers to a memory optimization which is extremely useful when you have many events, or many object instances, all with many events. The most basic support for creating events in C# is to use the event keyword. This keyword is syntactic sugar for the following generated code:
- A field to contain a delegate. Delegates form linked lists. This is the head of the list, and additions are inserted at the head.
- Event accessors, where the add method inserts into the linked list using the delegate field, and the remove method removes from the linked list. The linked list addition and removal also has syntactic sugar hiding it, so you see only "+=" and "-=" to add to, or remove from, the delegate list.
In this sense, the event keyword produces code similar to the generated code from C# auto-implemented properties.
The overhead comes in maintaining a separate field for each event. This is not necessary, just as it is not necessary to maintain a separate field for the data backing each property exposed by a class. We can virtualize both event fields and property fields.
How do we eliminate the overhead for events specifically? We use this method in libraries such as VG.net, and Microsoft uses similar methods in their code: keep a collection of events in a single field. In most cases, few instances have many event subscribers. The simplest collection is a linked list of class instances. Each element in the collection consists of a class instance containing the following properties:
- An event identifier. There is one unique identifier per unique type of event. It is best to use something small, like a byte or integer, since you are unlikely to have millions of event types, even across a huge library.
- A delegate. The delegate can be weakly typed (Delegate).
When you need to add an event handler for a subscriber, you look up the delegate in your collection, using the unique event type identifier. The first time you look it up, the collection will not contain it. In the case of event handler additions, you will add an element to your collection, and within that element, add to the delegate stored there, using Delegate.Combine. To remove a handler, you use Delegate.Remove.
Here is an example from real code in VG.net:
private static readonly int MouseDownEvent = EventsProperty.CreateEventKey();
public event ElementMouseEventHandler MouseDown
{
add { AddHandler(MouseDownEvent, value); }
remove { RemoveHandler(MouseDownEvent, value); }
}
public virtual void OnMouseDown(ElementMouseEventArgs args)
{
ElementMouseEventHandler handler =
FindHandler(MouseDownEvent) as ElementMouseEventHandler;
if (handler != null)
handler(this, args);
}
internal void AddHandler(int key, Delegate value)
{
EventsProperty p = (EventsProperty)GetOrInsertProperty(EventsProperty.Key);
p.AddHandler(key, value);
}
internal void RemoveHandler(int key, Delegate value)
{
EventsProperty p = (EventsProperty)GetProperty(EventsProperty.Key);
if (p == null)
return;
p.RemoveHandler(key, value);
}
internal Delegate FindHandler(int key)
{
EventsProperty p = (EventsProperty)GetProperty(EventsProperty.Key);
if (p == null)
return null;
return p[key];
}
We virtualized not only events, but properties as well. For VG.net, all events are contained in one virtual property (EventProperty), and most public properties are also virtualized, although we bundle together property values that are most likely used together. This enables us to provide many properties and events on all instances, while there is zero memory used by these properties or events per instance, unless:
- For properties, the property is set to a non-default value.
- For events, something subscribes to the event.
These types of optimization make VG.net efficient even when there are millions of graphical objects in memory, even if running on low-end hardware.
Ideally, we should have programming tools that do not force us to optimize data structures explicitly. Specifying exactly how objects are laid out in memory is a burden better handled by a profiler or smart run-time system. We are still in the stone age in this respect, in every programming language I have ever worked in.