16

2011 年 1 月 6 日更新:

信不信由你,我继续将此接口合并到我已经启动的开源库 Tao.NET中。我写了一篇博客文章来解释这个库的IArray<T>接口,它不仅解决了我最初在这个问题中提出的问题(一年前?!),而且还提供了一个协变索引接口,这是 BCL 中非常缺乏的(在我看来)。


问题(简而言之):

我问为什么 .NET 有IList<T>, 它实现ICollection<T>并因此提供了修改列表的方法(Add,Remove等),但不提供任何中间接口,例如在IArray<T>不修改任何列表的情况下通过索引提供随机访问。


编辑 2010 年 1 月 21 日下午 2:22 EST:

在对 Jon Skeet 的原始答案的评论中(他在其中质疑人们多久需要一次合同,例如IArray<T>),我提到类的KeysValues属性分别是和,Jon 回答说:SortedList<TKey, TValues>IList<TKey>IList<Value>

但在这种情况下,它被声明为 IList 并且您知道只使用索引器。. . . 我同意,它并不是非常优雅——但它实际上并没有给我带来任何痛苦。

这是合理的,但我会回答说它不会给你带来任何痛苦,因为你只是知道你做不到。但是您知道的原因并不是从代码中可以清楚地看出;就是你有SortedList<TKey, TValue>上课的经验。

如果我这样做,Visual Studio 不会给我任何警告:

SortedList<string, int> mySortedList = new SortedList<string, int>();

// ...

IList<string> keys = mySortedList.Keys;
keys.Add("newkey");

这是合法的,根据IList<string>. 但我们都知道,它会导致异常。

纪尧姆也提出了一个恰当的观点:

好吧,接口并不完美,但开发人员可以在调用 Add/Remove/Set 之前检查 IsReadOnly 属性...

同样,这是合理的,但是:这不是让你觉得有点迂回吗?

假设我定义了一个接口如下:

public interface ICanWalkAndRun {
    bool IsCapableOfRunning { get; }

    void Walk();
    void Run();
}

现在,还假设我将实现这个接口作为一种常见的做法,但仅限于它的Walk方法;在许多情况下,我会选择设置IsCapableOfRunningfalse抛出NotSupportedException...Run

然后我可能有一些看起来像这样的代码:

var walkerRunners = new Dictionary<string, ICanWalkAndRun>();

// ...

ICanWalkAndRun walkerRunner = walkerRunners["somekey"];

if (walkerRunner.IsCapableOfRunning) {
    walkerRunner.Run();
} else {
    walkerRunner.Walk();
}

我是疯了,还是这种破坏了名为 的接口的目的ICanWalkAndRun


原帖

我发现在 .NET 中,当我设计一个具有通过索引(或返回索引集合的方法等)提供随机访问的集合属性的类时,不应该或不能通过添加 /删除 items,如果我想在 OOP 方面“做正确的事”并提供一个接口以便我可以在不破坏 API 的情况下更改内部实现,我必须使用IList<T>.

看来,标准方法是使用一些IList<T>明确定义方法的实现AddInsert等等 - 通常通过执行以下操作:

private List<T> _items;
public IList<T> Items {
    get { return _items.AsReadOnly(); }
}

但我有点讨厌这个。如果另一个开发人员正在使用我的类,并且我的类具有 type 的属性IList<T>,并且接口的整个想法是:“这些是一些可用的属性和方法”,我为什么要抛出一个NotSupportedException(或任何情况下)当他/她试图做一些根据界面应该完全合法的事情?

我觉得实现一个界面并明确定义它的一些成员就像开一家餐馆并将一些项目放在菜单上——也许在菜单的一些不起眼、容易错过的部分,但仍然菜单上——根本不可用。

似乎应该有一个IArray<T>接口,通过索引提供非常基本的随机访问,但不添加/删除,如下所示:

public interface IArray<T> {
    int Length { get; }
    T this[int index] { get; }
}

然后IList<T>可以实现ICollection<T>IArray<T>添加它的IndexOf,InsertRemoveAt方法。

当然,我总是可以只编写这个接口并自己使用它,但这对所有未实现它的预先存在的 .NET 类没有帮助。(是的,我知道我可以编写一个包装器来接收任何内容IList<T>并吐出一个IArray<T>, 但是......认真吗?)

有没有人知道为什么接口System.Collections.Generic是这样设计的?我错过了什么吗?是否有一个令人信服的论点反对我所说的关于明确定义成员的方法的问题IList<T>

我并不想显得自大,好像我比设计 .NET 类和接口的人更了解;这对我来说没有意义。但我已经准备好承认有很多我可能没有考虑到。

4

3 回答 3

6

设计问题并不总是非黑即白。

一方面是针对每种情况的精确接口,这使得实际实现接口的整个过程非常痛苦。

另一个是少数(呃)多用途接口,它们并不总是被实现者完全支持,但使许多事情变得更容易,例如传递相似但不会获得“精确接口”设计中分配的相同接口的实例.

所以 BCL 的设计者选择了第二种方式。有时我也希望接口少一点多用途,特别是对于集合和 C#4 接口协变/逆变特性(不能应用于大多数集合接口,除了 IEnumerable<> 因为它们都包含 co-以及逆变部分)。

此外,令人遗憾的是,诸如字符串和原始类型之类的基类不支持某些接口,例如 ICharStream(用于字符串,可用于正则表达式等,以允许使用除string实例之外的其他源进行模式匹配)或 IArithmetic 用于数字基元,以便通用数学成为可能。但我想所有框架都有一些弱点。

于 2010-01-21T15:49:07.130 回答
2

好吧,接口并不完美,但开发人员可以在调用 Add/Remove/Set 之前检查IsReadOnly属性...

于 2010-01-21T15:56:40.217 回答
0

您可以获得的最接近的是返回IEnumerable< T >然后客户端(又名调用者)可以自己调用 .ToArray() 。

于 2010-01-21T15:38:12.510 回答