【问题标题】:Objects need to be disposed in a specific order: is this a code smell?对象需要按特定顺序处理:这是代码异味吗?
【发布时间】:2011-03-11 23:19:45
【问题描述】:

上下文

我正在使用具有工作区概念的 Winforms 应用程序 (.NET 3.5 SP1),它可以包含 n 个面板。每个面板(源自Panel)都有视图:

.------------------------。 |工作空间 | |.--------. .--------。 | ||面板1 | |面板2 | | ||.-----。 | |.-----。 | | |||查看1| | ||查看2| | | ||'-----' | |'-----' | | |'--------' '--------' | '------------'

所有面板都被添加到 Workspace 类的 this.Controls 集合中(它派生自 UltraTabPageControl,一个 Infragistics GUI 控件)。每个视图都添加到其父级的Controls 集合中。因此,当在 Workspace 上调用 Dispose 时,面板和视图会自动释放,这完全符合预期。

我们还有另一个概念,称为ViewManager。它跟踪工作区中的所有View 控件,并负责维护单个“主”视图。每当创建 View 时,它都会向此管理器注册自己。这会将View 添加到列表中,然后运行一些逻辑来确定新的“主”视图,然后在每个视图上调用Synchronize() 方法。

当前的设计是,每当调用View.Dispose() 时,它都会从ViewManager 中注销自己。这会从列表中删除 View,然后运行相应的逻辑来检查剩余视图中的新主节点。

转折

当我们关闭整个工作区时,有一个特殊的Panel 类型需要在其他面板之前关闭。所以我们在Dispose 方法中有如下代码:

protected override void Dispose(bool disposing)
{
    var theSpecialPanel = GetSpecialPanel();
    if (theSpecialPanel != null)
    {
        theSpecialPanel.Dispose();
    }
    base.Dispose(disposing);
}

如果我们取出该代码,那么其他面板可能会在theSpecialPanel 之前被丢弃。这会导致检查新主面板的逻辑运行,在每个 View 上调用 Synchronize(),包括这个特殊面板。这会引发

“InvalidComObjectException:无法使用已与其底层 RCW 分离的 COM 对象。”

问题

这种设计是代码味道吗?强制一个特定的对象在其他对象之前被释放是不是很奇怪?

【问题讨论】:

  • 看起来确实很糟糕。如果客户端代码不调用 Dispose 会发生什么?是否会因此异常而使终结器线程崩溃?这是一个不可调试的例外。阅读:blogs.msdn.com/b/visualstudio/archive/2010/03/01/…
  • 这将是我的下一个问题:) - 什么会导致这个异常?感谢指点;正是我想要的。
  • 注意:您的 Dispose 代码没有遵循良好的做法 - 如果 disposing==false,您正在访问其他托管对象。如果您有自己的非标准析构函数实现,或者如果终结器调用 Dispose(false),则可能会造成混淆。见msdn.microsoft.com/en-us/library/aa720161(v=VS.71).aspx
  • @AlexeiLevenkov 我故意不遵循微软经常引用的 Dispose 模式。我们不会实现终结器,派生类也不会这样做,所以我们可以forgo much of that cruft。感谢您的提醒。

标签: c# winforms events dispose


【解决方案1】:

要求某些对象按特定顺序放置是完全合理的。请注意,终结器的一个主要限制是它们不能保证关于处置顺序的任何事情。考虑一个 Froboz9000Connection 对象(用于管理与 Froboz 9000 计算机的连接),该对象又包含一个用于实际通信的 SerialPort。有必要在程序结束之前,必须向远程机器发送一定的命令序列。正确的事件序列是 Frobozz9000Connection 对象的 Dispose 方法将发送必要的命令序列,然后 Dispose SerialPort。如果先处理 SerialPort,Frobozz9000Connection 对象将无法发送正确的命令序列来通知远程机器不再需要其服务。

顺便说一句,这个问题是我不喜欢终结器的另一个(众多)原因之一。虽然有时终结器肯定有用,但我认为在绝大多数情况下,简单地确保正确使用 Dispose 更为重要。

【讨论】:

    【解决方案2】:

    SpecialPane 是否需要明确处置?或者您只是想确保对Synchronize() 的调用始终正确?我在这里假设您的 View 派生自 Control

    public void Synchronize()
    {
        if (this.IsDisposed || this.Disposing) return; // or return a 'remove me' flag
        ... 
        // sync
    }
    

    在不了解您的其余代码的情况下,我假设这应该工作得相对较好,而不依赖于终结器。您让每个 View 负责了解它们是否可以执行特定操作,而无需表单容器了解您系统的实现细节。

    我相信它也将有助于维护。依赖处理顺序可能会很棘手,如果另一个开发人员出现,他们可能会完全错过这一点。如果 View 通过其他方式处理,而不是从 Form 处理方法中调用,它也可以工作。

    哦,如果 View 没有扩展 Control,那么给 View 一个对其父控件的引用,并检查父控件的 Disposal 状态。

    【讨论】:

    • 我认为您的建议总体上是好的。我尝试在视图中添加此检查,但就我而言,它不起作用。我有一个 AxWindowsMediaPlayer 对象,该对象在 Dispose 发生之前完成,从而产生 InvalidComObjectException。在出现异常时,IsDisposedDisposing 都是错误的。我正在尝试确定发生这种情况的原因。
    【解决方案3】:

    在您的代码 sn-p 中,我没有看到您在哪里处理其他面板。

    如果您处置所有面板,我认为决定您希望的处置顺序没有任何问题。如果我没听错的话,你可以这样做:

    foreach (panel in Panels.Where(p => p != theSpecialPanel))
    {
       panel.Dispose();
    }
    theSpecialPanel.Dispose();
    

    【讨论】:

    • 其他面板由于位于工作区的Controls 集合中而被丢弃。我没有明确地处理其他面板。这会改变事情吗? [您的顺序错误(我想先处理 theSpecialPanel),但您的示例中的想法是正确的。]
    • 我明白了。所以循环出现在theSpecialPanel.Dispose(); 之后,它隐含在base.Dispose(disposing); 调用中。对我来说似乎还可以,语义相似。您可以要求在面板之前放置视图,那么为什么不能要求在另一个面板之前放置一个面板?
    猜你喜欢
    • 1970-01-01
    • 2023-03-15
    • 1970-01-01
    • 1970-01-01
    • 2019-03-02
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2012-07-12
    相关资源
    最近更新 更多