How to take all but the last element in a sequence

2020-01-27 02:58发布

Let's say I have a sequence.

IEnumerable<int> sequence = GetSequenceFromExpensiveSource();
// sequence now contains: 0,1,2,3,...,999999,1000000

Getting the sequence is not cheap and is dynamically generated, and I want to iterate through it once only.

I want to get 0 - 999999 (i.e. everything but the last element)

I recognize that I could do something like:

sequence.Take(sequence.Count() - 1);

but that results in two enumerations over the big sequence.

Is there a LINQ construct that lets me do:

sequence.TakeAllButTheLastElement();

标签: c# .net linq
22条回答
Explosion°爆炸
2楼-- · 2020-01-27 03:34

My traditional IEnumerable approach:

/// <summary>
/// Skips first element of an IEnumerable
/// </summary>
/// <typeparam name="U">Enumerable type</typeparam>
/// <param name="models">The enumerable</param>
/// <returns>IEnumerable of type skipping first element</returns>
private IEnumerable<U> SkipFirstEnumerable<U>(IEnumerable<U> models)
{
    using (var e = models.GetEnumerator())
    {
        if (!e.MoveNext()) return;
        for (;e.MoveNext();) yield return e.Current;
        yield return e.Current;
    }
}

/// <summary>
/// Skips last element of an IEnumerable
/// </summary>
/// <typeparam name="U">Enumerable type</typeparam>
/// <param name="models">The enumerable</param>
/// <returns>IEnumerable of type skipping last element</returns>
private IEnumerable<U> SkipLastEnumerable<U>(IEnumerable<U> models)
{
    using (var e = models.GetEnumerator())
    {
        if (!e.MoveNext()) return;
        yield return e.Current;
        for (;e.MoveNext();) yield return e.Current;
    }
}
查看更多
看我几分像从前
3楼-- · 2020-01-27 03:37

This is a general and IMHO elegant solution that will handle all cases correctly:

using System;
using System.Collections.Generic;
using System.Linq;

public class Program
{
    public static void Main()
    {
        IEnumerable<int> r = Enumerable.Range(1, 20);
        foreach (int i in r.AllButLast(3))
            Console.WriteLine(i);

        Console.ReadKey();
    }
}

public static class LinqExt
{
    public static IEnumerable<T> AllButLast<T>(this IEnumerable<T> enumerable, int n = 1)
    {
        using (IEnumerator<T> enumerator = enumerable.GetEnumerator())
        {
            Queue<T> queue = new Queue<T>(n);

            for (int i = 0; i < n && enumerator.MoveNext(); i++)
                queue.Enqueue(enumerator.Current);

            while (enumerator.MoveNext())
            {
                queue.Enqueue(enumerator.Current);
                yield return queue.Dequeue();
            }
        }
    }
}
查看更多
我只想做你的唯一
4楼-- · 2020-01-27 03:38

Because I'm not a fan of explicitly using an Enumerator, here's an alternative. Note that the wrapper methods are needed to let invalid arguments throw early, rather than deferring the checks until the sequence is actually enumerated.

public static IEnumerable<T> DropLast<T>(this IEnumerable<T> source)
{
    if (source == null)
        throw new ArgumentNullException("source");

    return InternalDropLast(source);
}

private static IEnumerable<T> InternalDropLast<T>(IEnumerable<T> source)
{
    T buffer = default(T);
    bool buffered = false;

    foreach (T x in source)
    {
        if (buffered)
            yield return buffer;

        buffer = x;
        buffered = true;
    }
}

As per Eric Lippert's suggestion, it easily generalizes to n items:

public static IEnumerable<T> DropLast<T>(this IEnumerable<T> source, int n)
{
    if (source == null)
        throw new ArgumentNullException("source");

    if (n < 0)
        throw new ArgumentOutOfRangeException("n", 
            "Argument n should be non-negative.");

    return InternalDropLast(source, n);
}

private static IEnumerable<T> InternalDropLast<T>(IEnumerable<T> source, int n)
{
    Queue<T> buffer = new Queue<T>(n + 1);

    foreach (T x in source)
    {
        buffer.Enqueue(x);

        if (buffer.Count == n + 1)
            yield return buffer.Dequeue();
    }
}

Where I now buffer before yielding instead of after yielding, so that the n == 0 case does not need special handling.

查看更多
女痞
5楼-- · 2020-01-27 03:38

With C# 8.0 you can use Ranges and indices for that.

var allButLast = sequence[..^1];

By default C# 8.0 requires .NET Core 3.0 or .NET Standard 2.1 (or above). Check this thread to use with older implementations.

查看更多
冷血范
6楼-- · 2020-01-27 03:39

The Enumerable.SkipLast(IEnumerable<TSource>, Int32) method was added in .NET Standard 2.1. It does exactly what you want.

IEnumerable<int> sequence = GetSequenceFromExpensiveSource();

var allExceptLast = sequence.SkipLast(1);

From https://docs.microsoft.com/en-us/dotnet/api/system.linq.enumerable.skiplast

Returns a new enumerable collection that contains the elements from source with the last count elements of the source collection omitted.

查看更多
不美不萌又怎样
7楼-- · 2020-01-27 03:40

You could write:

var list = xyz.Select(x=>x.Id).ToList();
list.RemoveAt(list.Count - 1);
查看更多
登录 后发表回答