web-dev-qa-db-fra.com

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

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 souhaite parcourir cette séquence une fois seulement.

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 aboutit à deux énumérations sur la grande séquence.

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

sequence.TakeAllButTheLastElement();
108
Mike

Je ne connais pas de solution Linq - Mais vous pouvez facilement coder l'algorithme vous-même à l'aide de générateurs (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, en tant que 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()));
}
49
Dario

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

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

Parce que je ne suis pas fan de l'utilisation explicite d'une Enumerator, voici une alternative. Notez que les méthodes d'encapsulation sont nécessaires pour permettre aux arguments non valides de commencer plus tôt, plutôt que de différer les vérifications jusqu'à l'énumération de la séquence.

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 tampon maintenant avant cédant au lieu de céder après, de sorte que le cas n == 0 ne nécessite pas de traitement spécial.

39
Joren

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

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

Pour ceux qui utilisent une version plus récente de .net, la méthode Enumerable.SkipLast(IEnumerable<TSource>, Int32) a été ajoutée à .NET Core 2.0.

var sequence = GetSequence();

var allExceptLast = sequence.SkipLast(1);

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

5
Justin Lessard

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;
    });
4
SmallBizGuy

Une légère expansion sur 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 compte à terme pour supprimer le premier left nombreux éléments et le même tampon supprimé pour supprimer le dernier right nombreux éléments. 

3
silasdavis

Une légère variation sur la réponse acceptée, ce 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
jr76

Si vous pouvez obtenir la Count ou Length d'un énumérable, ce que vous pouvez dans la plupart des cas, alors Take(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);
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

Je ne pense pas que cela puisse être plus succinct que cela - en veillant également à éliminer le 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
Robert Schmidt

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

Ma classe statique util contient une méthode d'extension MarkEnd qui convertit les éléments T- en éléments EndMarkedItem<T>-. Chaque élément est marqué avec un int supplémentaire, qui est soit 0 ; ou (si les 3 derniers éléments sont particulièrement intéressants)/3 , -2 ou -1 pour les 3 derniers éléments.

Cela pourrait être utile en soi, p. lorsque vous voulez créer une liste dans une simple boucle foreach- avec des virgules après chaque élément sauf le dernier 2, 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 complète sans les derniers éléments n, la méthode d'extension ButLast itère simplement le EndMarkedItem<T>s avec le 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 en 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
Adhemar

Ceci est une solution générale et élégante IMHO qui traitera tous les cas correctement:

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

Pourquoi ne pas simplement .ToList<type>() sur la séquence, puis compter les appels et prendre comme vous l’avez fait à l’origine ... mais comme il a été placé dans une liste, il ne devrait pas faire une énumération coûteuse deux fois. Droite?

0
Brady Moritz

Vous pouvez écrire:

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