对于如下问题的答案:list或ilist似乎总是同意返回接口比返回集合的具体实现要好。但我正在为此努力。实例化一个接口是不可能的,所以如果您的方法返回一个接口,它实际上仍然返回一个特定的实现。我通过写两个小方法对此做了一些实验:
1 2 3 4 5 6 7 8 9
| public static IList <int> ExposeArrayIList ()
{
return new[] { 1, 2, 3 };
}
public static IList <int> ExposeListIList ()
{
return new List <int> { 1, 2, 3 };
} |
在我的测试程序中使用它们:
1 2 3 4 5 6 7 8 9 10
| static void Main(string[] args)
{
IList<int> arrayIList = ExposeArrayIList();
IList<int> listIList = ExposeListIList();
//Will give a runtime error
arrayIList.Add(10);
//Runs perfectly
listIList.Add(10);
} |
在这两种情况下,当我试图添加一个新值时,编译器都不会给我任何错误,但显然,当我试图向它添加一些东西时,将数组公开为IList的方法会给我一个运行时错误。因此,那些不知道我的方法中发生了什么,并且必须为它添加值的人,被迫首先将我的IList复制到List,以便能够在不冒错误风险的情况下添加值。当然,他们可以做一个类型检查,看看他们是在处理一个List还是一个Array,但如果他们不这样做,他们想添加项目到集合,他们没有其他选择来复制IList到一个List,即使它已经是List。数组应该永远不会暴露为EDOCX1[1]吗?
我的另一个顾虑基于关联问题的公认答案(强调我的问题):
If you are exposing your class through a library that others will use, you generally want to expose it via interfaces rather than concrete implementations. This will help if you decide to change the implementation of your class later to use a different concrete class. In that case the users of your library won't need to update their code since the interface doesn't change.
If you are just using it internally, you may not care so much, and using List may be ok.
假设有人真的使用了我的IList,他们从我的ExposeListIlist()方法得到的,就像这样添加/删除值。一切正常。但现在正如答案所示,因为返回接口更灵活,所以我返回的是数组而不是列表(我这边没问题!)然后他们要请客…
TLDR:
1)暴露接口会导致不必要的强制转换?这不重要吗?
2)有时,如果库的用户不使用强制转换,则当您更改方法时,他们的代码可能会中断,即使方法仍然很好。
我可能是在过度考虑这个问题,但我不认为返回接口比返回实现更可取。
- 然后返回IEnumerable,您就有了编译时安全性。您仍然可以使用通常试图通过将其强制转换为特定类型来优化性能的所有LINQ扩展方法(如ICollection使用Count属性而不是枚举它)。
- 还是第一点的问题?如果我以IEnumerable的形式返回列表,他们必须将其复制到列表中,即使它已经是一个列表。做不必要的复制不是坏事吗?
- 阵列通过"黑客"实现IList。如果要公开返回它的方法,请返回实际实现它的类型。
- 如果您的实例实际上是一个列表,那么强制转换就变得微不足道。但是,您可以调用ToList,当您没有一个列表(但有一个数组,例如)时,这可能是成本密集型的。
- @Himbrombeere的确,这是我的观点,它已经是一个列表了,但是他们必须把它复制到另一个列表中,以确保他们的代码不会被破坏。
- @编解码器所以我标题中的问题是正确的?在这种情况下,实现>IList?
- 不要许下你不能兑现的承诺。当客户端程序员读到你的"我将返回一个列表"合同时,很可能会失望。不是这样的,只是不要这样做。
- 来自msdn:ilist是ICollection接口的后代,是所有非泛型列表的基接口。IList实现分为三类:只读、固定大小和可变大小。无法修改只读IList。固定大小的ilist不允许添加或删除元素,但允许修改现有元素。可变大小的ilist允许添加、删除和修改元素。
- @亚历山德罗德克:如果你想让消费者在你的列表中添加物品,首先不要让它成为IEnumerable。然后可以退回IList或List。
- 从我的观点来看,这取决于:当我创建一个类时,我返回了正在使用的具体类型。如果我创建了一个接口,我会尽可能地保持常规并返回一个接口。
- @那么,微软不创建3个不同的接口,这对Doctor来说不是很糟糕吗?
- 您的客户机不应该强制转换从您的API接收到的对象。您更改实现所导致的任何破坏都是它们的问题。从技术上讲,IList并不保证是可修改的,所以您最好添加一个post条件,即Result>().IsReadOnly == false。
- @李:我可以接受这样的解释,但对我来说,界面的用户应该检查一下才能正确使用界面,这似乎是错误的。我一直都知道接口应该是固定的契约,我想这是个奇怪的例子
- @Alexanderderck是的,我认为应该把它分成更小的接口。但是你可以查一下IList.IsFixedSize和IList.IsReadOnly。
- 如果您不希望客户需要知道您返回的实际类型,那么返回ilist。
- 附带说明:以一种允许调用者修改状态(而对象本身没有注意到)的方式公开对象的部分状态通常是一个坏主意。返回副本或不可修改的视图可以更好地控制对象的状态空间。
也许这不是直接回答您的问题,但在.NET 4.5+中,我更喜欢在设计公共或受保护的API时遵循这些规则:
- 如果只有枚举可用,则返回IEnumerable;
- 如果枚举和项计数都可用,则返回IReadOnlyCollection;
- 如果枚举、项计数和索引访问可用,则返回IReadOnlyList;
- 如果枚举、项计数和修改可用,则返回ICollection;
- 如果枚举、项计数、索引访问和修改可用,则返回IList。
最后两个选项假定,该方法不能作为IList实现返回数组。
- 的确不是一个答案,但是很好的经验法则,对我很有帮助:)我最近一直在努力选择回报什么。干杯!
- 这确实是正确的原则+1。为了完整起见,我将添加IReadOnlyCollection和ICollection,它们用于非索引容器(例如,后者实际上是ef实体子集合导航属性的标准)。
- @伊万斯托夫,你能用这些添加的内容来编辑答案吗?如果我最终得到一个在哪里使用哪个接口的列表就好了
- 不幸的是,ICollection和IList都没有从IReadOnlyCollection继承,IReadOnlyList也没有继承;但是,List和T[]类都继承了。在某些情况下,这可能是使用后者之一的合理理由。
- @超级卫星:没错。不幸的是,从体系结构的角度来看,BCL中的集合是痛苦的。
- 需要注意的是,EDCOX1的2和EDCOX1的3具有与C++ EDCOX1×8的相同的缺陷。有两种可能:要么集合是不可变的,要么只有您访问集合的方式禁止您修改它。你不能区分彼此。
- @安德烈:事实上,有三种可能性。接口可以保证实现可以安全地公开,而不给接收者修改集合的可能性("readonly"),或者可以拒绝提供修改集合的方法,而不保证接收引用的人无法转换为可以修改集合的类型("readable")。
- 当您提到某些可用功能时,您是在讨论具体类型是否支持它们,还是在讨论消费者是否需要它们?例如,如果具体类型是List,但实际上您只是在构建列表以收集一组要枚举的内容,那么您是建议返回IList以完全支持可以使用它进行的操作,还是建议返回IEnumerable,因为它将覆盖您试图公开的内容,并且因为它更容易操作要更改的实现吗?
- @panzercrisis:我会选择第二个选项,当然是IEnumerable。作为一个API开发人员,您完全了解允许的用例。如果只打算枚举方法结果,那么就没有理由公开IList。对结果的处理越少,方法实现就越灵活。例如,公开IEnumerable允许您将实现更改为yield return,IList不允许这样做。
不,因为消费者应该知道iList是什么:
IList is a descendant of the ICollection interface and is the base
interface of all non-generic lists. IList implementations fall into
three categories: read-only, fixed-size, and variable-size. A
read-only IList cannot be modified. A fixed-size IList does not allow
the addition or removal of elements, but it allows the modification of
existing elements. A variable-size IList allows the addition, removal,
and modification of elements.
您可以检查IList.IsFixedSize和IList.IsReadOnly,并根据您的需要使用它。
我认为IList是一个fat接口的例子,它应该被分割成多个较小的接口,并且当您返回一个数组作为IList时,它也违反了liskov替换原则。
如果要决定返回接口,请阅读更多信息
更新
进一步挖掘发现,IList没有实现IList,IsReadOnly可以通过基础接口ICollection访问,但IList没有IsFixedSize。了解更多有关generic ilist<>为何不继承非generic ilist的信息?
- 这些属性是好的,但在我看来,它仍然破坏了接口的目的。对我来说,接口是一个合同,不应该依赖于任何检查或其他
- 如果他们提供了一个具有(非固定大小/非只读)List语义的附加接口,那么对开发人员来说确实会更友好,但对于为什么不包括这些内容,可能有合理的理由……但是,是的,我认为您对包含各种特性的几个接口的想法将是一个更合理的选择;
- @Alexanderderck是的,那很理想。但这里的情况并非如此,所以你必须接受你所拥有的(例如,Stream)。如果愿意,可以将结果包装在自己的接口中。
- Jup,这会更好,但我想这就是我的生活:D这显然不是一个大问题,只是困扰我的一些事情,它使我更难理解使用接口给新程序员(如我)带来的好处。
- IList是一个胖接口,但它是从数组和列表中获得最多的。因此,如果它可以分解为多个接口,那么您可能无法对列表和数组使用某些方法,但只能对其中一个方法使用,即使您要使用的所有方法都受支持(如IList.Count或索引器)。所以在我看来这是个好决定。如果它在极少数情况下导致一个NotSupportedException,在一个数组上使用Add,那么,这就是交易。
- @如果IList被执行为IList : IArray,Timschmelter会有缺点吗?
- @Timschmelter,但它破坏了固体!
- @亚历山德罗德克:它将添加另一个接口IArray,有什么好处?
- @timschmelter数组的特征没有列表的特征?或者数组有一些列表没有的功能?我不确定
- @多巧啊,昨天我在读关于李斯科夫代换原理的文章,这让我思考了一下:谢谢你的回答。
- @亚历山德罗德克:你的意思是说,IList有Add等,而IArray没有?有什么区别?如果您想要提供一个与列表和数组一起工作的方法,那么您必须返回(或接受为参数)IList,问题仍然存在。
- 好吧,这个答案结合丹尼斯的规则当然可以提高对如何处理这种困境的理解。不用说在.NET框架中到处都违反了Liskov替换原则,这不是什么特别值得担心的事情。
- @然后我可以返回一个接口,人们知道他们不能向实现该接口的集合中添加内容。现在他们只能猜测(与IEnumerable相比,它有随机访问的好处)
- @TimschMelter:不幸的是,集合接口缺少一些重要的属性。例如,任何类型的IEnumerable都应该能够毫无困难地回答诸如它是否有限/无限/未知、它的大小是否固定/变量/未知、它是否可变/不可变/只读/未知等问题。对于许多查询,一些实现都不能做比回答"未知"更好的事情,但是能够提出这些问题将使调用者能够针对他们使用的实现能够得到很好的回答的情况进行优化。
与所有"接口与实现"问题一样,您必须认识到公开一个公共成员意味着什么:它定义了这个类的公共API。
如果将List公开为成员(字段、属性、方法,…),则会告诉该成员的使用者:通过访问此方法获得的类型是List或派生的类型。
现在,如果您公开了一个接口,您可以使用具体的类型隐藏类的"实现细节"。当然,您不能实例化IList,但是您可以使用Collection、List、其派生或您自己实现IList的类型。
实际问题是"为什么Array实现IList",或者"为什么IList接口的成员这么多"。
它还取决于您希望该成员的消费者做什么。如果您通过您的Expose...成员实际返回一个内部成员,那么您无论如何都要返回一个new List(internalMember),否则消费者可以尝试将其强制转换为IList并通过它修改您的内部成员。
如果您只是希望消费者迭代结果,那么应该公开IEnumerable或IReadOnlyCollection。
- "实际的问题是"为什么数组实现ilist",或者"为什么ilist接口有这么多成员"。这确实是我一直在努力的事情。
- @Fabjan-IList确实有添加和删除项的方法,这就是为什么数组不适合实现的原因。IsReadOnly属性是一个用来掩盖差异的黑客,当它应该被分成(至少)两个接口时。
- @李:哎呀,你说得对,删除了我不准确的评论。
- @Alexanderderck这是我之前问过的,stackoverflow.com/q/5968708/706456
- @Oleksii确实很相似,但我不认为它是复制品
注意断章取义的总括引语。
Returning an interface is better than returning a concrete implementation
这句话只有在坚实原则的背景下才有意义。有5条原则,但为了讨论这个问题,我们只讨论最后3条。
依赖倒置原则
one should"Depend upon Abstractions. Do not depend upon concretions."
在我看来,这个原则是最难理解的。但如果你仔细看一下报价单,它看起来很像你原来的报价单。
Depend on interfaces (abstractions). Do no depend on concrete implementations (concretions).
这仍然有点令人困惑,但如果我们开始一起应用其他原则,它就会变得更有意义。
里氏代换原则
"objects in a program should be replaceable with instances of their subtypes without altering the correctness of that program."
正如您所指出的,返回Array显然不同于返回List的行为,尽管它们都实现了IList。这当然违反了LSP。
重要的是要认识到接口是关于消费者的。如果您返回一个接口,那么您已经创建了一个契约,可以在不改变程序行为的情况下使用该接口上的任何方法或属性。
界面分离原理
"many client-specific interfaces are better than one general-purpose interface."
如果要返回接口,则应返回实现支持的最特定于客户端的接口。换句话说,如果您不希望客户机调用Add方法,则不应该返回一个带有Add方法的接口。
不幸的是,.NET框架中的接口(尤其是早期版本)并不总是理想的特定于客户端的接口。尽管正如@dennis在回答中指出的那样,.NET 4.5+中还有很多选择。
- 读到里斯科夫的原理,我首先对江户十一〔0〕感到困惑。
- 在特定情况下,是数组违反了LSP。如果返回的是IList,则永远不应返回数组,因为如果发生此冲突。您已经与调用者签订了合同,要求他们返回一个可以添加和删除项目的列表。
返回接口并不一定比返回集合的具体实现要好。您应该总是有充分的理由使用接口而不是具体的类型。在您的示例中,这样做似乎毫无意义。
使用接口的有效原因可能是:
您不知道返回接口的方法的实现是什么样子的,随着时间的推移,可能会开发出许多方法。可能是其他人写的,来自其他公司。因此,您只想就基本需求达成一致,并让他们决定如何实现该功能。
您希望以一种类型安全的方式公开一些独立于类层次结构的公共功能。应提供相同方法的不同基类型的对象将实现您的接口。
可以说1和2基本上是相同的原因。它们是最终导致相同需求的两种不同场景。
"这是一份合同"。如果合同是与您自己签订的,并且您的应用程序在功能和时间上都是关闭的,那么使用界面通常是没有意义的。
- 我不太同意,如果可以返回一个接口,我会返回一个接口,因为这是唯一需要的。如果一个方法需要一个接口,我可以传递一个只实现该接口的类,也可以传递一个实现接口+加载其他东西的类。通过接口,在不重写现有内容的情况下扩展应用程序更容易。
- 开发所有的软件(尤其是你的类和类库)时都要考虑到"如果其他人使用这个类(库)会怎么样?",这真的很有启发性。以及"如果我想向我想要返回的具体类型添加功能,该怎么办?".bcl类型可以密封,因此不能扩展它们。然后,如果您想返回其他内容,就陷入困境,因为您的API承诺公开确切的类型,而不是接口。返回接口几乎总是比返回具体的实现更好。
- 我同意(实际上),我应该添加第三个原因:"将返回对象的功能限制在其本质上,以便向调用者澄清消息"。我也尽可能返回IReadoNLYXX接口,而不是用于组装集合的完整对象。