【问题标题】:Declare IDisposable for the class or interface?为类或接口声明 IDisposable?
【发布时间】:2012-05-17 20:12:23
【问题描述】:

从以下情况开始:

public interface ISample
{
}

public class SampleA : ISample
{
   // has some (unmanaged) resources that needs to be disposed
}

public class SampleB : ISample
{
   // has no resources that needs to be disposed
}

类 SampleA 应该实现接口 IDisposable 来释放资源。 您可以通过两种方式解决此问题:

1.在类 SampleA 中添加所需的接口:

public class SampleA : ISample, IDisposable
{
   // has some (unmanaged) resources that needs to be disposed
}

2。将其添加到接口 ISample 并强制派生类实现它:

public interface ISample : IDisposable
{
}

如果你把它放到接口中,你会强制任何实现实现 IDisposable,即使它们没有什么可处置的。另一方面,很清楚地看到接口的具体实现需要一个 dispose/using 块,并且您不需要强制转换为 IDisposable 进行清理。两种方式可能都有更多的优点/缺点......您为什么建议使用一种优于另一种的方式?

【问题讨论】:

  • 针对接口编写的代码(大概是已构建的实例)负责结束该实例的(有用)生命周期的可能性有多大?
  • @Damien_The_Unbeliever:好的,假设 ISample 来自工厂方法结果或通过依赖注入。
  • 就是这样 - 如果它来自工厂,那么您的代码可能负责处理 - 所以我会把它放在接口。但如果它被注入,那么我假设注入器也负责生命周期,所以它不会适合界面 - 我不认为有一个万能的回答问题。

标签: c# .net dispose idisposable


【解决方案1】:

遵循SOLIDInteface Segregation Principle,如果您将IDisposable 添加到接口,您将向不感兴趣的客户端提供方法,因此您应该将其添加到A。

除此之外,接口永远不是一次性的,因为一次性与接口的具体实现有关,与接口本身无关。

任何接口都可以在有或没有需要处理的元素的情况下实现。

【讨论】:

  • 虽然理论上您的建议是最正确的,但在实践中处理具有潜在一次性实现的接口是很麻烦的。很大程度上取决于谁管理对象的生命周期。更好的答案 IMO 在这里:stackoverflow.com/a/14368765/661933
  • 如果接口本身实现了 IDisposable,代码分析工具也更容易发出警告。
  • @nawfal:如果理论与实践不符,则意味着我们使用的工具是错误的,而不是相反。根据定义,接口不能是一次性的,因为您只能处理实现,而不是操作合同。
【解决方案2】:

如果您将using(){} 模式应用于所有接口,最好让ISample 派生自IDisposable,因为设计接口时的经验法则是倾向于“易用性” em> 优于 “易于实现”

【讨论】:

  • 如何在 foreach 中使用 using 语句?
  • "...你只看到界面..." 这就是关键。很多好的,OOP 解决方案只会使用接口,所以我认为接口实现 IDisposable 是有意义的。这样,消费代码可以以相同的方式处理所有子类。
【解决方案3】:

就个人而言,如果所有ISample 都应该是一次性的,我会把它放在界面上,如果只有一些,我只会把它放在应该放在的类上。

听起来你有后一种情况。

【讨论】:

  • 但是如果你的库的用户不知道 Dispose() 并且可能需要一个实现来处理它,如何确保调用 Dispose()?
  • @Stegi - 简单。只需测试 is IDisposable 并调用 Dispose 如果是这样。
  • 是的,我知道,但这会破坏代码......因为所有其他实现(例如 IList、I......)都可能是 IDisposable。在我看来, IDisposable 应该是一个通用的对象基类实现(即使是空的)。
  • @Stegi - 那你真的回答了你自己的问题。
  • @Stegi 假设您对 tryf 的性能影响感到满意
【解决方案4】:

一个接口IFoo可能应该实现IDisposable,如果至少一些实现可能会实现IDisposable,并且至少在某些情况下,对实例的最后幸存引用将存储在变量中或IFoo 类型的字段。如果任何实现可能实现IDisposable,它几乎肯定应该实现IDisposable,并且实例将通过工厂接口创建(就像IEnumerator<T>的实例一样,在许多情况下是通过工厂接口IEnumerable<T>创建的)。

比较 IEnumerable<T>IEnumerator<T> 是有启发性的。一些实现IEnumerable<T> 的类型也实现了IDisposable,但是创建此类类型实例的代码将知道它们是什么,知道它们需要处理,并将它们用作它们的特定类型。此类实例可能会以IEnumerable<T> 类型传递给其他例程,而那些其他例程将不知道对象最终将需要处置,但在大多数情况下,那些其他例程不会是最后保留的例程对对象的引用。相比之下,IEnumerator<T> 的实例通常是由对这些实例的底层类型一无所知的代码创建、使用并最终放弃的,除了它们是由IEnumerable<T> 返回的事实之外。 IEnumerable<T>.GetEnumerator() 的某些实现返回 IEnumerator<T> 的实现如果在它们被放弃之前没有调用它们的 IDisposable.Dispose 方法,则将泄漏资源,并且大多数接受 IEnumerable<T> 类型参数的代码将无法知道这些类型是否可能传递给它。尽管IEnumerable<T> 可以包含属性EnumeratorTypeNeedsDisposal 来指示返回的IEnumerator<T> 是否必须被释放,或者只是要求调用GetEnumerator() 的例程检查返回对象的类型以查看是否它实现了IDisposable,与确定Dispose 是否必要并仅在必要时调用它相比,无条件调用可能不会做任何事情的Dispose 方法更快更容易。

【讨论】:

  • IEnumerator<T> 是一个很好的接口示例,其中生命周期管理绝对是其 API 的一部分。这样做可能是为了让事情变得更简单。但是,可以设想有人想在一种方法中调用GetEnumerator(),并在某些时候将枚举数传递给另一种方法。理想情况下,其他方法无法访问Dispose()。如果框架有interface IDisposableEnumerator<T> : IEnumerator<T>, IDisposable {}interface IEnumerator<T> : IEnumerator { T Current { get; } },这可能会使一些东西“更干净”(但也更复杂:-/)。
  • @binki:如果IEnumerator 实现IDisposable,会让事情变得更干净。接收到它一无所知的IEnumerable 的代码必须假定可能有必要在它返回的IEnumerator 上调用Dispose。如果它将返回的枚举数传递给其他方法以供稍后使用,则后一种方法可能有责任在其上调用Dispose。因为在已知实现 IDisposable 的东西上调用无操作 Dispose 比检查对象是否实现 IDisposable 更快...
  • ...对象履行其义务的最简单方法是在放弃之前对他们拥有的枚举器调用Dispose,而不管这些枚举器是否会关心。
  • IMO,IEnumerator<T> 一开始就不应该实现IEnumerator,因为现有代码会调用IEnumerable.GetEnumerator() 并收到IEnumerator<T>,而它只用于处理IEnumerator。我仍然认为将“内部”消费 API 与“外部”生命周期管理的东西分开是可取的,但我想它对于同步场景和内存中的集合来说已经足够好了。我试图通过将生命周期和“消费者”API 分开来解决我的异步问题,但要对 enumerable 做同样的事情,我需要发明一个全新的 API。
  • @binki:如果GetEnumerator 返回的实例实现了foreach,则foreach 的C# 代码调用IDisposable
【解决方案5】:

IDispoable 是一个非常常见的接口,让你的接口继承它并没有什么坏处。您将因此避免在代码中进行类型检查,唯一的代价是在您的某些 ISample 实现中实现无操作实现。所以从这个角度来看,你的第二个选择可能会更好。

【讨论】:

  • 这根本不是真的。如果您的类有一个实现 IDisposable 的字段,则该类也必须是 IDisposable 才能调用该字段的 Dispose。如果您在不需要 IDisposable 时添加它,那么最终您的一半课程都是 IDisposable。
【解决方案6】:

我开始认为将IDisposable 放在接口上会导致一些问题。这意味着实现该接口的所有对象的生命周期都可以安全地同步结束。也就是说,它允许任何人编写这样的代码,并且需要 all 实现来支持IDisposable:

using (ISample myInstance = GetISampleInstance())
{
    myInstance.DoSomething();
}

只有访问具体类型的代码才能知道控制对象生命周期的正确方法。例如,一个类型可能一开始就不需要处理,它可能支持IDisposable,或者在您使用完它之后可能需要awaiting 一些异步清理过程(例如,something like option 2 here)。

接口作者无法预测实现类的所有可能的未来生命周期/范围管理需求。接口的目的是允许对象公开一些 API,以便它可以对某些消费者有用。一些接口可能与生命周期管理相关(例如IDisposable 本身),但是将它们与与生命周期管理无关的接口混合可能会使编写接口的实现变得困难或不可能。如果你的接口实现很少,并且你的代码结构使得接口的消费者和生命周期/作用域管理器在同一个方法中,那么这种区别一开始并不清晰。但是如果你开始传递你的对象,这会更清楚。

void ConsumeSample(ISample sample)
{
    // INCORRECT CODE!
    // It is a developer mistake to write “using” in consumer code.
    // “using” should only be used by the code that is managing the lifetime.
    using (sample)
    {
        sample.DoSomething();
    }

    // CORRECT CODE
    sample.DoSomething();
}

async Task ManageObjectLifetimesAsync()
{
    SampleB sampleB = new SampleB();
    using (SampleA sampleA = new SampleA())
    {
        DoSomething(sampleA);
        DoSomething(sampleB);
        DoSomething(sampleA);
    }

    DoSomething(sampleB);

    // In the future you may have an implementation of ISample
    // which requires a completely different type of lifetime
    // management than IDisposable:
    SampleC = new SampleC();
    try
    {
        DoSomething(sampleC);
    }
    finally
    {
        sampleC.Complete();
        await sampleC.Completion;
    }
}

class SampleC : ISample
{
    public void Complete();
    public Task Completion { get; }
}

在上面的代码示例中,我演示了三种类型的生命周期管理场景,添加到您提供的两种。

  1. SampleAIDisposable,支持同步 using () {}
  2. SampleB 使用纯垃圾回收(不消耗任何资源)。
  3. SampleC 使用的资源会阻止它被同步处理,并在其生命周期结束时需要 await(以便它可以通知生命周期管理代码它已完成消耗资源并冒泡任何异步遇到的异常)。

通过将生命周期管理与其他接口分开,您可以防止开发人员错误(例如,意外调用 Dispose())并更清晰地支持未来未预料到的生命周期/范围管理模式。

【讨论】:

  • 不需要清理的类型可以通过void IDisposable.Dispose() {};轻松正确地实现IDisposable。如果从工厂函数返回的某些对象需要清理,让它返回一个实现IDisposable 的类型并要求客户端平衡对工厂函数的每次调用与对返回对象的Dispose 的调用,而不是要求客户端确定哪些对象需要清理。
【解决方案7】:

我个人会选择 1,除非你为 2 举一个具体的例子。 两个很好的例子是IList

IList 表示您需要为您的集合实现一个索引器。 但是,IList 实际上也意味着您是 IEnumerable,您的班级应该有一个 GetEnumerator()

在您的情况下,您对实现ISample 的类是否需要实现IDisposable 犹豫不决,如果不是每个实现您的接口的类都必须实现IDisposable,那么不要强迫它们。

特别关注IDispoable,特别是IDispoable 迫使程序员使用您的类编写一些相当丑陋的代码。例如,

foreach(item in IEnumerable<ISample> items)
{
    try
    {
        // Do stuff with item
    }
    finally
    {
        IDisposable amIDisposable = item as IDisposable;
        if(amIDisposable != null)
            amIDisposable.Dispose();  
    }
}

不仅代码很糟糕,而且即使Dispose() 只是在实现中返回,在确保该列表的每次迭代都有一个 finally 块来处理该项目时也会显着降低性能。

把回答其中一个问题的代码贴在这里,更容易阅读。

【讨论】:

  • 但是,如果有工厂(或简单使用 IoC-Container)交付实现对象,无论是否需要处理它们,该怎么办?您必须通过强制转换和显式调用 Dispose() 来调用它。这样,您的代码需要了解(可能的)实现......某种耦合?否则,您可以使用非常简单的 using 块。
  • @Stegi 使用块可能看起来并不难看,但它仍然会降低性能。另外,例如,我看不到您将如何在 foreach 中使用 using 块。微软的代码中有很多例子,IDisposable amIDisposable = object as IDisposable; if(amIDisposable != null) amIDisposable.Dispose();因为如果 as 不能将其强制转换为 IDisposable 则不会引发异常,因此性能损失几乎不存在。
  • 即使只有 1% 的实现或继承自某个特定类型的类会在 IDisposable.Dispose 中执行任何操作,但如果有必要在声明为的变量或字段上调用 ​​IDisposable.Dispose该类型(与实现或派生类型之一相反)是一个好兆头,表明该类型本身应该继承或实现IDisposable。在运行时测试对象实例是否实现IDisposable 并在其上调用IDisposable.Dispose(对于非泛型IEnumerable 来说是必要的)是主要的代码异味。
猜你喜欢
  • 2013-01-17
  • 1970-01-01
  • 2013-09-19
  • 1970-01-01
  • 2013-11-13
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2017-05-28
相关资源
最近更新 更多