简体   繁体   中英

Can I have strong exception safety and events?

Suppose I have a method which changes the state of an object, and fires an event to notify listeners of this state change:

public class Example
{
   public int Counter { get; private set; }

   public void IncreaseCounter()
   {
      this.Counter = this.Counter + 1;
      OnCounterChanged(EventArgs.Empty);
   }

   protected virtual void OnCounterChanged(EventArgs args)
   {
      if (CounterChanged != null)
         CounterChanged(this,args);
   }

   public event EventHandler CounterChanged;
}

The event handlers may throw an exception even if IncreaseCounter successfully completed the state change. So we do not have strong exception safety here:

The strong guarantee: that the operation has either completed successfully or thrown an exception, leaving the program state exactly as it was before the operation started.

Is it possible to have strong exception safety when you need to raise events?

To prevent an exception in a handler from propagating to the event generator, the answer is to manually invoke each item in the MultiCast Delegate (ie the event handler) inside of a try-catch

All handlers will get called, and the exception won't propagate.

public EventHandler<EventArgs> SomeEvent;

protected void OnSomeEvent(EventArgs args)
{
    var handler = SomeEvent;
    if (handler != null)
    {
        foreach (EventHandler<EventArgs> item in handler.GetInvocationList())
        {
            try
            {
                item(this, args);
            }
            catch (Exception e)
            {
                // handle / report / ignore exception
            }
        }                
    }
}

What remains is for you to implement the logic for what do do when one or more event recipients throws and the others don't. The catch() could catch a specific exception as well and roll back any changes if that is what makes sense, allowing the event recipient to signal the event source that an exceptional situation has occurred.

As others point out, using exceptions as control flow isn't recommended. If it's truly an exceptional circumstance, then by all means use an exception. If you're getting a lot of exceptions you probably want to use something else.

The general pattern you will see for frameworks is:

public class Example
{
   public int Counter { get; private set; }

   public void IncreaseCounter()
   {
      OnCounterChanging(EventArgs.Empty);
      this.Counter = this.Counter + 1;
      OnCounterChanged(EventArgs.Empty);
   }

   protected virtual void OnCounterChanged(EventArgs args)
   {
      if (CounterChanged != null)
         CounterChanged(this, args);
   }

   protected virtual void OnCounterChanging(EventArgs args)
   {
      if (CounterChanging != null)
         CounterChanging(this, args);
   }

   public event EventHandler<EventArgs> CounterChanging;
   public event EventHandler<EventArgs> CounterChanged;
}

If a user would like to throw an exception to prevent the changing of the value then they should be doing it in the OnCounterChanging() event instead of the OnCounterChanged(). By definition of the name (past tense, suffix of -ed) that implies the value has been changed.

Edit:

Note that you generally want to stay away from copious amounts of extra try..catch/finally blocks as exception handlers (including try..finally) are expensive depending on the language implementation. ie The win32 stack framed model or the PC-mapped exception model both have their pros and cons, however if there are too many of these frames they will both be costly (either in space or execution speed). Just another thing to keep in mind when creating a framework.

Well, you could compensate if it was critical:

public void IncreaseCounter()
{
  int oldCounter = this.Counter;
  try {
      this.Counter = this.Counter + 1;
      OnCounterChanged(EventArgs.Empty);
  } catch {
      this.Counter = oldCounter;
      throw;
  }
}

Obviously this would be better if you can talk to the field (since assigning a field won't error). You could also wrap this up in bolierplate:

void SetField<T>(ref T field, T value, EventHandler handler) {
    T oldValue = field;
    try {
         field = value;
         if(handler != null) handler(this, EventArgs.Empty);
    } catch {
         field = oldField;
         throw;
    }
}

and call:

SetField(ref counter, counter + 1, CounterChanged);

or something similar...

Will not simply swapping the order of the two lines in IncreaseCounter enforcestrong exception safety for you?

Or if you need to increment Counter before you raise the event:

public void IncreaseCounter()
{
    this.Counter += 1;

    try
    {
        OnCounterChanged(EventArgs.Empty);
    }
    catch
    {
        this.Counter -= 1;

        throw;
    }
}

In a situation where you have more complex state changes (side effects) going on, then it becomes rather more complicated (you might have to clone certain objects for example), but in for this example it would seem quite easy.

In this case, I think you should take a cue from Workflow Foundation. In the OnCounterChanged method, surround the call to the delegate with a generic try-catch block. In the catch handler, you'll have to do what is effectively a compensating action -- rolling back the counter.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM