给定一个集合,有没有办法得到该集合的最后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
其他回答
如果可以选择使用第三方库,则MoreLinq定义了TakeLast()。
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描述的一次传递算法。这两种方法都使用临时缓冲区来保存枚举时的项,一旦找到集合的末尾,就会产生这些项。
使用EnumerableEx。在RX的系统中使用last。交互式装配。这是一个类似于@Mark的O(N)实现,但它使用队列而不是环形缓冲区结构(并且当达到缓冲区容量时将项目从队列中移除)。
(注意:这是IEnumerable版本-不是IObservable版本,尽管两者的实现几乎相同)
我知道现在回答这个问题已经太晚了。但是,如果您正在处理类型为IList<>的集合,并且您不关心返回集合的顺序,那么此方法工作得更快。我使用了Mark Byers的答案并做了一些改变。TakeLast方法是:
public static IEnumerable<T> TakeLast<T>(IList<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; }
if (source.Count > takeCount)
{
for (int z = source.Count - 1; takeCount > 0; z--)
{
takeCount--;
yield return source[z];
}
}
else
{
for(int i = 0; i < source.Count; i++)
{
yield return source[i];
}
}
}
我用Mark Byers的方法和kbrimington的方法进行测试。这是测试:
IList<int> test = new List<int>();
for(int i = 0; i<1000000; i++)
{
test.Add(i);
}
Stopwatch stopwatch = new Stopwatch();
stopwatch.Start();
IList<int> result = TakeLast(test, 10).ToList();
stopwatch.Stop();
Stopwatch stopwatch1 = new Stopwatch();
stopwatch1.Start();
IList<int> result1 = TakeLast2(test, 10).ToList();
stopwatch1.Stop();
Stopwatch stopwatch2 = new Stopwatch();
stopwatch2.Start();
IList<int> result2 = test.Skip(Math.Max(0, test.Count - 10)).Take(10).ToList();
stopwatch2.Stop();
下面是取10个元素的结果:
取1000001个元素的结果为: