返回IList <T>比返回T 或List <T>更糟?

这样的问题的答案: List <T>或IList <T>似乎总是认为返回一个接口比返回一个集合的具体实现更好。 但是我正在为此而苦苦挣扎。 实例化一个接口是不可能的,所以如果你的方法正在返回一个接口,它实际上还是返回一个特定的实现。 我正在通过编写两个小方法来进行一些尝试:

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

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

 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>给我一个运行时错误,当我尝试添加一些东西时。 因此,不知道我的方法中发生了什么并且不得不为其添加值的人,我们不得不先将我的IList复制到List ,以便能够添加值而不会冒错误。 当然,他们可以做一个types检查,看看他们是处理一个ListArray ,但如果他们不这样做,他们想要添加项目的集合, 他们没有其他select将IList复制到List ,即使它已经是一个List 。 数组不应该暴露为IList

我的另一个问题是基于对相关问题的接受答案(我的重点):

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

如果你只是在内部使用它,你可能不会太在意,而使用List也可以。

想象一下,实际上有人使用我的IList<T>他们从我的ExposeListIlist()方法获得,就像添加/删除值一样。 一切工作正常。 但现在像回答build议,因为返回一个接口更灵活,我返回一个数组而不是一个列表(在我这边没问题!),然后他们在一个治疗…

TLDR:

1)暴露一个接口导致不必要的强制转换? 这不重要吗?

2)有时如果库的用户不使用强制转换,即使方法保持完好,他们的代码可能会在您更改方法时中断。

我可能会推翻这一点,但我没有得到普遍的一致意见,认为返回一个接口比返回一个实现更受欢迎。

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

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

最后两个选项假定该方法不能像IList<T>实现那样返回数组。

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

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

你可以检查IList.IsFixedSizeIList.IsReadOnly并做它你想要的。

我认为IList是一个胖接口的例子,它应该被拆分成多个较小的接口,并且当您将一个数组作为IList返回时,它也违反了Liskovreplace原则 。

如果你想做出关于返回界面的决定,请阅读更多内容


UPDATE

挖掘更多,我发现IList<T>不实施IListIsReadOnly可通过基本接口ICollection<T>但没有IList<T> IsFixedSize 。 阅读更多关于为什么通用IList <>不会inheritance非通用IList?

就像所有的“接口与实现”问题一样,你必须认识到公开成员意味着什么:它定义了这个类的公共API。

如果公开List<T>作为成员(field,property,method,…),则告诉该成员的使用者:通过访问此方法获得的types List<T> ,或者派生。

现在,如果你公开一个接口,你可以使用一个具体的types来隐藏你的类的“实现细节”。 当然,你不能实例化IList<T> ,但是你可以使用一个Collection<T>List<T> ,它的派生或者你自己实现IList<T>的types。

实际的问题是“为什么Array实现IList<T>“为什么IList<T>接口有这么多成员”

这也取决于你希望这个成员的消费者做什么。 如果实际上通过Expose...成员返回内部成员,则无论如何您都要返回一个new List<T>(internalMember) ,否则消费者可以尝试将它们投射到IList<T>并修改您的内部通过这个成员。

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

小心引用上下文中的粗体引号。

返回一个接口比返回一个具体的实现要好

这个引用只有在SOLID原则的上下文中才有意义。 有5个原则,但为了讨论的目的,我们只谈最后3个。

依赖倒置原则

应该“依靠抽象。 不要依赖结核。“

在我看来,这个原则是最难理解的。 但如果仔细看看报价,看起来很像你原来的报价。

取决于接口(抽象)。 不要依赖于具体的实现(结核)。

这仍然有点令人困惑,但如果我们开始一起应用其他原则,它开始变得更有意义。

Liskov替代原则

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

正如你所指出的那样,返回一个Array与返回一个List<T>显然是不同的行为,即使它们都实现了IList<T> 。 这当然是对LSP的违反。

要认识到的重要一点是接口是关于消费者的 。 如果你正在返回一个接口,你已经创build了一个合约,该接口上的任何方法或属性都可以被使用而不改变程序的行为。

界面隔离原则

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

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

不幸的是,.NET框架中的接口(特别是早期版本)并不总是理想的客户端特定接口。 尽pipe@Dennis在他的回答中指出,.NET 4.5+中有更多的select。

返回一个接口不一定比返回一个集合的具体实现更好。 你应该总是有一个很好的理由来使用一个接口,而不是一个具体的types。 在你的例子中,这似乎毫无意义。

使用接口的有效原因可能是:

  1. 你不知道返回接口方法的实现是什么样的,可能有很多,随着时间的推移发展。 可能是其他人从其他公司那里写的。 所以,你只是想就最基本的必需品达成一致,让他们知道如何实现这些function。

  2. 您希望以types安全的方式公开与您的类层次结构无关的一些常见function。 应提供相同方法的不同基本types的对象将实现您的接口。

有人可能会说,1和2基本上是一样的原因。 他们是两种不同的情况,最终导致相同的需求。

“这是一个合同”。 如果合同与您自己并且您的应用程序在function和时间上都是closures的,那么使用界面往往没有意义。