返回IList <T>比返回T []或List <T>差吗?


80

这样的问题的答案:List <T>或IList <T>似乎总是同意,返回接口比返回集合的具体实现更好。但是我为此感到挣扎。实例化接口是不可能的,因此,如果您的方法正在返回接口,则实际上它仍在返回特定的实现。我通过编写2个小方法对此进行了一些试验:

public static IList<int> ExposeArrayIList()
{
    return new[] { 1, 2, 3 };
}

public static IList<int> ExposeListIList()
{
    return new List<int> { 1, 2, 3 };
}

并在我的测试程序中使用它们:

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<T>当我尝试向其中添加某些内容时,将数组公开为的方法会导致运行时错误。因此,不知道我的方法正在发生什么而必须为其添加值的人,被迫首先将my复制IList到a List,以便能够添加值而不会冒错误的风险。当然,他们可以进行类型检查以查看是否正在处理aList或an Array,但如果不这样做,并且他们想将项目添加到集合中,则别无选择将其复制IList到a List,即使它已经是一个List。不应将数组公开为IList吗?

我的另一个关注点是基于链接问题的接受答案(强调我的意思):

如果要通过供他人使用的库公开类,则通常希望通过接口而不是具体的实现公开它。如果您以后决定更改类的实现以使用其他具体类,则这将有所帮助。在这种情况下,库的用户无需更新其代码,因为界面不会更改。

如果您仅在内部使用它,则可能不太在意,并且可以使用List。

想象有人实际上使用了我IList<T>从我的ExposeListIlist()方法中获得的东西,就像添加/删除值一样。一切正常。但是现在就像答案所暗示的那样,因为返回接口更灵活,所以我返回一个数组而不是一个List(对我来说这没问题!),那么他们可以请客了……

TLDR:

1)暴露接口会导致不必要的强制转换?没关系吗?

2)有时,如果库的用户不使用强制转换,则即使您的方法仍然可以正常使用,当您更改方法时,他们的代码也会中断。

我可能对此过于考虑,但是我没有得到普遍的共识,即返回接口比返回实现更可取。


9
然后返回IEnumerable<T>,您将具有编译时安全性。您仍然可以使用所有LINQ扩展方法,这些方法通常通过将其强制转换为特定类型来尝试优化性能(例如ICollection<T>使用Count属性而不是枚举属性)。
蒂姆·施梅尔特

4
数组IList<T>通过“ hack”实现。如果要公开返回它的方法,请返回实际实现它的类型。
CodeCaster

3
只是不要许诺你不能遵守。客户程序员在阅读您的“我将返回清单”合同时很可能会感到失望。并非只是不要这样做。
汉斯·帕桑

7
MSDNIList是ICollection接口的后代,并且是所有非泛型列表的基本接口。IList实现分为三类:只读,固定大小和可变大小。只读的IList无法修改。固定大小的IList不允许添加或删除元素,但允许修改现有元素。大小可变的IList允许添加,删除和修改元素。
Hamid Pourjam 2015年

3
@AlexanderDerck:如果您希望消费者可以将商品添加到列表中,请不要IEnumerable<T>放在第一位。然后可以返回IList<T>List<T>
蒂姆·施梅尔特

Answers:


108

也许这不是直接回答您的问题,但是在.NET 4.5+中,我更喜欢在设计公共或受保护的API时遵循以下规则:

  • IEnumerable<T>如果仅枚举可用,则返回return ;
  • IReadOnlyCollection<T>如果枚举和项目计数均可用,则返回;
  • IReadOnlyList<T>如果枚举,项目计数和索引访问可用,请返回;
  • ICollection<T>如果枚举,项目计数和修改可用,则返回;
  • IList<T>如果有枚举,项目计数,索引访问和修改可用,则返回return 。

最后两个选项假定,该方法不得将数组作为IList<T>实现返回。


2
的确不是一个答案,但是良好的经验法则对我非常有帮助:)我一直在努力选择最近的回报。干杯!
亚历山大·德克

1
这确实是正确的原则+1。为了完整起见,我将添加它们IReadOnlyCollection<T>ICollection<T>并将它们用于不可索引的容器(例如,后者实际上是EF实体子集合导航属性的标准)
Ivan Stoev 2015年

@IvanStoev您可以用添加的内容编辑答案吗?如果我能最终获得在哪里使用什么接口的列表,那就太好了
Alexander Derck 2015年

3
必须指出的是IReadOnlyCollection<T>,它具有IReadOnlyList<T>与C ++相同的缺陷const。有两种可能性:集合是不可变的,或者只有您访问集合的方式才禁止您对其进行修改。而且您无法彼此分辨。
ACH

1
@Panzercrisis:当然,我会选择第二个选项- IEnumerable<T>。作为API开发人员,您对允许的用例有充分的了解。如果仅打算枚举方法结果,则没有理由公开IList<T>。允许的结果越少,方法的实现就越灵活。例如,公开IEnumerable<T>允许您将实现更改为yield returns,IList<T>不允许这样做。
丹尼斯,

31

不,因为消费者应该知道IList到底是什么:

IList是ICollection接口的后代,并且是所有非泛型列表的基本接口。IList实现分为三类:只读,固定大小和可变大小。只读的IList无法修改。固定大小的IList不允许添加或删除元素,但允许修改现有元素。大小可变的IList允许添加,删除和修改元素。

您可以检查IList.IsFixedSizeIList.IsReadOnly做你想做的事情是什么。

我认为这IList是一个胖接口的示例,应该将其拆分为多个较小的接口,并且当您将数组返回为时也违反了Liskov替换原则IList

如果您想决定返回接口的内容,请阅读更多内容


更新

挖掘更多,我发现IList<T>它没有实现IList并且 IsReadOnly可以通过基本接口访问,ICollection<T>但是没有IsFixedSizefor IList<T>。阅读有关为何为什么通用IList <>不继承非通用IList的更多信息?


8
这些属性很好,但是我认为它仍然违反了界面的目的。对我而言,接口是一种合同,不应依赖任何支票或其他任何东西
Alexander Derck

1
如果开发人员提供了一个具有(非固定大小/非只读)List<T>语义的附加接口,对开发人员来说确实会更加友善,但对于为什么不包含此接口,可能有合理的理由。但是,是的,我认为您对涵盖各种功能的多个接口的想法将是一个更合理的选择;
支出者

1
IList<T>是一个胖接口,但这是您可以从数组列表中获得的最多的接口。因此,即使将其拆分为多个接口,即使支持您要使用的所有方法(例如IList.Count或索引器),您也可能无法对列表和数组使用某些方法,而只能对其中之一使用。因此,我认为这是一个不错的决定。如果它NotSupportedException在极少数情况下导致要Add在数组上使用,那是很重要的。
蒂姆·施密特

1
@dotctor真是巧合,我昨天读到有关Liskov替代原理的消息,这实际上让我思考了:)感谢您的回答
Alexander Derck

6
好吧,这个答案与Dennis规则相结合无疑可以增进对如何解决这一难题的理解。不用说,.NET框架在各处都违反了Liskov替换原理,这并不是特别需要担心的事情。
Fabjan

13

与所有“接口与实现”问题一样,您必须意识到公开公共成员的含义:它定义了此类的公共API。

如果你暴露List<T>的成员(字段,属性,方法,...),你就告诉该成员的消费:通过访问该方法获得的类型一个List<T>或派生的的东西。

现在,如果公开一个接口,则使用具体类型隐藏类的“实现细节”。当然,你不能实例化IList<T>,但可以使用Collection<T>List<T>,及其衍生或您自己的类型实施IList<T>

实际的问题是:“为什么不Array执行IList<T>,或“为什么有IList<T>这么多的会员界面”

它还取决于您希望该成员的消费者执行的操作。如果您实际上是通过您的Expose...成员返回内部成员的,那么您还是要返回一个内部成员new List<T>(internalMember),否则消费者可以尝试将其强制转换为IList<T>内部成员,并通过该成员修改您的内部成员。

如果您只是希望消费者对结果进行迭代,请公开IEnumerable<T>IReadOnlyCollection<T>改为。


3
“实际的问题是“为什么Array实现IList <T>”,或者“为什么IList <T>接口有这么多成员”。
亚历山大·德克

2
@Fabjan-IList<T>确实具有添加和删除项目的方法,这就是为什么数组难以实现的原因。该IsReadOnly属性是一种技巧,可以掩盖应该将其拆分为(至少)两个接口时的差异。

@Lee Whoops,您是对的,删除了我不准确的评论
Fabjan 2015年

1
@AlexanderDerck,这是我之前曾问过的问题stackoverflow.com/q/5968708/706456
oleksii 2015年

@oleksii确实很相似,但我不会称其为重复
Alexander Derck

8

小心不要引用上下文的笼统报价。

返回接口比返回具体实现更好

仅当在SOLID原则的上下文中使用此引号时才有意义。有5条原则,但出于讨论目的,我们只讨论最后3条。

依赖倒置原则

一个应该“取决于抽象。不要依赖混凝土。”

我认为,这一原则是最难理解的。但是,如果仔细看一下报价,它看起来很像原始报价。

取决于接口(抽象)。不依赖于具体的实现(命令)。

这仍然有点令人困惑,但是如果我们将其他原则一起应用,就会变得更加有意义。

里斯科夫替代原则

“程序中的对象应该可以用其子类型的实例替换,而不会改变程序的正确性。”

正如您所指出的那样,即使Array返回和List<T>都实现了,也明显不同于返回a的行为IList<T>。这无疑是对LSP的违反。

重要的是要认识到接口与消费者有关。如果要返回接口,则已创建一个合同,该合同可以使用该接口上的任何方法或属性,而无需更改程序的行为。

接口隔离原理

“许多特定于客户端的接口比一个通用接口要好。”

如果要返回接口,则应返回实现支持的最特定于客户端的接口。换句话说,如果您不希望客户端调用该Add方法,则不应返回带有Add方法的接口。

不幸的是,.NET框架(尤其是早期版本)中的接口并不总是理想的客户端特定接口。尽管正如@Dennis在回答中指出的那样,.NET 4.5+中还有很多选择。


首先,让我感到困惑的是,我读了Liskov的原理IList<T>
Alexander Derck

在特定情况下,是违反LSP的阵列。如果返回的是IList,则永远不要返回数组,因为如果发生这种情况。您已经与呼叫者签订了合同,要求他们返回一个可以添加和删除项目的列表。
Craftworkgames 2015年

5

返回接口不一定比返回集合的具体实现更好。您应该总是有充分的理由使用接口而不是具体类型。在您的示例中,这样做似乎毫无意义。

使用接口的有效理由可能是:

  1. 您不知道返回接口的方法的实现将是什么样,并且随着时间的推移可能会有很多实现。可能是其他公司的其他人在写它们。因此,您只想就最基本的需求达成一致,然后由他们决定如何实现功能。

  2. 您想以类型安全的方式公开一些独立于类层次结构的通用功能。应该提供相同方法的不同基本类型的对象将实现您的接口。

有人可能会说1和2基本上是相同的原因。它们是最终导致相同需求的两种不同方案。

“这是合同”。如果与您自己签订合同,并且您的应用程序在功能和时间上都处于关闭状态,那么使用接口通常毫无意义。


1
我真的不同意,如果有可能返回一个接口,我会返回一个接口,因为这是唯一需要的东西。如果一个方法需要一个接口,我可以传递一个仅实现该接口的类,也可以传递一个实现接口+其他负载的类。有了界面,您就可以更轻松地扩展应用程序而无需重写现有的东西
Alexander Derck

2
“如果其他人要使用该类(库)怎么办?”的思路来开发所有软件(尤其是类和类库)则可能会很有启发性“如果要向要返回的具体类型添加功能怎么办?” 。BCL类型可以被密封,因此您不能扩展它们。然后,如果您想返回其他内容,就会陷入困境,因为您的API承诺公开确切的类型,而不是接口。返回接口几乎总是比返回具体实现更好。
CodeCaster

1
我同意(真的),我应该添加第三个原因:“将返回的对象的功能限制在其本质上,以便向调用者阐明消息”。我也尽可能返回IReadOnlyXxx接口,而不是返回用于组装集合的完整对象。
马丁·马特
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.