Comment prendre tout sauf le dernier élément d'une séquence en utilisant LINQ?


131

Disons que j'ai une séquence.

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

Obtenir la séquence n'est pas bon marché et est généré dynamiquement, et je veux l'itérer une seule fois.

Je veux obtenir 0 - 999999 (c'est-à-dire tout sauf le dernier élément)

Je reconnais que je pourrais faire quelque chose comme:

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

mais cela se traduit par deux énumérations sur la grande séquence.

Existe-t-il une construction LINQ qui me permet de faire:

sequence.TakeAllButTheLastElement();

3
Vous devez choisir entre un algorithme d'efficacité spatiale O (2n) ou O (count), où ce dernier doit également déplacer des éléments dans un tableau interne.
Dykam

1
Dario, pourriez-vous s'il vous plaît expliquer à quelqu'un qui n'aime pas ça en gros o-notation?
alexn


J'ai fini par le mettre en cache en convertissant la collection en liste, puis en appelant sequenceList.RemoveAt(sequence.Count - 1);. Dans mon cas, c'est acceptable car après toutes les manipulations LINQ, je dois le convertir en tableau ou de IReadOnlyCollectiontoute façon. Je me demande quel est votre cas d'utilisation où vous n'envisagez même pas la mise en cache? Comme je peux le voir, même la réponse approuvée fait une certaine mise en cache, si simple la conversion en Listest beaucoup plus facile et plus courte à mon avis.
Pavels Ahmadulins

Réponses:


64

Je ne connais pas de solution Linq - Mais vous pouvez facilement coder l'algorithme vous-même à l'aide de générateurs (rendement de rendement).

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 comme solution généralisée en supprimant les n derniers éléments (en utilisant une file d'attente comme suggéré dans les commentaires):

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
Maintenant pouvez-vous généraliser pour prendre tout sauf le n final?
Eric Lippert

4
Agréable. Une petite erreur; la taille de la file d'attente doit être initialisée à n + 1 car c'est la taille maximale de la file d'attente.
Eric Lippert

ReSharper ne comprend pas votre code ( affectation dans une expression conditionnelle ) mais je l'aime un peu +1
Грозный

44

Au lieu de créer votre propre méthode et dans un cas où l'ordre des éléments n'est pas important, la suivante fonctionnera:

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

49
Notez que cela nécessite que vous ayez suffisamment de mémoire pour stocker la séquence entière, et bien sûr, il itère TOUJOURS la séquence entière deux fois, une fois pour construire la séquence inversée et une fois pour l'itérer. C'est à peu près pire que la solution Count, quelle que soit la manière dont vous la découpez; c'est plus lent ET prend beaucoup plus de mémoire.
Eric Lippert

Je ne sais pas comment fonctionne la méthode «Reverse», donc je ne suis pas sûr de la quantité de mémoire utilisée. Mais je suis d'accord sur deux itérations. Cette méthode ne doit pas être utilisée sur de grandes collections ou si une performance est importante.
Kamarey

5
Eh bien, comment implémenteriez- vous Reverse? Pouvez-vous trouver un moyen en général de le faire sans stocker la séquence entière?
Eric Lippert

2
Je l'aime bien, et il répond aux critères de ne pas générer la séquence deux fois.
Amy B

12
et en outre, vous devrez également inverser à nouveau toute la séquence pour la conserver car ce n'est equence.Reverse().Skip(1).Reverse()pas une bonne solution
shashwat

42

Parce que je ne suis pas fan de l'utilisation explicite d'un Enumerator, voici une alternative. Notez que les méthodes wrapper sont nécessaires pour laisser les arguments non valides être lancés tôt, plutôt que de différer les vérifications jusqu'à ce que la séquence soit réellement énumérée.

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

Selon la suggestion d'Eric Lippert, il se généralise facilement à n éléments:

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

Où je tamponne maintenant avant de céder au lieu après avoir cédé, de sorte que le n == 0cas ne nécessite pas de traitement spécial.


Dans le premier exemple, il serait probablement un peu plus rapide de définir buffered=falseune clause else avant de l'assigner buffer. La condition est déjà vérifiée de toute façon, mais cela éviterait un réglage redondant à bufferedchaque fois dans la boucle.
James

Quelqu'un peut-il me dire les avantages / inconvénients de ceci par rapport à la réponse choisie?
Sinjai

Quel est l'avantage d'avoir l'implémentation dans une méthode distincte qui ne dispose pas des contrôles d'entrée? En outre, je supprimerais simplement l'implémentation unique et donnerais à l'implémentation multiple une valeur par défaut.
jpmc26

@ jpmc26 Avec l'enregistrement dans une méthode distincte, vous obtenez en fait la validation au moment où vous appelez DropLast. Sinon, la validation se produit uniquement lorsque vous énumérez réellement la séquence (lors du premier appel à MoveNextsur le résultat IEnumerator). Ce n'est pas une chose amusante à déboguer quand il pourrait y avoir une quantité arbitraire de code entre la génération IEnumerableet l'énumération réelle. Aujourd'hui, j'écrirais en InternalDropLasttant que fonction interne de DropLast, mais cette fonctionnalité n'existait pas en C # lorsque j'ai écrit ce code il y a 9 ans.
Joren

28

La Enumerable.SkipLast(IEnumerable<TSource>, Int32)méthode a été ajoutée dans .NET Standard 2.1. Il fait exactement ce que vous voulez.

IEnumerable<int> sequence = GetSequenceFromExpensiveSource();

var allExceptLast = sequence.SkipLast(1);

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

Renvoie une nouvelle collection énumérable qui contient les éléments de la source avec les derniers éléments de comptage de la collection source omis.


2
Cela existe également dans MoreLinq
Leperkawn

+1 pour SkipLast. Je ne le savais pas depuis que je suis récemment venu de .NET Framework et ils n'ont pas pris la peine de l'ajouter ici.
PRMan le

12

Rien dans le BCL (ou MoreLinq je crois), mais vous pouvez créer votre propre méthode d'extension.

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

Ce code ne fonctionnera pas ... devrait probablement l'être if (!first)et sortez first = falsedu if.
Caleb

Ce code semble éteint. La logique semble être «renvoyer le non initialisé prevdans la première itération et boucler pour toujours après cela».
Phil Miller du

Le code semble avoir une erreur "à la compilation". Vous souhaitez peut-être le corriger. Mais oui, nous pouvons écrire un extender qui itère une fois et retourne tout sauf le dernier élément.
Manish Basantani

@Caleb: Vous avez tout à fait raison - j'ai écrit ceci très rapidement! Corrigé maintenant. @Amby: Euh, je ne sais pas quel compilateur vous utilisez. Je n'avais rien de tel. : P
Noldorin

@RobertSchmidt Oups, oui. J'ai ajouté une usingdéclaration maintenant.
Noldorin

7

Il serait utile que .NET Framework soit livré avec une méthode d'extension comme celle-ci.

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

Une légère extension de la solution élégante 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);
}

Où shrink implémente un simple comptage en avant pour supprimer les premiers leftéléments et le même tampon rejeté pour supprimer les derniers rightéléments.


3

si vous n'avez pas le temps de déployer votre propre extension, voici un moyen plus rapide:

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

2

Une légère variation sur la réponse acceptée, qui (à mon goût) est un peu plus 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);
        }
    }

2

Si vous pouvez obtenir le Countou Lengthd'un énumérable, ce que vous pouvez dans la plupart des cas, alorsTake(n - 1)

Exemple avec des tableaux

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

Exemple avec IEnumerable<T>

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

La question concerne IEnumerables et votre solution est ce qu'OP essaie d'éviter. Votre code a un impact sur les performances.
nawfal

1

Pourquoi pas seulement .ToList<type>()sur la séquence, puis appelez count et prenez comme vous l'avez fait à l'origine ... mais comme il a été inséré dans une liste, il ne devrait pas faire une énumération coûteuse deux fois. Droite?


1

La solution que j'utilise pour ce problème est légèrement plus élaborée.

Ma classe util static contient une méthode d'extension MarkEndqui convertit les T-items en EndMarkedItem<T>-items. Chaque élément est marqué d'un extra int, qui est soit 0 ; ou (au cas où l'on serait particulièrement intéressé par les 3 derniers éléments) -3 , -2 ou -1 pour les 3 derniers éléments.

Cela peut être utile en soi, par exemple lorsque vous souhaitez créer une liste dans une simple foreachboucle avec des virgules après chaque élément sauf les 2 derniers, avec l'avant-dernier élément suivi d'un mot de conjonction (tel que " et " ou " ou »), et le dernier élément suivi d'un point.

Pour générer la liste entière sans les n derniers éléments, la méthode d'extension ButLastitère simplement sur le EndMarkedItem<T>s while EndMark == 0.

Si vous ne spécifiez pas tailLength, seul le dernier élément est marqué (dans MarkEnd()) ou supprimé (dans ButLast()).

Comme les autres solutions, cela fonctionne par mise en mémoire tampon.

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

Je ne pense pas que cela puisse être plus succinct que cela - en veillant également à éliminer 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: techniquement identique à cette réponse .


1

Avec C # 8.0, vous pouvez utiliser des plages et des index pour cela.

var allButLast = sequence[..^1];

Par défaut, C # 8.0 requiert .NET Core 3.0 ou .NET Standard 2.1 (ou supérieur). Cochez ce fil à utiliser avec des implémentations plus anciennes.


0

Vous pourriez écrire:

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

0

Il s'agit d'une solution élégante générale et à mon humble avis qui gérera correctement tous les cas:

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

Mon IEnumerableapproche traditionnelle :

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

Votre SkipLastEnumerable peut être traditionnel, mais il est cassé. Le premier élément qu'il renvoie est toujours un U non défini - même lorsque "models" a 1 élément. Dans ce dernier cas, je m'attendrais à un résultat vide.
Robert Schmidt

En outre, IEnumerator <T> est IDisposable.
Robert Schmidt

Vrai / noté. Je vous remercie.
Chibueze Opata

-1

Un moyen simple serait de simplement convertir en file d'attente et de retirer la file d'attente jusqu'à ce qu'il ne reste que le nombre d'éléments que vous souhaitez ignorer.

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

Il est Take utilisé pour prendre nombre connu d'éléments. Et la file d'attente pour assez grand énumérable est horrible.
Sinatr

-2

Pourrait être:

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

Je suppose que ça devrait être comme de "Où" mais en préservant l'ordre (?).


3
C'est une façon très inefficace de le faire. Pour évaluer la séquence.Last (), il doit parcourir toute la séquence, en procédant ainsi pour chaque élément de la séquence. Efficacité O (n ^ 2).
Mike

Vous avez raison. Je ne sais pas à quoi je pensais quand j'ai écrit ce XD. Quoi qu'il en soit, êtes-vous sûr que Last () traversera toute la séquence? Pour certaines implémentations de IEnumerable (comme Array), cela doit être O (1). Je n'ai pas vérifié l'implémentation de List, mais il pourrait aussi avoir un itérateur "inversé", commençant dans le dernier élément, qui prendrait également O (1). De plus, vous devez tenir compte du fait que O (n) = O (2n), au moins techniquement parlant. Par conséquent, si cette procédure n'est pas absolument critique pour les performances de vos applications, je m'en tiendrai à la séquence beaucoup plus claire.Take (sequence.Count () - 1).
Guillermo Ares

@Mike Je ne suis pas d'accord avec vous mate, sequence.Last () est O (1) pour qu'il n'ait pas besoin de parcourir toute la séquence. stackoverflow.com/a/1377895/812598
GoRoS

1
@GoRoS, ce n'est que O (1) si la séquence implémente ICollection / IList ou est un tableau. Toutes les autres séquences sont O (N). Dans ma question, je ne suppose pas qu'une des sources O (1)
Mike

3
La séquence peut avoir plusieurs items qui satisfont cette condition e == sequence.Last (), par exemple new [] {1, 1, 1, 1}
Sergey Shandar

-2

Si la vitesse est une exigence, cette méthode à l'ancienne devrait être la plus rapide, même si le code ne semble pas aussi fluide que linq pourrait le faire.

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

Cela nécessite que la séquence soit un tableau car elle a une longueur fixe et des éléments indexés.


2
Vous avez affaire à un IEnumerable qui n'autorise pas l'accès aux éléments via un index. Votre solution ne fonctionne pas. En supposant que vous le fassiez correctement, cela nécessite de parcourir la séquence pour déterminer la longueur, d'allouer un tableau de longueur n-1, de copier tous les éléments. - 1. Opérations 2n-1 et (2n-1) * (4 ou 8 octets) de mémoire. Ce n'est même pas rapide.
Tarik

-4

Je ferais probablement quelque chose comme ça:

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

Ceci est une itération avec une vérification que ce n'est pas la dernière à chaque fois.


3
Deux raisons pour lesquelles ce n'est pas une bonne chose à faire. 1) .LastOrDefault () nécessite une itération de la séquence entière, et cela est appelé pour chaque élément de la séquence (dans le .Where ()). 2) Si la séquence est [1,2,1,2,1,2] et que vous avez utilisé votre technique, vous vous retrouverez avec [1,1,1].
Mike le
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.