51

We're all familiar with the horror that is C# event declaration. To ensure thread-safety, the standard is to write something like this:

public event EventHandler SomethingHappened;
protected virtual void OnSomethingHappened(EventArgs e)
{            
    var handler = SomethingHappened;
    if (handler != null)
        handler(this, e);
}

Recently in some other question on this board (which I can't find now), someone pointed out that extension methods could be used nicely in this scenario. Here's one way to do it:

static public class EventExtensions
{
    static public void RaiseEvent(this EventHandler @event, object sender, EventArgs e)
    {
        var handler = @event;
        if (handler != null)
            handler(sender, e);
    }
    static public void RaiseEvent<T>(this EventHandler<T> @event, object sender, T e)
        where T : EventArgs
    {
        var handler = @event;
        if (handler != null)
            handler(sender, e);
    }
}

With these extension methods in place, all you need to declare and raise an event is something like this:

public event EventHandler SomethingHappened;

void SomeMethod()
{
    this.SomethingHappened.RaiseEvent(this, EventArgs.Empty);
}

My question: Is this a good idea? Are we missing anything by not having the standard On method? (One thing I notice is that it doesn't work with events that have explicit add/remove code.)

Bill Tarbell
  • 4,933
  • 2
  • 32
  • 52
Ryan Lundy
  • 204,559
  • 37
  • 180
  • 211
  • Maybe you were thinking of this question: http://stackoverflow.com/questions/192980/boiler-plate-code-replacement-is-there-anything-bad-about-this-code – Benjol Jun 11 '09 at 10:31

6 Answers6

59

It will still work with events that have an explicit add/remove - you just need to use the delegate variable (or however you've stored the delegate) instead of the event name.

However, there's an easier way to make it thread-safe - initialize it with a no-op handler:

public event EventHandler SomethingHappened = delegate {};

The performance hit of calling an extra delegate will be negligible, and it sure makes the code easier.

By the way, in your extension method you don't need an extra local variable - you could just do:

static public void RaiseEvent(this EventHandler @event, object sender, EventArgs e)
{
    if (@event != null)
        @event(sender, e);
}

static public void RaiseEvent<T>(this EventHandler<T> @event, object sender, T e)
    where T : EventArgs
{
    if (@event != null)
        @event(sender, e);
}

Personally I wouldn't use a keyword as a parameter name, but it doesn't really change the calling side at all, so do what you want :)

EDIT: As for the "OnXXX" method: are you planning on your classes being derived from? In my view, most classes should be sealed. If you do, do you want those derived classes to be able to raise the event? If the answer to either of these questions is "no" then don't bother. If the answer to both is "yes" then do :)

Jon Skeet
  • 1,421,763
  • 867
  • 9,128
  • 9,194
  • 1
    Good point; @event can't change once you're in the method. I was aware that you can subscribe an empty delegate, but my concern was more about whether it's good or bad to dispense with the On method. – Ryan Lundy Oct 23 '08 at 21:15
  • I recognize this suggestion to use delegate{} from your excellent book! This is awesome :) – Igal Tabachnik Oct 23 '08 at 21:16
  • Just remember if your class sets the delegate to null, you'll still have an error. I really don't understand why so many people have a problem with this. – Robert Paulson Oct 23 '08 at 21:30
  • You're right, Jon; I see now that the add/remove wasn't the problem. The problem was that the event was in a base class. I was testing in Form1 on an event declared in Form. – Ryan Lundy Oct 23 '08 at 21:32
  • @Jon Skeet: I just tested this on an event that didn't use an explicit add/remove and it still worked correctly. – Scott Dorman Apr 17 '09 at 16:12
  • @Scott: I don't think anyone suggested that it wouldn't, did they? – Jon Skeet Apr 17 '09 at 16:36
  • @Jon Skeet: Sorry...mis-read your first sentence and somehow thought the "still" meant "only". – Scott Dorman Apr 17 '09 at 18:38
  • 3
    You guys need to add the [MethodImpl(MethodImplOptions.NoInlining)] attribute to these extension methods or else your attempt to copy the delegate to a temporary variable could be optimized away by the JITter, allowing for a null reference exception. This could occur in both Kyralessa's and Jon Skeet's versions. So just add [MethodImpl(MethodImplOptions.NoInlining)] and you are fine, and, yes, you would no longer need to explicitly copy to a temporary variable since simply passing in the value via the method's parameter accomplishes this. – Mike Rosenblum Jun 25 '09 at 17:44
  • Can there really be a race condition when the method is not marked as NoInlining? See this question http://stackoverflow.com/questions/2327343/can-method-inlining-optimization-cause-race-conditions – Jeff Cyr Feb 24 '10 at 15:54
  • 6
    @Mike: No, I don't believe the JITter *is* allowed to do that. This was raised as an issue a while back, but I've spoken with someone (I can't remember whether it was Joe Duffy or Vance Morrison; someone like that) who said that was completely against the rules of what's allowed by the JITter and the memory model. – Jon Skeet Feb 24 '10 at 16:05
  • Interesting point Jon, it looks like it is probably necessary. My source was Juval Lowy, but I replied further to your answer here: http://stackoverflow.com/questions/2327343/can-method-inlining-optimization-cause-race-conditions – Mike Rosenblum Feb 24 '10 at 21:51
  • You should be update to upvote several time, I stop counting how many time I used this answer. Thank you. – Thomas May 02 '14 at 13:40
  • Why wouldn't there be chance for additional race condition by not copying to temp variable? Cf. [Eric Lippert on this question](http://blogs.msdn.com/b/ericlippert/archive/2009/04/29/events-and-races.aspx) – Ghopper21 Jul 28 '15 at 16:37
  • @Ghopper21: There's a race condition in terms of a "late" subscriber (or unsubscription) not being seen - but there's no risk of a `NullReferenceException`, which there is normally. – Jon Skeet Jul 28 '15 at 16:38
  • From Eric's blog: "The temporary variable ensures that this is “thread safe”. If the event’s handlers are being modified on another thread it is possible for Foo to be non-null at the point of the check, then set to null on a different thread, and then the invocation throws." No? – Ghopper21 Jul 28 '15 at 16:44
  • @Ghopper21: Sorry, I still don't see what you're getting at. That's the unsafe version, yes. Which of *my* examples do you think isn't safe? – Jon Skeet Jul 28 '15 at 16:45
  • I mean the code below the "by the way" in your answer, taken on its own. Or are you assuming initialization with no-op delegate as you note above in that code? – Ghopper21 Jul 28 '15 at 16:48
  • 3
    @Ghopper21: No, in that case the parameter acts as the local variable, effectively - any chances to the event handler won't be visible within `RaiseEvent`, because `@event` is just a parameter... a normal local variable. When the method is called, the current value of the field will be copied as the value of the parameter... and further changes to the field won't affect it. – Jon Skeet Jul 28 '15 at 16:50
  • Ah ok! Thanks for helping me understand! – Ghopper21 Jul 28 '15 at 16:53
  • For greater protection what one can do is iterate over @event.GetInvocationList().Cast>() and then invoke the delegate within try {...} catch {...}. Otherwise an exception in one of the event handlers will prevent the other ones from running. – sjb-sjb Mar 25 '18 at 01:02
15

Now C# 6 is here, there is a more compact, thread-safe way to fire an event:

SomethingHappened?.Invoke(this, e);

Invoke() is only called if delegates are registered for the event (i.e. it's not null), thanks to the null-conditional operator, "?".

The threading problem the "handler" code in the question sets out to solve is sidestepped here because, like in that code, SomethingHappened is only accessed once, so there is no possibility of it being set to null between test and invocation.

This answer is perhaps tangential to the original question, but very relevent for those looking for a simpler method to raise events.

Bob Sammers
  • 3,080
  • 27
  • 33
5

[Here's a thought]

Just write the code once in the recommended way and be done with it. Then you won't confuse your colleagues looking over the code thinking you did something wrong?

[I read more posts trying to find ways around writing an event handler than I ever spend writing an event handler.]

Robert Paulson
  • 17,603
  • 5
  • 34
  • 53
3

Less code, more readable. Me like.

If you're not interested in performance you can declare your event like this to avoid the null check:

public event EventHandler SomethingHappened = delegate{};
Cristian Libardo
  • 9,260
  • 3
  • 35
  • 41
1

You're not "ensuring" thread safety by assigning the handler to a local variable. Your method could still be interrupted after the assignment. If for example the class that used to listen for the event gets disposed during the interruption, you're calling a method in a disposed class.

You're saving yourself from a null reference exception, but there are easier ways to do that, as Jon Skeet and cristianlibardo pointed out in their answers.

Another thing is that for non-sealed classes, the OnFoo method should be virtual which I don't think is possible with extension methods.

Isak Savo
  • 34,957
  • 11
  • 60
  • 92
0

To take the above answers a step further you could protect yourself against one of your handlers throwing an exception. If this were to happen then the subsequent handlers wouldn't be called.

Likewise, you could taskify the handlers to prevent a long-running handler from causing an excessive delay for the latter handlers to be informed. This can also protect the source thread from being hijacked by a long-running handler.

  public static class EventHandlerExtensions
  {
    private static readonly log4net.ILog _log = log4net.LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType);

    public static void Taskify(this EventHandler theEvent, object sender, EventArgs args)
    {
      Invoke(theEvent, sender, args, true);
    }

    public static void Taskify<T>(this EventHandler<T> theEvent, object sender, T args)
    {
      Invoke(theEvent, sender, args, true);
    }

    public static void InvokeSafely(this EventHandler theEvent, object sender, EventArgs args)
    {
      Invoke(theEvent, sender, args, false);
    }

    public static void InvokeSafely<T>(this EventHandler<T> theEvent, object sender, T args)
    {
      Invoke(theEvent, sender, args, false);
    }

    private static void Invoke(this EventHandler theEvent, object sender, EventArgs args, bool taskify)
    {
      if (theEvent == null)
        return;

      foreach (EventHandler handler in theEvent.GetInvocationList())
      {
        var action = new Action(() =>
        {
          try
          {
            handler(sender, args);
          }
          catch (Exception ex)
          {
            _log.Error(ex);
          }
        });

        if (taskify)
          Task.Run(action);
        else
          action();
      }
    }

    private static void Invoke<T>(this EventHandler<T> theEvent, object sender, T args, bool taskify)
    {
      if (theEvent == null)
        return;

      foreach (EventHandler<T> handler in theEvent.GetInvocationList())
      {
        var action = new Action(() =>
        {
          try
          {
            handler(sender, args);
          }
          catch (Exception ex)
          {
            _log.Error(ex);
          }
        });

        if (taskify)
          Task.Run(action);
        else
          action();
      }
    }
  }
Bill Tarbell
  • 4,933
  • 2
  • 32
  • 52