I know about C# events and delegates. I find them incredibly useful for event-driven sub-systems. One thing I don't understand, however, is why all the .NET documentation for events uses a very specific pattern for delegates:
public class Foo
{
public class CustomEventArgs : System.EventArgs
{
...
}
public delegate void CustomEventHandler(object sender, EventArgs e);
public event CustomEventHandler CustomEvent;
public void OnCustomEvent(int x, float y, bool z)
{
if (CustomEvent != null)
CustomEvent(this, new CustomEventArgs(x, y, z));
}
}
I find this method of declaring and using delegates incredibly unsecure and clunky compared to using custom delegates. For example:
public class Foo
{
public delegate void CustomEventHandler(Foo foo, int x, float y, bool z);
public event CustomEventHandler CustomEvent;
public void OnCustomEvent(int x, int y, int z)
{
if (CustomEvent != null)
CustomEvent(this, x, y, z);
}
}
Besides the obvious advantage of "It's the pattern most familiar to .NET programmers", I fail to see any other practical advantages to using delegates with the void CustomEventHandler(object sender, CustomEventArgs e)
signatures as opposed to custom delegates. Mainly, you have the following advantages with custom delegates:
- You can guarantee the sender is of a specific type.
- You don't need a whole new class just to pass the event data, leading to messier code and name pollution
I would be interested to know if there are any other advantages to using the .NET pattern for events and delegates that I might be missing.
Edit:
Mainly as a response to @MainMa, I wanted to give a more concrete example. The example is a simple representation of a character with a concept of health and death. The character throws events when its hurt, or dies. Consider the scenario below, using custom delegates:
public class Character
{
public delegate void DeathEventHandler(Character character);
public delegate void HurtEventHandler(Character character, float damage);
public event DeathEventHandler DeathEvent;
public event HurtEventHandler HurtEvent;
public bool IsDead { ... }
public void ApplyDamage(float damage)
{
...
OnHurt(damage);
...
if (IsDead)
OnDeath();
}
public void OnHurt(float damage)
{
if (HurtEvent != null)
HurtEvent(this, damage);
}
public void OnDeath()
{
if (DeathEvent != null)
DeathEvent(this);
}
}
To me, this is simple. It separates concerns, and enforces encapsulation. A handler that is only concerned about the death of a character doesn't need to know how much damage was applied to the character before death. So on and so forth.
Now compare this to the .NET pattern:
public class Character
{
public class CharacterHurtEventArgs : EventArgs
{
public float Damage { ... }
public bool Dead { ... }
}
public event EventHandler<CharacterHurtEventArgs> HurtEvent;
public event EventHandler<EventArgs> DeathEvent; // No args ...?
// ... The rest is the same story as above more or less ...
}
Using this, I've just added an extra class. The handler still needs different delegates for these events, and there is no guarantee that the object which died was a character. Anything could throw this.
Of course, going by @MainMa's suggestion, we could change this into:
public class Character
{
public class HealthConditionEventArgs : EventArgs
{
public float Damage { ... }
public bool Dead { ... }
public float Hitpoints { ... }
...
}
public event EventHandler<HealthConditionEventArgs> HealthConditionChangedEvent;
public void ApplyDamage(float damage)
{
...
HealthConditionChangedEventArgs e = new HealthConditionChangedEventArgs();
...
OnHealthConditionChanged(e);
...
}
public void OnHealthConditionChanged(HealthConditionEventArgs e)
{
if (HealthConditionChangedEvent != null)
HealthConditionChangedEvent(e);
}
}
This is slightly neater, but it eliminates separation of concerns. Now if the handler only cares about death, it will have to receive all hurt events. Of course, we could have separate events taking in the same event args, but you'd still be passing around the entire character health condition as part of the event. Again, if you care only about death, there is no need for the handler to know any more than the fact that the character died.
In my opinion, the first example, however, resolves all these encapsulation issues. Discrete delegates for discrete events, with only the necessary information passed in each event. No new classes either.