使用IEnumerable相较于数组有哪些优势?

4

在我在Code Review.stackexchange上提出问题后,我得到了使用以下代码片段的建议。我注意到在传输过程中,字符串数组Lines被设置为IEnumerable。

在浏览IEnumerable函数一段时间后,我没有发现任何表明性能改进的东西。那么这只是为了可读性吗?或者使用IEnumerable是否真的有性能差异或一般优势,而不是使用数组?

private void ProcessSingleItem(String fileName, String oldId, String newId)
{
    string[] lines = File.ReadAllLines(fileName);

    File.WriteAllText(fileName, ProcessLines(lines, oldId, newId));
}  

private String ProcessLines(IEnumerable<String> lines, String oldId, String newId)
{
    StringBuilder sb = new StringBuilder(2048);
    foreach (String line in lines)
    {
        sb.AppendLine(line.Replace(oldId, newId));
    }
    return sb.ToString();
}  

这取决于IEnumerable<T>在性能方面的实现方式。 - Daniel A. White
@DanielA.White 你所说的“implemented”具体指什么? - MX D
2
如果你编写代码以接受 IEnumerable<T>,那么只要它实现了 IEnumerable<T>,你就不必关心它是 ArrayList<T> 还是其他任何对象。这对于想要传递 Where 结果的情况非常有用。换句话说,对我来说,这更多的是关于灵活性而不是性能。 - crashmstr
2
这不是性能问题,而是明智的做法是将方法接受一个IEnumerable参数,因为您可以传递实现IEnumerable接口的任何类型,如果参数是string[] lines,那么您只能传递字符串数组。 - Ben Robinson
1
你的问题暗示着在做出选择时只有两个值得考虑的因素:性能和可读性。尽管这些因素确实很重要,但还有许多其他问题需要考虑。 - Eric Lippert
显示剩余5条评论
5个回答

15
到目前为止,所有的回答都说接受更通用的类型可以使您的帮助方法更有用。这是正确的。但是,还有其他考虑因素。
  • 优点:使用序列而不是数组向调用您代码的开发人员传达"我不会改变您传递给我的对象"。当我调用一个接受数组的方法时,我怎么知道它不会改变数组?

  • 缺点:使用更通用的类型意味着您的方法必须对更通用类型的任何实例都正确。你怎么知道它是正确的?测试。因此,使用更通用的类型可能意味着更大的测试负担。如果您使用数组,则只需考虑一些情况:空数组、协变数组等。如果您使用序列,则需要测试的情况更多。

  • 您提到了性能。请记住,根据感觉做微观决策而不是经验数据是获得良好性能的可怕方式。相反,设置性能目标,根据该目标衡量进展,并使用分析器查找最慢的内容;首先解决那些问题。对于数组上的 foreach 循环将编译成等效的 for 循环;在 IEnumerable 上的代码更为复杂,可能会慢几微秒 。您的应用程序是否在市场上的成功或失败将取决于这些微秒?如果是这样,那么 仔细地 测量性能。如果不是,请按照自己喜欢的方式编写代码,如果引入了导致你不再达到目标的回归,则自动化测试会告诉你问题所在。您正在运行自动化性能测试,对吗?如果您非常关心性能,那么您应该这样做。


我经常运行性能测试,尽管它们有些受限(因为它是在Unity引擎上运行)。现在只是试图改善性能瓶颈。 - MX D
@MXD:听起来你正在走向良好的性能之路! - Eric Lippert
既然你提到了Unity,这里有一些需要考虑的事情:当使用foreach循环迭代时,IEnumerable<T>T[]都会分配到托管堆上。通常最好使用带有数组的for循环或特定集合类型的foreach循环,不是因为一个比另一个更快,而是因为这样做可以减少对Unity陈旧、古老版本的Mono垃圾收集器的压力。在使用托管代码编写游戏时,集合通常是性能问题的主要来源。 - Cole Campbell
2
@ColeCampbell:在数组的情况下,是什么导致了分配?编译器允许将foreach实现为for循环,Microsoft C#编译器也这样做。Unity使用的编译器是否也这样做? - Eric Lippert
@EricLippert:我不确定;事实上,我已经完全忘记了Microsoft C#编译器会这样做。我没有理由相信Mono有任何不同。感谢指出错误! - Cole Campbell

12

数组IEnumerable的一种实现。

在你的方法中仅使用了IEnumerable中定义的成员,因此你可以选择将IEnumerable作为参数类型,并接受最少限制,允许任何IEnumerable实现被提供为参数。

考虑下面的例子:

ProcessLines(GetItems(), ...);

public IEnumerable<string> GetItems()
{
    yield return "ItemAlwaysGetsIncluded";

    if (!once_in_blue_moon)
    {
        yield break;
    }

    yield return "ItemIncludedOnceInABlueMoon";
}

很难确定性能影响,因为 IEnumerable 可以是任何东西。


1
好的,数组实现IEnumerable - crashmstr
@crashmstr 我不知道“is”是否错误,但我同意你的措辞更好。已更新答案。 - C.Evenhuis
好的,new int[]{0} is IEnumerablenew int[]{0} is IEnumerable<int> 都等于 true,所以我认为 is 至少是合理的 - Brian

4
请看迪米特法则。您希望参数尽可能通用,以便它们可以在更多情况下使用。
现在,您可以传递任何实现IEnumerable的集合,而不仅仅是数组。
在这种情况下,这更多是一个设计问题而不是性能问题。返回值则是另一种情况,因为那里肯定有一些性能增益。
在这种情况下,您只是进行迭代,所以IEnumerable就是您需要的全部内容。

尽管您的建议是很好的,符合所谓“迪米特法则”的目标,但这并不是所谓“法则”通常的描述方式。所谓“迪米特法则”通常被认为是一条规则,即如果方法M需要服务C,则取提供服务B的对象A,在M中使用“A.B.C”来获取C是错误的;M应该要么取一个C,要么修改A以直接提供服务C,而不是通过B。在我看来,这个所谓“法则”的版本有点疯狂;您的描述是有道理的。 - Eric Lippert

3

IEnumerable 只提供了最基本的“可迭代”功能。您可以遍历序列,但仅限于此。这有劣势--例如,使用 IEnumerable 计算元素数量或获取第 n 个元素非常低效--但也有优势--例如,IEnumerable 可以是无限序列,比如质数序列。

Array 是一个具有随机访问(即可以索引)的固定大小集合。


1
在上面的代码中,如果您使用了已实例化的数组,则没有任何区别,但如果您使用IEnumerable,则会非常有帮助。
System.IO.StreamReader file = new System.IO.StreamReader("c:\\test.txt");

while((line = file.ReadLine()) != null)
{
   yield return line;
}

file.Close();

在上面的代码中,我们将始终在内存中只有一行,这有助于使用少量内存读取更大的文件。

此外,通常法则是在向函数传递输入参数时应该选择广泛接受的类型,因此由于所有集合和基于集合的接口都实现了IEnumerable,因此最好将参数类型设置为IEnumerable,以便可以传递List或任何其他集合类型。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接