158 votes

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

L'obtention de la séquence n'est pas bon marché et est généré dynamiquement, et je veux parcourir 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 que les résultats dans les deux énumérations sur le gros de la séquence.

Est-il LINQ construction qui me permet de le faire:

sequence.TakeAllButTheLastElement();

?

Merci!

-Mike

67voto

Dario Points 26259

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

49voto

Kamarey Points 4416

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

42voto

Joren Points 7911

Parce que je pense que la solution retenue est moins élégant qu'il pourrait être une alternative. Notez que le wrapper méthodes sont nécessaires pour laisser arguments invalides jeter début, plutôt que de s'en remettre les chèques jusqu'à ce que la séquence est dénombrés.

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

Comme par Eric Lippert la suggestion, il se généralise à 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ù j'ai maintenant de la mémoire tampon avant de céder la place après ce, afin que l' n == 0 de cas n'a pas besoin d'un traitement spécial.

12voto

Noldorin Points 67794

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)
{
    var enumerator = source.GetEnumerator();
    bool first = true;
    T prev;
    while(enumerator.MoveNext())
    {
        if (!first)
            yield return prev;
        first = false;
        prev = enumerator.Current;
    }
}
 

7voto

Alex Aza Points 29204

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

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X