Description
Background
Currently, Linq does not offer an API for getting the last N elements of a sequence. This has resulted in no less than 5 StackOverflow questions (with ~300 votes) popping up as the first result when you Google "skip last n elements of enumerable."
Proposal
We should add SkipLast
and TakeLast
to Enumerable, which will skip/take the last N elements of the sequence, respectively. If there are less than N elements, SkipLast
will return an empty sequence, and TakeLast
will return a sequence with contents equivalent to the original enumerable.
namespace System.Linq
{
public static class Enumerable
{
public static IEnumerable<TSource> SkipLast<TSource>(this IEnumerable<TSource> source, int count);
public static IEnumerable<TSource> TakeLast<TSource>(this IEnumerable<TSource> source, int count);
}
public static class Queryable
{
// Lots of existing methods for parity with Enumerable
public static IQueryable<T> SkipLast<T>(this IQueryable<T> source, int count);
public static IQueryable<T> TakeLast<T>(this IQueryable<T> source, int count);
}
}
Remarks
-
In
SkipLast
instead of evaluating the whole thing at once, we will read in the firstcount
items into a circular buffer. Then we will interleave between yield returning the oldest element & overwriting that with a new one. -
In
TakeLast
we will still evaluate the whole sequence during the first iteration, but again maintain a circular buffer of lengthcount
and overwrite the oldest elements with newer ones.
Original (incorrect) - Implementation remarks
These overloads will have subtly different semantics from Skip
and Take
. Since for lazy enumerables we can't determine the count of the sequence in advance, we will have to capture it into an array during our first iteration (like Reverse
does today), then skip/take the last N items from that array between yields.
public static IEnumerable<TSource> SkipLast<TSource>(this IEnumerable<TSource> source, int count)
{
var array = source.ToArray();
for (int i = count; i < array.Length; i++)
{
yield return array[i];
}
}
As a result, these will have different outputs:
int[] a = { 1, 2, 3, 4, 5 };
foreach (var item in a.Skip(3))
{
a[4] = 10;
Console.WriteLine(item); // 4 10
}
a[4] = 5;
foreach (var item in a.TakeLast(2))
{
a[4] = 10;
Console.WriteLine(item); // 4 5
}
Another consequence is that this will lead to more allocations for lists/arrays. Perhaps this is worth it for added convenience, though.