何时使用 T[]、List<T> 和 IEnumerable<T> 中的每个类型?

8

我通常会做这样的事情:

string[] things = arrayReturningMethod();
int index = things.ToList<string>.FindIndex((s) => s.Equals("FOO"));
//do something with index
return things.Distinct(); //which returns an IEnumerable<string>

我发现类型和接口的混合有些令人困惑,会触发我潜在的性能问题感应器(当然,除非证明是正确的)。

这种方法是C#中惯用且正确的方法吗?还是有更好的替代方案来避免来回转换以访问正确的方法以处理数据?

编辑: 这个问题实际上是双重的:

  • 何时适用于直接使用IEnumerable接口、数组或列表(或任何其他IEnumerable实现类型)(在接收参数时)?

  • 你应该自由地在IEnumerable(实现未知)和列表以及IEnumerable和数组之间移动,还是那不符惯例(有更好的方法来做这件事)/ 非高性能(通常不相关,但在某些情况下可能是)/ 简直是丑陋的(难以维护,难以阅读)?


3
你没有使用索引做任何事情 :) - µBio
7个回答

8
关于性能方面...
- 从List转换为T[]需要将所有数据从原始列表复制到新分配的数组中。 - 从T[]转换为List也需要将所有数据从原始列表复制到新分配的List中。 - 从List或T[]转换为IEnumerable涉及到强制类型转换,这需要几个CPU周期。 - 从IEnumerable转换为List涉及到向上转型,这也需要几个CPU周期。 - 从IEnumerable转换为T[]还涉及到向上转型。 - 除非它最初就是T[]或List,否则无法将IEnumerable转换为T[]或List。您可以使用ToArray或ToList函数,但这也会导致进行复制。 - 在T[]中按顺序访问所有值时,在简单的循环中,将被优化为使用直接指针算术--这使得它成为所有方法中最快的。 - 在List中按顺序访问所有值需要在每次迭代时进行检查,以确保您没有访问数组范围外的值,然后才能访问数组值。 - 在IEnumerable中按顺序访问所有值需要创建枚举器对象,调用Next()函数增加索引指针,然后调用Current属性,该属性给出实际值并将其放入您在foreach语句中指定的变量中。一般来说,这并不像听起来那么糟糕。 - 在IEnumerable中访问任意值需要从开头开始,调用Next()函数多次,直到到达该值。一般来说,这就像听起来那么糟糕。
关于习语方面...
通常情况下,IEnumerable对于公共属性、函数参数以及通常用作返回值非常有用--仅当您知道将按顺序使用值时。
例如,如果您有一个名为PrintValues的函数,如果它被编写为PrintValues(List<T> values),它只能处理List值,因此用户首先必须进行转换,例如他们正在使用T[]。同样,如果函数是PrintValues(T[] values),但如果它是PrintValues(IEnumerable<T> values),它将能够处理Lists、T[]s、stacks、hashtables、dictionaries、strings、sets等--任何实现IEnumerable的集合,几乎所有集合都是如此。
关于内部使用方面...
- 仅在不确定其中有多少项时才使用List。 - 如果您知道其中需要多少项,但需要以任意顺序访问值,请使用T[]。 - 如果给定了IEnumerable并且您只需要按顺序使用它,则坚持使用IEnumerable。许多函数将返回IEnumerables。如果确实需要按任意顺序访问IEnumerable中的值,请使用ToArray()。
此外,需要注意的是强制转换与使用ToArray()或ToList()不同--后者涉及复制值,如果你有大量元素,则确实会影响性能和内存。前者只是说“狗是一种动物,所以像任何动物一样,它可以吃”(向下转换),或者“这个动物碰巧是一只狗,所以它可以叫”(向上转换)。同样,所有列表和T[]都是IEnumerables,但只有一些IEnumerables是列表或T[]。

+1,非常好的比较。在许多情况下 - 尽管可能超出了原始问题 - 使用IList<T>而不是IEnumerable<T>是一个有趣的选择,特别是当需要添加和删除数据时。 - Dirk Vollmar

7

一个好的经验法则是:除非你有充分的理由不这样做,否则始终使用IEnumerable(在声明变量/方法参数/方法返回类型/属性等时)。这是与其他(特别是扩展)方法最兼容的类型。


任何与 IEnumerable<T> 一起使用的扩展方法也将始终适用于 List<T> 和数组,因为它们实现了它。(我同意尽可能使用它,但这里的推理是不正确的...) - Reed Copsey
IEnumerable不是数据类型,它是一个接口。你不能“使用”IEnumerable,你只能使用“实现”IEnumerable的东西,其中有相当多的东西(包括字符串数组和列表)。 - riwalk
不仅与其他程序兼容,而且功能强大。您可以链接它、隐藏它、延迟加载它、转换它——所有这些都隐藏在.NET中最精心设计的合同之后 :) - Rex M
@Kirk,他想知道它们的区别以及何时使用它们。如果说“始终使用IEnumerable”,这忽略了您无法声明IEnumerable实例的事实。当您需要实例化一个对象时,string[]和List<>都实现了IEnumerable,因此说“使用IEnumerable”并不能区分两者之间的区别。不要误解——当接受对象时,始终使用IEnumerable,但这只是问题的一部分。 - riwalk
1
我想修改你的答案,改为:除非需要使用.Count,否则始终使用IEnumerable<T>作为公共参数、返回类型、属性等。在这种情况下,请使用IList<T>。对于本地变量,只需使用实际拥有的类型即可。 - Daniel Pryden
显示剩余6条评论

4

好的,你有两个苹果和一个橙子要进行比较。

这两个苹果分别是数组和列表。

  • C#中的数组是具有垃圾收集功能的C样式数组。使用它们的好处是它们几乎没有开销,假设您不需要移动东西。坏处是当您添加东西,删除东西和其他更改数组时,它们不像在内存中移动数据那样高效。

  • 列表是C#风格的动态数组(类似于C ++中的vector<>类)。虽然有更多的开销,但当您需要频繁移动东西时,它们更加高效,因为它们不会尝试保持内存使用连续。

我能给出的最好的比较是说,数组就像字符串,而列表就像StringBuilder。

橙子是“IEnumerable”。这不是一种数据类型,而是一个接口。当一个类实现IEnumerable接口时,它允许该对象在foreach()循环中使用。

当您返回列表(如您在示例中所做的那样),您并没有转换列表为IEnumerable。列表已经是一个IEnumerable对象。

编辑:何时在两者之间转换:

这取决于应用程序。使用列表可以完成几乎所有数组可以完成的任务,因此我通常建议使用列表。最好的做法可能是做出一个设计决策,即您要使用其中之一,这样您就不必在两者之间切换。如果依赖于外部库,请将其抽象化以保持一致的使用。

希望这能稍微解释一下。


接口是一种不同的东西,这是一个好观点(+1),尽管这并没有真正回答何时/什么时候进行转换。 - Vinko Vrsalovic

1

在我看来,问题似乎是你没有学习如何搜索数组。提示: 根据数组是否排序,使用 Array.IndexOfArray.BinarySearch

将数组转换为列表确实是一个不好的想法:它浪费空间和时间,并使代码不够可读。而且,盲目地上升到 IEnumerable 会减慢速度,也完全阻止使用某些算法 (例如二分搜索)。


你不需要切换到IEnumerable,它本身就是IEnumerable。这就叫做多态性...唉 - riwalk
1
@Vinko:System.Array不是一个命名空间,它是一个类。 @Stargazer:我把“switch”改成了“upcast”。这样更清楚吗? 多态性确实会带来性能和功能上的代价。 - Ben Voigt
非常好,谢谢。让我感到不舒服的是,人们把 IEquivalent 看做和 List 处于同一水平。它们是完全不同的东西。 - riwalk
1
我所说的“switch”是指更改参数和返回值的正式类型,但“upcast”更准确、更具信息性地描述了当进行此更改时发生的情况,因此希望没有人会考虑转换这个概念。 - Ben Voigt

0

何时使用什么?

我建议返回最具体的类型,并采用最灵活的类型。

就像这样:

public int[] DoSomething(IEnumerable<int> inputs)
{
    //...
}

public List<int> DoSomethingElse(IList<int> inputs)
{
    //...
}

这样你就可以在从方法返回的任何东西上调用 List<T> 的方法,同时将其视为 IEnumerable。在输入方面,尽可能灵活,以便不要强制方法的用户创建什么类型的集合。


0

如果可以避免,我尽量避免快速跳转数据类型。

必须是这样的情况:你所描述的每种类似情况都足够不同,以防止关于转换类型的教条规则;然而,通常最好选择一个数据结构,它能尽可能地提供你需要的接口,而无需不必要地将元素复制到新的数据结构中。


-2

在你真正遇到性能问题之前,忽略“性能问题”的警报是正确的。绝大多数性能问题都来自于执行过多的I/O操作或者过多的锁定操作,或者是对它们的错误使用,而这些都与这个问题无关。

我的一般处理方法为:

  1. 对于“静态”或“快照”形式的信息,请使用T[]。对于那些调用.Add()没有意义的事情和您不需要List<T>提供的附加方法的东西,请使用这种方式。
  2. 如果您并不介意得到什么样的结果,并且不需要一个常数时间的.Length/.Count,请接受IEnumerable<T>。
  3. 只有在对输入的IEnumerable<T>进行简单操作或者特别需要利用yield语法来慢慢地完成工作时,才返回IEnumerable<T>。
  4. 在所有其他情况下,请使用List<T>。它实在是太灵活了。

第四点的推论是:不要害怕使用ToList()。ToList()就像你的朋友一样。它可以强制IEnumerable<T>进行求值(当你需要堆叠多个where子句时非常有用)。不要过度使用它,但在构建完整的where子句后,在执行foreach循环之前(或者类似操作之前)随时可以调用它。

当然,这只是一个大致的指南。请尽量遵循同一个代码库中相同的模式,代码风格的跳跃会使维护程序员更难理解和维护你的代码。


2
你只在谈论网站开发。如果你要一直在游戏中、算法工作中或者处理数百万元素的大型列表时进行转换,那你肯定是疯了。列表是三种数据结构中最昂贵、也是不必要的具体化,只有在填充数组时不知道有多少元素才使用它。不要将列表作为参数传递。如果需要任意访问参数值,请接受 IList;否则只需接受 IEnumerable。否则你只会带来重构地狱。 - Rei Miyasaka

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