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条回答
▲ chillily
2楼-- · 2020-01-27 03:50

I don't know a Linq solution - But you can easily code the algorithm by yourself using generators (yield return).

public static IEnumerable<T> TakeAllButLast<T>(this IEnumerable<T> source) {
    var it = source.GetEnumerator();
    bool hasRemainingItems = false;
    bool isFirst = true;
    T item = default(T);

    do {
        hasRemainingItems = it.MoveNext();
        if (hasRemainingItems) {
            if (!isFirst) yield return item;
            item = it.Current;
            isFirst = false;
        }
    } while (hasRemainingItems);
}

static void Main(string[] args) {
    var Seq = Enumerable.Range(1, 10);

    Console.WriteLine(string.Join(", ", Seq.Select(x => x.ToString()).ToArray()));
    Console.WriteLine(string.Join(", ", Seq.TakeAllButLast().Select(x => x.ToString()).ToArray()));
}

Or as a generalized solution discarding the last n items (using a queue like suggested in the comments):

public static IEnumerable<T> SkipLastN<T>(this IEnumerable<T> source, int n) {
    var  it = source.GetEnumerator();
    bool hasRemainingItems = false;
    var  cache = new Queue<T>(n + 1);

    do {
        if (hasRemainingItems = it.MoveNext()) {
            cache.Enqueue(it.Current);
            if (cache.Count > n)
                yield return cache.Dequeue();
        }
    } while (hasRemainingItems);
}

static void Main(string[] args) {
    var Seq = Enumerable.Range(1, 4);

    Console.WriteLine(string.Join(", ", Seq.Select(x => x.ToString()).ToArray()));
    Console.WriteLine(string.Join(", ", Seq.SkipLastN(3).Select(x => x.ToString()).ToArray()));
}
查看更多
劫难
3楼-- · 2020-01-27 03:50

The solution that I use for this problem is slightly more elaborate.

My util static class contains an extension method MarkEnd which converts the T-items in EndMarkedItem<T>-items. Each element is marked with an extra int, which is either 0; or (in case one is particularly interested in the last 3 items) -3, -2, or -1 for the last 3 items.

This could be useful on its own, e.g. when you want to create a list in a simple foreach-loop with commas after each element except the last 2, with the second-to-last item followed by a conjunction word (such as “and” or “or”), and the last element followed by a point.

For generating the entire list without the last n items, the extension method ButLast simply iterates over the EndMarkedItem<T>s while EndMark == 0.

If you don’t specify tailLength, only the last item is marked (in MarkEnd()) or dropped (in ButLast()).

Like the other solutions, this works by buffering.

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

namespace Adhemar.Util.Linq {

    public struct EndMarkedItem<T> {
        public T Item { get; private set; }
        public int EndMark { get; private set; }

        public EndMarkedItem(T item, int endMark) : this() {
            Item = item;
            EndMark = endMark;
        }
    }

    public static class TailEnumerables {

        public static IEnumerable<T> ButLast<T>(this IEnumerable<T> ts) {
            return ts.ButLast(1);
        }

        public static IEnumerable<T> ButLast<T>(this IEnumerable<T> ts, int tailLength) {
            return ts.MarkEnd(tailLength).TakeWhile(te => te.EndMark == 0).Select(te => te.Item);
        }

        public static IEnumerable<EndMarkedItem<T>> MarkEnd<T>(this IEnumerable<T> ts) {
            return ts.MarkEnd(1);
        }

        public static IEnumerable<EndMarkedItem<T>> MarkEnd<T>(this IEnumerable<T> ts, int tailLength) {
            if (tailLength < 0) {
                throw new ArgumentOutOfRangeException("tailLength");
            }
            else if (tailLength == 0) {
                foreach (var t in ts) {
                    yield return new EndMarkedItem<T>(t, 0);
                }
            }
            else {
                var buffer = new T[tailLength];
                var index = -buffer.Length;
                foreach (var t in ts) {
                    if (index < 0) {
                        buffer[buffer.Length + index] = t;
                        index++;
                    }
                    else {
                        yield return new EndMarkedItem<T>(buffer[index], 0);
                        buffer[index] = t;
                        index++;
                        if (index == buffer.Length) {
                            index = 0;
                        }
                    }
                }
                if (index >= 0) {
                    for (var i = index; i < buffer.Length; i++) {
                        yield return new EndMarkedItem<T>(buffer[i], i - buffer.Length - index);
                    }
                    for (var j = 0; j < index; j++) {
                        yield return new EndMarkedItem<T>(buffer[j], j - index);
                    }
                }
                else {
                    for (var k = 0; k < buffer.Length + index; k++) {
                        yield return new EndMarkedItem<T>(buffer[k], k - buffer.Length - index);
                    }
                }
            }    
        }
    }
}
查看更多
聊天终结者
4楼-- · 2020-01-27 03:51

A simple way would be to just convert to a queue and dequeue until only the number of items you want to skip is left.

public static IEnumerable<T> SkipLast<T>(this IEnumerable<T> source, int n)
{
    var queue = new Queue<T>(source);

    while (queue.Count() > n)
    {
        yield return queue.Dequeue();
    }
}
查看更多
劳资没心,怎么记你
5楼-- · 2020-01-27 03:51

Could be:

var allBuLast = sequence.TakeWhile(e => e != sequence.Last());

I guess it should be like de "Where" but preserving the order(?).

查看更多
聊天终结者
6楼-- · 2020-01-27 03:53

Nothing in the BCL (or MoreLinq I believe), but you could create your own extension method.

public static IEnumerable<T> TakeAllButLast<T>(this IEnumerable<T> source)
{
    using (var enumerator = source.GetEnumerator())
        bool first = true;
        T prev;
        while(enumerator.MoveNext())
        {
            if (!first)
                yield return prev;
            first = false;
            prev = enumerator.Current;
        }
    }
}
查看更多
混吃等死
7楼-- · 2020-01-27 03:54

if you don't have time to roll out your own extension, here's a quicker way:

var next = sequence.First();
sequence.Skip(1)
    .Select(s => 
    { 
        var selected = next;
        next = s;
        return selected;
    });
查看更多
登录 后发表回答