Skip to content

Add Enumerable.SkipLast, TakeLast to get the last N elements of a sequence. #19431

Closed
@jamesqo

Description

@jamesqo

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 first count 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 length count 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.

Metadata

Metadata

Labels

api-approvedAPI was approved in API review, it can be implementedarea-System.Linqhelp wanted[up-for-grabs] Good issue for external contributors

Type

No type

Projects

No projects

Milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions