关于c#:返回IList< T>

Is returning IList<T> worse than returning T[] or List<T>?

对于如下问题的答案: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)有时,如果库的用户不使用强制转换,则当您更改方法时,他们的代码可能会中断,即使方法仍然很好。

我可能是在过度考虑这个问题,但我不认为返回接口比返回实现更可取。


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

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

最后两个选项假定,该方法不能作为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.IsFixedSizeIList.IsReadOnly,并根据您的需要使用它。

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

如果要决定返回接口,请阅读更多信息

更新

进一步挖掘发现,IList没有实现IListIsReadOnly可以通过基础接口ICollection访问,但IList没有IsFixedSize。了解更多有关generic ilist<>为何不继承非generic ilist的信息?


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

如果将List公开为成员(字段、属性、方法,…),则会告诉该成员的使用者:通过访问此方法获得的类型是List或派生的类型。

现在,如果您公开了一个接口,您可以使用具体的类型隐藏类的"实现细节"。当然,您不能实例化IList,但是您可以使用CollectionList、其派生或您自己实现IList的类型。

实际问题是"为什么Array实现IList",或者"为什么IList接口的成员这么多"。

它还取决于您希望该成员的消费者做什么。如果您通过您的Expose...成员实际返回一个内部成员,那么您无论如何都要返回一个new List(internalMember),否则消费者可以尝试将其强制转换为IList并通过它修改您的内部成员。

如果您只是希望消费者迭代结果,那么应该公开IEnumerableIReadOnlyCollection


注意断章取义的总括引语。

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+中还有很多选择。


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

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

  • 您不知道返回接口的方法的实现是什么样子的,随着时间的推移,可能会开发出许多方法。可能是其他人写的,来自其他公司。因此,您只想就基本需求达成一致,并让他们决定如何实现该功能。

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

  • 可以说1和2基本上是相同的原因。它们是最终导致相同需求的两种不同场景。

    "这是一份合同"。如果合同是与您自己签订的,并且您的应用程序在功能和时间上都是关闭的,那么使用界面通常是没有意义的。