Collection Expressions (C# 12)
Overview
Collection expressions provide a unified, concise syntax [...] for creating and initializing various collection types including arrays, lists, spans, and more.
Syntax
// Arrays
int[] numbers = [1, 2, 3, 4, 5];
// Lists
List<string> names = ["Alice", "Bob", "Charlie"];
// Spans
Span<int> span = [10, 20, 30];
// ImmutableArray
ImmutableArray<double> values = [1.5, 2.5, 3.5];
Key Features
1. Unified Syntax Across Collection Types
Before C# 12, each collection type had different initialization syntax:
// ❌ Old way - different syntax for each type
int[] array = new int[] { 1, 2, 3 };
List<int> list = new List<int> { 1, 2, 3 };
Span<int> span = stackalloc int[] { 1, 2, 3 };
// ✅ New way - unified syntax
int[] array = [1, 2, 3];
List<int> list = [1, 2, 3];
Span<int> span = [1, 2, 3];
2. Spread Operator (..)
Combine collections using the spread operator:
int[] first = [1, 2, 3];
int[] second = [4, 5, 6];
// Combine arrays
int[] combined = [..first, ..second]; // [1, 2, 3, 4, 5, 6]
// Mix elements and spreads
int[] mixed = [0, ..first, 99, ..second, 100];
// Result: [0, 1, 2, 3, 99, 4, 5, 6, 100]
3. Empty Collections
// Empty collections
int[] empty = [];
List<string> emptyList = [];
4. Type Inference
The compiler infers the collection type based on context:
// Inferred as List<int>
var numbers = CreateList([1, 2, 3]);
List<int> CreateList(List<int> items) => items;
// Inferred as int[]
var array = CreateArray([1, 2, 3]);
int[] CreateArray(int[] items) => items;
Advanced Patterns
Pattern 1: Combining Multiple Collections
public class DataAggregator
{
public List<int> MergeData(List<int> cache, int[] newData, Span<int> buffer)
{
// Combine different collection types seamlessly
return [..cache, ..newData, ..buffer];
}
}
Pattern 2: Conditional Elements
public int[] BuildArray(bool includeZero, int[] baseNumbers)
{
return includeZero
? [0, ..baseNumbers]
: [..baseNumbers];
}
Pattern 3: Nested Collections
// 2D arrays
int[][] matrix =
[
[1, 2, 3],
[4, 5, 6],
[7, 8, 9]
];
// Lists of lists
List<List<string>> groups =
[
["Alice", "Bob"],
["Charlie", "David"],
["Eve", "Frank"]
];
Pattern 4: Method Arguments
public void ProcessItems(IEnumerable<int> items)
{
// Process items
}
// Call with collection expression
ProcessItems([1, 2, 3, 4, 5]);
// Combine with spread
var existing = new List<int> { 10, 20 };
ProcessItems([..existing, 30, 40]);
Performance Considerations
Stack vs Heap Allocation
// Heap allocation (array/list)
int[] heapArray = [1, 2, 3, 4, 5];
// Stack allocation (span) - more efficient for small, short-lived collections
Span<int> stackSpan = [1, 2, 3, 4, 5];
When to Use Each Type
public class PerformanceExample
{
// ✅ Use List for collections that grow
public List<int> DynamicData() => [1, 2, 3];
// ✅ Use array for fixed-size collections
public int[] FixedData() => [1, 2, 3, 4, 5];
// ✅ Use Span for high-performance, short-lived operations
public void ProcessData()
{
Span<int> temp = [1, 2, 3, 4, 5];
// Process without heap allocation
}
// ✅ Use ImmutableArray for thread-safe, unchanging data
public ImmutableArray<string> Constants() => ["Red", "Green", "Blue"];
}
Supported Collection Types
Collection expressions work with any type that has a suitable constructor or collection initializer:
T[]- ArraysList\<T\>- ListsSpan\<T\>- SpansReadOnlySpan\<T\>- Read-only spansImmutableArray\<T\>- Immutable arraysImmutableList\<T\>- Immutable lists- Any type with
Addmethod and collection initializer support
Common Use Cases
1. API Return Values
public class UserService
{
public List<string> GetUserRoles(int userId)
{
if (userId == 1)
return ["Admin", "User"];
if (userId == 2)
return ["User"];
return [];
}
}
2. Test Data
[Test]
public void TestProcessing()
{
var input = [1, 2, 3, 4, 5];
var expected = [2, 4, 6, 8, 10];
var result = Processor.Double(input);
Assert.Equal(expected, result);
}
3. Configuration
public class AppSettings
{
public List<string> AllowedOrigins { get; } =
[
"https://example.com",
"https://api.example.com"
];
public int[] AllowedPorts { get; } = [80, 443, 8080];
}
Interview Questions
Q: What's the difference between [1,2,3] assigned to an array vs a List?
A: The collection expression syntax is the same, but the compiler generates different code based on the target type. For arrays, it creates an array; for List, it creates a List. The syntax is unified, but the underlying implementation respects the target type.
Q: Can collection expressions improve performance?
A: Yes! When used with Span<T>, they enable stack allocation instead of heap allocation. They also reduce syntax overhead and make code more readable, and the compiler can optimize the generated code.
Q: What's the spread operator and when should you use it?
A: The spread operator .. expands a collection into individual elements. Use it to combine collections, add elements to existing collections, or flatten nested collections. It's more efficient than using LINQ's Concat or manually iterating.
Practice Exercise
- Create a method that merges three different collection types (array, list, span) using collection expressions
- Implement a method that conditionally builds a collection based on multiple boolean flags
- Write a high-performance method using Span<T> and collection expressions to process data without heap allocations
Related Concepts
- Span<T> and Memory<T>
- Inline Arrays
- Performance optimization patterns