为什么要使用Enumerable.ElementAt()而不是[]运算符?


69

这似乎是一个愚蠢的问题,但我还没有找到答案,所以就在这里。:)

在这两种情况下,如果您无法检查集合的边界,都会收到“超出范围”的异常。这仅仅是编码风格的偏好吗?

如果有人需要一个例子:

List<byte> myList = new List<byte>(){0x01, 0x02, 0x03};
byte testByte = myList.ElementAt(2);

byte testByte = myList[2];


1
值得一提的是要小心string。最好使用索引器代替字符串ElementAt。(即myString[999]优于myString.ElementAt(999))。这与性能有关。String没有实现IList', thus it won't be randomly-accessed by ElementAt`。
Thariq Nugrohotomo

Answers:


37

因为Enumerable比较通用,所以由enumerable表示的集合可能没有索引器。

但是,如果这样做了-不使用ElementAt()它可能不会那么有效。


29
ElementAt()对于ILists来说就很好了,如果您查看它的代码,它将转换为IList,如果成功,它将使用[]。所有LINQ扩展方法通常都是这样。
Matt Greer

我一直认为扩展方法将有特定于集合的实现,但是如果不检查,我就不会费力了……
Massif

22

ElementAt()为C#中的所有枚举提供统一的接口。我倾向于经常使用它,因为我喜欢通用API。

如果基础类型支持随机访问(即它支持[]运算符),则ElementAt将使用它。因此,唯一的开销是一个额外的方法调用(这几乎是不相关的)。

如果基础类型不支持随机访问,则ElementAt()通过迭代枚举直到它到达您关心的元素来对其进行仿真。这可能非常昂贵,有时甚至会有副作用。

还有一些ElementAtOrDefault()通常很方便。


1
+1好的论点。轻微挑剔。说If the underlying type supports random access (ie, it implements IList <T>) 说更准确If the underlying type supports random access (ie, it supports the [] operator)。只是说。
2014年

+1指出ElementAt()有时需要部分迭代集合及其潜在的缺点。
鲍勃·萨默斯

14

在某些情况下避免使用ElementAt()!

如果您知道要查找每个元素,并且有(或可能有)超过500个,则只需调用ToArray(),将其存储在可重用的数组变量中,然后以这种方式对其进行索引。

例如; 我的代码正在从Excel文件中读取数据。
我正在使用ElementAt()查找我的单元格正在引用的SharedStringItem。
使用500行或更少的行,您可能不会注意到差异。
使用16K线,需要100秒。

为了使情况变得更糟(每次读取的行),索引越大,每次迭代索引越多,因此花费的时间比应该多。
前1,000行耗时2.5秒,而后1,000行耗时10.7秒。

通过以下代码循环:

SharedStringItem ssi = sst.Elements<SharedStringItem>().ElementAt(ssi_index);

产生此记录的输出:

...Using ElementAt():
RowIndex:  1000 Read: 1,000 Seconds: 2.4627589
RowIndex:  2000 Read: 1,000 Seconds: 2.9460492
RowIndex:  3000 Read: 1,000 Seconds: 3.1014865
RowIndex:  4000 Read: 1,000 Seconds: 3.76619
RowIndex:  5000 Read: 1,000 Seconds: 4.2489844
RowIndex:  6000 Read: 1,000 Seconds: 4.7678506
RowIndex:  7000 Read: 1,000 Seconds: 5.3871863
RowIndex:  8000 Read: 1,000 Seconds: 5.7997721
RowIndex:  9000 Read: 1,000 Seconds: 6.4447562
RowIndex: 10000 Read: 1,000 Seconds: 6.8978011
RowIndex: 11000 Read: 1,000 Seconds: 7.4564455
RowIndex: 12000 Read: 1,000 Seconds: 8.2510054
RowIndex: 13000 Read: 1,000 Seconds: 8.5758217
RowIndex: 14000 Read: 1,000 Seconds: 9.2953823
RowIndex: 15000 Read: 1,000 Seconds: 10.0159931
RowIndex: 16000 Read: 1,000 Seconds: 10.6884988
Total Seconds: 100.6736451

一旦我创建了一个中间数组来存储SharedStringItem以便进行引用,我的时间就从100秒减少到10秒,现在以相同的时间处理每一行。

这行代码:

SharedStringItem[] ssia = sst == null ? null : sst.Elements<SharedStringItem>().ToArray();
Console.WriteLine("ToArray():" + watch.Elapsed.TotalSeconds + " Len:" + ssia.LongCount());

并遍历以下代码行:

SharedStringItem ssi = ssia[ssi_index];

产生此记录的输出:

...Using Array[]:
ToArray(): 0.0840583 Len: 33560
RowIndex:  1000 Read: 1,000 Seconds: 0.8057094
RowIndex:  2000 Read: 1,000 Seconds: 0.8183683
RowIndex:  3000 Read: 1,000 Seconds: 0.6809131
RowIndex:  4000 Read: 1,000 Seconds: 0.6530671
RowIndex:  5000 Read: 1,000 Seconds: 0.6086124
RowIndex:  6000 Read: 1,000 Seconds: 0.6232579
RowIndex:  7000 Read: 1,000 Seconds: 0.6369397
RowIndex:  8000 Read: 1,000 Seconds: 0.629919
RowIndex:  9000 Read: 1,000 Seconds: 0.633328
RowIndex: 10000 Read: 1,000 Seconds: 0.6356769
RowIndex: 11000 Read: 1,000 Seconds: 0.663076
RowIndex: 12000 Read: 1,000 Seconds: 0.6633178
RowIndex: 13000 Read: 1,000 Seconds: 0.6580743
RowIndex: 14000 Read: 1,000 Seconds: 0.6518182
RowIndex: 15000 Read: 1,000 Seconds: 0.6662199
RowIndex: 16000 Read: 1,000 Seconds: 0.6360254
Total Seconds: 10.7586264

如您所见,转换为数组仅花费了不到一秒的时间,可处理33,560件商品,这对于加速我的导入过程非常值得。


9

更新:我觉得这是一个有趣的话题,因此决定写博客

基本上,我的看法是,它很少有意义的利用随机访问的抽象,而不需要IList<T>。如果您要编写的代码想要按索引随机访问集合的元素,则只需要一个提供该功能的代码即可。而且,如果您需要从头IList<T>开始,我真的看不到使用ElementAt代替索引器。

当然,那只是我的意见。


请勿将其用于IList<T>诸如T[]或的实现List<T>。仅在需要用于不提供随机访问权限的集合类型时使用它,例如Queue<T>Stack<T>

var q = new Queue<int>();
var s = new Stack<int>();
for (int i = 0; i < 10; ++i)
{
    q.Enqueue(i);
    s.Push(i);
}

// q[1] would not compile.
int fromQueue = q.ElementAt(1);
int fromStack = s.ElementAt(1);

2
[]如果基础类型实现,它将使用IList。通常,LINQ总是尽可能地尝试从基础类型中找到有效的快捷方式。
Matt Greer

3
@Matt:也许是,但是我仍然不会将其用于此类类型。只是没有意义:索引器在那里;为什么不直接使用它?而且,LINQ扩展方法中的优化通常值得商and,而且在大多数情况下没有记录在案,这意味着BCL团队可以改变主意并将其淘汰。
丹涛

3
它们已记录在案。从MSDN:“如果源类型实现IList <T>,则该实现用于获取指定索引处的元素”。我通常发现LINQ使C#代码具有更高的水平,在这里我几乎从不关心(甚至不知道)底层类型,而只是对枚举执行操作。但这只是我的经验。
Matt Greer

3
@MattGreer和Dan Tao,很想一直使用ElementAt()它,因为它可以使您的代码更加一致,并且无论集合的类型如何,代码都将继续工作。但是,如果元素的获取发生在循环内,那么您将在O(n)中查找IList<T>vs. O(n ^ 2)。因此,假设在进入循环之前,您已明确转换为List<T>。在这种情况下,我主张使用[]而不是ElementAt()因为它传达出保证快速查找的目的。
devuxer 2012年

2
问候Dan,您到博客的链接似乎已断开,可以提供更新的链接吗?
ForceMagic


5

ElementAt()确实尝试IList<T>先转换后再使用索引器,因此与ElementAt()直接使用索引器相比,性能可能不会有显着差异。因此,即使您有一个IList<T>,也可能要考虑ElementAt(),如果将来有可能更改声明的类型。


关于可能切换声明的类型的观点是一个很好的观点!我认为我之所以使用.ElementAt()的原因是由于我使用的所有队列都有习惯。
EJA

5

从语义的角度来看,它们的作用完全相同。无论您使用它的哪个,都不会改变程序的含义。

您使用[]

  1. 如果可枚举被声明IList<T>,因为:

    一种。它更具可读性,惯用性并且被广泛理解。我使用它的第一原因。

    b。您想优化每一英寸。[]应该略有提高性能,因为它是直接查找并避免了强制转换(请注意,ElementAt如果aIEnumerable<T>是an IList<T>,则使用索引器,因此不会有太大的收获)。

    C。您仍处于.NET 3.5之前的时代。

您使用ElementAt

  1. 如果声明的类型仅仅是一个IEnumerable<T>

  2. 如果ElementAt使它更具可读性。例如,以流利的风格打电话:

    var x = GetThatList().ElementAt(1).Wash().Spin().Rinse().Dry();
    
    //is more readable than
    
    var x = GetThatList()[1].Wash().Spin().Rinse().Dry();
    
  3. 如果您确实坚持为IList<T>和都使用一致的风格IEnumerable<T>

简而言之,如果您将变量声明为IList<T>use [],否则请使用ElementAt我强调“声明的”这一点,因为即使您IEnumerable<T>位于IList<T>下面,也只能使用ElementAt。强制转换IList<T>为使用索引器是不可以的(它发生在内部ElementAt无论如何))。可读性高。如果您考虑到这一点,则可以轻松选择。在大多数情况下,我必须使用索引器样式。我被迫ElementAt很少使用。


3

IEnumerable不支持直接索引访问,如果您知道有列表,请继续使用第二种形式,因为它很容易阅读(可以说)。

ElementAt()支持所有枚举,而不仅限于列表-在性能上,当在一种类型IList(即通用列表)上使用时,它们是相同的,但是如果您使用索引访问,则列表更具表现力。如果您查看ElementAt()with反射器的源代码,您将看到,如果IEnumerableis类型为,它将在内部使用索引访问IList

..
IList<TSource> list = source as IList<TSource>;
if (list != null)
{
    return list[index];
}
..

@xanatos-当然,如果您想以IList这种方式构建自己的类型;-)List<T>至少使用数组作为后备存储,因此对于它们来说将是O(1)
BrokenGlass
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.