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

Disons que j’ai une séquence.

IEnumerable 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é dynamicment, et je veux le parcourir une seule fois.

Je veux obtenir 0 – 999999 (c.-à-d. 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.

Y a-t-il un construit LINQ qui me permet de faire:

 sequence.TakeAllButTheLastElement(); 

Je ne connais pas de solution Linq – Mais vous pouvez facilement coder l’algorithme par vous-même en utilisant des générateurs (rendement de retour).

 public static IEnumerable TakeAllButLast(this IEnumerable 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(ssortingng[] args) { var Seq = Enumerable.Range(1, 10); Console.WriteLine(ssortingng.Join(", ", Seq.Select(x => x.ToSsortingng()).ToArray())); Console.WriteLine(ssortingng.Join(", ", Seq.TakeAllButLast().Select(x => x.ToSsortingng()).ToArray())); } 

Ou en tant que solution généralisée en éliminant les n derniers éléments (en utilisant une queue comme suggéré dans les commentaires):

 public static IEnumerable SkipLastN(this IEnumerable source, int n) { var it = source.GetEnumerator(); bool hasRemainingItems = false; var cache = new Queue(n + 1); do { if (hasRemainingItems = it.MoveNext()) { cache.Enqueue(it.Current); if (cache.Count > n) yield return cache.Dequeue(); } } while (hasRemainingItems); } static void Main(ssortingng[] args) { var Seq = Enumerable.Range(1, 4); Console.WriteLine(ssortingng.Join(", ", Seq.Select(x => x.ToSsortingng()).ToArray())); Console.WriteLine(ssortingng.Join(", ", Seq.SkipLastN(3).Select(x => x.ToSsortingng()).ToArray())); } 

Comme je ne suis pas fan de l’utilisation explicite d’un Enumerator , voici une alternative. Notez que les méthodes d’encapsulation sont nécessaires pour que les arguments non valides soient lancés rapidement, plutôt que de reporter les contrôles jusqu’à ce que la séquence soit réellement énumérée.

 public static IEnumerable DropLast(this IEnumerable source) { if (source == null) throw new ArgumentNullException("source"); return InternalDropLast(source); } private static IEnumerable InternalDropLast(IEnumerable 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 est facile de généraliser à n éléments:

 public static IEnumerable DropLast(this IEnumerable 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 InternalDropLast(IEnumerable source, int n) { Queue buffer = new Queue(n + 1); foreach (T x in source) { buffer.Enqueue(x); if (buffer.Count == n + 1) yield return buffer.Dequeue(); } } 

Où je tampon maintenant avant de céder au lieu de céder, de sorte que la casse n == 0 ne nécessite pas de traitement spécial.

Au lieu de créer votre propre méthode et dans le cas où l’ordre des éléments n’est pas important, le suivant fonctionnera:

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

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

 public static IEnumerable TakeAllButLast(this IEnumerable source) { using (var enumerator = source.GetEnumerator()) bool first = true; T prev; while(enumerator.MoveNext()) { if (!first) yield return prev; first = false; prev = enumerator.Current; } } } 

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

 public static IEnumerable SkipLast(this IEnumerable source, int count) { var enumerator = source.GetEnumerator(); var queue = new Queue(count + 1); while (true) { if (!enumerator.MoveNext()) break; queue.Enqueue(enumerator.Current); if (queue.Count > count) yield return queue.Dequeue(); } } 

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

Une légère expansion sur la solution élégante de Joren:

 public static IEnumerable Shrink(this IEnumerable source, int left, int right) { int i = 0; var buffer = new Queue(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 WithoutLast(this IEnumerable source, int n = 1) { return source.Shrink(0, n); } public static IEnumerable WithoutFirst(this IEnumerable source, int n = 1) { return source.Shrink(n, 0); } 

Où shrink implémente un simple décompte avant pour laisser tomber le premier élément à left et le même tampon rejeté pour supprimer les derniers éléments à right .

Une légère variation sur la réponse acceptée, qui (à mon goût) est un peu plus simple:

  public static IEnumerable AllButLast(this IEnumerable 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(n); foreach (var item in enumerable) { if (queue.Count == n) yield return queue.Dequeue(); queue.Enqueue(item); } } 

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 MarkEnd qui convertit les éléments T dans les éléments EndMarkedItem . Chaque élément est marqué avec un int supplémentaire, qui est soit 0 ; ou (si l’on est 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 boucle foreach simple avec des virgules après chaque élément, à l’exception des 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 ButLast simplement une itération sur le EndMarkedItem alors 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 tamponnant.

 using System; using System.Collections.Generic; using System.Linq; namespace Adhemar.Util.Linq { public struct EndMarkedItem { 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 ButLast(this IEnumerable ts) { return ts.ButLast(1); } public static IEnumerable ButLast(this IEnumerable ts, int tailLength) { return ts.MarkEnd(tailLength).TakeWhile(te => te.EndMark == 0).Select(te => te.Item); } public static IEnumerable> MarkEnd(this IEnumerable ts) { return ts.MarkEnd(1); } public static IEnumerable> MarkEnd(this IEnumerable ts, int tailLength) { if (tailLength < 0) { throw new ArgumentOutOfRangeException("tailLength"); } else if (tailLength == 0) { foreach (var t in ts) { yield return new EndMarkedItem(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(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(buffer[i], i - buffer.Length - index); } for (var j = 0; j < index; j++) { yield return new EndMarkedItem(buffer[j], j - index); } } else { for (var k = 0; k < buffer.Length + index; k++) { yield return new EndMarkedItem(buffer[k], k - buffer.Length - index); } } } } } } 
  public static IEnumerable NoLast (this IEnumerable items) { if (items != null) { var e = items.GetEnumerator(); if (e.MoveNext ()) { T head = e.Current; while (e.MoveNext ()) { yield return head; ; head = e.Current; } } } } 

Je ne pense pas que cela puisse être plus succinct que cela – en veillant également à éliminer le IEnumerator :

 public static IEnumerable SkipLast(this IEnumerable 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 .

Pourquoi pas simplement .ToList() sur la séquence, puis le nombre d’appels et la prise comme vous l’avez fait à l’origine … mais comme il a été extrait dans une liste, il ne devrait pas faire deux fois une énumération coûteuse. Droite?

Si vous pouvez obtenir le nombre ou la 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

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

Vous pourriez écrire:

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

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

 using System; using System.Collections.Generic; using System.Linq; public class Program { public static void Main() { IEnumerable r = Enumerable.Range(1, 20); foreach (int i in r.AllButLast(3)) Console.WriteLine(i); Console.ReadKey(); } } public static class LinqExt { public static IEnumerable AllButLast(this IEnumerable enumerable, int n = 1) { using (IEnumerator enumerator = enumerable.GetEnumerator()) { Queue queue = new Queue(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(); } } } } 

Mon approche traditionnelle IEnumerable :

 ///  /// Skips first element of an IEnumerable ///  /// Enumerable type /// The enumerable /// IEnumerable of type skipping first element private IEnumerable SkipFirstEnumerable(IEnumerable models) { using (var e = models.GetEnumerator()) { if (!e.MoveNext()) return; for (;e.MoveNext();) yield return e.Current; yield return e.Current; } } ///  /// Skips last element of an IEnumerable ///  /// Enumerable type /// The enumerable /// IEnumerable of type skipping last element private IEnumerable SkipLastEnumerable(IEnumerable models) { using (var e = models.GetEnumerator()) { if (!e.MoveNext()) return; yield return e.Current; for (;e.MoveNext();) yield return e.Current; } } 

Pourrait être:

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

Je suppose que ça devrait être comme de “Where” mais en préservant la commande (?).

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

 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.

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 pour chaque fois cependant.