处置控件是否应该能够安全地忽略事件回调?

时间:2012-07-24 17:05:27

标签: c# .net winforms idisposable

我有一个类是一次性UI控件。 它订阅了一个moldel对象的变化来重新绘制它的内容。

另一方面,在某些情况下,同一模型对象的某些特殊更改会指示包含此控件的视图删除并处理它(控件)。

因此,模型的变化(取决于订阅顺序)首先导致控制处理,然后是方法调用 - 最终导致ObjectDisposedException

问题:控件是否应设计为安全地忽略事件回调,还是应该尝试阻止来自其他图层的此类调用

对于那些喜欢看更多代码的人,我已经准备了一个非常简单的例子:

//############################################
class View
{
    private Control m_Control;

    public View(Logic logic, Model model)
    {
        m_Control = new Control(model);
        logic.Changed += LogicChanged;
    }

    private void LogicChanged(object sender, EventArgs e)
    {
        m_Control.Dispose();
        m_Control = null;
    }
}

//############################################
class Control : IDisposable
{
    private readonly Model m_Model;

    public Control(Model model)
    {
        m_Model = model;
        m_Model.Changed += ModelOnChanged;
    }

    public bool IsDisposed { get; private set; }

    public void Dispose()
    {
        m_Model.Changed -= ModelOnChanged;
        IsDisposed = true;
    }

    private void ModelOnChanged(object sender, EventArgs e)
    {
        if (IsDisposed)
        {
            throw new ObjectDisposedException(ToString());
        }
        //Do something
    }
}

//############################################
class Model
{
    public event EventHandler<EventArgs> Changed;

    private void OnChanged(EventArgs e)
    {
        EventHandler<EventArgs> handler = Changed;
        if (handler != null)
            handler(this, e);
    }

    public void Change()
    {
        OnChanged(null);
    }
}

//############################################
class Logic
{
    private readonly Model m_Model;

    public Logic(Model model)
    {
        m_Model = model;
        m_Model.Changed += ModelOnChanged;
    }

    private void ModelOnChanged(object sender, EventArgs e)
    {
        OnChanged(null);
    }

    public event EventHandler<EventArgs> Changed;

    private void OnChanged(EventArgs e)
    {
        EventHandler<EventArgs> handler = Changed;
        if (handler != null)
            handler(this, e);
    }
}

//############################################
class Program
{
    private static void Main(string[] args)
    {
        var model = new Model();
        var logic = new Logic(model);

        var view = new View(logic, model);
        model.Change();
        //And crash!
    }
}

在给出的示例中,您会在哪里提出修复? ModelLogic类只是在不了解事件订阅顺序的情况下开展业务。我也看到ViewControl实现中没有设计缺陷。

想象一下,有三个不同的团队实施ModelLogicUI,而且不仅有这四个组件,还有数百个组件。这个问题无处不在。

我正在寻找的不是这种特殊情况下的本地修复,但我想找到一种模式来防止这种情况。例如:“控件必须优雅地忽略已处置实例上的事件调用”或“逻辑必须阻止模型上的订阅,只允许UI执行此操作”。等


除了回答

是的,处置对象事件回调不应该抛出异常。 更普遍的是:

  

...即使在事件被取消订阅后,事件处理程序也必须在被调用时保持健壮。

有很多原因 - 请参阅Eric Lippert的精彩文章Events and Races

4 个答案:

答案 0 :(得分:2)

我见过的一种模式如下。如果你被处置了,那就什么也不做,而不是抛出异常。

private void ModelOnChanged(object sender, EventArgs e)
{
    if (IsDisposed) { return; } // i.e. Do nothing

    //Do something
}

IDisposable模式的一个最大问题是它试图同时成为确定性和非确定性的内存管理。您可以致电Dispose(),或GC可以为您完成。它会使用Finalizers等创建所有混乱。

与仅保留引用计数器的语言不同 - 在最后一个引用消失时调用“析构函数” - .NET选择一种方法,其中对象的内存可能已被释放但对该对象仍然存在引用。因此,您必须确保代码不会访问处于无效状态的对象。这通常采用以下两种形式之一:

  1. 检查所有IsDisposed上的Public,如果已弃置ObjectDisposedException
  2. 如果对象被处置(早期返回),则隐式不做任何事情
  3. 从长远来看,第一个选项不太可能咬你,因为你知道你马上犯了错误。但是,如果行为不可预测并且您遇到temporal coupling问题,则最终必须处理整个程序中的ObjectDisposedException。在这种情况下,你可能会选择“无所事事”的方法,这样你的程序就会减少。不幸的是,它有可能咬你,因为看起来就像你所说的方法一样。

    我直到现在才考虑的另一个选项是订阅Disposed事件对象,这些事件对象具有类IDisposable的类级别引用。放置对象时,将该字段设置为null。类似地,您可以在对某个对象执行操作之前检查IsDisposed(如果它已暴露)(在跳转之前询问)。

    Public Class Foo
      Private _disposableObject As IDisposableFoo
    
      Private Sub OnBarDisposed(sender As Object, e As EventArgs) Handles IDisposableFoo
        _disposableObject = Nothing 
        'Hmm, now we'll get null-references everywhere
      End Sub
    

    和...

    Public Sub DoesStuffWithIDisposableObject()
      If Me.DisposableObjectReference.IsDisposed Then Exit Sub
    
      'Yay, valid reference! Let's get stuff done!
    End Sub
    

    仍然可能不是最好的选择,但不幸的是,语言的设计使得这种残留不可避免。

答案 1 :(得分:2)

在决定是否抛出ObjectDisposedException时,应该考虑几个因素:

  1. 对象是否满足特定方法调用的契约,而不必使用不再可用的资源?
  2. 如果函数成功返回,调用者可能会期望做一些没有必要资源就无法完成的事情,这意味着无论如何失败是不可避免的,应该提前而不是迟到,或者即使没有抛出异常,调用者也可能做正确的事情?

在许多情况下,特别是在“更新事件”场景中,调用者并不特别关心被调用方法的作用;呼叫的语义基本上是“做你认为需要做的事情”。这样的操作可以由任何对象成功地执行,即使它被处理,只需通过决定不需要做什么。如果正在使用的回调模式不允许事件处理程序通知事件发布者它不希望接收更多事件(Microsoft的正常事件模式不提供任何此类工具),则可能存在Disposed对象继续接收回调,因为事件发布者处于错误状态,但抛出异常可能对帮助解决这个问题没有多大帮助,并且可能会产生更多。

答案 2 :(得分:1)

我想我会抛出一个ObjectDisposedException,如果某些东西试图用处置控件做某事。放弃控制意味着你完成它,所以没有什么应该尝试再使用它。如果有的话,我会将其视为需要修复的程序中的错误。

答案 3 :(得分:1)

guideline about diposed objects我读到:

  

关于处置对象有一些规则。首先,调用Dispose   在已经处置的物体上应该什么都不做。第二,呼叫任何   已经处置的对象上的其他属性或方法应该抛出   的ObjectDisposedException

另一个guideline

  

一旦处置了一个物体,就应该将其视为禁区。   按照惯例,一次性对象应该抛出异常(如果有的话)   在调用Dispose后调用它的方法。有一个   系统中称为ObjectDisposedException的内置异常类型   为此精确添加到Framework类库的命名空间   目的

遵循这些规则,您的代码完全正常(尽管它会引发异常)。

两个引号都说如果调用任何的方法或属性,则被处置对象应抛出异常。如果调用任何公共方法或属性&#34;我倾向于说&#34;抛出异常。

如果调用私有方法,我认为不采取任何措施是安全的。因此,您的问题的答案是:&#34; 是的,处置控件应该能够安全地忽略事件回调&#34;。

顺便说一下:也许问题也可能是:&#34;谁负责处理对象?&#34;

在给定的示例中,您可以让控件将自己置于ModelOnChanged方法中。我没有找到一般指南,但是a few suggestions