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


当前回答

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();

其他回答

下面是一个方法,它适用于任何枚举对象,但只使用O(N)个临时存储:

public static class TakeLastExtension
{
    public static IEnumerable<T> TakeLast<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];
        }
    }
}

用法:

List<int> l = new List<int> {4, 6, 3, 6, 2, 5, 7};
List<int> lastElements = l.TakeLast(3).ToList();

它的工作原理是使用一个大小为N的环形缓冲区来存储它看到的元素,用新元素覆盖旧元素。当到达枚举对象的末尾时,循环缓冲区包含最后N个元素。

.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();
collection.Skip(Math.Max(0, collection.Count() - N));

这种方法保留了项目的顺序,不依赖于任何排序,并且在多个LINQ提供者之间具有广泛的兼容性。

重要的是要注意不要使用负数调用Skip。一些提供程序,比如实体框架,会在提供一个否定的参数时产生一个ArgumentException。对数学的呼唤。马克斯巧妙地避免了这一点。

下面的类具有扩展方法的所有基本要素,即:静态类、静态方法和this关键字的使用。

public static class MiscExtensions
{
    // Ex: collection.TakeLast(5);
    public static IEnumerable<T> TakeLast<T>(this IEnumerable<T> source, int N)
    {
        return source.Skip(Math.Max(0, source.Count() - N));
    }
}

关于性能的简要说明:

因为对Count()的调用可能导致某些数据结构的枚举,这种方法有导致两次数据传递的风险。对于大多数枚举对象来说,这并不是真正的问题;事实上,对于list、array甚至EF查询,已经有了优化,可以在O(1)时间内计算Count()操作。

但是,如果您必须使用只向前的枚举对象,并且希望避免进行两次传递,则可以考虑Lasse V. Karlsen或Mark Byers描述的一次传递算法。这两种方法都使用临时缓冲区来保存枚举时的项,一旦找到集合的末尾,就会产生这些项。

我的解决方案是基于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提出的解决方案在内存分配和原始性能方面是最有效的。