Comment prendre une liste (en utilisant LINQ) et la diviser en une liste de listes partitionnant la liste d'origine toutes les 8 entrées?
J'imagine que quelque chose comme ça impliquerait Skip et/ou Take, mais je suis encore assez nouveau sur LINQ.
Edit: Utilisation de C #/.Net 3.5
Edit2: Cette question est formulée différemment de l'autre question "dupliquée". Bien que les problèmes soient similaires, les réponses à cette question sont supérieures: la réponse "acceptée" est très solide (avec l'instruction yield
) ainsi que la suggestion de Jon Skeet d'utiliser MoreLinq (ce qui n'est pas recommandé dans le "autre" question.) Parfois, les doublons sont bons dans la mesure où ils obligent à réexaminer un problème.
Utilisez la méthode d'extension suivante pour diviser l'entrée en sous-ensembles
public static class IEnumerableExtensions
{
public static IEnumerable<List<T>> InSetsOf<T>(this IEnumerable<T> source, int max)
{
List<T> toReturn = new List<T>(max);
foreach(var item in source)
{
toReturn.Add(item);
if (toReturn.Count == max)
{
yield return toReturn;
toReturn = new List<T>(max);
}
}
if (toReturn.Any())
{
yield return toReturn;
}
}
}
Il vaut mieux utiliser une bibliothèque comme MoreLinq , mais si vous deviez vraiment le faire en utilisant "plain LINQ", vous pouvez utiliser GroupBy
:
var sequence = new[] {1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16};
var result = sequence.Select((x, i) => new {Group = i/8, Value = x})
.GroupBy(item => item.Group, g => g.Value)
.Select(g => g.Where(x => true));
// result is: { {1,2,3,4,5,6,7,8}, {9,10,11,12,13,14,15,16} }
Fondamentalement, nous utilisons la version de Select()
qui fournit un index pour la valeur consommée, nous divisons l'index par 8 pour identifier à quel groupe appartient chaque valeur. Ensuite, nous regroupons la séquence par cette clé de regroupement. Le dernier Select
réduit simplement le IGrouping<>
À un IEnumerable<IEnumerable<T>>
(Et n'est pas strictement nécessaire puisque IGrouping
est un IEnumerable
).
Il est assez facile de transformer cela en une méthode réutilisable en factorisant notre constante 8
Dans l'exemple et en la remplaçant par un paramètre spécifié. Ce n'est pas nécessairement la solution la plus élégante, et ce n'est plus une solution de streaming paresseux ... mais cela fonctionne.
Vous pouvez également écrire votre propre méthode d'extension en utilisant des blocs d'itérateur (yield return
) Qui pourraient vous donner de meilleures performances et utiliser moins de mémoire que GroupBy
. C'est ce que fait la méthode Batch()
de MoreLinq IIRC.
Ce n'est pas du tout ce que les concepteurs originaux de Linq avaient à l'esprit, mais découvrez cette mauvaise utilisation de GroupBy:
public static IEnumerable<IEnumerable<T>> BatchBy<T>(this IEnumerable<T> items, int batchSize)
{
var count = 0;
return items.GroupBy(x => (count++ / batchSize)).ToList();
}
[TestMethod]
public void BatchBy_breaks_a_list_into_chunks()
{
var values = new[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
var batches = values.BatchBy(3);
batches.Count().ShouldEqual(4);
batches.First().Count().ShouldEqual(3);
batches.Last().Count().ShouldEqual(1);
}
Je pense qu'il remporte le prix "golf" pour cette question. Le ToList
est très important car vous voulez vous assurer que le regroupement a effectivement été effectué avant d'essayer de faire quoi que ce soit avec la sortie. Si vous supprimez ToList
, vous obtiendrez des effets secondaires étranges.
Take ne sera pas très efficace, car il ne supprime pas les entrées prises.
pourquoi ne pas utiliser une simple boucle:
public IEnumerable<IList<T>> Partition<T>(this/* <-- see extension methods*/ IEnumerable<T> src,int num)
{
IEnumerator<T> enu=src.getEnumerator();
while(true)
{
List<T> result=new List<T>(num);
for(int i=0;i<num;i++)
{
if(!enu.MoveNext())
{
if(i>0)yield return result;
yield break;
}
result.Add(enu.Current);
}
yield return result;
}
}
from b in Enumerable.Range(0,8) select items.Where((x,i) => (i % 8) == b);
La solution la plus simple est donnée par Mel:
public static IEnumerable<IEnumerable<T>> Partition<T>(this IEnumerable<T> items,
int partitionSize)
{
int i = 0;
return items.GroupBy(x => i++ / partitionSize).ToArray();
}
Concis mais plus lent. La méthode ci-dessus divise un IEnumerable en morceaux de taille fixe souhaitée, le nombre total de morceaux étant sans importance. Pour diviser un IEnumerable en N nombre de morceaux de tailles égales ou proches de tailles égales, vous pouvez faire:
public static IEnumerable<IEnumerable<T>> Split<T>(this IEnumerable<T> items,
int numOfParts)
{
int i = 0;
return items.GroupBy(x => i++ % numOfParts);
}
Pour accélérer les choses, une approche simple ferait:
public static IEnumerable<IEnumerable<T>> Partition<T>(this IEnumerable<T> items,
int partitionSize)
{
if (partitionSize <= 0)
throw new ArgumentOutOfRangeException("partitionSize");
int innerListCounter = 0;
int numberOfPackets = 0;
foreach (var item in items)
{
innerListCounter++;
if (innerListCounter == partitionSize)
{
yield return items.Skip(numberOfPackets * partitionSize).Take(partitionSize);
innerListCounter = 0;
numberOfPackets++;
}
}
if (innerListCounter > 0)
yield return items.Skip(numberOfPackets * partitionSize);
}
C'est plus rapide que tout ce qui existe actuellement sur la planète :) Les méthodes équivalentes pour une opération Split
ici