Skip to main content

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[] - Arrays
  • List\<T\> - Lists
  • Span\<T\> - Spans
  • ReadOnlySpan\<T\> - Read-only spans
  • ImmutableArray\<T\> - Immutable arrays
  • ImmutableList\<T\> - Immutable lists
  • Any type with Add method 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

  1. Create a method that merges three different collection types (array, list, span) using collection expressions
  2. Implement a method that conditionally builds a collection based on multiple boolean flags
  3. Write a high-performance method using Span<T> and collection expressions to process data without heap allocations
  • Span<T> and Memory<T>
  • Inline Arrays
  • Performance optimization patterns