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

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

此类问题的答案:List<T> or IList<T> 似乎总是同意 return 接口优于 return 集合的具体实现。但我正在努力解决这个问题。实例化一个接口是不可能的,所以如果你的方法是 returning 一个接口,它实际上仍然是 returning 一个特定的实现。我通过编写 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> 的方法会出现运行时错误它。 因此,不知道我的方法中发生了什么并且必须向其添加值的人被迫首先将我的 IList 复制到 List 以便能够添加值而不会出错。当然,他们可以进行类型检查,看看他们处理的是 List 还是 Array,但如果他们不这样做,他们想向集合中添加项目 他们别无选择,无法将 IList 复制到 List,即使它已经是 List。数组是否应该永远不公开为 IList?

我的另一个问题是基于 linked question(强调我的)的公认答案:

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<T> 他们从我的 ExposeListIlist() 方法中得到的 add/remove 值。一切正常。但是现在就像答案所暗示的那样,因为 return 接口更灵活,我 return 一个数组而不是一个列表(我这边没问题!),然后他们就可以请客了.. .

TLDR:

1) 暴露接口会导致不必要的转换?这不重要吗?

2) 有时,如果库的用户不使用强制转换,他们的代码可能会在您更改方法时中断,即使该方法仍然完美无缺。

我可能想多了,但我没有得到普遍的共识,即 return 接口优于 return 实现。

不,因为消费者应该知道 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 是胖接口的一个例子,它应该被拆分成多个更小的接口,当你 return 数组作为 IList.

如果您想决定 return 接口


更新

挖掘更多,我发现 IList<T> 没有实现 IList 并且 IsReadOnly 可以通过基本接口访问 ICollection<T> but there is no IsFixedSize for IList<T>. Read more about why generic IList<> does not inherit non-generic IList?

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

  • 做returnIEnumerable<T>,如果只有枚举可用;
  • 如果枚举和项目计数都可用,则执行returnIReadOnlyCollection<T>
  • 做return IReadOnlyList<T>,如果枚举、项目计数和索引访问可用;
  • 做return ICollection<T>如果枚举、项数和修改可用;
  • 做returnIList<T>,如果枚举,项目计数,索引访问和修改可用。

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

与所有 "interface versus implementation" 问题一样,您必须了解公开 public 成员的含义:它定义了此 [=49] 的 public API =].

如果你暴露一个List<T>作为成员(字段,属性,方法,...),你告诉那个成员的消费者:通过访问这个方法获得的类型一个List<T>,或者是它的派生词。

现在,如果您公开一个接口,则可以使用具体类型隐藏 class 的 "implementation detail"。当然你不能实例化 IList<T>,但你可以使用 Collection<T>List<T>、它们的派生或你自己的类型实现 IList<T>.

实际问题是"Why does Array implement IList<T>",或者"Why has the IList<T> interface so many members".

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

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

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

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

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

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

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

"It's a contract"。如果合同是与您自己签订的,并且您的应用程序在功能和时间上都是封闭的,那么使用接口通常是没有意义的。

小心断章取意的引用。

Returning an interface is better than returning a concrete implementation

这句话只有在 SOLID principles 的上下文中使用才有意义。有 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.”

正如您所指出的,returning Array 与 returning List<T> 的行为明显不同,即使它们都实现了 IList<T>。这肯定违反了 LSP。

要认识到的重要一点是接口是关于消费者的。如果您return正在使用一个接口,那么您已经创建了一个契约,可以在不改变程序行为的情况下使用该接口上的任何方法或属性。

接口隔离原则

“many client-specific interfaces are better than one general-purpose interface.”

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

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