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


当前回答

我很惊讶没有人提到它,但是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()。

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

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

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

以下是我的解决方案:

public static class EnumerationExtensions
{
    public static IEnumerable<T> TakeLast<T>(this IEnumerable<T> input, int count)
    {
        if (count <= 0)
            yield break;

        var inputList = input as IList<T>;

        if (inputList != null)
        {
            int last = inputList.Count;
            int first = last - count;

            if (first < 0)
                first = 0;

            for (int i = first; i < last; i++)
                yield return inputList[i];
        }
        else
        {
            // Use a ring buffer. We have to enumerate the input, and we don't know in advance how many elements it will contain.
            T[] buffer = new T[count];

            int index = 0;

            count = 0;

            foreach (T item in input)
            {
                buffer[index] = item;

                index = (index + 1) % buffer.Length;
                count++;
            }

            // The index variable now points at the next buffer entry that would be filled. If the buffer isn't completely
            // full, then there are 'count' elements preceding index. If the buffer *is* full, then index is pointing at
            // the oldest entry, which is the first one to return.
            //
            // If the buffer isn't full, which means that the enumeration has fewer than 'count' elements, we'll fix up
            // 'index' to point at the first entry to return. That's easy to do; if the buffer isn't full, then the oldest
            // entry is the first one. :-)
            //
            // We'll also set 'count' to the number of elements to be returned. It only needs adjustment if we've wrapped
            // past the end of the buffer and have enumerated more than the original count value.

            if (count < buffer.Length)
                index = 0;
            else
                count = buffer.Length;

            // Return the values in the correct order.
            while (count > 0)
            {
                yield return buffer[index];

                index = (index + 1) % buffer.Length;
                count--;
            }
        }
    }

    public static IEnumerable<T> SkipLast<T>(this IEnumerable<T> input, int count)
    {
        if (count <= 0)
            return input;
        else
            return input.SkipLastIter(count);
    }

    private static IEnumerable<T> SkipLastIter<T>(this IEnumerable<T> input, int count)
    {
        var inputList = input as IList<T>;

        if (inputList != null)
        {
            int first = 0;
            int last = inputList.Count - count;

            if (last < 0)
                last = 0;

            for (int i = first; i < last; i++)
                yield return inputList[i];
        }
        else
        {
            // Aim to leave 'count' items in the queue. If the input has fewer than 'count'
            // items, then the queue won't ever fill and we return nothing.

            Queue<T> elements = new Queue<T>();

            foreach (T item in input)
            {
                elements.Enqueue(item);

                if (elements.Count > count)
                    yield return elements.Dequeue();
            }
        }
    }
}

代码有点粗,但作为一个可重用的插入组件,它应该在大多数场景中表现得很好,并且它将使使用它的代码保持良好和简洁。: -)

我的TakeLast for non-IList ' 1是基于与@Mark Byers和@MackieChan回答中相同的环形缓冲算法。有趣的是,它们是如此相似——我是完全独立写的。我猜只有一种方法可以正确地使用环形缓冲区。: -)

看看@kbrimington的答案,可以为IQuerable<T>添加一个额外的检查,以回到与实体框架一起工作的方法——假设我在这一点上没有。

我的解决方案是基于c#版本8中引入的范围。

        public static IEnumerable<T> TakeLast<T>(this IEnumerable<T> source, int N)
        {
            return source.ToArray()[(source.Count()-N)..];
        }

在用大多数评价的解决方案(以及我谦卑地提出的解决方案)运行了一个基准测试后:

    public static class TakeLastExtension
    {
        public static IEnumerable<T> TakeLastMarkByers<T>(this IEnumerable<T> source, int takeCount)
        {
            if (source == null) { throw new ArgumentNullException("source"); }
            if (takeCount < 0) { throw new ArgumentOutOfRangeException("takeCount", "must not be negative"); }
            if (takeCount == 0) { yield break; }

            T[] result = new T[takeCount];
            int i = 0;

            int sourceCount = 0;
            foreach (T element in source)
            {
                result[i] = element;
                i = (i + 1) % takeCount;
                sourceCount++;
            }

            if (sourceCount < takeCount)
            {
                takeCount = sourceCount;
                i = 0;
            }

            for (int j = 0; j < takeCount; ++j)
            {
                yield return result[(i + j) % takeCount];
            }
        }

        public static IEnumerable<T> TakeLastKbrimington<T>(this IEnumerable<T> source, int N)
        {
            return source.Skip(Math.Max(0, source.Count() - N));
        }

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

        public static IEnumerable<T> TakeLastAlex<T>(this IEnumerable<T> source, int N)
        {
            return source.ToArray()[(source.Count()-N)..];
        }
    }

Test

    [MemoryDiagnoser]
    public class TakeLastBenchmark
    {
        [Params(10000)]
        public int N;

        private readonly List<string> l = new();

        [GlobalSetup]
        public void Setup()
        {
            for (var i = 0; i < this.N; i++)
            {
                this.l.Add($"i");
            }
        }

        [Benchmark]
        public void Benchmark1_MarkByers()
        {
            var lastElements = l.TakeLastMarkByers(3).ToList();
        }

        [Benchmark]
        public void Benchmark2_Kbrimington()
        {
            var lastElements = l.TakeLastKbrimington(3).ToList();
        }

        [Benchmark]
        public void Benchmark3_JamesCurran()
        {
            var lastElements = l.TakeLastJamesCurran(3).ToList();
        }

        [Benchmark]
        public void Benchmark4_Alex()
        {
            var lastElements = l.TakeLastAlex(3).ToList();
        }
    }

Program.cs:

var summary = BenchmarkRunner.Run(typeof(TakeLastBenchmark).Assembly);

命令dotnet运行——project .\TestsConsole2。csproj -c Release——logBuildOutput

结果如下:

// *摘要* BenchmarkDotNet=v0.13.2, OS=Windows 10 (10.0.19044.1889/21H2/ novber2021update) AMD Ryzen 5 5600X, 1个CPU, 12个逻辑核和6个物理核 . net SDK = 6.0.401 [主机]:.NET 6.0.9 (6.0.922.41905), X64 RyuJIT AVX2 DefaultJob: .NET 6.0.9 (6.0.922.41905), X64 RyuJIT AVX2

Method N Mean Error StdDev Gen0 Gen1 Allocated
Benchmark1_MarkByers 10000 89,390.53 ns 1,735.464 ns 1,704.457 ns - - 248 B
Benchmark2_Kbrimington 10000 46.15 ns 0.410 ns 0.363 ns 0.0076 - 128 B
Benchmark3_JamesCurran 10000 2,703.15 ns 46.298 ns 67.862 ns 4.7836 0.0038 80264 B
Benchmark4_Alex 10000 2,513.48 ns 48.661 ns 45.517 ns 4.7607 - 80152 B

事实证明,@Kbrimington提出的解决方案在内存分配和原始性能方面是最有效的。

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

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>,如果是,则直接使用数组索引提取最后的值。