Como pegar todos, exceto o último elemento em uma sequência usando o LINQ?

131

Digamos que eu tenho uma sequência.

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

Obter a sequência não é barato e é gerado dinamicamente, e quero iterá-la apenas uma vez.

Quero obter 0 - 999999 (ou seja, tudo, menos o último elemento)

Reconheço que eu poderia fazer algo como:

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

mas isso resulta em duas enumerações na grande sequência.

Existe uma construção LINQ que me permite fazer:

sequence.TakeAllButTheLastElement();
Mike
fonte
3
Você deve escolher entre um algoritmo de eficiência de tempo O (2n) ou O (contagem) de espaço, onde o último também precisa mover itens em uma matriz interna.
Dykam 22/11/2009
1
Dario, você poderia, por favor, explicar para alguém que não gosta disso?
alexn
Veja também esta pergunta duplicada: stackoverflow.com/q/4166493/240733
stakx - não está mais contribuindo com o dia
Acabei armazenando em cache convertendo a coleção para List e depois ligando sequenceList.RemoveAt(sequence.Count - 1);. No meu caso, é aceitável porque, depois de todas as manipulações do LINQ, tenho que convertê-lo em array ou de IReadOnlyCollectionqualquer maneira. Gostaria de saber qual é o seu caso de uso em que você nem considera o cache? Como posso ver, mesmo a resposta aprovada faz algum armazenamento em cache tão simples de converter Listé muito mais fácil e mais curto na minha opinião.
Pavels Ahmadulins

Respostas:

64

Não conheço uma solução Linq - mas você pode codificar facilmente o algoritmo usando geradores (retorno de 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()));
}

Ou como uma solução generalizada descartando os últimos n itens (usando uma fila como sugerido nos comentários):

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()));
}
Dario
fonte
7
Agora você pode generalizar para levar tudo, exceto o n final?
22611 Eric Lippert
4
Agradável. Um pequeno erro; o tamanho da fila deve ser inicializado em n + 1, pois esse é o tamanho máximo da fila.
22611 Eric Lippert
O ReSharper não entendo o seu código ( atribuição em expressão condicional ), mas eu meio que gosto disso +1
Грозный
44

Como uma alternativa para criar seu próprio método e, em um caso, a ordem dos elementos não for importante, o próximo funcionará:

var result = sequence.Reverse().Skip(1);
Kamarey
fonte
49
Observe que isso requer que você tenha memória suficiente para armazenar a sequência inteira e, é claro, AINDA itera a sequência inteira duas vezes, uma vez para criar a sequência reversa e outra para iterá-la. Isso é pior do que a solução Count, não importa como você a corta; é mais lento E leva muito mais memória.
22611 Eric Lippert
Não sei como o método 'Reverse' funciona, por isso não tenho certeza da quantidade de memória usada. Mas concordo em duas iterações. Este método não deve ser usado em coleções grandes ou se um desempenho for importante.
Kamarey #
5
Bem, como você implementaria o Reverse? Você pode descobrir uma maneira geral de fazê-lo sem armazenar a sequência inteira?
Eric Lippert
2
Eu gosto, e ele atende aos critérios de não gerar a sequência duas vezes.
Amy B
12
e, além disso você também vai precisar para reverter a sequência inteira novamente para mantê-lo como ele é equence.Reverse().Skip(1).Reverse()não uma boa solução
Shashwat
42

Porque eu não sou fã de usar explicitamente um Enumerator, aqui está uma alternativa. Observe que os métodos do wrapper são necessários para permitir que argumentos inválidos sejam lançados mais cedo, em vez de adiar as verificações até que a sequência seja realmente enumerada.

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

De acordo com a sugestão de Eric Lippert, generaliza facilmente n itens:

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

Onde agora eu buffer antes de render em vez de depois de render, para que o n == 0caso não precise de tratamento especial.

Joren
fonte
No primeiro exemplo, provavelmente seria um pouco mais rápido definir buffered=falseuma cláusula else antes de atribuir buffer. A condição já está sendo verificada de qualquer maneira, mas isso evitaria a configuração redundante de bufferedcada vez no loop.
James
Alguém pode me dizer os prós / contras disso e a resposta selecionada?
Sinjai 8/09/17
Qual é a vantagem de ter a implementação em um método separado que não possui as verificações de entrada? Além disso, eu descartaria a implementação única e daria à implementação múltipla um valor padrão.
Jpmc26
@ jpmc26 Com a verificação em um método separado, você obtém a validação no momento em que chama DropLast. Caso contrário, a validação ocorrerá somente quando você realmente enumerar a sequência (na primeira chamada ou MoveNextna resultante IEnumerator). Não é uma coisa divertida para depurar quando pode haver uma quantidade arbitrária de código entre a geração IEnumerablee a enumeração. Hoje em dia eu escreveria InternalDropLastcomo uma função interna de DropLast, mas essa funcionalidade não existia em C # quando escrevi esse código há 9 anos.
Joren
28

O Enumerable.SkipLast(IEnumerable<TSource>, Int32)método foi adicionado no .NET Standard 2.1. Faz exatamente o que você quer.

IEnumerable<int> sequence = GetSequenceFromExpensiveSource();

var allExceptLast = sequence.SkipLast(1);

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

Retorna uma nova coleção enumerável que contém os elementos da origem com os últimos elementos de contagem omitidos.

Justin Lessard
fonte
2
Isso também existe no MoreLinq
Leperkawn
+1 para SkipLast. Eu não sabia disso desde que vim recentemente do .NET Framework e eles não se preocuparam em adicioná-lo lá.
PRMan 31/01
12

Nada no BCL (ou MoreLinq eu acredito), mas você pode criar seu próprio método de extensão.

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
fonte
Esse código não funcionará ... provavelmente deveria estar if (!first)e first = falsesair do if.
Caleb
Este código parece fora. A lógica parece ser 'retornar o não inicializado prevna primeira iteração e girar para sempre depois disso'.
22120 Phil Miller
O código parece ter erro "tempo de compilação". Pode ser que você queira corrigi-lo. Mas sim, podemos escrever um extensor que itera uma vez e retorna tudo, exceto o último item.
Manish Basantani 22/11/2009
@Caleb: Você está absolutamente certo - eu escrevi isso com pressa! Corrigido agora. @ Amby: Erm, não sei qual compilador você está usando. Eu não tinha nada disso. : P
Noldorin
@RobertSchmidt Ops, sim. Eu adicionei uma usingdeclaração agora.
Noldorin
7

Seria útil se o .NET Framework fosse fornecido com um método de extensão 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
fonte
3

Uma pequena expansão na solução elegante 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);
}

Onde o shrink implementa uma contagem simples para a frente para descartar os primeiros leftmuitos elementos e o mesmo buffer descartado para descartar os últimos rightmuitos elementos.

silasdavis
fonte
3

se você não tiver tempo para lançar sua própria extensão, eis uma maneira mais rápida:

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

Uma pequena variação na resposta aceita, que (para o meu gosto) é um pouco mais simples:

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

Se você pode obter o Countou Lengthde um enumerável, o que, na maioria dos casos, é possível, bastaTake(n - 1)

Exemplo com matrizes

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

Exemplo com IEnumerable<T>

IEnumerable<int> enu = Enumerable.Range(1, 100);
IEnumerable<int> sub = enu.Take(enu.Count() - 1);
Matthew Layton
fonte
A pergunta é sobre IEnumerables e sua solução é o que o OP está tentando evitar. Seu código tem impacto no desempenho.
Nawfal
1

Por que não apenas .ToList<type>()na sequência, então conte e atenda a chamada como você fez originalmente ... mas, como foi inserida em uma lista, ela não deve fazer uma enumeração cara duas vezes. Certo?

Brady Moritz
fonte
1

A solução que eu uso para esse problema é um pouco mais elaborada.

Minha classe estática util contém um método de extensão MarkEndque converte os Titens em EndMarkedItem<T>itens. Cada elemento é marcado com um extra int, que é 0 ; ou (caso um esteja particularmente interessado nos últimos 3 itens) -3 , -2 ou -1 nos últimos 3 itens.

Isso pode ser útil por si só, por exemplo , quando você deseja criar uma lista em um foreachloop simples com vírgulas após cada elemento, exceto os últimos 2, com o penúltimo item seguido por uma palavra conjunta (como " e " ou " ou ") e o último elemento seguido de um ponto.

Para gerar a lista inteira sem os últimos n itens, o método de extensão ButLastsimplesmente itera ao longo dos EndMarkedItem<T>segundos EndMark == 0.

Se você não especificar tailLength, apenas o último item será marcado (in MarkEnd()) ou descartado (in ButLast()).

Como as outras soluções, isso funciona em buffer.

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

Eu não acho que pode ser mais sucinto do que isso - também garantindo Dispose the 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;
            }
        }
    }
}

Edit: tecnicamente idêntico a esta resposta .

Robert Schmidt
fonte
1

Com o C # 8.0, você pode usar intervalos e índices para isso.

var allButLast = sequence[..^1];

Por padrão, o C # 8.0 requer o .NET Core 3.0 ou o .NET Standard 2.1 (ou superior). Marque esta discussão para usar com implementações mais antigas.

Emiliano Ruiz
fonte
0

Você pode escrever:

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

Esta é uma solução geral e elegante do IMHO que manipulará todos os casos corretamente:

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

Minha IEnumerableabordagem 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
fonte
Seu SkipLastEnumerable pode ser tradicional, mas está corrompido. O primeiro elemento que ele retorna é sempre um U indefinido - mesmo quando "modelos" possui 1 elemento. No último caso, eu esperaria um resultado vazio.
Robert Schmidt
Além disso, IEnumerator <T> é IDisposable.
9788 Robert Schmidt #
Verdadeiro / observado. Obrigado.
Chibueze Opata
-1

Uma maneira simples seria apenas converter para uma fila e desenfileirar até restar apenas o número de itens que você deseja pular.

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
fonte
Take usado para levar um número conhecido de itens. E fila para grandes o suficiente enumerável é horrível.
Sinatr 13/02/19
-2

Poderia ser:

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

Eu acho que deveria ser como "Onde", mas preservando a ordem (?).

Guillermo Ares
fonte
3
Essa é uma maneira muito ineficiente de fazer isso. Para avaliar sequence.Last (), é necessário percorrer a sequência inteira, fazendo-o para cada elemento na sequência. O (n ^ 2) eficiência.
22613 Mike
Você está certo. Não sei o que estava pensando quando escrevi este XD. Enfim, você tem certeza de que Last () atravessará toda a sequência? Para algumas implementações de IEnumerable (como Array), isso deve ser O (1). Não verifiquei a implementação da lista, mas também poderia ter um iterador "reverso", iniciando no último elemento, que também levaria O (1). Além disso, você deve levar em consideração O (n) = O (2n), pelo menos tecnicamente falando. Portanto, se esse procedimento não for absolutamente crítico para o desempenho de seus aplicativos, eu continuaria com a sequência muito mais clara.Tome (sequence.Count () - 1).
Guillermo Ares
@ Mike Eu não concordo com você companheiro, sequence.Last () é O (1) para que ele não precise percorrer toda a sequência. stackoverflow.com/a/1377895/812598
GoRoS 15/01
1
@GoRoS, é apenas O (1) se a sequência implementa ICollection / IList ou é uma matriz. Todas as outras sequências são O (N). Na minha pergunta, eu não assumir que um dos O (1) fontes
Mike
3
A sequência pode ter vários itens que satisfaz esta condição E == sequence.Last (), por exemplo novo [] {1, 1, 1, 1}
Sergey Shandar
-2

Se a velocidade é um requisito, essa maneira antiga deve ser a mais rápida, mesmo que o código não pareça tão suave quanto o linq poderia.

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

Isso requer que a sequência seja uma matriz, pois possui um comprimento fixo e itens indexados.

einord
fonte
2
Você está lidando com um IEnumerable que não permite acesso a elementos por meio de um índice. Sua solução não funciona. Supondo que você faça certo, é necessário percorrer a sequência para determinar o comprimento, alocar uma matriz de comprimento n-1, copiar todos os elementos. - 1. operações 2n-1 e (2n-1) * (4 ou 8 bytes) de memória. Isso nem é rápido.
Tarik
-4

Eu provavelmente faria algo assim:

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

Esta é uma iteração com uma verificação de que não é a última de cada vez.

einord
fonte
3
Duas razões que não é uma boa coisa a fazer. 1) .LastOrDefault () requer a iteração de toda a sequência, e isso é chamado para cada elemento na sequência (no .Where ()). 2) Se a sequência for [1,2,1,2,1,2] e você usou sua técnica, você ficará com [1,1,1].
Mike