C#
LINQ
programming
collection
duplicate

Split a collection into n parts with LINQ?

Master System Design with Codemia

Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.

Introduction

Splitting collections is common in batching, worker distribution, and pagination. The key design point is to distinguish splitting by chunk size from splitting into exactly N parts. If this distinction is unclear, code often drops items or creates uneven groups unexpectedly.

Define Split Semantics Upfront

Two different goals:

  • Fixed-size chunks, where each group has at most k elements.
  • Exactly N groups, where group count is fixed and sizes are balanced.

These goals are not interchangeable when total element count is not divisible.

Document expected behavior for remainders before writing code.

Fixed-Size Chunks with .NET Chunk

In modern .NET, Chunk is the simplest option for size-based batching.

csharp
1using System;
2using System.Linq;
3
4var items = Enumerable.Range(1, 10);
5var chunks = items.Chunk(3);
6
7foreach (var chunk in chunks)
8{
9    Console.WriteLine(string.Join(",", chunk));
10}

Final chunk may be smaller, which is usually desired in batch processing.

Fixed-Size Chunking for Older Frameworks

If Chunk is unavailable, group by index bucket.

csharp
1using System.Collections.Generic;
2using System.Linq;
3
4public static IEnumerable<List<T>> ChunkBySize<T>(IEnumerable<T> source, int size)
5{
6    if (size <= 0) throw new ArgumentOutOfRangeException(nameof(size));
7
8    return source
9        .Select((item, index) => new { item, index })
10        .GroupBy(x => x.index / size)
11        .Select(g => g.Select(x => x.item).ToList());
12}

This preserves order and gives predictable chunk boundaries.

Split into Exactly N Balanced Parts

For fixed group count, distribute remainders over earliest groups.

csharp
1using System;
2using System.Collections.Generic;
3using System.Linq;
4
5public static List<List<T>> SplitIntoParts<T>(IReadOnlyList<T> items, int parts)
6{
7    if (parts <= 0) throw new ArgumentOutOfRangeException(nameof(parts));
8
9    var result = new List<List<T>>(parts);
10    int baseSize = items.Count / parts;
11    int extra = items.Count % parts;
12    int offset = 0;
13
14    for (int i = 0; i < parts; i++)
15    {
16        int take = baseSize + (i < extra ? 1 : 0);
17        result.Add(items.Skip(offset).Take(take).ToList());
18        offset += take;
19    }
20
21    return result;
22}

This keeps group-size difference at most one element.

Materialization and Deferred Enumeration

If source is deferred query, repeated enumeration can be expensive or inconsistent. Materialize once before splitting if source has side effects or external dependencies.

csharp
var snapshot = source.ToList();
var parts = SplitIntoParts(snapshot, 4);

This creates stable input for deterministic grouping behavior.

Parallel Distribution Considerations

Static equal-size partitions do not guarantee equal processing time if items have different cost profiles. For skewed workloads, dynamic queues or work-stealing can outperform static partitions.

Still, static splitting is useful when:

  • Item processing cost is uniform.
  • Deterministic assignment is needed.
  • Simplicity matters more than perfect load balancing.

Validation and Test Invariants

Always test:

  • Total output count equals input count.
  • Every input item appears exactly once.
  • No duplicates introduced.
  • Edge cases such as empty input and invalid part count.

These invariants catch most split-function defects early.

API Design Suggestions

Name methods by semantics:

  • 'ChunkBySize for fixed size.'
  • 'SplitIntoParts for fixed part count.'

Clarity in naming prevents misuse in shared utility libraries.

Choose return type intentionally:

  • 'IEnumerable<T[]> for streaming.'
  • 'List<List<T>> for eager mutable post-processing.'

Naming and API Clarity

In shared utility packages, clear naming avoids misuse. A method named Chunk suggests size-based grouping, while a method named SplitIntoParts suggests fixed output count. This distinction reduces code-review confusion and prevents subtle logic bugs in batching pipelines.

Common Pitfalls

  • Confusing chunk size with number of parts.
  • Losing remainder elements due to incorrect loop math.
  • Re-enumerating deferred sources unintentionally.
  • Assuming equal element count means equal execution time.
  • Skipping validation for invalid sizes and zero-part input.

Summary

  • Start by defining whether split means fixed chunk size or fixed part count.
  • Use Chunk for simple size-based batching in modern .NET.
  • Use quotient-remainder distribution for balanced N-part splitting.
  • Materialize deferred sources when consistency matters.
  • Validate no-loss and no-duplication invariants in tests.

Course illustration
Course illustration

All Rights Reserved.