Fractionner la liste en sous-listes avec LINQ

377

Existe-t-il un moyen de séparer un List<SomeObject>en plusieurs listes distinctes de SomeObject, en utilisant l'index des éléments comme délimiteur de chaque division?

Permettez-moi d'illustrer:

J'ai un List<SomeObject>et j'ai besoin d'un List<List<SomeObject>>ou List<SomeObject>[], de sorte que chacune de ces listes résultantes contienne un groupe de 3 éléments de la liste d'origine (séquentiellement).

par exemple.:

  • Liste originale: [a, g, e, w, p, s, q, f, x, y, i, m, c]

  • Listes résultantes: [a, g, e], [w, p, s], [q, f, x], [y, i, m], [c]

J'aurais également besoin que la taille des listes résultantes soit un paramètre de cette fonction.

Felipe Lima
la source

Réponses:

378

Essayez le code suivant.

public static IList<IList<T>> Split<T>(IList<T> source)
{
    return  source
        .Select((x, i) => new { Index = i, Value = x })
        .GroupBy(x => x.Index / 3)
        .Select(x => x.Select(v => v.Value).ToList())
        .ToList();
}

L'idée est de regrouper d'abord les éléments par index. La division par trois a pour effet de les regrouper en groupes de 3. Ensuite, convertissez chaque groupe en une liste et le IEnumerablede Listen un Listde Lists

JaredPar
la source
21
GroupBy effectue un tri implicite. Cela peut tuer les performances. Ce dont nous avons besoin, c'est d'une sorte d'inverse de SelectMany.
yfeldblum
5
@Justice, GroupBy peut être implémenté par hachage. Comment savez-vous que la mise en œuvre de GroupBy "peut tuer les performances"?
Amy B
5
GroupBy ne renvoie rien tant qu'il n'a pas énuméré tous les éléments. Voilà pourquoi c'est lent. Les listes OP souhaitées sont contiguës, donc une meilleure méthode pourrait produire la première sous-liste [a,g,e]avant d'énumérer davantage la liste d'origine.
Colonel Panic
9
Prenons l'exemple extrême d'un IEnumerable infini. GroupBy(x=>f(x)).First()ne donnera jamais un groupe. OP a posé des questions sur les listes, mais si nous écrivons pour travailler avec IEnumerable, en ne faisant qu'une seule itération, nous récoltons l'avantage des performances.
Colonel Panic
8
@Nick Order n'est cependant pas préservé. C'est toujours une bonne chose à savoir, mais vous les regrouperiez en (0,3,6,9, ...), (1,4,7,10, ...), (2,5,8 , 11, ...). Si l'ordre n'a pas d'importance, c'est bien, mais dans ce cas, cela semble important.
Reafexus
325

Cette question est un peu ancienne, mais je viens de l'écrire, et je pense qu'elle est un peu plus élégante que les autres solutions proposées:

/// <summary>
/// Break a list of items into chunks of a specific size
/// </summary>
public static IEnumerable<IEnumerable<T>> Chunk<T>(this IEnumerable<T> source, int chunksize)
{
    while (source.Any())
    {
        yield return source.Take(chunksize);
        source = source.Skip(chunksize);
    }
}
CaseyB
la source
14
J'adore cette solution. Je recommanderais d'ajouter ce test de santé mentale pour éviter une boucle infinie: if (chunksize <= 0) throw new ArgumentException("Chunk size must be greater than zero.", "chunksize");
mroach
10
J'aime ça, mais ce n'est pas super efficace
Sam Saffron
51
J'aime celui-ci, mais l'efficacité du temps est O(n²). Vous pouvez parcourir la liste et obtenir un O(n)temps.
hIpPy
8
@hIpPy, comment est-il n ^ 2? Me semble linéaire
Vivek Maharajh
13
@vivekmaharajh sourceest remplacé par un enveloppé à IEnumerablechaque fois. sourceSkip
Prendre des
99

En général, l'approche suggérée par CaseyB fonctionne bien, en fait, si vous passez dans un, List<T>il est difficile de le blâmer, je le changerais peut-être en:

public static IEnumerable<IEnumerable<T>> ChunkTrivialBetter<T>(this IEnumerable<T> source, int chunksize)
{
   var pos = 0; 
   while (source.Skip(pos).Any())
   {
      yield return source.Skip(pos).Take(chunksize);
      pos += chunksize;
   }
}

Ce qui évitera des chaînes d'appel massives. Néanmoins, cette approche présente un défaut général. Il matérialise deux énumérations par bloc, pour mettre en évidence le problème en cours d'exécution:

foreach (var item in Enumerable.Range(1, int.MaxValue).Chunk(8).Skip(100000).First())
{
   Console.WriteLine(item);
}
// wait forever 

Pour surmonter cela, nous pouvons essayer l' approche de Cameron , qui réussit le test ci-dessus avec brio car elle ne parcourt l'énumération qu'une seule fois.

Le problème est qu'il a un défaut différent, il matérialise chaque élément de chaque morceau, le problème avec cette approche est que vous manquez de mémoire.

Pour illustrer cela, essayez de lancer:

foreach (var item in Enumerable.Range(1, int.MaxValue)
               .Select(x => x + new string('x', 100000))
               .Clump(10000).Skip(100).First())
{
   Console.Write('.');
}
// OutOfMemoryException

Enfin, toute implémentation devrait être capable de gérer l'itération dans le désordre des morceaux, par exemple:

Enumerable.Range(1,3).Chunk(2).Reverse().ToArray()
// should return [3],[1,2]

De nombreuses solutions hautement optimales comme ma première révision de cette réponse ont échoué là-bas. Le même problème peut être vu dans la réponse optimisée de casperOne .

Pour résoudre tous ces problèmes, vous pouvez utiliser les éléments suivants:

namespace ChunkedEnumerator
{
    public static class Extensions 
    {
        class ChunkedEnumerable<T> : IEnumerable<T>
        {
            class ChildEnumerator : IEnumerator<T>
            {
                ChunkedEnumerable<T> parent;
                int position;
                bool done = false;
                T current;


                public ChildEnumerator(ChunkedEnumerable<T> parent)
                {
                    this.parent = parent;
                    position = -1;
                    parent.wrapper.AddRef();
                }

                public T Current
                {
                    get
                    {
                        if (position == -1 || done)
                        {
                            throw new InvalidOperationException();
                        }
                        return current;

                    }
                }

                public void Dispose()
                {
                    if (!done)
                    {
                        done = true;
                        parent.wrapper.RemoveRef();
                    }
                }

                object System.Collections.IEnumerator.Current
                {
                    get { return Current; }
                }

                public bool MoveNext()
                {
                    position++;

                    if (position + 1 > parent.chunkSize)
                    {
                        done = true;
                    }

                    if (!done)
                    {
                        done = !parent.wrapper.Get(position + parent.start, out current);
                    }

                    return !done;

                }

                public void Reset()
                {
                    // per http://msdn.microsoft.com/en-us/library/system.collections.ienumerator.reset.aspx
                    throw new NotSupportedException();
                }
            }

            EnumeratorWrapper<T> wrapper;
            int chunkSize;
            int start;

            public ChunkedEnumerable(EnumeratorWrapper<T> wrapper, int chunkSize, int start)
            {
                this.wrapper = wrapper;
                this.chunkSize = chunkSize;
                this.start = start;
            }

            public IEnumerator<T> GetEnumerator()
            {
                return new ChildEnumerator(this);
            }

            System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
            {
                return GetEnumerator();
            }

        }

        class EnumeratorWrapper<T>
        {
            public EnumeratorWrapper (IEnumerable<T> source)
            {
                SourceEumerable = source;
            }
            IEnumerable<T> SourceEumerable {get; set;}

            Enumeration currentEnumeration;

            class Enumeration
            {
                public IEnumerator<T> Source { get; set; }
                public int Position { get; set; }
                public bool AtEnd { get; set; }
            }

            public bool Get(int pos, out T item) 
            {

                if (currentEnumeration != null && currentEnumeration.Position > pos)
                {
                    currentEnumeration.Source.Dispose();
                    currentEnumeration = null;
                }

                if (currentEnumeration == null)
                {
                    currentEnumeration = new Enumeration { Position = -1, Source = SourceEumerable.GetEnumerator(), AtEnd = false };
                }

                item = default(T);
                if (currentEnumeration.AtEnd)
                {
                    return false;
                }

                while(currentEnumeration.Position < pos) 
                {
                    currentEnumeration.AtEnd = !currentEnumeration.Source.MoveNext();
                    currentEnumeration.Position++;

                    if (currentEnumeration.AtEnd) 
                    {
                        return false;
                    }

                }

                item = currentEnumeration.Source.Current;

                return true;
            }

            int refs = 0;

            // needed for dispose semantics 
            public void AddRef()
            {
                refs++;
            }

            public void RemoveRef()
            {
                refs--;
                if (refs == 0 && currentEnumeration != null)
                {
                    var copy = currentEnumeration;
                    currentEnumeration = null;
                    copy.Source.Dispose();
                }
            }
        }

        public static IEnumerable<IEnumerable<T>> Chunk<T>(this IEnumerable<T> source, int chunksize)
        {
            if (chunksize < 1) throw new InvalidOperationException();

            var wrapper =  new EnumeratorWrapper<T>(source);

            int currentPos = 0;
            T ignore;
            try
            {
                wrapper.AddRef();
                while (wrapper.Get(currentPos, out ignore))
                {
                    yield return new ChunkedEnumerable<T>(wrapper, chunksize, currentPos);
                    currentPos += chunksize;
                }
            }
            finally
            {
                wrapper.RemoveRef();
            }
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            int i = 10;
            foreach (var group in Enumerable.Range(1, int.MaxValue).Skip(10000000).Chunk(3))
            {
                foreach (var n in group)
                {
                    Console.Write(n);
                    Console.Write(" ");
                }
                Console.WriteLine();
                if (i-- == 0) break;
            }


            var stuffs = Enumerable.Range(1, 10).Chunk(2).ToArray();

            foreach (var idx in new [] {3,2,1})
            {
                Console.Write("idx " + idx + " ");
                foreach (var n in stuffs[idx])
                {
                    Console.Write(n);
                    Console.Write(" ");
                }
                Console.WriteLine();
            }

            /*

10000001 10000002 10000003
10000004 10000005 10000006
10000007 10000008 10000009
10000010 10000011 10000012
10000013 10000014 10000015
10000016 10000017 10000018
10000019 10000020 10000021
10000022 10000023 10000024
10000025 10000026 10000027
10000028 10000029 10000030
10000031 10000032 10000033
idx 3 7 8
idx 2 5 6
idx 1 3 4
             */

            Console.ReadKey();


        }

    }
}

Il existe également une série d'optimisations que vous pourriez introduire pour une itération hors service des morceaux, ce qui est hors de portée ici.

Quant à quelle méthode choisir? Cela dépend totalement du problème que vous essayez de résoudre. Si vous n'êtes pas concerné par le premier défaut, la réponse simple est incroyablement attrayante.

Notez que comme avec la plupart des méthodes, ce n'est pas sûr pour le multi-threading, les choses peuvent devenir bizarres si vous souhaitez le rendre sûr pour le thread, vous devrez le modifier EnumeratorWrapper.

Sam Saffron
la source
Le bogue serait-il Enumerable.Range (0, 100) .Chunk (3) .Reverse (). ToArray () est incorrect, ou Enumerable.Range (0, 100) .ToArray (). Chunk (3) .Reverse () .ToArray () lançant une exception?
Cameron MacFarland
@SamSaffron J'ai mis à jour ma réponse et simplifié énormément le code pour ce que je pense être le cas d'utilisation le plus important (et reconnaître les mises en garde).
casperOne
Qu'en est-il du découpage d'IQueryable <>? Je suppose qu'une approche Take / Skip serait optimale si nous voulons déléguer un maximum des opérations au fournisseur
Guillaume86
@ Guillaume86 Je suis d'accord, si vous avez un IList ou IQueryable, vous pouvez prendre toutes sortes de raccourcis qui rendraient cela beaucoup plus rapide (Linq le fait en interne pour toutes sortes d'autres méthodes)
Sam Saffron
1
C'est de loin la meilleure réponse pour l'efficacité. Je rencontre un problème en utilisant SqlBulkCopy avec un IEnumerable qui exécute des processus supplémentaires sur chaque colonne, il doit donc fonctionner efficacement en une seule passe. Cela me permettra de diviser l'IEnumerable en morceaux de taille gérable. (Pour ceux qui se demandent, j'ai activé le mode de streaming SqlBulkCopy, qui semble être cassé).
Brain2000
64

Vous pourriez utiliser un certain nombre de requêtes qui utilisent Takeet Skip, mais cela ajouterait trop d'itérations sur la liste d'origine, je crois.

Je pense plutôt que vous devriez créer votre propre itérateur, comme ceci:

public static IEnumerable<IEnumerable<T>> GetEnumerableOfEnumerables<T>(
  IEnumerable<T> enumerable, int groupSize)
{
   // The list to return.
   List<T> list = new List<T>(groupSize);

   // Cycle through all of the items.
   foreach (T item in enumerable)
   {
     // Add the item.
     list.Add(item);

     // If the list has the number of elements, return that.
     if (list.Count == groupSize)
     {
       // Return the list.
       yield return list;

       // Set the list to a new list.
       list = new List<T>(groupSize);
     }
   }

   // Return the remainder if there is any,
   if (list.Count != 0)
   {
     // Return the list.
     yield return list;
   }
}

Vous pouvez ensuite appeler cela et il est activé LINQ afin que vous puissiez effectuer d'autres opérations sur les séquences résultantes.


À la lumière de la réponse de Sam , j'ai senti qu'il y avait un moyen plus facile de le faire sans:

  • Parcourir à nouveau la liste (ce que je n'ai pas fait à l'origine)
  • Matérialiser les éléments en groupes avant de libérer le bloc (pour les gros morceaux d'éléments, il y aurait des problèmes de mémoire)
  • Tout le code que Sam a publié

Cela dit, voici un autre passage, que j'ai codifié dans une méthode d'extension à IEnumerable<T>appeler Chunk:

public static IEnumerable<IEnumerable<T>> Chunk<T>(this IEnumerable<T> source, 
    int chunkSize)
{
    // Validate parameters.
    if (source == null) throw new ArgumentNullException("source");
    if (chunkSize <= 0) throw new ArgumentOutOfRangeException("chunkSize",
        "The chunkSize parameter must be a positive value.");

    // Call the internal implementation.
    return source.ChunkInternal(chunkSize);
}

Rien de surprenant là-haut, juste une vérification des erreurs de base.

Passons à ChunkInternal:

private static IEnumerable<IEnumerable<T>> ChunkInternal<T>(
    this IEnumerable<T> source, int chunkSize)
{
    // Validate parameters.
    Debug.Assert(source != null);
    Debug.Assert(chunkSize > 0);

    // Get the enumerator.  Dispose of when done.
    using (IEnumerator<T> enumerator = source.GetEnumerator())
    do
    {
        // Move to the next element.  If there's nothing left
        // then get out.
        if (!enumerator.MoveNext()) yield break;

        // Return the chunked sequence.
        yield return ChunkSequence(enumerator, chunkSize);
    } while (true);
}

Fondamentalement, il obtient le IEnumerator<T> et itère manuellement chaque élément. Il vérifie s'il y a actuellement des éléments à énumérer. Après l'énumération de chaque morceau, s'il ne reste aucun élément, il éclate.

Une fois qu'il détecte qu'il y a des éléments dans la séquence, il délègue la responsabilité de l' IEnumerable<T>implémentation interne à ChunkSequence:

private static IEnumerable<T> ChunkSequence<T>(IEnumerator<T> enumerator, 
    int chunkSize)
{
    // Validate parameters.
    Debug.Assert(enumerator != null);
    Debug.Assert(chunkSize > 0);

    // The count.
    int count = 0;

    // There is at least one item.  Yield and then continue.
    do
    {
        // Yield the item.
        yield return enumerator.Current;
    } while (++count < chunkSize && enumerator.MoveNext());
}

Comme MoveNexta déjà été appelé sur le IEnumerator<T>passé à ChunkSequence, il renvoie l'élément renvoyé par Current, puis incrémente le nombre, en veillant à ne jamais renvoyer plus que les chunkSizeéléments et en passant à l'élément suivant dans la séquence après chaque itération (mais court-circuité si le nombre de les articles produits dépassent la taille des morceaux).

S'il ne reste aucun élément, la InternalChunkméthode effectuera une autre passe dans la boucle externe, mais lorsqu'elle MoveNextest appelée une deuxième fois, elle retournera toujours false, selon la documentation (c'est moi qui souligne):

Si MoveNext passe la fin de la collection, l'énumérateur est positionné après le dernier élément de la collection et MoveNext renvoie false. Lorsque l'énumérateur est à cette position, les appels suivants à MoveNext renvoient également false jusqu'à ce que Reset soit appelé.

À ce stade, la boucle se rompra et la séquence de séquences se terminera.

Ceci est un test simple:

static void Main()
{
    string s = "agewpsqfxyimc";

    int count = 0;

    // Group by three.
    foreach (IEnumerable<char> g in s.Chunk(3))
    {
        // Print out the group.
        Console.Write("Group: {0} - ", ++count);

        // Print the items.
        foreach (char c in g)
        {
            // Print the item.
            Console.Write(c + ", ");
        }

        // Finish the line.
        Console.WriteLine();
    }
}

Production:

Group: 1 - a, g, e,
Group: 2 - w, p, s,
Group: 3 - q, f, x,
Group: 4 - y, i, m,
Group: 5 - c,

Remarque importante, cela ne fonctionnera pas si vous ne videz pas la séquence enfant entière ou ne vous interrompez à aucun moment de la séquence parent. Ceci est une mise en garde importante, mais si votre cas d'utilisation est que vous consommerez chaque élément de la séquence de séquences, alors cela fonctionnera pour vous.

De plus, cela fera des choses étranges si vous jouez avec l'ordre, tout comme Sam l'a fait à un moment donné .

casperOne
la source
Je pense que c'est la meilleure solution ... le seul problème est que la liste n'a pas de longueur ... elle a Count. Mais c'est facile à changer. Nous pouvons améliorer cela en ne construisant même pas de listes mais en renvoyant des ienumerables qui contiennent des références à la liste principale avec une combinaison décalage / longueur. Donc, si la taille du groupe est grande, nous ne perdons pas de mémoire. Commentez si vous voulez que je l'écrive.
Amir
@Amir, je voudrais que cela soit écrit
samandmoore
C'est agréable et rapide - Cameron en a posté un très similaire après le vôtre, seule mise en garde est qu'il tamponne les morceaux, cela peut entraîner une mémoire insuffisante si les morceaux et les tailles des articles sont gros. Voir ma réponse pour une réponse alternative, bien que beaucoup plus velue.
Sam Saffron
@SamSaffron Oui, si vous avez un grand nombre d'éléments dans le List<T>, vous allez évidemment avoir des problèmes de mémoire à cause de la mise en mémoire tampon. Rétrospectivement, j'aurais dû le noter dans la réponse, mais il semblait à l'époque que l'accent était mis sur trop d'itérations. Cela dit, votre solution est en effet plus velue. Je ne l'ai pas testé, mais maintenant, je me demande s'il existe une solution moins velue.
casperOne
@casperOne ouais ... Google m'a donné cette page lorsque je cherchais un moyen de diviser les énumérables, pour mon cas d'utilisation spécifique, je fractionne une liste incroyablement grande d'enregistrements qui sont renvoyés par la base de données, si je les matérialise en un liste il exploserait (en fait dapper a un tampon: fausse option juste pour ce cas d'utilisation)
Sam Saffron
48

Ok, voici mon point de vue:

  • complètement paresseux: fonctionne sur d'innombrables infinis
  • pas de copie / tampon intermédiaire
  • O (n) temps d'exécution
  • fonctionne également lorsque les séquences internes ne sont que partiellement consommées

public static IEnumerable<IEnumerable<T>> Chunks<T>(this IEnumerable<T> enumerable,
                                                    int chunkSize)
{
    if (chunkSize < 1) throw new ArgumentException("chunkSize must be positive");

    using (var e = enumerable.GetEnumerator())
    while (e.MoveNext())
    {
        var remaining = chunkSize;    // elements remaining in the current chunk
        var innerMoveNext = new Func<bool>(() => --remaining > 0 && e.MoveNext());

        yield return e.GetChunk(innerMoveNext);
        while (innerMoveNext()) {/* discard elements skipped by inner iterator */}
    }
}

private static IEnumerable<T> GetChunk<T>(this IEnumerator<T> e,
                                          Func<bool> innerMoveNext)
{
    do yield return e.Current;
    while (innerMoveNext());
}

Exemple d'utilisation

var src = new [] {1, 2, 3, 4, 5, 6}; 

var c3 = src.Chunks(3);      // {{1, 2, 3}, {4, 5, 6}}; 
var c4 = src.Chunks(4);      // {{1, 2, 3, 4}, {5, 6}}; 

var sum   = c3.Select(c => c.Sum());    // {6, 15}
var count = c3.Count();                 // 2
var take2 = c3.Select(c => c.Take(2));  // {{1, 2}, {4, 5}}

Explications

Le code fonctionne en imbriquant deux yielditérateurs basés.

L'itérateur externe doit garder une trace du nombre d'éléments qui ont été effectivement consommés par l'itérateur interne (bloc). Cela se fait en terminant remainingavec innerMoveNext(). Les éléments non consommés d'un bloc sont rejetés avant que le bloc suivant ne soit fourni par l'itérateur externe. Ceci est nécessaire car sinon vous obtenez des résultats incohérents, lorsque les énumérables internes ne sont pas (complètement) consommés (par exemple c3.Count()renverraient 6).

Remarque: La réponse a été mise à jour pour combler les lacunes signalées par @aolszowka.

3dGrabber
la source
2
Très agréable. Ma "bonne" solution était bien plus compliquée que ça. Ceci est la réponse n ° 1 à mon humble avis.
CaseyB
Cela souffre d'un comportement inattendu (du point de vue de l'API) lorsque ToArray () est appelé, il n'est pas non plus thread-safe.
aolszowka
@aolszowka: pourriez-vous s'il vous plaît développer?
3dGrabber
@ 3dGrabber C'est peut-être comme ça que j'ai refactorisé votre code (désolé, c'est un peu trop long à passer ici, essentiellement au lieu d'une méthode d'extension que j'ai passée dans le sourceEnumerator). Le cas de test que j'ai utilisé était quelque chose à cet effet: int [] arrayToSort = new int [] {9, 7, 2, 6, 3, 4, 8, 5, 1, 10, 11, 12, 13}; var source = Chunkify <int> (arrayToSort, 3) .ToArray (); Résultat: Source indiquant qu'il y avait 13 morceaux (le nombre d'éléments). Cela avait du sens pour moi car à moins que vous ne demandiez les énumérations internes, l'énumérateur n'a pas été incrémenté.
aolszowka
1
@aolszowka: points très valables. J'ai ajouté un avertissement et une section d'utilisation. Le code suppose que vous parcourez l'énumérable interne. Avec votre solution, vous perdez la paresse. Je pense qu'il devrait être possible d'obtenir le meilleur des deux mondes avec un IEnumerator personnalisé et en cache. Si je trouve une solution, je la
posterai
18

complètement paresseux, sans compter ni copier:

public static class EnumerableExtensions
{

  public static IEnumerable<IEnumerable<T>> Split<T>(this IEnumerable<T> source, int len)
  {
     if (len == 0)
        throw new ArgumentNullException();

     var enumer = source.GetEnumerator();
     while (enumer.MoveNext())
     {
        yield return Take(enumer.Current, enumer, len);
     }
  }

  private static IEnumerable<T> Take<T>(T head, IEnumerator<T> tail, int len)
  {
     while (true)
     {
        yield return head;
        if (--len == 0)
           break;
        if (tail.MoveNext())
           head = tail.Current;
        else
           break;
     }
  }
}
xtofs
la source
Cette solution est si élégante que je suis désolé de ne pas pouvoir voter plus d'une fois.
Mark
3
Je ne pense pas que cela échouerait jamais, exactement. Mais cela pourrait certainement avoir un comportement étrange. Si vous aviez 100 articles et que vous vous divisiez en lots de 10 et que vous dénombriez tous les lots sans énumérer aucun élément de ces lots, vous vous retrouveriez avec 100 lots de 1.
CaseyB
1
Comme mentionné @CaseyB, cela souffre du même 3dGrabber défaillant adressé ici stackoverflow.com/a/20953521/1037948 , mais l'homme est-il rapide!
drzaus
1
C'est une belle solution. Fait exactement ce qu'il promet.
Rod Hartzell
De loin la solution la plus élégante et au point. La seule chose est, vous devez ajouter une vérification des nombres négatifs et remplacer l'argumentNullException par une ArgumentException
Romain Vergnory
13

Je pense que la suggestion suivante serait la plus rapide. Je sacrifie la paresse de la source Enumerable pour pouvoir utiliser Array.Copy et connaître à l'avance la longueur de chacune de mes sous-listes.

public static IEnumerable<T[]> Chunk<T>(this IEnumerable<T> items, int size)
{
    T[] array = items as T[] ?? items.ToArray();
    for (int i = 0; i < array.Length; i+=size)
    {
        T[] chunk = new T[Math.Min(size, array.Length - i)];
        Array.Copy(array, i, chunk, 0, chunk.Length);
        yield return chunk;
    }
}
Marc-André Bertrand
la source
Non seulement le plus rapide, il gère également correctement les autres opérations énumérables sur le résultat, à savoir les éléments.Chunk (5) .Reverse (). SelectMany (x => x)
aussi
9

Nous pouvons améliorer la solution de @ JaredPar pour faire une véritable évaluation paresseuse. Nous utilisons une GroupAdjacentByméthode qui produit des groupes d'éléments consécutifs avec la même clé:

sequence
.Select((x, i) => new { Value = x, Index = i })
.GroupAdjacentBy(x=>x.Index/3)
.Select(g=>g.Select(x=>x.Value))

Étant donné que les groupes sont générés un par un, cette solution fonctionne efficacement avec des séquences longues ou infinies.

Colonel Panic
la source
8

J'ai écrit une méthode d'extension Clump il y a plusieurs années. Fonctionne très bien et est la mise en œuvre la plus rapide ici. : P

/// <summary>
/// Clumps items into same size lots.
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="source">The source list of items.</param>
/// <param name="size">The maximum size of the clumps to make.</param>
/// <returns>A list of list of items, where each list of items is no bigger than the size given.</returns>
public static IEnumerable<IEnumerable<T>> Clump<T>(this IEnumerable<T> source, int size)
{
    if (source == null)
        throw new ArgumentNullException("source");
    if (size < 1)
        throw new ArgumentOutOfRangeException("size", "size must be greater than 0");

    return ClumpIterator<T>(source, size);
}

private static IEnumerable<IEnumerable<T>> ClumpIterator<T>(IEnumerable<T> source, int size)
{
    Debug.Assert(source != null, "source is null.");

    T[] items = new T[size];
    int count = 0;
    foreach (var item in source)
    {
        items[count] = item;
        count++;

        if (count == size)
        {
            yield return items;
            items = new T[size];
            count = 0;
        }
    }
    if (count > 0)
    {
        if (count == size)
            yield return items;
        else
        {
            T[] tempItems = new T[count];
            Array.Copy(items, tempItems, count);
            yield return tempItems;
        }
    }
}
Cameron MacFarland
la source
ça devrait marcher mais ça tamponne 100% des morceaux, j'essayais d'éviter ça ... mais ça s'avère incroyablement velu.
Sam Saffron
@SamSaffron Yep. Surtout si vous jetez des choses comme plinq dans le mix, ce à quoi était à l'origine mon implémentation.
Cameron MacFarland
élargi ma réponse, laissez-moi savoir ce que vous pensez
Sam Saffron
@CameronMacFarland - pouvez-vous expliquer pourquoi la deuxième vérification du nombre de comptages == est nécessaire? Merci.
dugas
8

System.Interactive prévoit Buffer()à cet effet. Certains tests rapides montrent que les performances sont similaires à la solution de Sam.

dahlbyk
la source
1
connaissez-vous la sémantique de mise en mémoire tampon? Par exemple: si vous avez un énumérateur qui crache des chaînes de 300k et essayez de le diviser en morceaux de 10 000, obtiendrez-vous une mémoire insuffisante?
Sam Saffron
Buffer()renvoie IEnumerable<IList<T>>donc oui, vous auriez probablement un problème là-bas - il ne diffuse pas comme le vôtre.
dahlbyk
7

Voici une routine de fractionnement de liste que j'ai écrite il y a quelques mois:

public static List<List<T>> Chunk<T>(
    List<T> theList,
    int chunkSize
)
{
    List<List<T>> result = theList
        .Select((x, i) => new {
            data = x,
            indexgroup = i / chunkSize
        })
        .GroupBy(x => x.indexgroup, x => x.data)
        .Select(g => new List<T>(g))
        .ToList();

    return result;
}
Amy B
la source
6

Je trouve que ce petit extrait fait très bien l'affaire.

public static IEnumerable<List<T>> Chunked<T>(this List<T> source, int chunkSize)
{
    var offset = 0;

    while (offset < source.Count)
    {
        yield return source.GetRange(offset, Math.Min(source.Count - offset, chunkSize));
        offset += chunkSize;
    }
}
erlando
la source
5

Et celui-ci?

var input = new List<string> { "a", "g", "e", "w", "p", "s", "q", "f", "x", "y", "i", "m", "c" };
var k = 3

var res = Enumerable.Range(0, (input.Count - 1) / k + 1)
                    .Select(i => input.GetRange(i * k, Math.Min(k, input.Count - i * k)))
                    .ToList();

Pour autant que je sache, GetRange () est linéaire en termes de nombre d'éléments pris. Cela devrait donc bien fonctionner.

Roman Pekar
la source
5

C'est une vieille question mais c'est ce avec quoi j'ai fini; il énumère l'énumérable une seule fois, mais crée des listes pour chacune des partitions. Il ne souffre pas d'un comportement inattendu lorsqu'il ToArray()est appelé comme le font certaines implémentations:

    public static IEnumerable<IEnumerable<T>> Partition<T>(IEnumerable<T> source, int chunkSize)
    {
        if (source == null)
        {
            throw new ArgumentNullException("source");
        }

        if (chunkSize < 1)
        {
            throw new ArgumentException("Invalid chunkSize: " + chunkSize);
        }

        using (IEnumerator<T> sourceEnumerator = source.GetEnumerator())
        {
            IList<T> currentChunk = new List<T>();
            while (sourceEnumerator.MoveNext())
            {
                currentChunk.Add(sourceEnumerator.Current);
                if (currentChunk.Count == chunkSize)
                {
                    yield return currentChunk;
                    currentChunk = new List<T>();
                }
            }

            if (currentChunk.Any())
            {
                yield return currentChunk;
            }
        }
    }
aolszowka
la source
Serait bon de convertir cela en une méthode d'extension:public static IEnumerable<IEnumerable<T>> Partition<T>(this IEnumerable<T> source, int chunkSize)
krizzzn
+1 pour votre réponse. Cependant, je recommande deux choses: 1. utilisez foreach au lieu de while et utilisez block. 2. Passez chunkSize dans le constructeur de List pour que cette liste connaisse sa taille maximale attendue.
Usman Zafar
4

Nous avons trouvé que la solution de David B fonctionnait le mieux. Mais nous l'avons adapté à une solution plus générale:

list.GroupBy(item => item.SomeProperty) 
   .Select(group => new List<T>(group)) 
   .ToArray();
mwjackson
la source
3
C'est bien, mais assez différent de ce que le demandeur d'origine demandait.
Amy B
4

Cette solution suivante est la plus compacte que j'ai pu trouver, c'est O (n).

public static IEnumerable<T[]> Chunk<T>(IEnumerable<T> source, int chunksize)
{
    var list = source as IList<T> ?? source.ToList();
    for (int start = 0; start < list.Count; start += chunksize)
    {
        T[] chunk = new T[Math.Min(chunksize, list.Count - start)];
        for (int i = 0; i < chunk.Length; i++)
            chunk[i] = list[start + i];

        yield return chunk;
    }
}
Marc-André Bertrand
la source
4

Ancien code, mais voici ce que j'utilise:

    public static IEnumerable<List<T>> InSetsOf<T>(this IEnumerable<T> source, int max)
    {
        var 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;
        }
    }
Robert McKee
la source
Après la publication, je me suis rendu compte que c'est à peu près exactement le même code que casperOne a publié il y a 6 ans avec le changement d'utilisation de .Any () au lieu de .Count () car je n'ai pas besoin du nombre entier, il suffit de savoir s'il en existe. .
Robert McKee
3

Si la liste est de type system.collections.generic, vous pouvez utiliser la méthode "CopyTo" disponible pour copier des éléments de votre tableau vers d'autres sous-tableaux. Vous spécifiez l'élément de départ et le nombre d'éléments à copier.

Vous pouvez également créer 3 clones de votre liste d'origine et utiliser le "RemoveRange" sur chaque liste pour réduire la liste à la taille souhaitée.

Ou créez simplement une méthode d'aide pour le faire pour vous.

Jobo
la source
2

C'est une vieille solution mais j'avais une approche différente. J'utilise Skippour passer à l'offset souhaité et Takeextraire le nombre d'éléments souhaité:

public static IEnumerable<IEnumerable<T>> Chunk<T>(this IEnumerable<T> source, 
                                                   int chunkSize)
{
    if (chunkSize <= 0)
        throw new ArgumentOutOfRangeException($"{nameof(chunkSize)} should be > 0");

    var nbChunks = (int)Math.Ceiling((double)source.Count()/chunkSize);

    return Enumerable.Range(0, nbChunks)
                     .Select(chunkNb => source.Skip(chunkNb*chunkSize)
                     .Take(chunkSize));
}
Bertrand
la source
1
Très similaire à une approche que j'ai utilisée, mais je recommande que la source ne soit pas IEnumerable. Par exemple, si la source est le résultat d'une requête LINQ, le Skip / Take déclencherait des énumérations nbChunk de la requête. Pourrait coûter cher. Il serait préférable d'utiliser IList ou ICollection comme type de source. Cela évite complètement le problème.
RB Davidson
2

Pour toute personne intéressée par une solution packagée / maintenue, la bibliothèque MoreLINQ fournit la Batchméthode d'extension qui correspond à votre comportement demandé:

IEnumerable<char> source = "Example string";
IEnumerable<IEnumerable<char>> chunksOfThreeChars = source.Batch(3);

L' Batchimplémentation est similaire à la réponse de Cameron MacFarland , avec l'ajout d'une surcharge pour transformer le bloc / lot avant de revenir, et fonctionne assez bien.

Kevinoid
la source
ce devrait être la réponse acceptée. Au lieu de réinventer la roue, morelinq devrait être utilisé
Otabek Kholikov
1

Utilisation du partitionnement modulaire:

public IEnumerable<IEnumerable<string>> Split(IEnumerable<string> input, int chunkSize)
{
    var chunks = (int)Math.Ceiling((double)input.Count() / (double)chunkSize);
    return Enumerable.Range(0, chunks).Select(id => input.Where(s => s.GetHashCode() % chunks == id));
}
Janosz G.
la source
1

Je mets juste mes deux cents. Si vous vouliez "regrouper" la liste (visualiser de gauche à droite), vous pouvez procéder comme suit:

 public static List<List<T>> Buckets<T>(this List<T> source, int numberOfBuckets)
    {
        List<List<T>> result = new List<List<T>>();
        for (int i = 0; i < numberOfBuckets; i++)
        {
            result.Add(new List<T>());
        }

        int count = 0;
        while (count < source.Count())
        {
            var mod = count % numberOfBuckets;
            result[mod].Add(source[count]);
            count++;
        }
        return result;
    }
mattylantz
la source
1

Une autre façon utilise l' opérateur Rx Buffer

//using System.Linq;
//using System.Reactive.Linq;
//using System.Reactive.Threading.Tasks;

var observableBatches = anAnumerable.ToObservable().Buffer(size);

var batches = aList.ToObservable().Buffer(size).ToList().ToTask().GetAwaiter().GetResult();
frhack
la source
IMHO réponse la plus porper.
Stanislav Berkov
1
public static List<List<T>> GetSplitItemsList<T>(List<T> originalItemsList, short number)
    {
        var listGroup = new List<List<T>>();
        int j = number;
        for (int i = 0; i < originalItemsList.Count; i += number)
        {
            var cList = originalItemsList.Take(j).Skip(i).ToList();
            j += number;
            listGroup.Add(cList);
        }
        return listGroup;
    }
Joy Zhu
la source
0

J'ai pris la réponse principale et en ai fait un conteneur du CIO pour déterminer où diviser. ( Pour qui cherche vraiment à ne diviser que sur 3 éléments, en lisant ce post tout en recherchant une réponse? )

Cette méthode permet de diviser sur n'importe quel type d'élément selon les besoins.

public static List<List<T>> SplitOn<T>(List<T> main, Func<T, bool> splitOn)
{
    int groupIndex = 0;

    return main.Select( item => new 
                             { 
                               Group = (splitOn.Invoke(item) ? ++groupIndex : groupIndex), 
                               Value = item 
                             })
                .GroupBy( it2 => it2.Group)
                .Select(x => x.Select(v => v.Value).ToList())
                .ToList();
}

Donc, pour l'OP, le code serait

var it = new List<string>()
                       { "a", "g", "e", "w", "p", "s", "q", "f", "x", "y", "i", "m", "c" };

int index = 0; 
var result = SplitOn(it, (itm) => (index++ % 3) == 0 );
ΩmegaMan
la source
0

Aussi performatique que l' approche de Sam Saffron .

public static IEnumerable<IEnumerable<T>> Batch<T>(this IEnumerable<T> source, int size)
{
    if (source == null) throw new ArgumentNullException(nameof(source));
    if (size <= 0) throw new ArgumentOutOfRangeException(nameof(size), "Size must be greater than zero.");

    return BatchImpl(source, size).TakeWhile(x => x.Any());
}

static IEnumerable<IEnumerable<T>> BatchImpl<T>(this IEnumerable<T> source, int size)
{
    var values = new List<T>();
    var group = 1;
    var disposed = false;
    var e = source.GetEnumerator();

    try
    {
        while (!disposed)
        {
            yield return GetBatch(e, values, group, size, () => { e.Dispose(); disposed = true; });
            group++;
        }
    }
    finally
    {
        if (!disposed)
            e.Dispose();
    }
}

static IEnumerable<T> GetBatch<T>(IEnumerator<T> e, List<T> values, int group, int size, Action dispose)
{
    var min = (group - 1) * size + 1;
    var max = group * size;
    var hasValue = false;

    while (values.Count < min && e.MoveNext())
    {
        values.Add(e.Current);
    }

    for (var i = min; i <= max; i++)
    {
        if (i <= values.Count)
        {
            hasValue = true;
        }
        else if (hasValue = e.MoveNext())
        {
            values.Add(e.Current);
        }
        else
        {
            dispose();
        }

        if (hasValue)
            yield return values[i - 1];
        else
            yield break;
    }
}

}

leandromoh
la source
0

Peut fonctionner avec des générateurs infinis:

a.Zip(a.Skip(1), (x, y) => Enumerable.Repeat(x, 1).Concat(Enumerable.Repeat(y, 1)))
 .Zip(a.Skip(2), (xy, z) => xy.Concat(Enumerable.Repeat(z, 1)))
 .Where((x, i) => i % 3 == 0)

Code de démonstration: https://ideone.com/GKmL7M

using System;
using System.Collections.Generic;
using System.Linq;

public class Test
{
  private static void DoIt(IEnumerable<int> a)
  {
    Console.WriteLine(String.Join(" ", a));

    foreach (var x in a.Zip(a.Skip(1), (x, y) => Enumerable.Repeat(x, 1).Concat(Enumerable.Repeat(y, 1))).Zip(a.Skip(2), (xy, z) => xy.Concat(Enumerable.Repeat(z, 1))).Where((x, i) => i % 3 == 0))
      Console.WriteLine(String.Join(" ", x));

    Console.WriteLine();
  }

  public static void Main()
  {
    DoIt(new int[] {1});
    DoIt(new int[] {1, 2});
    DoIt(new int[] {1, 2, 3});
    DoIt(new int[] {1, 2, 3, 4});
    DoIt(new int[] {1, 2, 3, 4, 5});
    DoIt(new int[] {1, 2, 3, 4, 5, 6});
  }
}
1

1 2

1 2 3
1 2 3

1 2 3 4
1 2 3

1 2 3 4 5
1 2 3

1 2 3 4 5 6
1 2 3
4 5 6

Mais en fait, je préférerais écrire la méthode correspondante sans linq.

Qwertiy
la source
0

Regarde ça! J'ai une liste d'éléments avec un compteur de séquence et une date. Pour chaque redémarrage de la séquence, je souhaite créer une nouvelle liste.

Ex. liste des messages.

 List<dynamic> messages = new List<dynamic>
        {
            new { FcntUp = 101, CommTimestamp = "2019-01-01 00:00:01" },
            new { FcntUp = 102, CommTimestamp = "2019-01-01 00:00:02" },
            new { FcntUp = 103, CommTimestamp = "2019-01-01 00:00:03" },

            //restart of sequence
            new { FcntUp = 1, CommTimestamp = "2019-01-01 00:00:04" },
            new { FcntUp = 2, CommTimestamp = "2019-01-01 00:00:05" },
            new { FcntUp = 3, CommTimestamp = "2019-01-01 00:00:06" },

            //restart of sequence
            new { FcntUp = 1, CommTimestamp = "2019-01-01 00:00:07" },
            new { FcntUp = 2, CommTimestamp = "2019-01-01 00:00:08" },
            new { FcntUp = 3, CommTimestamp = "2019-01-01 00:00:09" }
        };

Je souhaite diviser la liste en listes distinctes au redémarrage du compteur. Voici le code:

var arraylist = new List<List<dynamic>>();

        List<dynamic> messages = new List<dynamic>
        {
            new { FcntUp = 101, CommTimestamp = "2019-01-01 00:00:01" },
            new { FcntUp = 102, CommTimestamp = "2019-01-01 00:00:02" },
            new { FcntUp = 103, CommTimestamp = "2019-01-01 00:00:03" },

            //restart of sequence
            new { FcntUp = 1, CommTimestamp = "2019-01-01 00:00:04" },
            new { FcntUp = 2, CommTimestamp = "2019-01-01 00:00:05" },
            new { FcntUp = 3, CommTimestamp = "2019-01-01 00:00:06" },

            //restart of sequence
            new { FcntUp = 1, CommTimestamp = "2019-01-01 00:00:07" },
            new { FcntUp = 2, CommTimestamp = "2019-01-01 00:00:08" },
            new { FcntUp = 3, CommTimestamp = "2019-01-01 00:00:09" }
        };

        //group by FcntUp and CommTimestamp
        var query = messages.GroupBy(x => new { x.FcntUp, x.CommTimestamp });

        //declare the current item
        dynamic currentItem = null;

        //declare the list of ranges
        List<dynamic> range = null;

        //loop through the sorted list
        foreach (var item in query)
        {
            //check if start of new range
            if (currentItem == null || item.Key.FcntUp < currentItem.Key.FcntUp)
            {
                //create a new list if the FcntUp starts on a new range
                range = new List<dynamic>();

                //add the list to the parent list
                arraylist.Add(range);
            }

            //add the item to the sublist
            range.Add(item);

            //set the current item
            currentItem = item;
        }
Claes-Philip Staiger
la source
-1

Pour insérer mes deux cents ...

En utilisant le type de liste pour la source à fragmenter, j'ai trouvé une autre solution très compacte:

public static IEnumerable<IEnumerable<TSource>> Chunk<TSource>(this IEnumerable<TSource> source, int chunkSize)
{
    // copy the source into a list
    var chunkList = source.ToList();

    // return chunks of 'chunkSize' items
    while (chunkList.Count > chunkSize)
    {
        yield return chunkList.GetRange(0, chunkSize);
        chunkList.RemoveRange(0, chunkSize);
    }

    // return the rest
    yield return chunkList;
}
Patrick
la source