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

Posted

技术标签:

【中文标题】您如何防止 IDisposable 传播到您的所有班级?【英文标题】:How do you prevent IDisposable from spreading to all your classes? 【发布时间】:2010-10-14 07:01:04 【问题描述】:

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

假设我有这样一组简单的类:

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 上的某些操作可以是多线程的,所以我添加了一个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 并不意味着它需要一个终结器。 【参考方案1】:

如何使用控制反转?

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 可以用于此类事情,但在线程方面则不然。【参考方案2】:

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

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

如果您可以在实际使用 WaitHandle 时通过映射将 WaitHandle 移动到松散关联中,那么您可以打破这个链。

【讨论】:

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

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

尽可能避免使用 IDisposable 类,在单个位置锁定或等待事件,将昂贵的资源保留在单个位置等 仅在需要时创建它们并在之后处理它们(using 模式)

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

【讨论】:

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

为防止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() 并立即返回)。【参考方案5】:

如果您的设计保持如此紧密的耦合,我不认为有防止 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:是的,需要某种通知机制。例如,可以使用基于引用计数的处置策略。有一些额外的复杂性,但您可能知道,“没有免费的鞋子”:)【参考方案6】:

有趣的是,如果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 变量。【参考方案7】:

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

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

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

【讨论】:

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

以上是关于您如何防止 IDisposable 传播到您的所有班级?的主要内容,如果未能解决你的问题,请参考以下文章

如何将android分区的图像制作到您的PC

如何将 cuDNN 更新到较新版本?

链接到您的GitHub新闻源?

将咖啡脚本合并到您的节点项目中?

如何创建打开电子邮件的链接到您的个人电子邮件地址[重复]

您是不是忽略 .gitignore 中的 git 子模块或将其提交到您的仓库?