ChatGPT解决这个技术问题 Extra ChatGPT

您如何防止 IDisposable 传播到您的所有班级?

从这些简单的类开始......

假设我有一组简单的类,如下所示:

class Bus
{
    Driver busDriver = new Driver();
}

class Driver
{
    Shoe[] shoes = { new Shoe(), new Shoe() };
}

class Shoe
{
    Shoelace lace = new Shoelace();
}

class Shoelace
{
    bool tied = false;
}

Bus 有一个 DriverDriver 有两个 Shoe,每个 Shoe 有一个 Shoelace。都非常傻。

将 IDisposable 对象添加到 Shoelace

后来我决定 Shoelace 上的某些操作可以是多线程的,所以我添加了一个 EventWaitHandle 以便线程与之通信。所以 Shoelace 现在看起来像这样:

class Shoelace
{
    private AutoResetEvent waitHandle = new AutoResetEvent(false);
    bool tied = false;
    // ... other stuff ..
}

在鞋带上实现 IDisposable

但现在 Microsoft's FxCop 会抱怨:“在 'Shoelace' 上实现 IDisposable,因为它会创建以下 IDisposable 类型的成员:'EventWaitHandle'。”

好的,我在 Shoelace 上实现了 IDisposable,我整洁的小课堂变得如此糟糕:

class Shoelace : IDisposable
{
    private AutoResetEvent waitHandle = new AutoResetEvent(false);
    bool tied = false;
    private bool disposed = false;

    public void Dispose()
    {
        Dispose(true);
        GC.SuppressFinalize(this);
    }

    ~Shoelace()
    {
        Dispose(false);
    }

    protected virtual void Dispose(bool disposing)
    {
        if (!this.disposed)
        {
            if (disposing)
            {
                if (waitHandle != null)
                {
                    waitHandle.Close();
                    waitHandle = null;
                }
            }
            // No unmanaged resources to release otherwise they'd go here.
        }
        disposed = true;
    }
}

或者(正如评论者所指出的)因为 Shoelace 本身没有非托管资源,我可能会使用更简单的 dispose 实现而不需要 Dispose(bool) 和析构函数:

class Shoelace : IDisposable
{
    private AutoResetEvent waitHandle = new AutoResetEvent(false);
    bool tied = false;

    public void Dispose()
    {
        if (waitHandle != null)
        {
            waitHandle.Close();
            waitHandle = null;
        }
        GC.SuppressFinalize(this);
    }
}

惊恐地看着 IDisposable 传播

没错,就是这么固定的。但是现在 FxCop 会抱怨 Shoe 创建了一个 Shoelace,所以 Shoe 也必须是 IDisposable

并且 Driver 创建 Shoe,因此 Driver 必须是 IDisposable。并且 Bus 创建 Driver,因此 Bus 必须是 IDisposable 等等。

突然间,我对 Shoelace 的小改动给我带来了很多工作,我的老板想知道为什么我需要结帐 Bus 才能对 Shoelace 进行更改。

问题

您如何防止 IDisposable 的这种传播,但仍确保您的非托管对象得到正确处置?

一个非常好的问题,我相信答案是尽量减少它们的使用并尝试保持高级 IDisposables 的短暂使用,但这并不总是可能的(尤其是那些 IDisposables 是由于与 C++ dll 或类似的互操作而导致的)。看看答案。
好的,丹,我已经更新了问题以显示在鞋带上实现 IDisposable 的两种方法。
我通常对依赖其他类的实现细节来保护我持谨慎态度。如果我能轻易地阻止它,那么冒险是没有意义的。也许我过于谨慎,或者我作为 C 程序员的时间太长了,但我宁愿采用爱尔兰的方法:“确定,确定”:)
@Dan:仍然需要进行空检查以确保对象本身未设置为空,在这种情况下,对 waitHandle.Dispose() 的调用将引发 NullReferenceException。
无论如何,您实际上仍应使用“在鞋带上实现 IDisposable”部分中所示的 Dispose(bool) 方法,因为它(减去终结器)是完整的模式。仅仅因为一个类实现了 IDisposable 并不意味着它需要一个终结器。

G
Grant BlahaErath

你不能真正“阻止” IDisposable 传播。有些类需要被释放,例如 AutoResetEvent,最有效的方法是在 Dispose() 方法中完成,以避免终结器的开销。但是这个方法必须以某种方式调用,所以就像在你的例子中一样,封装或包含 IDisposable 的类必须处理这些,所以它们也必须是一次性的,等等。避免它的唯一方法是:

尽可能避免使用 IDisposable 类,在单个位置锁定或等待事件,将昂贵的资源保存在单个位置等

仅在需要时创建它们并在之后处理它们(使用模式)

在某些情况下,可以忽略 IDisposable,因为它支持可选情况。例如,WaitHandle 实现 IDisposable 以支持命名的 Mutex。如果未使用名称,则 Dispose 方法不执行任何操作。 MemoryStream 是另一个例子,它不使用系统资源,而且它的 Dispose 实现也不做任何事情。仔细考虑是否正在使用非托管资源可能具有指导意义。因此可以检查 .net 库的可用源或使用反编译器。


你可以忽略它的建议是因为今天的实现没有做任何事情是不好的,因为未来的版本实际上可能会在 Dispose 中做一些重要的事情,现在你很难追查泄漏。
“保留昂贵的资源”,IDisposable 并不一定很昂贵,事实上,这个使用均匀等待句柄的示例大约是“轻量级”。
B
Bridge

就正确性而言,如果父对象创建并基本上拥有一个现在必须是一次性的子对象,则您无法阻止 IDisposable 通过对象关系传播。 FxCop 在这种情况下是正确的,并且父级必须是 IDisposable。

您可以做的是避免将 IDisposable 添加到对象层次结构中的叶类。这并不总是一件容易的事,但它是一个有趣的练习。从逻辑的角度来看,鞋带没有理由必须是一次性的。除了在此处添加 WaitHandle 之外,是否还可以在 ShoeLace 和 WaitHandle 的使用点之间添加关联。最简单的方法是通过 Dictionary 实例。

如果您可以在实际使用 WaitHandle 的点通过映射将 WaitHandle 移动到松散关联中,那么您可以断开此链。


在那里建立联系感觉很奇怪。这个 AutoResetEvent 对 Shoelace 实现是私有的,所以在我看来公开它是错误的。
@GrahamS,我不是说公开揭露它。我是说能不能挪到系鞋带的地步。也许如果系鞋带是一项如此复杂的任务,那么他们应该是一个鞋带层级。
你能用一些代码片段 JaredPar 扩展你的答案吗?我可能会稍微扩展我的示例,但我想象 Shoelace 创建并启动 Tyer 线程,该线程在 waitHandle.WaitOne() 处耐心等待 Shoelace 在希望线程开始绑定时会调用 waitHandle.Set()。
对此+1。我认为只有 Shoelace 会同时被调用而不是其他的会很奇怪。想想什么应该是聚合根。在这种情况下,它应该是 Bus,恕我直言,尽管我不熟悉该域。因此,在这种情况下,总线应该包含等待句柄,并且所有针对总线的操作和它的所有子节点都将同步。
J
Jordão

为了防止 IDisposable 传播,您应该尝试将一次性对象的使用封装在单个方法中。尝试以不同的方式设计 Shoelace

class Shoelace { 
  bool tied = false; 

  public void Tie() {

    using (var waitHandle = new AutoResetEvent(false)) {

      // you can even pass the disposable to other methods
      OtherMethod(waitHandle);

      // or hold it in a field (but FxCop will complain that your class is not disposable),
      // as long as you take control of its lifecycle
      _waitHandle = waitHandle;
      OtherMethodThatUsesTheWaitHandleFromTheField();

    } 

  }
} 

等待句柄的范围仅限于 Tie 方法,并且该类不需要具有一次性字段,因此本身也不需要是一次性的。

由于等待句柄是 Shoelace 内的一个实现细节,它不应以任何方式更改其公共接口,例如在其声明中添加新接口。那么当您不再需要一次性字段时会发生什么,您会删除 IDisposable 声明吗?如果您考虑Shoelace 抽象,您很快就会意识到它不应该被基础架构依赖项所污染,例如 IDisposableIDisposable 应保留给其抽象封装了需要确定性清理的资源的类;即,对于可处置性是抽象的一部分的类。


我完全同意 Shoelace 的实现细节不应该污染公共接口,但我的观点是这很难避免。您在这里建议的内容并不总是可能的: AutoResetEvent() 的目的是在线程之间进行通信,因此它的范围通常会超出单个方法。
@GrahamS:这就是我说要尝试以这种方式设计的原因。您还可以将一次性传递给其他方法。只有当对类的外部调用控制了一次性用品的生命周期时,它才会发生故障。我会相应地更新答案。
抱歉,我知道你可以传递一次性用品,但我仍然认为它不起作用。在我的示例中,AutoResetEvent 用于在同一类中运行的不同线程之间进行通信,因此它必须是成员变量。您不能将其范围限制为方法。 (例如,假设一个线程通过阻塞 waitHandle.WaitOne() 等待某些工作。然后主线程调用 shoelace.Tie() 方法,该方法只执行 waitHandle.Set() 并立即返回)。
H
Henk Holterman

这基本上是当您将组合或聚合与一次性类混合时发生的情况。如前所述,第一个出路是用鞋带重构waitHandle。

话虽如此,当您没有非托管资源时,您可以大大减少 Disposable 模式。 (我仍在为此寻找官方参考。)

但是你可以省略析构函数和 GC.SuppressFinalize(this);并且可能稍微清理一下虚拟 void Dispose(bool disposing)。


谢谢亨克。如果我从 Shoelace 中取出 waitHandle 的创建,那么仍然有人必须在某个地方处理它,那么有人怎么知道 Shoelace 已经完成了它(并且 Shoelace 没有将它传递给任何其他类)?
您不仅需要移动等待句柄的创建,还需要移动等待句柄的“责任”。 jaredPar 的答案有一个有趣的想法。
此博客介绍了 IDisposable 的实现,特别是解决如何通过避免在单个类中混合托管和非托管资源来简化任务blog.stephencleary.com/2009/08/…
G
GrahamS

有趣的是,如果 Driver 定义如上:

class Driver
{
    Shoe[] shoes = { new Shoe(), new Shoe() };
}

然后当 Shoe 变为 IDisposable 时,FxCop (v1.36) 不会抱怨 Driver 也应该是 IDisposable

但是,如果它是这样定义的:

class Driver
{
    Shoe leftShoe = new Shoe();
    Shoe rightShoe = new Shoe();
}

然后它会抱怨。

我怀疑这只是 FxCop 的限制,而不是解决方案,因为在第一个版本中,Shoe 实例仍由 Driver 创建,并且仍需要以某种方式处理。


如果 Show 实现 IDisposable,则在字段初始化程序中创建它是危险的。有趣的是 FXCop 不允许这样的初始化,因为在 C# 中,安全地执行它们的唯一方法是使用 ThreadStatic 变量(非常可怕)。在 vb.net 中,可以在没有 ThreadStatic 变量的情况下安全地初始化 IDisposable 对象。代码仍然很丑陋,但并不那么可怕。我希望 MS 能提供一种安全使用此类字段初始值设定项的好方法。
@supercat:谢谢。你能解释一下为什么这是危险的吗?我猜如果在 Shoe 构造函数之一中引发异常,那么 shoes 将处于困难状态?
除非知道可以安全地放弃特定类型的 IDisposable,否则应确保创建的每个对象都被 Dispose。如果在对象的字段初始化程序中创建了 IDisposable,但在对象完全构造之前引发了异常,则该对象及其所有字段都将被放弃。即使对象的构造被包装在使用“try”块来检测构造失败的工厂方法中,工厂也很难获得对需要处理的对象的引用。
我知道在 C# 中处理该问题的唯一方法是使用 ThreadStatic 变量来保留在当前构造函数抛出时需要处理的对象列表。然后让每个字段初始化程序在创建每个对象时注册它,如果它成功完成,让工厂清除列表,如果没有清除,则使用“finally”子句从列表中处理所有项目。这将是一种可行的方法,但 ThreadStatic 变量很难看。在 vb.net 中,可以使用类似的技术而不需要任何 ThreadStatic 变量。
J
Joh

如果您的设计保持如此紧密的耦合,我认为没有一种技术方法可以防止 IDisposable 传播。然后人们应该怀疑设计是否正确。

在您的示例中,我认为让鞋子拥有鞋带是有意义的,也许司机应该拥有他/她的鞋子。但是,公共汽车不应该拥有司机。通常,公共汽车司机不会跟随公共汽车去废品场:) 对于司机和鞋子,司机很少自己制作鞋子,这意味着他们并不真正“拥有”它们。

另一种设计可能是:

class Bus
{
   IDriver busDriver = null;
   public void SetDriver(IDriver d) { busDriver = d; }
}

class Driver : IDriver
{
   IShoePair shoes = null;
   public void PutShoesOn(IShoePair p) { shoes = p; }
}

class ShoePairWithDisposableLaces : IShoePair, IDisposable
{
   Shoelace lace = new Shoelace();
}

class Shoelace : IDisposable
{
   ...
}

不幸的是,新设计更加复杂,因为它需要额外的类来实例化和处理鞋子和驱动程序的具体实例,但这种复杂性是要解决的问题所固有的。好消息是,公交车不再需要仅仅为了处理鞋带而被丢弃。


但这并不能真正解决最初的问题。它可能会让 FxCop 保持安静,但仍然应该处理鞋带。
我认为您所做的只是将问题转移到其他地方。 ShoePairWithDisposableLaces 现在拥有 Shoelace(),所以它也必须设为 IDisposable - 那么谁处理鞋子?你要把它留给一些 IoC 容器来处理吗?
@AndrewS:确实,仍然必须处理司机、鞋子和鞋带,但不应由公共汽车发起处理。 @GrahamS:你是对的,我将问题转移到别处,因为我相信它属于别处。通常,会有一个类实例化鞋子,它也将负责处理鞋子。我已经编辑了代码以使 ShoePairWithDisposableLaces 也是一次性的。
@Joh:谢谢。正如您所说,将其移至其他地方的问题是您会产生更多的复杂性。如果 ShoePairWithDisposableLaces 是由其他类创建的,那么当 Driver 完成他的鞋子时,是否必须通知该类,以便它可以正确 Dispose() 它们?
@Graham:是的,需要某种通知机制。例如,可以使用基于引用计数的处置策略。有一些额外的复杂性,但正如您可能知道的那样,“没有免费的鞋子”:)
D
Darragh

如何使用控制反转?

class Bus
{
    private Driver busDriver;

    public Bus(Driver busDriver)
    {
        this.busDriver = busDriver;
    }
}

class Driver
{
    private Shoe[] shoes;

    public Driver(Shoe[] shoes)
    {
        this.shoes = shoes;
    }
}

class Shoe
{
    private Shoelace lace;

    public Shoe(Shoelace lace)
    {
        this.lace = lace;
    }
}

class Shoelace
{
    bool tied;
    private AutoResetEvent waitHandle;

    public Shoelace(bool tied, AutoResetEvent waitHandle)
    {
        this.tied = tied;
        this.waitHandle = waitHandle;
    }
}

class Program
{
    static void Main(string[] args)
    {
        using (var leftShoeWaitHandle = new AutoResetEvent(false))
        using (var rightShoeWaitHandle = new AutoResetEvent(false))
        {
            var bus = new Bus(new Driver(new[] {new Shoe(new Shoelace(false, leftShoeWaitHandle)),new Shoe(new Shoelace(false, rightShoeWaitHandle))}));
        }
    }
}

现在你已经把它变成了调用者的问题,并且 Shoelace 不能依赖等待句柄在需要时可用。
@andy“鞋带不能依赖等待句柄在需要时可用”是什么意思?它被传递给构造函数。
在将 autoresetevents 状态提供给 Shoelace 之前,可能有其他东西弄乱了它,并且它可能以 ARE 处于错误状态开始;在 Shoelace 执行其操作时,ARE 的状态可能会出现其他问题,从而导致 Shoelace 执行错误操作。这与您只锁定私人成员的原因相同。 “一般来说,.. 避免锁定超出您的代码控制范围的实例”docs.microsoft.com/en-us/dotnet/csharp/language-reference/…
我同意只锁定私人成员。但似乎等待句柄是不同的。事实上,将它们传递给对象似乎更有用,因为需要使用同一个实例来跨线程进行通信。尽管如此,我认为 IoC 为 OP 的问题提供了一个有用的解决方案。
鉴于 OP 示例将等待句柄作为私有成员,安全的假设是对象期望对其进行独占访问。使句柄在实例外部可见违反了这一点。通常,IoC 可以很好地处理这样的事情,但在线程方面则不然。

关注公众号,不定期副业成功案例分享
关注公众号

不定期副业成功案例分享

领先一步获取最新的外包任务吗?

立即订阅