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

时间:2015-12-17 12:15:09

标签: c# .net interface abstraction

这样的问题的答案:List<T> or IList<T>似乎总是同意返回一个接口比返回一个集合的具体实现更好。但我正在努力解决这个问题。实例化接口是不可能的,因此如果您的方法返回一个接口,它实际上仍然返回一个特定的实现。我通过编写两个小方法来尝试一下这个:

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(强调我的)的接受答案:

  

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

     

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

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

TLDR:

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

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

我可能已经过度思考了这个问题,但我没有得到普遍的共识,即返回一个接口比返回一个实现更受欢迎。

5 个答案:

答案 0 :(得分:107)

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

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

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

答案 1 :(得分:31)

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

  

IList是ICollection接口的后代,是基础   所有非通用列表的接口。 IList实现属于   三类:只读,固定大小和可变大小。一个   只读IList无法修改。固定大小的IList不允许   添加或删除元素,但它允许修改   现有要素。可变大小的IList允许添加,删除,   和修改元素。

您可以查看IList.IsFixedSizeIList.IsReadOnly并使用它做您想做的事。

我认为IList是一个胖接口的示例,它应该被拆分为多个较小的接口,并且当您将数组作为IList返回时,它也会违反Liskov substitution principle。< / p>

Read more如果你想做出关于返回接口的决定

<强>更新

挖掘更多内容,我发现IList<T>未实现IListIsReadOnly可通过基接口ICollection<T>访问,但IsFixedSize没有IList<T> 1}}。详细了解why generic IList<> does not inherit non-generic IList?

答案 2 :(得分:12)

与所有&#34;界面与实施&#34;问题,你必须意识到暴露公共成员意味着什么:它定义了这个类的公共API。

如果您将List<T>公开为成员(字段,属性,方法......),则告诉该成员的使用者:通过访问此方法获得的类型是一个List<T>,或者从中得到的东西。

现在,如果您公开界面,则隐藏&#34;实施细节&#34;您的班级使用具体类型。当然,您无法实例化IList<T>,但您可以使用Collection<T>List<T>,其派生或您自己的类型IList<T>

实际问题是&#34;为什么Array实施IList<T>&#34; &#34 ;为什么IList<T>接口有这么多成员&#34;

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

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

答案 3 :(得分:8)

请注意脱离背景的一揽子报价。

  

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

如果它在SOLID principles的上下文中使用,则该引用才有意义。有5条原则,但为了讨论的目的,我们只谈谈最后的3条。

依赖性倒置原则

  

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

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

  

依赖于接口(抽象)。不依赖于具体的实现(具体结果)。

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

利斯科夫替代原则

  

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

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

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

界面隔离原则

  

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

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

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

答案 4 :(得分:4)

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

使用界面的有效理由可能是:

  1. 您不知道返回接口的方法的实现是什么样的,并且随着时间的推移可能会有很多。可能是其他人从其他公司写的。所以你只想就必需品达成一致,并让他们了解如何实现这些功能。

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

  3. 有人可能会说1和2基本上是相同的原因。它们是两种不同的场景,最终会产生相同的需求。

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