Generic Collections, Dictionaries & High-Performance Memory (Span<T>)
Architecting scalable enterprise backends requires choosing appropriate data structures and managing memory allocations defensively. The .NET base class library provides rich, type-safe generic collections residing in the System.Collections.Generic namespace. Furthermore, modern .NET introduces revolutionary low-level abstractions like Span<T> and Memory<T> that enable zero-allocation memory slicing. In this lesson, we study key generic collections, dictionary mechanics, and allocation-free high-performance memory design.
Core Generic Collections in .NET
Generic collections provide compile-time type safety and eliminate the performance degradation of boxing and unboxing value types associated with legacy collections like ArrayList or Hashtable:
List<T>: A dynamically resized contiguous array offering O(1) random index access. Ideal for general sequential data. When resizing, it doubles internal buffer capacity.Dictionary<TKey, TValue>: A high-performance hash table mapping unique keys to values with amortized O(1) lookups, insertions, and removals.HashSet<T>: An unordered collection of unique elements optimized for rapid mathematical set operations (union, intersection, difference) with O(1) membership checks.Queue<T>andStack<T>: Specialized FIFO and LIFO collections optimized for operational buffering and undo operations.
Big-O Complexity Comparison
| Collection Type | Random Access | Search | Insertion (Tail) | Insertion (Middle) |
|---|---|---|---|---|
List<T> |
O(1) | O(n) | O(1) amortized | O(n) |
Dictionary<TKey, TValue> |
O(1) by Key | O(1) by Key | O(1) amortized | N/A |
HashSet<T> |
N/A | O(1) amortized | O(1) amortized | N/A |
LinkedList<T> |
O(n) | O(n) | O(1) | O(1) once node is located |
Dictionary Internals: Hashing and Equality Comparers
Under the hood, a .NET Dictionary<TKey, TValue> stores entries across two parallel arrays: an int[] _buckets array and an Entry[] _entries array. When a key is inserted, its 32-bit hash code is computed via GetHashCode() and mapped to a bucket index using modulo arithmetic. When multiple keys share a bucket, entries form a collision chain.
When using strings as dictionary keys, always supply an explicit StringComparer (such as StringComparer.OrdinalIgnoreCase or StringComparer.Ordinal). This avoids unintended locale-dependent sorting bugs and significantly boosts hashing throughput.
Span<T> and ReadOnlySpan<T>: Zero-Allocation Performance
Prior to modern .NET, parsing substrings from large text payloads (such as JSON or HTTP headers) using string.Substring() caused numerous ephemeral heap allocations that overwhelmed the garbage collector. .NET solved this by introducing Span<T> and ReadOnlySpan<T>.
A Span<T> is a ref struct allocated exclusively on the call stack that represents a contiguous window of arbitrary memory—whether managed heap arrays, stack-allocated buffers, or native unmanaged pointers. Slicing a span creates a new view over the existing memory without copying a single byte, enabling lightning-fast parsing with zero garbage collection overhead.
Memory Pooling with ArrayPool<T>
In high-throughput microservices handling large binary payloads, allocating temporary buffers repeatedly places pressure on the Garbage Collector Gen 2 or Large Object Heap (LOH). The .NET framework provides ArrayPool<T>.Shared to rent and return reusable array buffers, completely avoiding heap churn during intensive network I/O cycles.
Read-Only Optimizations: Frozen Collections in .NET 8
For lookup tables that are populated once during startup and never mutated thereafter (such as routing tables or system metadata), .NET 8 introduced FrozenDictionary<TKey, TValue> and FrozenSet<T>. These collections analyze the exact key distribution at creation time to generate perfectly balanced, unmodifiable lookup structures that execute faster than standard dictionaries.
Defensive Sizing and Capacity Pre-Allocation
Whenever the approximate size of a collection is known in advance, always specify its initial capacity in the constructor (e.g., new List<Order>(expectedCount)). This eliminates unnecessary memory reallocations and array copying cycles as the collection expands.
using System;
using System.Collections.Generic;
namespace CodingRider.CollectionsDemo {
public class Program {
public static void Main() {
Console.WriteLine("=== C# Generic Collections & High-Performance Memory ===");
// High-throughput generic dictionary
var serviceMetrics = new Dictionary<string, long>(StringComparer.OrdinalIgnoreCase) {
["auth-service"] = 145000L,
["payment-gateway"] = 89200L,
["search-indexer"] = 320100L
};
// Efficient upsert using modern TryAdd / indexer
serviceMetrics["auth-service"] += 1500L;
foreach (var kvp in serviceMetrics) {
Console.WriteLine($"Service: {kvp.Key,-18} | Processed: {kvp.Value:N0} requests");
}
// Zero-allocation memory slicing using ReadOnlySpan<char>
string payloadHeader = "CR_VERSION:2.4;STATUS:OK;REGION:US_EAST";
ReadOnlySpan<char> span = payloadHeader.AsSpan();
int versionEnd = span.IndexOf(';');
ReadOnlySpan<char> versionSlice = span.Slice(0, versionEnd);
Console.WriteLine($"Header Substring (Zero Heap Allocation): {versionSlice.ToString()}");
}
}
}