3

我有一个类是一次性 UI 控件。它订阅模型对象的更改以重绘其内容。

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

结果,模型中的更改取决于订阅顺序-首先导致控制处置,然后是方法调用-最终以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实现。

想象一下,有三个不同的团队在实施ModelLogic并且UI不仅有这四个组件,而且还有数百个。这个问题可能在任何地方发生。

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


除了接受的答案

是的,已处置的对象事件回调不应引发异常。更一般地说:

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

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

4

4 回答 4

2

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

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

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

于 2012-07-24T22:50:44.103 回答
2

我见过的一种模式如下。如果您已被处置,则什么也不做,而不是抛出异常。

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

    //Do something
}

该模式的最大问题之一IDisposable是它试图同时进行确定性和非确定性内存管理。你可以打电话Dispose(),或者 GC 可以为你做。它用终结器之类的东西制造了所有的混乱。

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

  1. 检查IsDisposed一切,如果它被丢弃Public,抛出一个ObjectDisposedException
  2. 如果对象被释放,则不执行任何操作(提前返回)

从长远来看,第一种选择不太可能咬你,因为你知道你马上犯了一个错误。但是,如果行为不可预测并且您有时间耦合问题,您最终将不得不处理整个ObjectDisposedException程序。在这种情况下,您可能会采用“无所事事”的方法,这样您的程序中的绒毛就会减少。不幸的是,它有可能会咬你,因为它看起来像你调用的方法完成了它的工作。

直到现在我还没有考虑过的另一个选项是订阅Disposed你有类级别引用的事件对象IDisposable。释放对象时,将该字段设置为空。同样,您可以在对对象执行某些操作之前检查 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

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

于 2012-07-24T17:45:43.453 回答
1

我想我会抛出一个 ObjectDisposedException 如果有东西试图用处置的控件做某事。放置控件意味着您已经完成了它,因此不应再尝试使用它。如果有问题,我会将其视为程序中需要修复的错误。

于 2012-07-24T17:48:25.190 回答
1

关于处置对象的指南中,我读到:

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

另一个准则

一旦一个物体被丢弃,它就应该被视为禁区。按照惯例,如果在调用 Dispose 之后调用了一次性对象的任何方法,则一次性对象应该抛出异常。在 System 命名空间中有一个名为 ObjectDisposedException 的内置异常类型,已为此确切目的添加到框架类库中。

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

两个引号都表示,如果调用其任何方法或属性,则已处置对象应引发异常。我更愿意说“如果调用任何公共方法或属性,则抛出异常”。

如果调用私有方法,我认为什么都不做是安全的。因此,您的问题的答案是:“是的,处置的控件应该能够安全地忽略事件回调”。

顺便说一句:也许问题也可能是:“谁负责处置一个对象?”

在给定的示例中,您可以让控件在 ModelOnChanged 方法中自行处理。我没有找到通用的指导方针,但有一些建议

于 2012-07-24T20:09:25.647 回答