声明 ID是类或接口的可操作的
本文关键字:接口 可操作 ID 声明 | 更新日期: 2023-09-27 18:33:32
从以下情况开始:
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/use 块,你不需要转换为 IDisposable 进行清理。这两种方式可能都可能有更多的优点/缺点......为什么你会建议使用一种首选方式而不是另一种方式?
遵循 SOLID 的 Inteface 隔离原则,如果您将 IDisposable 添加到接口中,您将为不感兴趣的客户端提供方法,因此您应该将其添加到 A 中。
除此之外,接口永远不是一次性的,因为可处置性与接口的具体实现有关,而不是与接口本身相关。
任何接口都可以在有或没有需要释放的元素的情况下实现。
using(){}
模式应用于所有接口,最好ISample
从IDisposable
派生,因为设计接口时的经验法则是倾向于"易用性">而不是"易于实现"。
就个人而言,如果所有ISample
都应该是一次性的,我会把它放在界面上,如果只有一些,我只会把它放在它应该存在的类上。
听起来你有后一种情况。
至少有一些实现可能会实现IDisposable
,并且至少在某些情况下,对实例的最后幸存引用将存储在类型为 IFoo
的变量或字段中,则接口IFoo
可能应该实现IDisposable
。 如果任何实现可能实现IDisposable
并且实例将通过工厂接口创建,它几乎肯定应该实现IDisposable
(就像IEnumerator<T>
实例一样,在许多情况下是通过工厂接口IEnumerable<T>
创建的(。
比较IEnumerable<T>
和IEnumerator<T>
是有启发性的。 一些实现IEnumerable<T>
的类型也实现IDisposable
,但是创建此类类型实例的代码将知道它们是什么,知道它们需要处置,并将它们用作它们的特定类型。 这样的实例可以作为类型IEnumerable<T>
传递给其他例程,并且这些其他例程不知道对象最终需要释放,但在大多数情况下,这些其他例程不会是最后一个保存对对象的引用的例程。 相比之下,IEnumerator<T>
的实例通常是由代码创建、使用并最终放弃的,这些代码对这些实例的基础类型一无所知,除了它们由 IEnumerable<T>
返回的事实。 如果IEnumerator<T>
的 IDisposable.Dispose
方法在被放弃之前未调用它们,则返回实现IEnumerable<T>.GetEnumerator()
的某些实现将泄漏资源,并且大多数接受 IEnumerable<T>
类型参数的代码将无法知道此类类型是否可以传递给它。 尽管IEnumerable<T>
可以包含一个属性EnumeratorTypeNeedsDisposal
来指示是否必须释放返回的IEnumerator<T>
,或者只是要求调用GetEnumerator()
例程检查返回对象的类型以查看它是否实现IDisposable
,但无条件调用可能不执行任何操作的Dispose
方法更快、更容易, 而不是确定Dispose
是否必要,并且仅在必要时才调用它。
IDispoable 是一个非常常见的接口,从它继承你的接口没有坏处。因此,您将避免代码中的类型检查,而代价是在某些 ISample 实现中实现无操作。因此,从这个角度来看,您的第二选择可能会更好。
我开始认为将IDisposable
放在接口上会导致一些问题。这意味着实现该接口的所有对象的生存期可以安全地同步结束。也就是说,它允许任何人编写这样的代码,并要求所有实现都支持IDisposable
:
using (ISample myInstance = GetISampleInstance())
{
myInstance.DoSomething();
}
只有访问具体类型的代码才能知道控制对象生存期的正确方法。例如,一个类型可能首先不需要处置,它可能支持 IDisposable
,或者在你使用它之后可能需要awaiting
一些异步清理过程(例如,类似于这里的选项 2(。
接口作者无法预测实现类的所有可能的未来生存期/范围管理需求。接口的目的是允许对象公开一些 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; }
}
在上面的代码示例中,我演示了三种类型的生存期管理方案,并添加了你提供的两种方案。
-
SampleA
IDisposable
具有同步using () {}
支持。 -
SampleB
使用纯垃圾回收(它不消耗任何资源(。 -
SampleC
使用阻止其同步释放的资源,并在其生存期结束时需要await
(以便它可以通知生存期管理代码它已完成资源消耗并冒泡任何异步遇到的异常(。
通过将生存期管理与其他接口分开,您可以防止开发人员错误(例如,意外调用Dispose()
(,并更清晰地支持未来意外的生存期/范围管理模式。
就个人而言,我会选择 1,除非您为两个举一个具体的例子。两个很好的例子是 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()
只是在实现中返回,也会有显著的性能损失。
粘贴代码以回答此处的注释之一,更易于阅读。