Come prendere tutto tranne l'ultimo elemento di una sequenza usando LINQ?


131

Diciamo che ho una sequenza.

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

Ottenere la sequenza non è economico e viene generato dinamicamente e voglio iterarlo una sola volta.

Voglio ottenere 0 - 999999 (ovvero tutto tranne l'ultimo elemento)

Riconosco che potrei fare qualcosa del tipo:

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

ma ciò si traduce in due enumerazioni sulla sequenza grande.

Esiste un costrutto LINQ che mi permette di fare:

sequence.TakeAllButTheLastElement();

3
Devi scegliere tra un algoritmo di efficienza dello spazio O (2n) o O (conteggio), in cui quest'ultimo deve anche spostare gli elementi in un array interno.
Dykam,

1
Dario, potresti spiegare per qualcuno che non è così in grande notazione?
alex

Si veda anche questa domanda duplicato: stackoverflow.com/q/4166493/240733
stakx - non contribuendo

Ho finito per memorizzarlo nella cache convertendo la raccolta in Elenco e quindi chiamando sequenceList.RemoveAt(sequence.Count - 1);. Nel mio caso è accettabile perché dopo tutte le manipolazioni di LINQ devo convertirlo in array o IReadOnlyCollectioncomunque. Mi chiedo qual è il tuo caso d'uso in cui non consideri nemmeno la memorizzazione nella cache? Come vedo anche la risposta approvata fa un po 'di memorizzazione nella cache così semplice conversione in Listè molto più facile e più breve secondo me.
Pavels Ahmadulins l'

Risposte:


64

Non conosco una soluzione Linq - Ma puoi facilmente codificare l'algoritmo da solo usando i generatori (rendimento).

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

O come soluzione generalizzata che elimina gli ultimi n elementi (usando una coda come suggerita nei commenti):

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

7
Ora puoi generalizzarlo per prendere tutto tranne l'ultimo n?
Eric Lippert,

4
Bello. Un piccolo errore; la dimensione della coda deve essere inizializzata su n + 1 poiché questa è la dimensione massima della coda.
Eric Lippert,

ReSharper non capisce il tuo codice ( assegnazione nell'espressione condizionale ) ma mi piace un po '+1
Грозный,

44

In alternativa alla creazione del proprio metodo e in un caso l'ordine degli elementi non è importante, il prossimo funzionerà:

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

49
Si noti che ciò richiede che si disponga di memoria sufficiente per memorizzare l'intera sequenza e, naturalmente, ANCORA itera due volte l'intera sequenza, una volta per creare la sequenza invertita e una volta per iterarla. Praticamente questo è peggio della soluzione Count, indipendentemente da come la tagli; è più lento E richiede molta più memoria.
Eric Lippert,

Non so come funzioni il metodo "Reverse", quindi non sono sicuro della quantità di memoria utilizzata. Ma sono d'accordo su due iterazioni. Questo metodo non deve essere utilizzato su raccolte di grandi dimensioni o se una performance è importante.
Kamarey,

5
Bene, come vuoi tu implementare inversa? Riesci a trovare un modo in generale per farlo senza memorizzare l'intera sequenza?
Eric Lippert,

2
Mi piace e soddisfa i criteri per non generare la sequenza due volte.
Amy B,

12
e inoltre dovrai anche invertire nuovamente l'intera sequenza per mantenerla poiché non è equence.Reverse().Skip(1).Reverse()una buona soluzione
shashwat

42

Perché non sono un fan dell'uso esplicito di un Enumerator, ecco un'alternativa. Si noti che i metodi wrapper sono necessari per consentire agli argomenti non validi di essere lanciati in anticipo, piuttosto che rinviare i controlli fino a quando la sequenza non viene effettivamente elencata.

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

Come suggerito da Eric Lippert, si generalizza facilmente in n articoli:

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

Dove ora bufferizzo prima di cedere invece che dopo aver ceduto, in modo che il n == 0caso non abbia bisogno di una gestione speciale.


Nel primo esempio, sarebbe probabilmente sempre più veloce impostare buffered=falseuna clausola else prima di assegnare buffer. La condizione è già stata verificata comunque, ma ciò eviterebbe l'impostazione ridondante bufferedogni volta attraverso il loop.
James,

Qualcuno può dirmi i pro / contro di questo rispetto alla risposta selezionata?
Sinjai,

Qual è il vantaggio di avere l'implementazione in un metodo separato privo dei controlli di input? Inoltre, lascerei cadere l'implementazione singola e darei all'implementazione multipla un valore predefinito.
jpmc26

@ jpmc26 Con il check in un metodo separato, si ottiene effettivamente la convalida nel momento in cui si chiama DropLast. In caso contrario, la convalida si verifica solo quando si enumera effettivamente la sequenza (dalla prima chiamata a MoveNextquella risultante IEnumerator). Non è una cosa divertente da eseguire il debug quando potrebbe esserci una quantità arbitraria di codice tra la generazione IEnumerablee l'enumerazione effettiva. Oggi scrivo InternalDropLastcome una funzione interiore di DropLast, ma quella funzionalità non esisteva in C # quando scrissi questo codice 9 anni fa.
Joren,

28

Il Enumerable.SkipLast(IEnumerable<TSource>, Int32)metodo è stato aggiunto in .NET Standard 2.1. Fa esattamente quello che vuoi.

IEnumerable<int> sequence = GetSequenceFromExpensiveSource();

var allExceptLast = sequence.SkipLast(1);

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

Restituisce una nuova raccolta enumerabile che contiene gli elementi dalla fonte con gli ultimi elementi di conteggio della raccolta della fonte omessi.


2
Questo esiste anche in MoreLinq
Leperkawn il

+1 per SkipLast. Non lo sapevo da quando sono venuto recentemente da .NET Framework e non si sono preoccupati di aggiungerlo lì.
PRMan

12

Niente nel BCL (o credo più in LinLin), ma potresti creare il tuo metodo di estensione.

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

Questo codice non funzionerà ... probabilmente dovrebbe essere if (!first)ed estrarre first = falsel'if.
Caleb,

Questo codice sembra fuori. La logica sembra essere "restituire il non inizializzato prevnella prima iterazione, e ripetere per sempre dopo".
Phil Miller,

Il codice sembra avere l'errore "tempo di compilazione". Forse vuoi correggerlo. Sì, possiamo scrivere un extender che scorre una volta e restituisce tutto tranne l'ultimo elemento.
Manish Basantani,

@Caleb: hai assolutamente ragione - l'ho scritto di corsa! Riparato ora. @Amby: Ehm, non sono sicuro di quale compilatore stai usando. Non avevo niente del genere. : P
Noldorin,

@RobertSchmidt Oops, sì. Ho aggiunto una usingdichiarazione ora.
Noldorin,

7

Sarebbe utile se .NET Framework fosse fornito con un metodo di estensione come questo.

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

3

Una leggera espansione sull'elegante soluzione di 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);
}

Dove riduci implementa un semplice conteggio in avanti per eliminare i primi leftmolti elementi e lo stesso buffer scartato per eliminare gli ultimi rightmolti elementi.


3

se non hai tempo per implementare la tua estensione, ecco un modo più veloce:

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

2

Una leggera variazione sulla risposta accettata, che (per i miei gusti) è un po 'più semplice:

    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

Se è possibile ottenere la Counto Lengthdi un enumerabile, che nella maggior parte dei casi è possibile, poi bastaTake(n - 1)

Esempio con array

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

Esempio con IEnumerable<T>

IEnumerable<int> enu = Enumerable.Range(1, 100);
IEnumerable<int> sub = enu.Take(enu.Count() - 1);

La domanda riguarda IEnumerables e la tua soluzione è cosa OP sta cercando di evitare. Il tuo codice ha un impatto sulle prestazioni.
nawfal,

1

Perché non solo .ToList<type>()sulla sequenza, quindi chiama il conteggio e prendi come hai fatto in origine ... ma dal momento che è stato inserito in un elenco, non dovrebbe fare due volte un costoso elenco. Destra?


1

La soluzione che utilizzo per questo problema è leggermente più elaborata.

La mia classe statica util contiene un metodo di estensione MarkEndche converte T-items in EndMarkedItem<T>-items. Ogni elemento è contrassegnato da un ulteriore int, ovvero 0 ; oppure (nel caso in cui uno sia particolarmente interessato agli ultimi 3 articoli) -3 , -2 o -1 per gli ultimi 3 elementi.

Questo potrebbe essere utile da solo, ad esempio quando si desidera creare un elenco in un semplice foreach-loop con virgole dopo ogni elemento tranne gli ultimi 2, con il penultimo elemento seguito da una parola congiunta (come “ e ” o “ o ”) e l'ultimo elemento seguito da un punto.

Per generare l'intero elenco senza gli ultimi n elementi, il metodo di estensione ButLastscorre semplicemente nel EndMarkedItem<T>frattempoEndMark == 0 .

Se non si specifica tailLength, solo l'ultimo elemento viene contrassegnato (in MarkEnd()) o rilasciato (in ButLast()).

Come le altre soluzioni, questo funziona bufferando.

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

Non penso che possa essere più conciso di questo, assicurando anche di disporre IEnumerator<T>:

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

Modifica: tecnicamente identico a questa risposta .


1

Con C # 8.0 è possibile utilizzare intervalli e indici per questo.

var allButLast = sequence[..^1];

Per impostazione predefinita, C # 8.0 richiede .NET Core 3.0 o .NET Standard 2.1 (o versioni successive). Controlla questo thread per usarlo con implementazioni precedenti.


0

Puoi scrivere:

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

0

Questa è una soluzione elegante generale e IMHO che gestirà correttamente tutti i casi:

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

-1

Il mio IEnumerableapproccio tradizionale :

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

SkipLastEnumerable può essere tradizionale, ma è rotto. Il primo elemento che restituisce è sempre una U indefinita, anche quando "modelli" ha 1 elemento. In quest'ultimo caso, mi aspetto un risultato vuoto.
Robert Schmidt,

Inoltre, IEnumerator <T> è IDisposable.
Robert Schmidt,

Vero / osservato. Grazie.
Chibueze Opata,

-1

Un modo semplice sarebbe quello di convertire in una coda e dequeue fino a quando rimane solo il numero di elementi che si desidera saltare.

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

C'è Take utilizzato per prendere il numero noto di elementi. E la coda per un numero abbastanza grande è orribile.
Sinatr,

-2

Potrebbe essere:

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

Immagino che dovrebbe essere come de "Where" ma preservare l'ordine (?).


3
Questo è un modo molto inefficiente per farlo. Per valutare sequenza.Last () deve attraversare l'intera sequenza, facendolo per ogni elemento della sequenza. O (n ^ 2) efficienza.
Mike,

Hai ragione. Non so cosa stavo pensando quando ho scritto questo XD. Ad ogni modo, sei sicuro che Last () attraverserà l'intera sequenza? Per alcune implementazioni di IEnumerable (come Array), questo dovrebbe essere O (1). Non ho verificato l'implementazione dell'elenco, ma potrebbe anche avere un iteratore "inverso", a partire dall'ultimo elemento, che richiederebbe anche O (1). Inoltre, dovresti considerare che O (n) = O (2n), almeno tecnicamente parlando. Quindi, se questa procedura non è assolutamente critica per le prestazioni delle tue app, mi atterrerei con la sequenza molto più chiara.Take (sequence.Count () - 1) .Regards!
Guillermo Ares,

@Mike Non sono d'accordo con te amico, sequence.Last () è O (1), quindi non è necessario attraversare l'intera sequenza. stackoverflow.com/a/1377895/812598
Goros

1
@ GoRoS, è solo O (1) se la sequenza implementa ICollection / IList o è un array. Tutte le altre sequenze sono O (N). Nella mia domanda, non presumo che una delle fonti O (1)
Mike

3
La sequenza può contenere diversi elementi che soddisfano questa condizione e == sequence.Last (), ad esempio new [] {1, 1, 1, 1}
Sergey Shandar,

-2

Se la velocità è un requisito, questa vecchia scuola dovrebbe essere la più veloce, anche se il codice non sembra così fluido come potrebbe farlo Linq.

int[] newSequence = int[sequence.Length - 1];
for (int x = 0; x < sequence.Length - 1; x++)
{
    newSequence[x] = sequence[x];
}

Ciò richiede che la sequenza sia un array poiché ha una lunghezza fissa e elementi indicizzati.


2
Hai a che fare con un IEnumerable che non consente l'accesso agli elementi tramite un indice. La tua soluzione non funziona Supponendo di farlo nel modo giusto, è necessario attraversare la sequenza per determinare la lunghezza, allocare un array di lunghezza n-1, copiare tutti gli elementi. - 1. Operazioni 2n-1 e (2n-1) * (4 o 8 byte) di memoria. Non è nemmeno veloce.
Tarik,

-4

Probabilmente farei qualcosa del genere:

sequence.Where(x => x != sequence.LastOrDefault())

Questa è un'iterazione con un controllo che non è l'ultimo per ogni volta però.


3
Due motivi per cui non è una buona cosa. 1) .LastOrDefault () richiede l'iterazione dell'intera sequenza, e questo è chiamato per ogni elemento della sequenza (in .Where ()). 2) Se la sequenza è [1,2,1,2,1,2] e hai usato la tua tecnica, rimarrai con [1,1,1].
Mike,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.