¿Cómo tomar todo menos el último elemento en una secuencia usando LINQ?

131

Digamos que tengo una secuencia.

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

Obtener la secuencia no es barata y se genera dinámicamente, y quiero repetirla solo una vez.

Quiero obtener 0 - 999999 (es decir, todo menos el último elemento)

Reconozco que podría hacer algo como:

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

pero eso da como resultado dos enumeraciones sobre la secuencia grande.

¿Hay una construcción LINQ que me permite hacer:

sequence.TakeAllButTheLastElement();
Miguel
fuente
3
Debe elegir entre un algoritmo de eficiencia de espacio O (2n) u O (conteo), donde este último también necesita mover elementos en una matriz interna.
Dykam el
1
Darío, ¿podrías explicarle a alguien a quien no le gusta la gran notación?
alexn
Consulte también esta pregunta duplicada: stackoverflow.com/q/4166493/240733
stakx - ya no contribuye
Terminé con el almacenamiento en caché convirtiendo la colección en Lista y luego llamando sequenceList.RemoveAt(sequence.Count - 1);. En mi caso es aceptable porque después de todas las manipulaciones de LINQ tengo que convertirlo a una matriz o de IReadOnlyCollectiontodos modos. Me pregunto cuál es su caso de uso en el que ni siquiera considera el almacenamiento en caché. Como puedo ver, incluso la respuesta aprobada hace un almacenamiento en caché, por lo que la conversión simple Listes mucho más fácil y más corta en mi opinión.
Pavels Ahmadulins

Respuestas:

64

No conozco una solución de Linq, pero puede codificar fácilmente el algoritmo usted mismo utilizando generadores (rendimiento de rendimiento).

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 como una solución generalizada descartando los últimos n elementos (usando una cola como se sugiere en los comentarios):

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()));
}
Darío
fuente
77
¿Ahora puedes generalizarlo para tomar todo menos el n final?
Eric Lippert el
44
Agradable. Un pequeño error el tamaño de la cola debe inicializarse en n + 1 ya que ese es el tamaño máximo de la cola.
Eric Lippert el
ReSharper no entiende su código ( asignación en expresión condicional ) pero me gusta un poco +1
Грозный
44

Como alternativa a la creación de su propio método y, en un caso, el orden de los elementos no es importante, el siguiente funcionará:

var result = sequence.Reverse().Skip(1);
Kamarey
fuente
49
Tenga en cuenta que esto requiere que tenga suficiente memoria para almacenar toda la secuencia y, por supuesto, TODAVÍA itera la secuencia completa dos veces, una para construir la secuencia invertida y otra para repetirla. Más o menos, esto es peor que la solución Count, sin importar cómo la corte; es más lento y requiere mucha más memoria.
Eric Lippert el
No sé cómo funciona el método 'Reverse', por lo que no estoy seguro de la cantidad de memoria que usa. Pero estoy de acuerdo con dos iteraciones. Este método no debe usarse en colecciones grandes o si un rendimiento es importante.
Kamarey el
55
Pues bien, ¿cómo se implementar inversa? ¿Puedes encontrar una manera en general de hacerlo sin almacenar toda la secuencia?
Eric Lippert el
2
Me gusta y cumple con los criterios de no generar la secuencia dos veces.
Amy B el
12
y además también necesitará invertir la secuencia completa nuevamente para mantenerla, ya que equence.Reverse().Skip(1).Reverse()no es una buena solución
shashwat
42

Como no soy fanático de usar explícitamente una Enumerator, aquí hay una alternativa. Tenga en cuenta que los métodos de envoltura son necesarios para permitir que los argumentos no válidos se lancen temprano, en lugar de diferir las comprobaciones hasta que la secuencia se enumere realmente.

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

Según la sugerencia de Eric Lippert, se generaliza fácilmente a n elementos:

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

Donde ahora amortiguo antes de ceder en lugar de después de ceder, para que el n == 0caso no necesite un manejo especial.

Joren
fuente
En el primer ejemplo, probablemente sería un poco más rápido establecer buffered=falseuna cláusula else antes de asignar buffer. La condición ya se está verificando de todos modos, pero esto evitaría configurar de forma redundante bufferedcada vez a través del bucle.
James
¿Alguien puede decirme los pros / contras de esto versus la respuesta seleccionada?
Sinjai
¿Cuál es la ventaja de tener la implementación en un método separado que carece de las verificaciones de entrada? Además, simplemente abandonaría la implementación única y le daría a la implementación múltiple un valor predeterminado.
jpmc26
@ jpmc26 Con el cheque en un método separado, realmente obtiene la validación en el momento en que llama DropLast. De lo contrario, la validación ocurre solo cuando realmente enumera la secuencia (en la primera llamada al MoveNextresultado IEnumerator). No es una cosa divertida de depurar cuando podría haber una cantidad arbitraria de código entre generarlo IEnumerabley realmente enumerarlo. Hoy en día escribiría InternalDropLastcomo una función interna de DropLast, pero esa funcionalidad no existía en C # cuando escribí este código hace 9 años.
Joren
28

El Enumerable.SkipLast(IEnumerable<TSource>, Int32)método fue agregado en .NET Standard 2.1. Hace exactamente lo que quieres.

IEnumerable<int> sequence = GetSequenceFromExpensiveSource();

var allExceptLast = sequence.SkipLast(1);

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

Devuelve una nueva colección enumerable que contiene los elementos de origen con los últimos elementos de recuento de la colección de origen omitidos.

Justin Lessard
fuente
2
Esto también existe en MoreLinq
Leperkawn
+1 para SkipLast. No sabía esto ya que recientemente vine de .NET Framework y no se molestaron en agregarlo allí.
PRMan
12

No hay nada en el BCL (o en MoreLinq, creo), pero podría crear su propio método de extensión.

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;
        }
    }
}
Noldorin
fuente
Este código no funcionará ... probablemente debería serlo if (!first)y sacarlo first = falsedel if.
Caleb el
Este código se ve mal. La lógica parece ser 'devolver lo no inicializado preven la primera iteración y hacer un ciclo para siempre después de eso'.
Phil Miller el
El código parece tener un error de "tiempo de compilación". Puede ser que quieras corregirlo. Pero sí, podemos escribir un extensor que itera una vez y devuelve todos menos el último elemento.
Manish Basantani el
@Caleb: Tienes toda la razón: ¡escribí esto con mucha prisa! Corregido ahora. @Amby: Erm, no estoy seguro de qué compilador estás usando. No tenía nada de eso. : P
Noldorin
@RobertSchmidt Vaya, sí. Agregué una usingdeclaración ahora.
Noldorin el
7

Sería útil que .NET Framework se enviara con un método de extensión como este.

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();
    }
}
Alex Aza
fuente
3

Una ligera expansión en la elegante solución de 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);
}

Donde shrink implementa un conteo simple hacia adelante para descartar los primeros leftelementos y el mismo búfer descartado para descartar los últimos rightelementos.

silasdavis
fuente
3

Si no tiene tiempo para implementar su propia extensión, esta es una forma más rápida:

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

Una ligera variación en la respuesta aceptada, que (para mis gustos) es un poco más simple:

    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);
        }
    }
jr76
fuente
2

Si puede obtener el Counto Lengthde un enumerable, que en la mayoría de los casos puede, entonces simplementeTake(n - 1)

Ejemplo con matrices

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

Ejemplo con IEnumerable<T>

IEnumerable<int> enu = Enumerable.Range(1, 100);
IEnumerable<int> sub = enu.Take(enu.Count() - 1);
Matthew Layton
fuente
La pregunta es sobre IEnumerables y su solución es qué OP está tratando de evitar. Su código tiene impacto en el rendimiento.
nawfal
1

¿Por qué no solo .ToList<type>()en la secuencia, luego llame y cuente como lo hizo originalmente ... pero como se ha incluido en una lista, no debería hacer una enumeración costosa dos veces? ¿Correcto?

Brady Moritz
fuente
1

La solución que uso para este problema es un poco más elaborada.

Mi clase estática util contiene un método de extensión MarkEndque convierte los T-items en EndMarkedItem<T>-items. Cada elemento está marcado con un extra int, que es 0 ; o (en caso de que uno esté particularmente interesado en los últimos 3 elementos) -3 , -2 o -1 para los últimos 3 elementos.

Esto podría ser útil por sí solo, por ejemplo , cuando desee crear una lista en un foreachbucle simple con comas después de cada elemento, excepto los 2 últimos, con el penúltimo elemento seguido de una palabra de conjunción (como " y " o " o "), y el último elemento seguido de un punto.

Para generar la lista completa sin los últimos n elementos, el método de extensión ButLastsimplemente itera sobre el EndMarkedItem<T>s while EndMark == 0.

Si no especifica tailLength, solo el último elemento se marca (en MarkEnd()) o se suelta (en ButLast()).

Al igual que las otras soluciones, esto funciona mediante almacenamiento en búfer.

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

No creo que pueda ser más sucinto que esto, y también me aseguro de desechar 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;
            }
        }
    }
}

Editar: técnicamente idéntico a esta respuesta .

Robert Schmidt
fuente
1

Con C # 8.0 puede usar rangos e índices para eso.

var allButLast = sequence[..^1];

De manera predeterminada, C # 8.0 requiere .NET Core 3.0 o .NET Standard 2.1 (o superior). Verifique este hilo para usar con implementaciones anteriores.

Emiliano Ruiz
fuente
0

Podrías escribir:

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

Esta es una solución elegante general y en mi humilde opinión que manejará todos los casos correctamente:

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

Mi IEnumerableenfoque tradicional :

/// <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;
    }
}
Chibueze Opata
fuente
Su SkipLastEnumerable puede ser tradicional, pero está roto. El primer elemento que devuelve siempre es una U indefinida, incluso cuando "modelos" tiene 1 elemento. En el último caso, esperaría un resultado vacío.
Robert Schmidt, el
Además, IEnumerator <T> es IDisposable.
Robert Schmidt, el
Verdadero / notado. Gracias.
Chibueze Opata
-1

Una forma simple sería simplemente convertir a una cola y retirar hasta que solo quede la cantidad de elementos que desea omitir.

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();
    }
}
John Stevens
fuente
Hay Take usado para tomar el número conocido de artículos. Y la cola para lo suficientemente grande enumerable es horrible.
Sinatr
-2

Podría ser:

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

Supongo que debería ser como "Dónde" pero preservando el orden (?).

Guillermo Ares
fuente
3
Esa es una forma muy ineficiente de hacerlo. Para evaluar la secuencia. Last () tiene que atravesar la secuencia completa, haciéndolo para cada elemento de la secuencia. O (n ^ 2) eficiencia.
Mike
Tienes razón. No sé lo que estaba pensando cuando escribí este XD. De todos modos, ¿estás seguro de que Last () atravesará toda la secuencia? Para algunas implementaciones de IEnumerable (como Array), esto debería ser O (1). No verifiqué la implementación de la Lista, pero también podría tener un iterador "inverso", comenzando en el último elemento, que también tomaría O (1). Además, debe tener en cuenta que O (n) = O (2n), al menos técnicamente hablando. Por lo tanto, si este procedimiento no es absolutamente crítico para el rendimiento de sus aplicaciones, me quedaría con la secuencia mucho más clara. Tome (secuencia.Count () - 1).
Guillermo Ares
@ Mike, no estoy de acuerdo con tu amigo, secuencia. Last () es O (1), por lo que no necesita recorrer toda la secuencia. stackoverflow.com/a/1377895/812598
GoRoS
1
@GoRoS, es solo O (1) si la secuencia implementa ICollection / IList o es una matriz. Todas las demás secuencias son O (N). En mi pregunta, no asumo que una de las fuentes O (1)
Mike
3
La secuencia puede tener varios elementos que satisfagan esta condición e == secuencia. Último (), por ejemplo, nuevo [] {1, 1, 1, 1}
Sergey Shandar
-2

Si la velocidad es un requisito, este método de la vieja escuela debería ser el más rápido, a pesar de que el código no se ve tan fluido como lo podría hacer linq.

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

Esto requiere que la secuencia sea una matriz, ya que tiene una longitud fija y elementos indexados.

einord
fuente
2
Se trata de un IEnumerable que no permite el acceso a elementos a través de un índice. Tu solución no funciona. Suponiendo que lo hace bien, requiere atravesar la secuencia para determinar la longitud, asignar una matriz de longitud n-1, copiar todos los elementos. - 1. Operaciones 2n-1 y (2n-1) * (4 u 8 bytes) de memoria. Eso ni siquiera es rápido.
Tarik
-4

Probablemente haría algo como esto:

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

Sin embargo, esta es una iteración con una verificación de que no es la última.

einord
fuente
3
Dos razones por las que no es bueno hacer eso. 1) .LastOrDefault () requiere iterar la secuencia completa, y se llama a cada elemento de la secuencia (en .Where ()). 2) Si la secuencia es [1,2,1,2,1,2] y usaste tu técnica, te quedaría con [1,1,1].
Mike