web-dev-qa-db-fra.com

Fractionner un IEnumerable <T> en morceaux de taille fixe (renvoyer un IEnumerable <IEnumerable <T>> où les séquences internes sont de longueur fixe)

Je veux prendre un IEnumerable<T> et le diviser en morceaux de taille fixe.

Je l'ai, mais cela semble inélégant en raison de toute la création/copie de la liste:

private static IEnumerable<IEnumerable<T>> Partition<T>(this IEnumerable<T> items, int partitionSize)
{
    List<T> partition = new List<T>(partitionSize);
    foreach (T item in items)
    {
        partition.Add(item);
        if (partition.Count == partitionSize)
        {
            yield return partition;
            partition = new List<T>(partitionSize);
        }
    }
    // Cope with items.Count % partitionSize != 0
    if (partition.Count > 0) yield return partition;
}

Y a-t-il quelque chose de plus idiomatique?

EDIT: Bien que cela ait été marqué comme un doublon de Diviser le tableau en un tableau de sous-tablea ce n'est pas le cas - cette question concerne le fractionnement d'un tableau, alors qu'il s'agit de IEnumerable<T>. De plus, cette question nécessite que la dernière sous-séquence soit complétée. Les deux questions sont étroitement liées mais ne sont pas les mêmes.

44
Alastair Maw

Vous pouvez essayer d'implémenter vous-même la méthode Batch mentionnée ci-dessus comme ceci:

    static class MyLinqExtensions 
    { 
        public static IEnumerable<IEnumerable<T>> Batch<T>( 
            this IEnumerable<T> source, int batchSize) 
        { 
            using (var enumerator = source.GetEnumerator()) 
                while (enumerator.MoveNext()) 
                    yield return YieldBatchElements(enumerator, batchSize - 1); 
        } 

        private static IEnumerable<T> YieldBatchElements<T>( 
            IEnumerator<T> source, int batchSize) 
        { 
            yield return source.Current; 
            for (int i = 0; i < batchSize && source.MoveNext(); i++) 
                yield return source.Current; 
        } 
    }

J'ai récupéré ce code depuis http://blogs.msdn.com/b/pfxteam/archive/2012/11/16/plinq-and-int32-maxvalue.aspx .

[~ # ~] mise à jour [~ # ~] : veuillez noter que cette implémentation évalue non seulement les lots mais aussi les articles à l'intérieur des lots, ce qui signifie produira uniquement des résultats corrects lorsque le lot est énuméré uniquement après que tous les lots précédents ont été énumérés. Par exemple:

public static void Main(string[] args)
{
    var xs = Enumerable.Range(1, 20);
    Print(xs.Batch(5).Skip(1)); // should skip first batch with 5 elements
}

public static void Print<T>(IEnumerable<IEnumerable<T>> batches)
{
    foreach (var batch in batches)
    {
        Console.WriteLine($"[{string.Join(", ", batch)}]");
    }
}

affichera:

[2, 3, 4, 5, 6] //only first element is skipped.
[7, 8, 9, 10, 11]
[12, 13, 14, 15, 16]
[17, 18, 19, 20]

Donc, si vous utilisez le cas suppose que le traitement par lots lorsque les lots sont évalués séquentiellement, alors la solution paresseuse ci-dessus fonctionnera, sinon si vous ne pouvez pas garantir un traitement par lots strictement séquentiel (par exemple lorsque vous souhaitez traiter des lots en parallèle), vous aurez probablement besoin d'une solution qui énumère avec impatience le contenu du lot, similaire à celui mentionné dans la question ci-dessus ou dans le MoreLINQ

61
takemyoxygen

C'est comme si vous vouliez deux blocs itérateurs ("yield return méthodes "). J'ai écrit cette méthode d'extension:

static class Extensions
{
  public static IEnumerable<IEnumerable<T>> Partition<T>(this IEnumerable<T> items, int partitionSize)
  {
    return new PartitionHelper<T>(items, partitionSize);
  }

  private sealed class PartitionHelper<T> : IEnumerable<IEnumerable<T>>
  {
    readonly IEnumerable<T> items;
    readonly int partitionSize;
    bool hasMoreItems;

    internal PartitionHelper(IEnumerable<T> i, int ps)
    {
      items = i;
      partitionSize = ps;
    }

    public IEnumerator<IEnumerable<T>> GetEnumerator()
    {
      using (var enumerator = items.GetEnumerator())
      {
        hasMoreItems = enumerator.MoveNext();
        while (hasMoreItems)
          yield return GetNextBatch(enumerator).ToList();
      }
    }

    IEnumerable<T> GetNextBatch(IEnumerator<T> enumerator)
    {
      for (int i = 0; i < partitionSize; ++i)
      {
        yield return enumerator.Current;
        hasMoreItems = enumerator.MoveNext();
        if (!hasMoreItems)
          yield break;
      }
    }

    IEnumerator IEnumerable.GetEnumerator()
    {
      return GetEnumerator();      
    }
  }
}
13
Jeppe Stig Nielsen

Peut être?

public static IEnumerable<IEnumerable<T>> Partition<T>(this IEnumerable<T> items, int partitionSize)
{
    return items.Select((item, inx) => new { item, inx })
                .GroupBy(x => x.inx / partitionSize)
                .Select(g => g.Select(x => x.item));
}

Il y en a un déjà implémenté aussi: morelinq's Batch.

11
L.B

Solution la plus folle (avec Extensions réactives ):

public static IEnumerable<IList<T>> Partition<T>(this IEnumerable<T> items, int partitionSize)
{
    return items
            .ToObservable() // Converting sequence to observable sequence
            .Buffer(partitionSize) // Splitting it on spececified "partitions"
            .ToEnumerable(); // Converting it back to ordinary sequence
}

Je sais que j'ai changé de signature mais de toute façon nous savons tous que nous aurons une collection de taille fixe en tant que morceau.

BTW si vous utiliserez le bloc itérateur, n'oubliez pas de diviser votre implémentation en deux méthodes pour valider les arguments avec impatience!

7
Sergey Teplyakov

Pour une solution élégante, vous pouvez également consulter MoreLinq.Batch.

Il regroupe la séquence source dans des compartiments de taille.

Exemple:

int[] ints = new int[] {1,2,3,4,5,6};
var batches = ints.Batch(2); // batches -> [0] : 1,2 ; [1]:3,4 ; [2] :5,6
3
Tilak
public static IEnumerable<IEnumerable<T>> Partition<T>(this IEnumerable<T> items, 
                                                       int partitionSize)
{
    int i = 0;
    return items.GroupBy(x => i++ / partitionSize).ToArray();
}
2
JustAnotherUser

Vous pouvez le faire en utilisant une surcharge de Enumerable.GroupBy et en profitant de la division entière.

return items.Select((element, index) => new { Element = element, Index = index })
    .GroupBy(obj => obj.Index / partitionSize, (_, partition) => partition);
1
Adam Maras

Qu'en est-il des classes de partitionnement dans l'espace de noms System.Collections.Concurrent ?

0
Christoffer