Enumerable.ElementAt<TSource>有什么作用?

6

IEnumerable<T>暴露了一个枚举器,因此可以枚举对象。这个接口没有关于索引的任何信息。而IList<T>是关于索引的,因为它公开了IndexOf方法。

那么Enumerable.ElementAt有什么用呢?我刚刚读了这个LINQ扩展方法的文档

返回序列中指定索引处的元素。

好的,是关于序列的,不仅仅是IEnumerable。继续阅读备注:

如果源类型实现了IList,则使用该实现来获取指定索引处的元素。否则,此方法获取指定的元素。

好的,所以如果具体类型实现了从IList<T>继承的东西(这是一个真正的序列),那么它就与IndexOf()相同。如果没有,它就会迭代直到达到索引。

下面是一个示例场景:

// Some extension method exposed by a lib
// I know it's not a good piece of code, but let's say it's coded this way:
public static class EnumerableExtensions
{
    // Returns true if all elements are ordered
    public static bool IsEnumerableOrdered(this IEnumerable<int> value)
    {
        // Iterates over elements using an index
        for (int i = 0; i < value.Count() - 1; i++)
        {
            if (value.ElementAt(i) > value.ElementAt(i + 1))
            {
                return false;
            }
        }

        return true;
    }
}

// Here's a collection that is enumerable, but doesn't always returns
// its objects in the same order
public class RandomAccessEnumerable<T> : IEnumerable<T>
{
    private List<T> innerList;
    private static Random rnd = new Random();

    public RandomAccessEnumerable(IEnumerable<T> list)
    {
        innerList = list.ToList();
    }

    public IEnumerator<T> GetEnumerator()
    {
        var listCount = this.innerList.Count;
        List<int> enumeratedIndexes = new List<int>();

        for (int i = 0; i < listCount; i++)
        {
            int randomIndex = -1;
            while (randomIndex < 0 || enumeratedIndexes.Contains(randomIndex))
            {
                randomIndex = rnd.Next(listCount);
            }

            enumeratedIndexes.Add(randomIndex);
            yield return this.innerList[randomIndex];
        }
    }

    IEnumerator IEnumerable.GetEnumerator()
    {
        return this.GetEnumerator();
    }
}

// Here's some test program
internal class Program
{
    private static void Main()
    {
        var test0 = new List<int> { 0, 1, 2, 3 };
        var test1 = new RandomAccessEnumerable<int>(test0);

        Console.WriteLine("With List");
        Console.WriteLine(test0.IsEnumerableOrdered()); // true
        Console.WriteLine(test0.IsEnumerableOrdered()); // true
        Console.WriteLine(test0.IsEnumerableOrdered()); // true
        Console.WriteLine(test0.IsEnumerableOrdered()); // true
        Console.WriteLine(test0.IsEnumerableOrdered()); // true

        Console.WriteLine("With RandomAccessEnumerable");
        Console.WriteLine(test1.IsEnumerableOrdered()); // might be true or false
        Console.WriteLine(test1.IsEnumerableOrdered()); // might be true or false
        Console.WriteLine(test1.IsEnumerableOrdered()); // might be true or false
        Console.WriteLine(test1.IsEnumerableOrdered()); // might be true or false
        Console.WriteLine(test1.IsEnumerableOrdered()); // might be true or false

        Console.Read();
    }
}

因此,由于 RandomAccessEnumerable 可能以随机顺序返回枚举对象,您不能仅依赖简单的 IEnumerable <T> 接口来假定您的元素被索引。因此,您不想使用 IEnumerable 中的 ElementAt
在上面的示例中,我认为 IsEnumerableOrdered 应该需要一个 IList<T>参数,因为它暗示元素是一个序列。实际上,我找不到使用 ElementAt 方法有用且不易出错的情况。

2
这可能更适用于 IOrderedEnumerable<T> 类型。 - jrummell
4
“这涉及到一个序列,不仅仅是IEnumerable接口:那么你如何定义‘序列’呢?对我来说,IEnumerable接口代表了一系列元素,没有更多或更少……大多数情况下,IEnumerable中的元素将按相同顺序返回;你的RandomAccessEnumerable示例过于牵强,无法真正相关。” - Thomas Levesque
2
嗯,LinkedList是一个序列,但它没有实现IList :( - tukaef
7
仅仅因为你想出了一个使用 ElementAt 不当的代码,以及一个在许多 Enumerable 方法中提供非确定性结果的 IEnumerable,并不意味着 ElementAt 是无用或容易出错的。 - Rawling
3
当你编写一个库时,如果只是被提供了一个IEnumerable,那么你就会面临各种各样的未知情况。例如,是否可以安全地多次枚举它?因此,你需要记录你的库方法的期望,并要求使用你的库的人满足这些期望。 - Damien_The_Unbeliever
显示剩余8条评论
1个回答

7

有许多 IEnumerable 类型,例如数组或列表。所有 IList 类型(包括 Array 实现的类型)都有一个 索引器,您可以使用它来访问特定索引处的元素。

如果序列可以成功转换为 IList,则 Enumerable.ElementAt 将使用此功能。否则,将进行枚举。

因此,这只是一种方便的方式,可以访问各种 IEnumerable 类型中给定索引处的元素。

这样做的好处是,您可以稍后更改类型,而无需更改所有出现的 arr[index]

值得一提的是,以下是反射(ILSpy)方法,以证明我所说的:

public static TSource ElementAt<TSource>(this IEnumerable<TSource> source, int index)
{
    if (source == null)
    {
        throw Error.ArgumentNull("source");
    }
    IList<TSource> list = source as IList<TSource>;
    if (list != null)
    {
        return list[index];
    }
    if (index < 0)
    {
        throw Error.ArgumentOutOfRange("index");
    }
    TSource current;
    using (IEnumerator<TSource> enumerator = source.GetEnumerator())
    {
        while (enumerator.MoveNext())
        {
            if (index == 0)
            {
                current = enumerator.Current;
                return current;
            }
            index--;
        }
        throw Error.ArgumentOutOfRange("index");
    }
    return current;
}

我确实理解ElementAt的工作方式,但我想知道为什么有人会在非IList对象上使用它,因为我们不能假设IEnumerable对象总是以相同的顺序返回其项。 - ken2k
因为他今天可能使用查询,明天又用 IList。例如:items.Where(i=>i%2==0).Take(10).OrderBy(i=>i).ElementAt(5)。如果你想要另一个集合,你不需要修改实现,只需在前面加上 ToListToArray 就可以了,它仍然可以工作。 - Tim Schmelter
我理解你的(正确)观点。但我仍然不明白为什么有人会在第一时间使用items.Where(i=>i%2==0).Take(10).OrderBy(i=>i).ElementAt(5),尤其是当items是一个IEnumerable时,因为根据定义,IEnumerable是无序的(所以没有索引)。 - ken2k
我想我可能只是过于思考了,也许就像其他人说的那样,就像Skip()或Take()一样。 - ken2k
@ken2k:上面的查询是有序的。并不是每个 IEnumerable 都是无序的(例如,Dictionary 是无序的 也是 IEnumerable)。 - Tim Schmelter
没错,我没看到OrderBy() :) 我觉得你通过提供一个有效的使用ElementAt的例子回答了我的问题。 - ken2k

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