返回IList <T>比返回T []或List <T>更糟糕吗?

Ale*_*rck 80 .net c# abstraction interface

像这样的问题的答案: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 };
}
Run Code Online (Sandbox Code Playgroud)

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

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);
}
Run Code Online (Sandbox Code Playgroud)

在这两种情况下,当我尝试添加新值时,我的编译器没有给我任何错误,但显然IList<T>当我尝试向其添加内容时,将我的数组公开为暴露的方法会产生运行时错误.因此,那些不知道我的方法中发生了什么,并且必须为其添加值的人,被迫首先将我复制IList到a List以便能够添加值而不会有错误.当然,他们可以做一个类型检查,看看他们是否正在处理a List或者a Array,但是如果他们不这样做,并且他们想要将项目添加到集合中,他们别无选择将其复制IList到a List,即使它已经是一个List.数组永远不会暴露为IList

我的另一个问题是基于相关问题的接受答案(强调我的):

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

如果你只是在内部使用它,你可能不在乎,使用List可能没问题.

想象一下有人实际上使用了我IList<T>从他的ExposeListIlist()方法中获得的,就像添加/删除值一样.一切正常.但现在就像答案所暗示的那样,因为返回一个接口更灵活,我返回一个数组而不是一个List(在我这边没问题!),然后他们正在接受治疗......

TLDR:

1)暴露界面会导致不必要的演员表?这没关系吗?

2)有时,如果库的用户不使用强制转换,那么当您更改方法时,它们的代码可能会中断,即使该方法仍然完好无损.

我可能正在过度思考这个问题,但我并不认为返回接口比返回实现更受欢迎.

Den*_*nis 107

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

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

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

  • 必须注意的是,`IReadOnlyCollection <T>`和`IReadOnlyList <T>`具有与C++的`const`相同的缺陷.有两种可能性:集合是不可变的,或者只是您访问集合的方式,禁止您修改它.你不能告诉别人. (3认同)
  • 确实不是答案,但良好的经验法则对我很有帮助:)我一直在努力选择最近返回的东西.干杯! (2认同)

Ham*_*jam 31

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

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

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

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

如果您想决定返回界面,请阅读更多内容


UPDATE

挖掘更多,我发现,IList<T>没有实现IListIsReadOnly通过基座接口进行访问ICollection<T>,但没有IsFixedSize进行IList<T>.阅读更多有关通用IList <>不继承非通用IList的原因?

  • 这些属性很好,但在我看来它仍然违背了界面的目的.对我来说,界面是一份合同,不应该依赖任何支票或其他任何东西 (8认同)
  • 那么这个答案加上丹尼斯的规则当然可以提高对如何应对这种困境的理解.毋庸置疑,在.NET框架中,Liskov替换原则在这里和那里都被违反了,并不是特别值得担心的事情. (6认同)

Cod*_*ter 12

与所有"接口与实现"问题一样,您必须意识到暴露公共成员意味着什么:它定义了此类的公共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>代替.

  • "实际的问题是"为什么Array实现了IList <T>",或者"为什么IList <T>界面有这么多成员".",这确实是我一直在努力的事情. (3认同)
  • @Fabjan - `IList <T>`确实有添加和删除项的方法,这就是数组实现的原因.`IsReadOnly`属性是一个黑客,它应该将差异分解为(至少)两个接口. (2认同)

cra*_*mes 8

小心使用脱离上下文的一揽子引用.

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

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

依赖倒置原则

一个应该"取决于抽象.不要依赖于结核."

在我看来,这个原则是最难理解的.但是如果仔细看一下这个引用,它看起来很像你原来的引用.

取决于接口(抽象).不依赖具体实现(具体结果).

这仍然有点混乱,但如果我们开始将其他原则应用于一起,它就会开始变得更有意义.

利斯科夫替代原则

"程序中的对象应该可以替换为其子类型的实例,而不会改变该程序的正确性."

正如你所指出的那样,即使他们都实现了,返回一个Array明显不同于返回a的行为.这肯定违反了LSP.List<T>IList<T>

要认识到的重要一点是接口是关于消费者的.如果您要返回一个接口,那么您已经创建了一个契约,该接口可以使用该接口上的任何方法或属性,而无需更改程序的行为.

界面隔离原理

"许多客户端特定的接口比一个通用接口更好."

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

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


Mar*_*aat 5

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

使用接口的正当理由可能是:

  1. 你不知道返回接口的方法的实现是什么样的,随着时间的推移可能会有很多。可能是其他人写的,来自其他公司。因此,您只想就基本要求达成一致,而让他们决定如何实现该功能。

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

有人可能会争辩说,1 和 2 的原因基本相同。它们是最终导致相同需求的两种不同场景。

“这是合同”。如果合同是与您自己签订的,并且您的应用程序在功能和时间方面都已关闭,那么使用接口通常没有意义。

  • 以_“如果其他人要使用这个类(库)怎么办?”_ 和 _“如果我想添加功能怎么办?到我想返回的具体类型?”_. BCL 类型可以被密封,所以你不能扩展它们。然后,如果您想返回其他内容,您就会陷入困境,因为您的 API 承诺公开该确切类型,而不是接口。返回一个接口几乎总是比返回一个具体的实现要好。 (2认同)