给定一个集合,有没有一种方法可以获取该集合的最后N个元素?如果框架中没有提供方法,编写一个扩展方法的最佳方式是什么?
collection.Skip(Math.Max(0, collection.Count() - N));
这种方法保留了项目顺序,而不依赖于任何排序,并且在几个LINQ提供程序中具有广泛的兼容性。
重要的是要注意不要使用负数调用Skip
。一些提供程序,例如Entity Framework,在提供负参数时会产生ArgumentException。对Math.Max
的调用可以很好地避免这种情况。
下面的类具有扩展方法的所有基本要素,包括:静态类、静态方法和使用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()
可能会导致对某些数据结构进行枚举,所以这种方法有可能导致对数据进行两次遍历。对于大多数可枚举对象来说,这并不是问题;事实上,已经存在针对列表、数组和甚至 EF 查询的优化,以在 O(1) 时间内计算 Count()
操作。
然而,如果您必须使用前向只枚举,并希望避免进行两次遍历,请考虑像 Lasse V. Karlsen 或 Mark Byers 描述的一遍算法。这两种方法都使用临时缓冲区来保存在枚举时的项目,一旦找到集合的末尾就会产生。
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();
List<string> mystring = new List<string>() { "one", "two", "three" }; mystring = mystring.Reverse().Take(2).Reverse();
我会得到一个编译器错误,因为.Reverse()返回void,编译器选择了该方法而不是返回IEnumerable的Linq方法。有什么建议吗? - Clinton PierceN
条记录后不关心顺序,则可以跳过第二个Reverse
。 - ZoolWay.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
netcoreapp1.x
),而仅适用于 dotnetcore 的 v2.0 和 v2.1 (netcoreapp2.x
)。您可能正在针对完整框架(例如 net472
)进行定位,这也是不受支持的。(.net standard 库可以被上述任何一个使用,但只能公开特定于目标框架的某些 API。请参见 https://learn.microsoft.com/en-us/dotnet/standard/frameworks) - Ray注意:我错过了您的问题标题,标题中写着使用Linq,因此我的答案实际上没有使用Linq。
如果你想避免缓存整个集合的非惰性副本,你可以编写一个简单的方法,使用链表来实现。
下面的方法将会把它在原始集合中找到的每个值添加到一个链表中,并将链表裁剪到所需的项数。由于它保持整个迭代过程中的链表项数不超过N,因此它只会保留原始集合中最多N个项目的副本。
它不需要你知道原始集合中的项目数量,也不需要对其进行多次迭代。
用法:
IEnumerable<int> sequence = Enumerable.Range(1, 10000);
IEnumerable<int> last10 = sequence.TakeLast(10);
...
扩展方法:
public static class Extensions
{
public static IEnumerable<T> TakeLast<T>(this IEnumerable<T> collection,
int n)
{
if (collection == null)
throw new ArgumentNullException(nameof(collection));
if (n < 0)
throw new ArgumentOutOfRangeException(nameof(n), $"{nameof(n)} must be 0 or greater");
LinkedList<T> temp = new LinkedList<T>();
foreach (var value in collection)
{
temp.AddLast(value);
if (temp.Count > n)
temp.RemoveFirst();
}
return temp;
}
}
TakeLast
方法,该方法使用队列而不是链表。 - Panagiotis Kanavos这里有一种适用于任何可枚举对象的方法,但只使用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个元素。
n
时它仍然能正常工作。 - Lasse V. Karlsen我很惊讶没有人提到这一点,但是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)
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));
}
collection.OrderByDescending(c => c.Key).Take(3).OrderBy(c => c.Key);
如果您不介意将Rx作为单子的一部分,您可以使用TakeLast
:
IEnumerable<int> source = Enumerable.Range(1, 10000);
IEnumerable<int> lastThree = source.AsObservable().TakeLast(3).AsEnumerable();
我尝试结合效率和简洁,最终得出这个:
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>
,如果是,则直接使用数组索引提取最后的值。
TakeLast()
在.Net Core 2.0及更高版本以及.Net Standard 2.1及更高版本中可用。 - IowaEric