web-dev-qa-db-fra.com

Diviser la liste en sous-listes avec LINQ

Existe-t-il un moyen de séparer un List<SomeObject> en plusieurs listes distinctes de SomeObject, en utilisant l’index de l’élément comme séparateur de chaque division?

Laissez-moi illustrer:

J'ai un List<SomeObject> et j'ai besoin d'un List<List<SomeObject>> ou List<SomeObject>[], afin 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 aussi besoin que la taille des listes résultantes soit un paramètre de cette fonction.

344
Felipe Lima

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. Convertissez ensuite chaque groupe en une liste et la IEnumerable de List en une List de Lists

338
JaredPar

Cette question est un peu ancienne, mais je viens d'écrire ceci 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);
    }
}
293
CaseyB

En général, l'approche suggérée par CaseyB _ fonctionne bien. En fait, si vous transmettez un List<T>, il est difficile de le prendre en défaut, je le changerais peut-être comme suit:

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'appels massives. Néanmoins, cette approche a un défaut général. Il matérialise deux énumérations par morceau, pour mettre en évidence le problème, essayez de lancer: 

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

Pour résoudre ce problème, nous pouvons essayer la méthode { de Cameron , qui réussit le test ci-dessus haut la main car elle ne parcourt qu'une fois le dénombrement. 

Le problème est qu’il a un défaut différent, il matérialise chaque élément de chaque bloc. Le problème avec cette approche est que vous utilisez beaucoup de mémoire. 

Pour illustrer cela, essayez de courir:

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 doit être capable de gérer l'itération hors service 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é à cet endroit. 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();


        }

    }
}

Vous pouvez également introduire une série d’optimisations pour l’itération hors service des morceaux, ce qui est hors de portée ici. 

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

Note 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 sécuriser les threads, vous devrez modifier EnumeratorWrapper.

91
Sam Saffron

Vous pouvez utilisez un certain nombre de requêtes utilisant Take et 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 alors appeler cela et LINQ est activé afin que vous puissiez effectuer d'autres opérations sur les séquences résultantes.


À la lumière de la réponse de Sam , je sentais qu'il y avait un moyen plus facile de faire cela sans:

  • Itérer à nouveau dans la liste (ce que je n'avais pas fait à l'origine)
  • Matérialiser les éléments en groupes avant de libérer le bloc (pour les gros morceaux d'éléments, des problèmes de mémoire risquent de se produire)
  • Tout le code que Sam a posté

Cela dit, voici un autre passage, que j'ai codifié dans une méthode d'extension pour IEnumerable<T> appelé 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 d'étonnant là-haut, juste une vérification d'erreur de base.

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

En gros, il récupère le IEnumerator<T> et effectue une itération manuelle dans chaque élément. Il vérifie s'il existe des éléments à énumérer. Une fois que chaque morceau est énuméré, s'il ne reste plus aucun article, cela se déclenche.

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'implémentation IEnumerable<T> intérieure à 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());
}

Étant donné que MoveNext a déjà été appelé sur le IEnumerator<T> transmis à ChunkSequence, il renvoie l'élément renvoyé par Current , puis incrémente le nombre, en veillant à ne jamais renvoyer plus de chunkSize éléments et en passant à l'élément suivant du séquence après chaque itération (mais court-circuité si le nombre d’éléments générés dépasse la taille du bloc).

S'il ne reste plus d'éléments, la méthode InternalChunk fera un autre passage dans la boucle externe, mais lorsque MoveNext sera appelé une seconde fois, il retournera toujours false, conformément à la documentation (l'emphase mienne):

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 faux. Lorsque l'énumérateur est à cette position, la suite les appels à MoveNext retournent également false jusqu'à ce que Reset soit appelé.

À ce stade, la boucle se cassera 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();
    }
}

Sortie:

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

Une remarque importante, cela pas _ fonctionnera si vous ne drainez pas toute la séquence enfant ou ne vous interrompez pas à un moment quelconque de la séquence parent. Ceci est une mise en garde importante, mais si votre cas d'utilisation est que vous utiliserez tous élément de la séquence de séquences, 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é .

63
casperOne

Ok, voici mon point de vue:

  • complètement paresseux: travaille sur des énumérables infinis
  • pas de copie/mise en mémoire tampon intermédiaire
  • O (n) temps d'exécution 
  • fonctionne aussi 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 itérateurs basés sur yield.

L'itérateur externe doit garder une trace du nombre d'éléments effectivement consommés par l'itérateur interne (bloc). Pour ce faire, fermez remaining avec innerMoveNext(). Les éléments non consommés d'un morceau sont supprimés avant que le prochain morceau soit fourni par l'itérateur externe . C'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() renverrait 6).

Remarque: La réponse a été mise à jour pour remédier aux lacunes signalées par @aolszowka.

44
3dGrabber

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

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 durée 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;
    }
}
12

System.Interactive fournit Buffer() à cette fin. Certains tests rapides montrent que les performances sont similaires à la solution de Sam.

8
dahlbyk

Nous pouvons améliorer la solution de @ JaredPar pour réaliser de véritables évaluations paresseuses. Nous utilisons une méthode GroupAdjacentBy qui génère 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 cédés un par un, cette solution fonctionne efficacement avec des séquences longues ou infinies.

8
Colonel Panic

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

Voici une routine de division 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;
}
6
Amy B

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 de comportement inattendu lorsque ToArray() est appelé comme le font certaines des 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;
            }
        }
    }
5
aolszowka

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

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();
4
mwjackson

Cette solution suivante est la plus compacte possible: 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;
    }
}
4

Ancien code, mais voici ce que j'ai utilisé:

    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;
        }
    }
4
Robert McKee

Qu'en est-il de 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();

Autant que je sache, GetRange () est linéaire en termes de nombre d'éléments pris. Donc, cela devrait bien fonctionner.

3
Roman Pekar

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 dans 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 l’option "RemoveRange" de chaque liste pour réduire la liste à la taille souhaitée.

Ou créez simplement une méthode d'assistance pour le faire à votre place.

3
Jobo

C'est une vieille solution mais j'avais une approche différente. J'utilise Skip pour passer à l'offset souhaité et Take pour extraire 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));
}
2
Bertrand

Pour les personnes intéressées par une solution packagée/maintenue, la bibliothèque MoreLINQ fournit la méthode Batch extension qui correspond au comportement que vous avez demandé:

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

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

1
Kevinoid

Juste en mettant dans mes deux cents. Si vous vouliez "scinder" 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;
    }
1
user1883961

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));
}
1
Janosz G.

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

}

0
leandromoh

Une autre méthode consiste à utiliser RxOpérateur tampon

//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();
0
frhack

J'ai pris la réponse principale et en ai fait un conteneur IOC pour déterminer où scinder. ( Pour qui cherche vraiment à diviser sur 3 éléments, en lisant ce post en cherchant 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 le PO, 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 );
0
ΩmegaMan

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 réalité, je préférerais écrire la méthode correspondante sans linq.

0
Qwertiy