Spread Elements
Combine and flatten collections using the .. spread operator
Spread Elements in C# Collection Expressions
Master the power of spread elements in C# collection expressions with free flashcards and spaced repetition practice. This lesson covers spread syntax fundamentals, combining collections efficiently, and practical real-world applications—essential concepts for modern C# development and writing cleaner, more expressive code.
Welcome 💻
Spread elements represent one of the most elegant additions to C# collection expressions, introduced in C# 12. The spread element (using the .. operator) allows you to "unpack" elements from one collection directly into another during initialization. Think of it like emptying one container into another in a single, fluid motion—no loops, no explicit copying, just clean, declarative syntax.
Before spread elements, combining collections required verbose approaches like Concat(), AddRange(), or manual iteration. Now, you can express the same intent with minimal code that clearly communicates your purpose. This feature works seamlessly with arrays, lists, spans, and any enumerable type, making it a versatile tool in your C# toolkit.
Core Concepts 🎯
Understanding the Spread Operator (..)
The spread operator (..) in C# collection expressions "spreads" or "flattens" the elements of a collection into the containing collection expression. When the compiler encounters ..collection, it iterates through each element and adds them individually to the new collection.
Here's the basic syntax:
int[] numbers1 = [1, 2, 3];
int[] numbers2 = [4, 5, 6];
int[] combined = [..numbers1, ..numbers2];
// Result: [1, 2, 3, 4, 5, 6]
The spread operator works with:
- Arrays (both single and multi-dimensional, though multi-dimensional requires flattening)
- Lists and other collection types (
List<T>,ImmutableList<T>, etc.) - Spans and
ReadOnlySpan<T> - Any type implementing
IEnumerable<T> - Ranges created with the range operator
Mixing Spreads with Literal Elements
One of the most powerful aspects of spread elements is that you can mix them freely with literal values in the same collection expression:
int[] start = [1, 2];
int[] end = [9, 10];
int[] sequence = [0, ..start, 3, 4, 5, ..end, 11];
// Result: [0, 1, 2, 3, 4, 5, 9, 10, 11]
This flexibility allows you to:
- Add prefix or suffix elements to existing collections
- Insert elements between spread collections
- Build complex collections from multiple sources in a single expression
💡 Tip: The order matters! Elements appear in the final collection exactly as they're specified left-to-right in the expression.
Multiple Spreads in One Expression
You can use multiple spread operators in a single collection expression, spreading from different sources:
string[] fruits = ["apple", "banana"];
string[] vegetables = ["carrot", "broccoli"];
string[] grains = ["rice", "wheat"];
string[] food = [..fruits, ..vegetables, ..grains];
// Result: ["apple", "banana", "carrot", "broccoli", "rice", "wheat"]
This pattern is particularly useful for:
- Merging configuration from multiple sources
- Combining results from different queries or API calls
- Building composite collections from domain-specific subcollections
Performance Characteristics ⚡
The C# compiler optimizes spread operations intelligently:
| Scenario | Optimization | Performance |
|---|---|---|
| Known collection sizes | Pre-allocates exact capacity | O(n), single allocation |
| Array spreads | Uses Array.Copy or Buffer.MemoryCopy | Very fast, memory-efficient |
| Unknown sizes (IEnumerable) | May iterate twice or grow dynamically | O(n), possible multiple allocations |
| Multiple spreads | Calculates total size when possible | Still efficient with pre-allocation |
💡 Tip: When spreading from IEnumerable<T> (without known count), consider converting to an array or list first if you'll use it multiple times—this avoids repeated enumeration.
Type Inference and Target Typing
Spread elements work with C#'s target typing system. The collection expression's type is inferred from context:
int[] array = [1, ..otherNumbers]; // Creates int[]
List<int> list = [1, ..otherNumbers]; // Creates List<int>
ImmutableArray<int> immutable = [1, ..otherNumbers]; // Creates ImmutableArray<int>
Span<int> span = [1, ..otherNumbers]; // Creates Span<int>
The compiler generates the appropriate initialization code based on the target type, making spread elements extremely versatile.
Practical Examples 🔧
Example 1: Building Configuration Arrays
A common real-world scenario is combining configuration from multiple sources:
// Base configuration for all environments
string[] baseConfig = [
"logging=enabled",
"timeout=30"
];
// Development-specific settings
string[] devConfig = [
"debug=true",
"cache=disabled"
];
// Production overrides
string[] prodConfig = [
"debug=false",
"cache=enabled",
"cdn=https://cdn.example.com"
];
// Combine based on environment
bool isProduction = true;
string[] config = [
..baseConfig,
..(isProduction ? prodConfig : devConfig)
];
// Production result:
// ["logging=enabled", "timeout=30", "debug=false", "cache=enabled", "cdn=https://cdn.example.com"]
Why this works well: The spread syntax makes the configuration merging logic transparent. You can clearly see which settings come from where, and conditional spreading allows environment-specific configurations without complex if-else blocks.
Example 2: Pagination and Data Aggregation
When working with paginated APIs or batch processing:
public async Task<List<Product>> GetAllProductsAsync()
{
List<Product> allProducts = [];
int page = 1;
bool hasMore = true;
while (hasMore)
{
var pageResult = await FetchProductPageAsync(page);
// Accumulate results using spread
allProducts = [..allProducts, ..pageResult.Items];
hasMore = pageResult.HasNextPage;
page++;
}
return allProducts;
}
Alternative approach using collection expressions throughout:
public List<User> GetActiveUsers()
{
var adminUsers = GetAdminUsers(); // Returns List<User>
var regularUsers = GetRegularUsers(); // Returns List<User>
var guestUsers = GetGuestUsers(); // Returns List<User>
// Filter and combine in one expression
return [
..adminUsers.Where(u => u.IsActive),
..regularUsers.Where(u => u.IsActive),
..guestUsers.Where(u => u.IsActive)
];
}
Example 3: Testing with Dynamic Data Sets
Unit tests often need to combine known test cases with generated data:
[Theory]
public void TestValidation(string input)
{
// Test with both specific edge cases and generated data
string[] edgeCases = [
"",
" ",
null,
"a",
"verylongstringthatexceedsmaxlength"
];
string[] generatedInputs = GenerateRandomStrings(100);
string[] allTestCases = [..edgeCases, ..generatedInputs];
foreach (var testCase in allTestCases)
{
var result = Validator.Validate(testCase);
Assert.NotNull(result);
}
}
Example 4: Building Complex Data Structures
When constructing hierarchical or composite data:
public record NavigationMenu(string[] Items);
public NavigationMenu BuildNavigationMenu(User user)
{
// Base items everyone sees
string[] baseItems = ["Home", "About", "Contact"];
// Role-specific items
string[] userItems = user.IsAuthenticated
? ["Profile", "Settings", "Logout"]
: ["Login", "Register"];
string[] adminItems = user.IsAdmin
? ["Dashboard", "Users", "Reports"]
: [];
// Combine everything
return new NavigationMenu([
..baseItems,
..userItems,
..adminItems
]);
}
Key insight: Empty arrays spread to nothing, so conditional logic becomes cleaner. The adminItems array is either populated or empty, and spreading it "just works" in both cases.
Advanced Patterns 🚀
Spreading with LINQ Queries
Spread elements combine beautifully with LINQ:
var numbers = Enumerable.Range(1, 100);
var evens = numbers.Where(n => n % 2 == 0);
var odds = numbers.Where(n => n % 2 != 0);
// Reorder: evens first, then odds
int[] reordered = [..evens, ..odds];
// Or with inline queries
int[] filtered = [
0, // Add a zero at the start
..numbers.Where(n => n % 3 == 0), // Multiples of 3
1000 // Add a large number at the end
];
Spreading Spans for High Performance
When performance is critical, spread Span<T> and ReadOnlySpan<T>:
public Span<byte> BuildPacket(ReadOnlySpan<byte> header,
ReadOnlySpan<byte> payload)
{
// Efficient packet construction without allocations
Span<byte> packet = stackalloc byte[header.Length + payload.Length + 4];
int pos = 0;
header.CopyTo(packet[pos..]);
pos += header.Length;
payload.CopyTo(packet[pos..]);
pos += payload.Length;
// Add checksum
BitConverter.TryWriteBytes(packet[pos..], ComputeChecksum(payload));
return packet;
}
🧠 Memory tip: For stack-allocated spans, you can't use collection expressions directly (since they create heap allocations by default), but you can spread existing spans into arrays when needed.
Pattern: Immutable Collection Building
Spread elements shine when building immutable collections:
public ImmutableArray<string> AddTags(ImmutableArray<string> existingTags,
string newTag)
{
// Create new immutable array with additional tag
return [..existingTags, newTag];
}
public ImmutableList<int> MergeAndSort(ImmutableList<int> list1,
ImmutableList<int> list2)
{
// Combine and sort in one expression
return [..list1, ..list2].OrderBy(x => x).ToImmutableList();
}
Nested Collections and Flattening
Spread can flatten one level of nesting:
List<int[]> nestedLists = [
[1, 2, 3],
[4, 5],
[6, 7, 8, 9]
];
// Flatten one level
int[] flattened = [
..nestedLists[0],
..nestedLists[1],
..nestedLists[2]
];
// Result: [1, 2, 3, 4, 5, 6, 7, 8, 9]
// Or use SelectMany for arbitrary nesting
int[] fullyFlattened = [..nestedLists.SelectMany(x => x)];
⚠️ Important: Spread only flattens one level. For deeply nested structures, you'll need to combine spread with LINQ's SelectMany or recursive flattening.
Common Mistakes ⚠️
Mistake 1: Forgetting the Spread Operator
❌ Wrong:
int[] arr1 = [1, 2, 3];
int[] arr2 = [arr1, 4, 5]; // ERROR: Can't convert int[] to int
✅ Correct:
int[] arr1 = [1, 2, 3];
int[] arr2 = [..arr1, 4, 5]; // Spreads elements: [1, 2, 3, 4, 5]
Why: Without .., you're trying to add the array itself as an element, not its contents.
Mistake 2: Spreading Non-Enumerable Types
❌ Wrong:
int single = 42;
int[] array = [..single]; // ERROR: int is not enumerable
✅ Correct:
int single = 42;
int[] array = [single]; // Just use the value directly
// Or if you need it as a collection:
int[] array = [..new[] { single }];
Mistake 3: Performance Issues with Multiple Enumerations
❌ Inefficient:
IEnumerable<int> query = database.GetLargeDataSet();
// Each spread enumerates the query again!
int[] arr1 = [..query];
int[] arr2 = [..query];
int[] arr3 = [..query];
✅ Efficient:
IEnumerable<int> query = database.GetLargeDataSet();
int[] cached = [..query]; // Enumerate once
// Reuse cached array
int[] arr1 = [..cached];
int[] arr2 = [..cached];
int[] arr3 = [..cached];
Mistake 4: Unexpected Type Inference
❌ Unclear:
var mystery = [..array1, ..array2]; // What type is this?
✅ Explicit:
int[] result = [..array1, ..array2]; // Clear type
// Or use explicit target type
List<int> result = [..array1, ..array2];
Why: Using var with collection expressions can make the code less readable. Being explicit about the collection type improves clarity.
Mistake 5: Modifying During Spread
❌ Dangerous:
List<int> source = [1, 2, 3];
List<int> result = [..source];
source.Add(4); // Modifies after spread
// result is still [1, 2, 3] - spread creates a snapshot
✅ Intentional:
List<int> source = [1, 2, 3];
List<int> result = [..source]; // Creates independent copy
// result and source are now separate collections
Why: Spread creates a new collection with copies of the elements (or references for reference types). It's not a live view.
Key Takeaways 🎯
📋 Quick Reference: Spread Elements
| Syntax | ..collection spreads elements into new collection |
| Use Cases | Combining arrays, merging configs, flattening one level |
| Mix & Match | Freely combine literals: [1, ..arr, 5] |
| Multiple Spreads | [..arr1, ..arr2, ..arr3] works perfectly |
| Type Support | Arrays, Lists, Spans, any IEnumerable<T> |
| Performance | Optimized by compiler, pre-allocates when possible |
| Flattening | Only one level; use SelectMany for deeper nesting |
| Immutability | Creates new collection; doesn't modify source |
Remember These Principles:
- Spread unpacks, not wraps:
..collectionextracts elements, not the collection itself - Order matters: Elements appear left-to-right as written
- One-level flattening: Spread doesn't recursively flatten nested collections
- New collection: Spread always creates a new collection instance
- Empty spreads are fine: Spreading an empty collection adds nothing
- Works with LINQ: Combine spreads with queries for powerful expressions
- Type inference: The target type determines what collection is created
💡 Pro Tip: Think of spread elements as "collection concatenation made simple." Anywhere you'd reach for Concat(), AddRange(), or manual loops to combine collections, consider using spread syntax for cleaner, more expressive code.
📚 Further Study
- Microsoft Docs - Collection Expressions: https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/operators/collection-expressions
- C# 12 Features Overview: https://learn.microsoft.com/en-us/dotnet/csharp/whats-new/csharp-12
- Performance Considerations for Collection Expressions: https://devblogs.microsoft.com/dotnet/performance-improvements-in-net-8/
🎓 Ready to Practice? Use the flashcards throughout this lesson to reinforce your understanding of spread elements, then test your knowledge with the quiz questions below!