【问题标题】:Can I have strong exception safety and events?我可以有强大的异常安全和事件吗?
【发布时间】:2023-03-14 09:56:01
【问题描述】:

假设我有一个方法可以改变一个对象的状态,并触发一个事件来通知监听器这个状态改变:

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;
}

即使IncreaseCounter 成功完成状态更改,事件处理程序也可能抛出异常。所以我们这里没有强大的exception safety

强有力的保证: 操作已完成 成功或抛出异常, 完全保持程序状态 那是在手术开始之前。

当你需要引发事件时,是否有可能拥有强大的异常安全性?

【问题讨论】:

    标签: c# exception events


    【解决方案1】:

    为了防止处理程序中的异常传播到事件生成器,答案是在 try-catch 内手动调用 MultiCast 委托(即事件处理程序)中的每个项目

    所有处理程序都会被调用,异常不会传播。

    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
                }
            }                
        }
    }
    

    剩下的就是让您实现当一个或多个事件接收者抛出而其他事件接收者不抛出时该做什么的逻辑。如果有意义,catch() 也可以捕获特定异常并回滚任何更改,从而允许事件接收者向事件源发出异常情况发生的信号。

    正如其他人指出的那样,不建议使用异常作为控制流。如果它确实是一个例外情况,那么一定要使用例外。如果您遇到很多异常,您可能想要使用其他东西。

    【讨论】:

    • 换句话说,在引发事件的方法中实现强大的异常安全性是以不得不吞下/记录异常为代价的。这几乎总是一件坏事,所以我得出结论,实现强大的异常安全性可能不值得。
    【解决方案2】:

    您将看到的框架的一般模式是:

    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;
    }
    

    如果用户想要抛出异常以防止更改值,那么他们应该在 OnCounterChanging() 事件中而不是在 OnCounterChanged() 中进行。根据名称的定义(过去时,-ed 的后缀)表示值已更改。

    编辑:

    请注意,您通常希望远离大量额外的 try..catch/finally 块,因为异常处理程序(包括 try..finally)的成本取决于语言实现。即 win32 堆栈框架模型或 PC 映射异常模型都有其优点和缺点,但是如果这些框架太多,它们都会很昂贵(无论是空间还是执行速度)。创建框架时要记住的另一件事。

    【讨论】:

    • 对于确保强大的异常安全性而言,这并不比约定“不要在事件处理程序中抛出异常”更好。它实际上并不能通过在不应该的地方抛出异常来阻止某人破坏系统。这是否真的是一个问题是不同的:)
    • 异常是一种有用的机制,在没有异常的情况下,try/catch 的成本是最小的。异常不应该用于正常的控制流,但我认为这不是 OP 所要求的。
    • try..catch 成本对于基于堆栈帧的异常来说是最小的,当没有异常时。 PC 映射异常具有与之相关的大小成本,因此大量此类异常处理程序可能会开始使您的 DS 膨胀。
    • 只是挑剔,但您应该首先将 EventHandler 保存到局部变量以防止竞争条件。 var localHandler = CounterChanging; if( handler != null ) handler(this, args);
    【解决方案3】:

    好吧,如果它很关键,你可以补偿:

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

    显然,如果您可以与字段对话,这会更好(因为分配字段不会出错)。你也可以用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;
        }
    }
    

    然后调用:

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

    或类似的东西......

    【讨论】:

    • 我考虑过回滚解决方案,但如果有事件处理程序已经成功处理了更改,那么他们必须知道它已回滚。然后我将不得不再次引发事件以进行回滚,但随后可能会再次引发相同的异常等等。
    【解决方案4】:

    在这种情况下,我认为您应该从 Workflow Foundation 中获得启发。在 OnCounterChanged 方法中,使用通用 try-catch 块围绕对委托的调用。在 catch 处理程序中,您必须执行有效的补偿操作 - 回滚计数器。

    【讨论】:

      【解决方案5】:

      不会简单地交换 IncreaseCounter 中两行的顺序为您实施强大的异常安全吗?

      或者如果您需要在引发事件之前增加Counter

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

      在您有更复杂的状态更改(副作用)的情况下,它会变得相当复杂(例如,您可能必须克隆某些对象),但在本示例中,这似乎很容易。

      【讨论】:

      • 唯一的问题是计数器的值将与它增加之前的值一样(这不是大多数开发人员的预期值)
      • 另外,不要忘记传播异常。目前,这既没有成​​功完成,也没有向调用者提出错误。
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2013-02-07
      • 2016-05-11
      相关资源
      最近更新 更多