Article 2XY6 Clean event handler invocation with C# 6

Clean event handler invocation with C# 6

by
jonskeet
from Jon Skeet's coding blog on (#2XY6)
The problem

Invoking event handlers in C# has always been a bit of a pain, because an event with no subscribers is usually represented as a null reference. This leads to code like this:

public event EventHandler Foo;public void OnFoo(){ EventHandler handler = Foo; if (handler != null) { handler(this, EventArgs.Empty); } }

It's important to use the handler local variable, as if instead you access the field twice, it's possible that the last subscriber will unsubscribe between the check and the invocation:

// Bad code, do not use!if (Foo != null){ // Foo could be null here, if the class is intended // to be used from other threads. Foo(this, EventArgs.Empty);}

Now this can be simplified slightly using an extension method:

public static void Raise(this EventHandler handler, object sender, EventArgs args){ if (handler != null) { handler(sender, args); } }

Then in each event, you can write a single line:

public void OnFoo(){ Foo.Raise(this, EventArgs.Empty);}

However, this means having a different extension method for each delegate type. It's not too bad if you're using EventHandler but it's still not ideal.

C# 6 to the rescue!

The null-conditional operator (?.) in C# 6 isn't just for properties. It can also be used for method calls. The compiler does the right thing (evaluating the expression only once) so you can do without the extension method entirely:

public void OnFoo(){ Foo?.Invoke(this, EventArgs.Empty);}

Hooray! This will never throw a NullReferenceException, and doesn't need any extra utility classes.

Admittedly it might be nicer if you could write Foo?(this, EventArgs.Empty) but that would no longer be a ?. operator, so would complicate the language quite a bit, I suspect. The extra slight cruft of Invoke really doesn't bother me much.

What is this thing you call thread-safe?

The code we've got so far is "thread-safe" in that it doesn't matter what other threads do - you won't get a NullReferenceException from the above code. However, if other threads are subscribing to the event or unsubscribing from it, you might not see the most recent changes for the normal reasons of memory models being complicated.

As of C# 4, field-like events are implemented using Interlocked.CompareExchange, so we can just use a corresponding Interlocked.CompareExchange call to make sure we get the most recent value. There's nothing new about being able to do that, admittedly, but it does mean we can just write:

public void OnFoo(){ Interlocked.CompareExchange(ref Foo, null, null)?.Invoke(this, EventArgs.Empty);}

with no other code, to invoke the absolute latest set of event subscribers, without failing if a NullReferenceException is thrown. Thanks to David Fowler for reminding me about this aspect.

Admittedly the CompareExchange call is ugly. In .NET 4.5 and up, there's Volatile.Read which may do the tricky, but it's not entirely clear to me (based on the documentation) whether it actually does the right thing. (The summary suggests it's about preventing the movement of later reads/writes earlier than the given volatile read; we want to prevent earlier writes from being moved later.)

public void OnFoo(){ // .NET 4.5+, may or may not be safe... Volatile.Read(ref Foo)?.Invoke(this, EventArgs.Empty);}

" but that makes me nervous in terms of whether I've missed something. Expert readers may well be able to advise me on why this is sufficiently foolish that it's not in the BCL.

An alternative approach

One alternative approach I've used in the past is to create a dummy event handler, usually using the one feature that anonymous methods have over lambda expressions - the ability to indicate that you don't care about the parameters by not even specifying a parameter list:

public event EventHandler Foo = delegate {}public void OnFoo(){ // Foo will never be null Volatile.Read(ref Foo).Invoke(this, EventArgs.Empty); }

This has all the same memory barrier issues as before, but it does mean you don't have to worry about the nullity aspect. It looks a little odd and presumably there's a tiny performance penalty, but it's a good alternative option to be aware of.

Conclusion

Yup, C# 6 rocks again. Really looking forward to the final release.


1492 b.gif?host=codeblog.jonskeet.uk&blog=717
External Content
Source RSS or Atom Feed
Feed Location http://codeblog.jonskeet.uk/feed/
Feed Title Jon Skeet's coding blog
Feed Link https://codeblog.jonskeet.uk/
Reply 0 comments