通用接口继承(设计问题)

3
我有两个接口;ISet 和 ISet。
两者都实现了Add、Remove和Clear方法,但参数类型不同;一个是object,另一个是T。
现在我有两个选择。让ISet从ISet继承,或者让它们彼此独立。
让ISet从ISet继承的一个很大的好处当然是我可以在任何需要ISet的地方使用ISet。但这样做,我也必须对带有T参数的成员添加“new”修饰符。我不太喜欢“new”修饰符,但也许在这种情况下这是更好的选择?
拥有一个既不是ISet也不是ISet的ISet感觉很奇怪。那是逻辑上的假设。我不能确切地说我为什么不喜欢“new”修饰符。就像“goto”关键字一样,我只是尽量不使用它。
你认为我应该选择继承还是不继承?
在.NET中,我们有ICollection和ICollection,它们不继承。IList和IList也是如此。但IEnumerable从IEnumerable继承。
我知道ISet接口已经存在(这只是一个例子)。

你说实现了相同的 Add、Remove、Clear...,但是你试图使用哪些不同的 "T" 类型作为参数。它们有什么不同之处,有多少种不同的类型(例如)? - DRapp
1
.NET中的非泛型集合类型已经被弃用或不再推荐使用;你确定你真的需要非泛型ISet接口吗?如果你需要一个可以容纳任何对象的集合,你可以始终使用ISet<object>。顺便说一下,在.NET中已经有了一个泛型的ISet<T>接口。 - dtb
1
如果ISet/ISet<T>只是一个例子,您应该在实际接口上提供更多细节。如果您正在设计集合类型,则已经发现了.NET框架如何解决其集合类型的问题。(IEnumerable<T>扩展IEnumerable是出于历史原因。)如果您不是设计集合类型,则情况可能完全不同;您可以在.NET框架中找到许多示例,其中泛型类型扩展了相同名称的非泛型类型。 - dtb
如果ISet<T>继承自ISet,当我将一个ISet<Giraffe>强制转换为ISet并向其中添加一个new Rhinoceros()时,您希望发生什么? - AakashM
AakashM:它好像想要崩溃。 - Andreas Zita
dtb:我需要非泛型版本来保存任何类型的集合实例,并且让我枚举和处理内容而不知道具体的内容类型。 - Andreas Zita
7个回答

4

我在其他情况下也遇到过这个问题。通常我会通过让 ISet<T> 拥有它自己版本的方法,而让 ISet 拥有其自己的除了对象之外的部分来解决它。就像你所提到的那样。此外,我还创建了一个名为 BaseSet<T> 的抽象类,它实现了 ISet<T>ISet 两者,如下所示:

public abstract class BaseSet<T> : ISet<T>, ISet {
  public abstract void Add<T>(T item);

  void ISet.Add(object item) {
     this.Add((T)item);
  } 
}

那么,只需要从 BaseSet<T> 继承并实现 ISet<T> 接口就可以应对最常见的情况,但如果 BaseSet<T> 方便类不符合其需求,他们仍然可以完全实现 ISet<T>ISet。这也消除了使用 new 关键字的需要。


我喜欢使用声明式接口实现来隐藏覆盖。 - Ian
3
如果您这样做,您可能希望匹配List<T>IList的实现:当类型不正确时,List.Add(object)会抛出异常,而这种实现会添加null。 - David Yaw
1
@DavidYaw - 我同意。 这不是一个完整的实现,只是展示如何组织类。 我更新了使用显式的转换运算符,这将强制引发异常。 - Charles Lambert

1

最好还是遵循一般的.NET范例,但你也可以考虑采用另一种方式来实现。

interface ISet : ISet<object>
{ }

interface ISet<T>
{
    void Add<T>(T item);
    void Clear();
    void Remove<T>(T item);
}

如果你正在实现一个集合,那么在.NET中是否有替代方案可以使用?


那不行。我需要ISet<T>成为ISet,而不是反过来。我需要一个共同点。 - Andreas Zita

1
为什么不直接使用ISet,并提供Object作为T,如果您想将其与任何类型一起使用呢?

1
唯一从非泛型形式继承的好处是能够在不必在编译时知道T的情况下对ISet执行某些操作。不能以类型安全的方式向这样的集合添加项目,读取可能有些低效(因为值类型必须被装箱),但像Count这样的方法或属性可能非常有用。
我的建议是您定义多个接口,类似于以下内容(请注意,我正在添加一些您没有的成员,因为您的集合目前仅可写,因此不太有用)
interface ICountable { int Count {get;} } interface IClearable { int Count {get;} } interface IAppendable { void Add(T item); } interface ICountableEnumerable : IConvertableToEnumerable, ICountable {IEnumerable CopyAsEnumerable();} interface IFetchable { T FetchAndRemove(ref bool wasNonEmpty); } interface ISet : ICountable, IClearable, IAppendable, IFetchable, IConvertableToEnumerable;

这种接口分离使得协变和逆变的使用变得有意义。


很多接口,但我喜欢使用协变和逆变的想法。 - Andreas Zita

0

为什么IEnumerable<T>IEnumerable继承,这有一个非常简单的原因。原因在于 foreach语句只适用于集合类型。 任何集合类型都应该实现非泛型接口IEnumerable(即使数组也实现了它)。以下是foreach背后发生的事情:

E enumerator = (collection).GetEnumerator(); // non-generic interface
try {
   while (enumerator.MoveNext()) { // enumerator also non-generic
      ElementType element = (ElementType)enumerator.Current;
      statement;
   }
}
finally {
   IDisposable disposable = enumerator as System.IDisposable;
   if (disposable != null) disposable.Dispose();
}

所以我们有通用接口,继承非通用接口,以允许在foreach语句中使用通用集合。另一种选择是更改编译器:)

没有核心的.NET功能依赖于IList接口。因此,我们有两个独立的接口-通用和非通用。

在您的情况下,我不认为创建非通用接口ISet有任何理由。如果您想要一个方法,该方法接受不同类型参数的通用ISet,那么只需创建接受通用ISet的通用方法:

public void Foo<T>(ISet<T> set)
{
    set.Bar();
}

0

我认为,如果你的问题领域需要一个通用的解决方案,那么从非通用的派生并不一定会给你带来什么好处。在你的类型安全版本中允许对象是否存在潜在的副作用?如果有的话,那么似乎至少是泛型设计旨在克服的问题之一...


0
为什么你有一个非泛型的ISet?.NET支持泛型,几乎没有理由不使用它们。我相信所有.NET语言都完全支持泛型。
是的,.NET库具有IList和IList。但是,我认为这是历史原因,来自.NET没有泛型的时期。如果从一开始就实现了泛型支持,我认为只会存在IList,而不是IList。

如果我没有非泛型的ISet,我将无法在不考虑内容实际类型的情况下使用集合。而且我不能使用“out T”,因为我既有输出(枚举),也有输入(Add(T item))。 - Andreas Zita

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接