wake-up-neo.com

Wie kann mit LINQ alles außer dem letzten Element in einer Sequenz genommen werden?

Nehmen wir an, ich habe eine Sequenz.

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

Die Sequenz zu bekommen ist nicht billig und wird dynamisch generiert, und ich möchte sie nur einmal durchlaufen.

Ich möchte 0 - 999999 erhalten (d. H. Alles außer dem letzten Element)

Ich erkenne, dass ich so etwas tun könnte:

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

dies führt jedoch zu zwei Aufzählungen über die große Sequenz.

Gibt es ein LINQ-Konstrukt, mit dem ich Folgendes tun kann:

sequence.TakeAllButTheLastElement();
108
Mike

Ich kenne keine Linq-Lösung - aber Sie können den Algorithmus einfach selbst mit Generatoren codieren (Renditeertrag). 

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()));
}

Oder als verallgemeinerte Lösung, die die letzten n Elemente verwirft (unter Verwendung einer Warteschlange wie in den Kommentaren vorgeschlagen):

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()));
}
49
Dario

Als Alternative zum Erstellen einer eigenen Methode und wenn die Reihenfolge der Elemente nicht wichtig ist, funktioniert die nächste:

var result = sequence.Reverse().Skip(1);
40
Kamarey

Da ich kein Fan von Enumerator bin, ist hier eine Alternative. Beachten Sie, dass die Wrapper-Methoden erforderlich sind, um ungültige Argumente früh werfen zu lassen, anstatt die Prüfungen zu verschieben, bis die Sequenz tatsächlich aufgelistet ist.

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;
    }
}

Wie von Eric Lippert vorgeschlagen, kann er leicht auf n Elemente verallgemeinert werden:

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();
    }
}

Wo ich jetzt vorher nachgebe statt nachgebe, puffere, so dass der n == 0-Fall keine besondere Behandlung erfordert.

39
Joren

Nichts in der BCL (oder MoreLinq glaube ich), aber Sie könnten Ihre eigene Erweiterungsmethode erstellen.

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;
        }
    }
}
12
Noldorin

Es wäre hilfreich, wenn .NET Framework mit einer solchen Erweiterungsmethode ausgeliefert würde.

public static IEnumerable<T> SkipLast<T>(this IEnumerable<T> source, int count)
{
    var enumerator = source.GetEnumerator();
    var queue = new Queue<T>(count + 1);

    while (true)
    {
        if (!enumerator.MoveNext())
            break;
        queue.Enqueue(enumerator.Current);
        if (queue.Count > count)
            yield return queue.Dequeue();
    }
}
7
Alex Aza

Für diejenigen, die eine neuere Version von .net verwenden, wurde die Methode Enumerable.SkipLast(IEnumerable<TSource>, Int32) in .NET Core 2.0 hinzugefügt

var sequence = GetSequence();

var allExceptLast = sequence.SkipLast(1);

Quelle: https://docs.Microsoft.com/en-us/dotnet/api/system.linq.enumerable.skiplast

5
Justin Lessard

wenn Sie keine Zeit haben, Ihre eigene Erweiterung einzuführen, ist dies ein schneller Weg:

var next = sequence.First();
sequence.Skip(1)
    .Select(s => 
    { 
        var selected = next;
        next = s;
        return selected;
    });
4
SmallBizGuy

Eine leichte Erweiterung der eleganten Lösung von Joren:

public static IEnumerable<T> Shrink<T>(this IEnumerable<T> source, int left, int right)
{
    int i = 0;
    var buffer = new Queue<T>(right + 1);

    foreach (T x in source)
    {
        if (i >= left) // Read past left many elements at the start
        {
            buffer.Enqueue(x);
            if (buffer.Count > right) // Build a buffer to drop right many elements at the end
                yield return buffer.Dequeue();    
        } 
        else i++;
    }
}
public static IEnumerable<T> WithoutLast<T>(this IEnumerable<T> source, int n = 1)
{
    return source.Shrink(0, n);
}
public static IEnumerable<T> WithoutFirst<T>(this IEnumerable<T> source, int n = 1)
{
    return source.Shrink(n, 0);
}

Wo Schrumpfen eine einfache Zählung vorwärts implementiert, um die ersten left viele Elemente und den gleichen verworfenen Puffer zu löschen, um die letzten right vielen Elemente zu löschen. 

3
silasdavis

Eine geringfügige Variation der akzeptierten Antwort, die (für meinen Geschmack) etwas einfacher ist:

    public static IEnumerable<T> AllButLast<T>(this IEnumerable<T> enumerable, int n = 1)
    {
        // for efficiency, handle degenerate n == 0 case separately 
        if (n == 0)
        {
            foreach (var item in enumerable)
                yield return item;
            yield break;
        }

        var queue = new Queue<T>(n);
        foreach (var item in enumerable)
        {
            if (queue.Count == n)
                yield return queue.Dequeue();

            queue.Enqueue(item);
        }
    }
2
jr76

Wenn Sie die Count oder Length einer Aufzählungsliste erhalten können, was in den meisten Fällen möglich ist, dann Take(n - 1)

Beispiel mit Arrays

int[] arr = new int[] { 1, 2, 3, 4, 5 };
int[] sub = arr.Take(arr.Length - 1).ToArray();

Beispiel mit IEnumerable<T>

IEnumerable<int> enu = Enumerable.Range(1, 100);
IEnumerable<int> sub = enu.Take(enu.Count() - 1);
1
series0ne
    public static IEnumerable<T> NoLast<T> (this IEnumerable<T> items) {
        if (items != null) {
            var e = items.GetEnumerator();
            if (e.MoveNext ()) {
                T head = e.Current;
                while (e.MoveNext ()) {
                    yield return head; ;
                    head = e.Current;
                }
            }
        }
    }
1
ddur

Ich glaube nicht, dass es prägnanter werden kann als dies - und auch den IEnumerator<T> zu entsorgen:

public static IEnumerable<T> SkipLast<T>(this IEnumerable<T> source)
{
    using (var it = source.GetEnumerator())
    {
        if (it.MoveNext())
        {
            var item = it.Current;
            while (it.MoveNext())
            {
                yield return item;
                item = it.Current;
            }
        }
    }
}

Edit: technisch identisch mit dieser Antwort .

1
Robert Schmidt

Die Lösung, die ich für dieses Problem verwende, ist etwas komplizierter.

Meine nützliche statische Klasse enthält eine Erweiterungsmethode MarkEnd, die die T- Elemente in EndMarkedItem<T>- Elemente konvertiert. Jedes Element ist mit einer zusätzlichen int markiert, die entweder 0 ; oder (falls man besonders an den letzten 3 Punkten interessiert ist) -3 , -2 oder -1 für die letzten 3 Elemente.

Dies kann allein nützlich sein, z., wenn Sie eine Liste in einer einfachen foreach-Schleife mit Kommas nach jedem Element mit Ausnahme der letzten 2 erstellen möchten, mit dem vorletzten Element, gefolgt von einem Konjunktions-Word (wie „undoder ”) und das letzte Element gefolgt von einem Punkt.

Für die Generierung der gesamten Liste ohne die letzten n - Elemente durchläuft die Erweiterungsmethode ButLast einfach die EndMarkedItem<T>s und EndMark == 0s.

Wenn Sie keine tailLength angeben, wird nur das letzte Element markiert (in MarkEnd()) oder gelöscht (in ButLast()).

Wie bei den anderen Lösungen funktioniert dies durch Pufferung.

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);
                    }
                }
            }    
        }
    }
}
1
Adhemar

Dies ist eine allgemeine und IMHO elegante Lösung, die alle Fälle richtig behandelt:

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();
            }
        }
    }
}
0
Tarik

Warum nicht einfach .ToList<type>() in der Sequenz, dann call count und take, wie Sie es ursprünglich getan haben .. aber da es in eine Liste aufgenommen wurde, sollte es nicht zweimal eine teure Aufzählung machen. Recht?

0
Brady Moritz

Sie könnten schreiben:

var list = xyz.Select(x=>x.Id).ToList();
list.RemoveAt(list.Count - 1);
0
RoJaIt