给定一个集合,有没有办法得到该集合的最后N个元素?如果框架中没有方法,那么编写一个扩展方法来实现这个目的的最佳方式是什么?


当前回答

我试图将效率和简单性结合起来,最后得到了这样的结果:

public static IEnumerable<T> TakeLast<T>(this IEnumerable<T> source, int count)
{
    if (source == null) { throw new ArgumentNullException("source"); }

    Queue<T> lastElements = new Queue<T>();
    foreach (T element in source)
    {
        lastElements.Enqueue(element);
        if (lastElements.Count > count)
        {
            lastElements.Dequeue();
        }
    }

    return lastElements;
}

关于 在c#中,Queue<T>是使用循环缓冲区实现的,因此每次循环都没有对象实例化(只有当队列增长时)。我没有设置队列容量(使用专用构造函数),因为有人可能使用count = int调用此扩展。MaxValue。为了获得额外的性能,您可以检查源实现IList是否<T>,如果是,则直接使用数组索引提取最后的值。

其他回答

我很惊讶没有人提到它,但是SkipWhile确实有一个使用元素索引的方法。

public static IEnumerable<T> TakeLastN<T>(this IEnumerable<T> source, int n)
{
    if (source == null)
        throw new ArgumentNullException("Source cannot be null");

    int goldenIndex = source.Count() - n;
    return source.SkipWhile((val, index) => index < goldenIndex);
}

//Or if you like them one-liners (in the spirit of the current accepted answer);
//However, this is most likely impractical due to the repeated calculations
collection.SkipWhile((val, index) => index < collection.Count() - N)

这种解决方案相对于其他解决方案的唯一明显好处是,您可以选择添加一个谓词,以生成更强大和更有效的LINQ查询,而不是使用两个单独的操作遍历IEnumerable两次。

public static IEnumerable<T> FilterLastN<T>(this IEnumerable<T> source, int n, Predicate<T> pred)
{
    int goldenIndex = source.Count() - n;
    return source.SkipWhile((val, index) => index < goldenIndex && pred(val));
}

如果可以选择使用第三方库,则MoreLinq定义了TakeLast()。

.NET Core 2.0+提供了LINQ方法TakeLast():

https://learn.microsoft.com/en-us/dotnet/api/system.linq.enumerable.takelast

例子:

Enumerable
    .Range(1, 10)
    .TakeLast(3) // <--- takes last 3 items
    .ToList()
    .ForEach(i => System.Console.WriteLine(i))

// outputs:
// 8
// 9
// 10
coll.Reverse().Take(N).Reverse().ToList();


public static IEnumerable<T> TakeLast<T>(this IEnumerable<T> coll, int N)
{
    return coll.Reverse().Take(N).Reverse();
}

更新:为了解决clintp的问题:a)使用我上面定义的TakeLast()方法解决了这个问题,但如果你真的想在没有额外方法的情况下做到这一点,那么你只需要认识到Enumerable.Reverse()可以用作扩展方法,你不需要这样使用它:

List<string> mystring = new List<string>() { "one", "two", "three" }; 
mystring = Enumerable.Reverse(mystring).Take(2).Reverse().ToList();

老实说,我对这个答案并不是特别自豪,但对于小的集合,你可以使用以下方法:

var lastN = collection.Reverse().Take(n).Reverse();

有点俗气,但它的工作;)