Diviser une liste en plus petites listes de taille N

Je tente de diviser une liste en une série de plus petites listes.

Mon problème: ma fonction de fractionnement de listes ne les divise pas en listes de la bonne taille. Il devrait les diviser en listes de taille 30 mais au lieu de cela il les divise en listes de taille 114?

Comment puis-je faire en sorte que ma fonction divise une liste en un nombre X de listes de taille 30 ou inférieure ?

public static List<List> splitList(List  locations, int nSize=30) { List<List> list = new List<List>(); for (int i=(int)(Math.Ceiling((decimal)(locations.Count/nSize))); i>=0; i--) { List  subLocat = new List (locations); if (subLocat.Count >= ((i*nSize)+nSize)) subLocat.RemoveRange(i*nSize, nSize); else subLocat.RemoveRange(i*nSize, subLocat.Count-(i*nSize)); Debug.Log ("Index: "+i.ToSsortingng()+", Size: "+subLocat.Count.ToSsortingng()); list.Add (subLocat); } return list; } 

Si j’utilise la fonction sur une liste de taille 144, la sortie est la suivante:

Index: 4, Taille: 120
Index: 3, Taille: 114
Index: 2, taille: 114
Index: 1, taille: 114
Index: 0, taille: 114

 public static List> splitList(List locations, int nSize=30) { var list = new List>(); for (int i=0; i < locations.Count; i+= nSize) { list.Add(locations.GetRange(i, Math.Min(nSize, locations.Count - i))); } return list; } 

Version générique:

 public static IEnumerable> splitList(List locations, int nSize=30) { for (int i=0; i < locations.Count; i+= nSize) { yield return locations.GetRange(i, Math.Min(nSize, locations.Count - i)); } } 

Je suggère d’utiliser cette méthode d’extension pour découper la liste des sources en sous-listes par la taille de bloc spécifiée:

 ///  /// Helper methods for the lists. ///  public static class ListExtensions { public static List> ChunkBy(this List source, int chunkSize) { return source .Select((x, i) => new { Index = i, Value = x }) .GroupBy(x => x.Index / chunkSize) .Select(x => x.Select(v => v.Value).ToList()) .ToList(); } } 

Par exemple, si vous jetez la liste des 18 objects par 5 éléments par morceau, vous obtenez la liste des 4 sous-listes contenant les éléments suivants: 5-5-5-3.

que diriez-vous:

 while(locations.Any()) { list.Add(locations.Take(nSize).ToList()); locations= locations.Skip(nSize).ToList(); } 

La solution Serj-Tm est bien, c’est aussi la version générique comme méthode d’extension pour les listes (placez-la dans une classe statique):

 public static List> Split(this List items, int sliceSize = 30) { List> list = new List>(); for (int i = 0; i < items.Count; i += sliceSize) list.Add(items.GetRange(i, Math.Min(sliceSize, items.Count - i))); return list; } 

Je trouve la réponse acceptée (Serj-Tm) la plus robuste, mais je voudrais suggérer une version générique.

  public static List> splitList(List locations, int nSize = 30) { var list = new List>(); for (int i = 0; i < locations.Count; i += nSize) { list.Add(locations.GetRange(i, Math.Min(nSize, locations.Count - i))); } return list; } 

J’ai une méthode générique qui prendrait n’importe quel type, y compris float, et elle a été testée en unité, en espérant qu’elle aide:

  ///  /// Breaks the list into groups with each group containing no more than the specified group size ///  ///  /// The values. /// Size of the group. ///  public static List> SplitList(IEnumerable values, int groupSize, int? maxCount = null) { List> result = new List>(); // Quick and special scenario if (values.Count() < = groupSize) { result.Add(values.ToList()); } else { List valueList = values.ToList(); int startIndex = 0; int count = valueList.Count; int elementCount = 0; while (startIndex < count && (!maxCount.HasValue || (maxCount.HasValue && startIndex < maxCount))) { elementCount = (startIndex + groupSize > count) ? count - startIndex : groupSize; result.Add(valueList.GetRange(startIndex, elementCount)); startIndex += elementCount; } } return result; } 

Ajout après commentaire très utile de mhand à la fin

Réponse originale

Bien que la plupart des solutions puissent fonctionner, je pense qu’elles ne sont pas très efficaces. Supposons que vous ne vouliez que les premiers éléments des premiers morceaux. Ensuite, vous ne voudrez pas parcourir tous les éléments de votre séquence.

La liste suivante énumère au maximum deux fois: une fois pour la prise et une fois pour le saut. Il ne va pas énumérer plus d’éléments que vous utiliserez:

 public static IEnumerable> ChunkBy (this IEnumerable source, int chunkSize) { while (source.Any()) // while there are elements left { // still something to chunk: yield return source.Take(chunkSize); // return a chunk of chunkSize source = source.Skip(chunkSize); // skip the returned chunk } } 

Combien de fois cela énumérera-t-il la séquence?

Supposons que vous divisez votre source en morceaux de chunkSize . Vous énumérez seulement les N premiers morceaux. À partir de chaque bloc énuméré, vous ne listerez que les premiers éléments M.

 While(source.Any()) { ... } 

le Any va obtenir l’énumérateur, faire 1 MoveNext () et retourne la valeur renvoyée après la disposition de l’énumérateur. Cela sera fait N fois

 yield return source.Take(chunkSize); 

Selon la source de référence, cela fera quelque chose comme:

 public static IEnumerable Take(this IEnumerable source, int count) { return TakeIterator(source, count); } static IEnumerable TakeIterator(IEnumerable source, int count) { foreach (TSource element in source) { yield return element; if (--count == 0) break; } } 

Cela ne fait pas beaucoup jusqu’à ce que vous commencez à énumérer sur le morceau récupéré. Si vous récupérez plusieurs blocs, mais que vous décidez de ne pas les énumérer sur le premier bloc, le foreach n’est pas exécuté, comme votre débogueur vous le montrera.

Si vous décidez de prendre les premiers éléments M du premier morceau, le rendement obtenu est exécuté exactement M fois. Ça signifie:

  • obtenir le recenseur
  • appelez MoveNext () et M fois en cours.
  • Éliminer le recenseur

Après le retour du premier morceau, nous sautons ce premier morceau:

 source = source.Skip(chunkSize); 

Encore une fois: nous allons regarder la source de référence pour trouver le skipiterator

 static IEnumerable SkipIterator(IEnumerable source, int count) { using (IEnumerator e = source.GetEnumerator()) { while (count > 0 && e.MoveNext()) count--; if (count < = 0) { while (e.MoveNext()) yield return e.Current; } } } 

Comme vous le voyez, SkipIterator appelle MoveNext() une fois pour chaque élément du bloc. Il n'appelle pas Current .

Donc, par Chunk, nous voyons que ce qui suit est fait:

  • Any (): GetEnumerator; 1 MoveNext (); Dispose Enumerator;
  • Prendre():

    • rien si le contenu du morceau n'est pas énuméré.
    • Si le contenu est énuméré: GetEnumerator (), un MoveNext et un Current par élément énuméré, Énumérateur Dispose;

    • Skip (): pour chaque segment qui est énuméré (PAS le contenu du bloc): GetEnumerator (), MoveNext () chunkSize times, no Current! Éliminer le recenseur

Si vous regardez ce qui se passe avec l'énumérateur, vous verrez qu'il y a beaucoup d'appels à MoveNext (), et que seuls les appels à Current pour les éléments TSource auxquels vous décidez d'accéder.

Si vous prenez N morceaux de taille chunkSize, alors les appels à MoveNext ()

  • N fois pour Any ()
  • pas encore de temps pour Take, tant que vous n'énumérer pas les morceaux
  • N fois chunkSize pour Skip ()

Si vous décidez d'énumérer uniquement les premiers éléments M de chaque segment récupéré, vous devez appeler M fois MoveNext par fragment énuméré.

Le total

 MoveNext calls: N + N*M + N*chunkSize Current calls: N*M; (only the items you really access) 

Donc, si vous décidez d'énumérer tous les éléments de tous les morceaux:

 MoveNext: numberOfChunks + all elements + all elements = about twice the sequence Current: every item is accessed exactly once 

Que MoveNext fasse beaucoup de travail ou non dépend du type de séquence source. Pour les listes et les tableaux, il s'agit d'un simple incrément d'index, avec peut-être une vérification hors plage.

Mais si votre IEnumerable est le résultat d'une requête de firebase database, assurez-vous que les données sont réellement matérialisées sur votre ordinateur, sinon les données seront récupérées plusieurs fois. DbContext et Dapper transfèreront correctement les données au processus local avant de pouvoir y accéder. Si vous énumérez plusieurs fois la même séquence, celle-ci n'est pas extraite plusieurs fois. Dapper renvoie un object qui est une liste, DbContext se souvient que les données sont déjà extraites.

Il dépend de votre référentiel s'il est judicieux d'appeler AsEnumerable () ou ToLists () avant de commencer à diviser les éléments en blocs.

Bibliothèque MoreLinq ont une méthode appelée Batch

 List ids = new List() { 1, 2, 3, 4, 5, 6, 7, 8, 9, 0 }; // 10 elements int counter = 1; foreach(var batch in ids.Batch(2)) { foreach(var eachId in batch) { Console.WriteLine("Batch: {0}, Id: {1}", counter, eachId); } counter++; } 

Le résultat est

 Batch: 1, Id: 1 Batch: 1, Id: 2 Batch: 2, Id: 3 Batch: 2, Id: 4 Batch: 3, Id: 5 Batch: 3, Id: 6 Batch: 4, Id: 7 Batch: 4, Id: 8 Batch: 5, Id: 9 Batch: 5, Id: 0 

ids sont divisés en 5 morceaux avec 2 éléments.

 public static IEnumerable> SplitIntoSets (this IEnumerable source, int itemsPerSet) { var sourceList = source as List ?? source.ToList(); for (var index = 0; index < sourceList.Count; index += itemsPerSet) { yield return sourceList.Skip(index).Take(itemsPerSet); } } 

Tandis que beaucoup de réponses ci-dessus font le travail, elles échouent toutes horriblement sur une séquence sans fin (ou une très longue séquence). Ce qui suit est une implémentation entièrement en ligne qui garantit la meilleure complexité possible en termes de temps et de mémoire. Nous n’itérons que la source énumérable exactement une fois et utilisons le rendement de retour pour une évaluation différée. Le consommateur pourrait jeter la liste à chaque itération en rendant l’encombrement mémoire égal à celui de la liste avec le nombre d’éléments batchSize .

 public static IEnumerable> BatchBy(this IEnumerable enumerable, int batchSize) { using (var enumerator = enumerable.GetEnumerator()) { List list = null; while (enumerator.MoveNext()) { if (list == null) { list = new List {enumerator.Current}; } else if (list.Count < batchSize) { list.Add(enumerator.Current); } else { yield return list; list = new List {enumerator.Current}; } } if (list?.Count > 0) { yield return list; } } } 

EDIT: Tout en réalisant que l’OP pose la question de diviser une List en une List plus petite List , mes commentaires concernant les énumérateurs infinis ne sont pas applicables à l’OP, mais peuvent aider les autres utilisateurs. Ces commentaires répondaient à d’autres solutions publiées utilisant IEnumerable comme entrée dans leur fonction, mais énumérant le source énumérable plusieurs fois.