ASP.NET Core

C# Collections

C# Collections - List<T>, HashSet<T>, Dictionary<TKey, TValue>, and ObservableCollection<T> for managing data structures.

https://learn.microsoft.com/en-us/dotnet/csharp/programming-guide/concepts/collections

Implement Collection Types

What are C# Collections?

C# Collections are specialized classes designed to store, manage, and manipulate groups of related objects. They provide dynamic data structures that automatically handle memory allocation, resizing, and organization.

When you use C# Collections, the system:

  1. Automatically manages memory allocation and resizing
  2. Provides type-safe storage with generics
  3. Offers optimized algorithms for common operations
  4. Enables LINQ queries for data manipulation
  5. Supports enumeration and iteration patterns

Collections in C# are part of the System.Collections.Generic namespace, which provides type-safe, high-performance data structures suitable for most programming scenarios.

Collections provide powerful, type-safe data structures for managing groups of objects efficiently. By understanding the characteristics, performance implications, and appropriate use cases for each collection type, you can write more efficient and maintainable code.


Why Use C# Collections?

1. Type Safety

Generic collections provide compile-time type checking, preventing runtime errors and eliminating the need for casting.

2. Dynamic Sizing

Collections automatically grow and shrink as needed, unlike fixed-size arrays that require manual resizing.

3. Performance Optimization

Each collection type is optimized for specific operations, enabling efficient data manipulation based on your use case.

4. Rich Functionality

Built-in methods for searching, sorting, filtering, and transforming data reduce the need for custom implementations.

5. LINQ Integration

Collections seamlessly integrate with Language Integrated Query (LINQ) for powerful data querying capabilities.

6. Standardization

Using standard collection types makes code more readable and maintainable across teams and projects.


How C# Collections Work

Basic Concept

C# Collections use generics to provide type-safe containers that can hold any type of object while maintaining performance and flexibility:

Collection Selection Flow:

Data Requirements
    ↓
├─ Ordered sequence with index access? → List<T>
├─ Unique elements only? → HashSet<T>
├─ Key-value pairs? → Dictionary<TKey, TValue>
├─ Observable changes? → ObservableCollection<T>
└─ First-in-first-out? → Queue<T>

Example - Choosing the Right Collection:

// Scenario 1: Store customer names in order
List<string> customerNames = new List<string>();
customerNames.Add("Alice");
customerNames.Add("Bob");
customerNames[0]; // Access by index

// Scenario 2: Track unique product IDs
HashSet<int> productIds = new HashSet<int>();
productIds.Add(101);
productIds.Add(101); // Duplicate ignored

// Scenario 3: Store user settings
Dictionary<string, string> settings = new Dictionary<string, string>();
settings["Theme"] = "Dark";
settings["Language"] = "en-US";

Important: Choosing the correct collection type impacts both performance and code clarity. Always select based on your specific data access patterns.


Core Collection Types

List<T>

List<T> is a dynamic array that provides indexed access to an ordered sequence of elements. It's the most commonly used collection type.

Key Characteristics:

  • Ordered: Elements maintain insertion order
  • Indexed: Fast access by position (O(1))
  • Dynamic: Automatically resizes as needed
  • Allows duplicates
  • Best for: Sequential access, frequent additions at the end

Basic Operations:

// Creating and initializing
List<int> numbers = new List<int>();
List<string> names = new List<string> { "Alice", "Bob", "Charlie" };

// Adding elements
numbers.Add(10);           // Add single element
numbers.AddRange(new[] { 20, 30, 40 }); // Add multiple

// Accessing elements
string firstName = names[0];              // By index
string lastItem = names[names.Count - 1]; // Last element

// Inserting at position
names.Insert(1, "David"); // Insert at index 1

// Removing elements
names.Remove("Bob");      // Remove by value
names.RemoveAt(0);        // Remove by index
numbers.RemoveAll(n => n > 25); // Remove all matching

// Searching
bool hasAlice = names.Contains("Alice");
int index = names.IndexOf("Charlie");
string found = names.Find(n => n.StartsWith("D"));

// Iterating
foreach (string name in names)
{
    Console.WriteLine(name);
}

// Sorting
numbers.Sort();                        // Ascending
numbers.Sort((a, b) => b.CompareTo(a)); // Descending

// Converting
string[] nameArray = names.ToArray();

Performance Characteristics:

Operation          | Time Complexity
-------------------|----------------
Add (at end)       | O(1) amortized
Insert (at index)  | O(n)
Remove             | O(n)
Access by index    | O(1)
Search by value    | O(n)
Sort               | O(n log n)

Common Use Cases:

// Use Case 1: Managing a todo list
List<TodoItem> todos = new List<TodoItem>();
todos.Add(new TodoItem { Id = 1, Task = "Buy groceries", Done = false });
todos.Add(new TodoItem { Id = 2, Task = "Call dentist", Done = true });

// Filter incomplete tasks
var pending = todos.Where(t => !t.Done).ToList();

// Use Case 2: Processing transaction history
List<Transaction> transactions = GetTransactions();
var recent = transactions.OrderByDescending(t => t.Date).Take(10).ToList();
decimal total = transactions.Sum(t => t.Amount);

// Use Case 3: Building dynamic menus
List<MenuItem> menu = new List<MenuItem>();
menu.Add(new MenuItem("File", new[] { "New", "Open", "Save" }));
menu.Add(new MenuItem("Edit", new[] { "Cut", "Copy", "Paste" }));

Best Practices:

// ✅ Good: Specify initial capacity if size is known
List<int> items = new List<int>(1000); // Avoids resizing

// ✅ Good: Use collection initializer for static data
var colors = new List<string> { "Red", "Green", "Blue" };

// ❌ Avoid: Frequent insertions at the beginning
for (int i = 0; i < 1000; i++)
{
    list.Insert(0, i); // Poor performance - O(n) each time
}

// ✅ Better: Add at end and reverse if order matters
for (int i = 0; i < 1000; i++)
{
    list.Add(i);
}
list.Reverse();

HashSet<T>

HashSet<T> is an unordered collection that stores unique elements with fast lookup, insertion, and deletion operations.

Key Characteristics:

  • Unordered: No guaranteed element order
  • Unique: Automatically prevents duplicates
  • Fast operations: O(1) for add, remove, contains
  • Best for: Uniqueness enforcement, set operations, membership testing

Basic Operations:

// Creating and initializing
HashSet<int> numbers = new HashSet<int>();
HashSet<string> tags = new HashSet<string> { "C#", "Azure", "SQL" };

// Adding elements
numbers.Add(10);        // Returns true if added
numbers.Add(10);        // Returns false (duplicate)

// Checking membership
bool hasAzure = tags.Contains("Azure"); // Fast O(1) lookup

// Removing elements
tags.Remove("SQL");
tags.RemoveWhere(t => t.StartsWith("A"));

// Set operations
HashSet<string> set1 = new HashSet<string> { "A", "B", "C" };
HashSet<string> set2 = new HashSet<string> { "B", "C", "D" };

// Union: All elements from both sets
set1.UnionWith(set2);           // set1 = { "A", "B", "C", "D" }

// Intersection: Only common elements
set1.IntersectWith(set2);       // set1 = { "B", "C" }

// Difference: Elements in set1 but not in set2
set1.ExceptWith(set2);          // set1 = { "A" }

// Symmetric difference: Elements in either set but not both
set1.SymmetricExceptWith(set2); // set1 = { "A", "D" }

// Subset/Superset checks
bool isSubset = set1.IsSubsetOf(set2);
bool isSuperset = set1.IsSupersetOf(set2);
bool overlaps = set1.Overlaps(set2);

// Iterating (order not guaranteed)
foreach (string tag in tags)
{
    Console.WriteLine(tag);
}

Performance Characteristics:

Operation          | Time Complexity
-------------------|----------------
Add                | O(1) average
Remove             | O(1) average
Contains           | O(1) average
Union              | O(n + m)
Intersection       | O(min(n, m))

Common Use Cases:

// Use Case 1: Remove duplicates from a list
List<int> numbers = new List<int> { 1, 2, 2, 3, 4, 4, 5 };
HashSet<int> unique = new HashSet<int>(numbers); // { 1, 2, 3, 4, 5 }

// Use Case 2: Track unique visitors
HashSet<string> uniqueVisitors = new HashSet<string>();
void TrackVisitor(string userId)
{
    if (uniqueVisitors.Add(userId))
    {
        Console.WriteLine("New visitor!");
    }
    else
    {
        Console.WriteLine("Returning visitor");
    }
}

// Use Case 3: Find common interests between users
HashSet<string> user1Interests = new HashSet<string> { "coding", "gaming", "music" };
HashSet<string> user2Interests = new HashSet<string> { "gaming", "sports", "music" };

var commonInterests = new HashSet<string>(user1Interests);
commonInterests.IntersectWith(user2Interests); // { "gaming", "music" }

// Use Case 4: Validate unique constraint
public class RegistrationService
{
    private HashSet<string> registeredEmails = new HashSet<string>();
    
    public bool RegisterUser(string email)
    {
        if (!registeredEmails.Add(email))
        {
            throw new InvalidOperationException("Email already registered");
        }
        // Continue registration...
        return true;
    }
}

// Use Case 5: Efficiently check blacklist
HashSet<string> blacklist = LoadBlacklist(); // Load once
bool IsBlocked(string ip) => blacklist.Contains(ip); // Fast O(1) check

Best Practices:

// ✅ Good: Use HashSet for membership testing
HashSet<int> allowedIds = new HashSet<int> { 1, 2, 3, 4, 5 };
if (allowedIds.Contains(userId)) { /* ... */ }

// ❌ Avoid: Using List for frequent membership tests
List<int> allowedIdsList = new List<int> { 1, 2, 3, 4, 5 };
if (allowedIdsList.Contains(userId)) { /* O(n) - slow! */ }

// ✅ Good: Specify equality comparer for custom types
var products = new HashSet<Product>(new ProductComparer());

// ✅ Good: Use set operations instead of manual loops
var combined = new HashSet<string>(collection1);
combined.UnionWith(collection2);

Custom Equality Comparison:

public class Product
{
    public int Id { get; set; }
    public string Name { get; set; }
}

public class ProductComparer : IEqualityComparer<Product>
{
    public bool Equals(Product x, Product y)
    {
        return x?.Id == y?.Id;
    }
    
    public int GetHashCode(Product obj)
    {
        return obj.Id.GetHashCode();
    }
}

// Usage
var products = new HashSet<Product>(new ProductComparer());
products.Add(new Product { Id = 1, Name = "Laptop" });
products.Add(new Product { Id = 1, Name = "Desktop" }); // Duplicate ID - ignored

Dictionary<TKey, TValue>

Dictionary<TKey, TValue> stores key-value pairs with fast lookup by key. It's similar to a hash table or associative array.

Key Characteristics:

  • Key-value pairs: Each key maps to exactly one value
  • Unique keys: Keys must be unique, values can duplicate
  • Fast lookup: O(1) average time complexity
  • Unordered: No guaranteed order of elements
  • Best for: Fast data retrieval, caching, lookup tables

Basic Operations:

// Creating and initializing
Dictionary<string, int> ages = new Dictionary<string, int>();
Dictionary<int, string> products = new Dictionary<int, string>
{
    { 1, "Laptop" },
    { 2, "Mouse" },
    { 3, "Keyboard" }
};

// Alternative initialization syntax (C# 6+)
var settings = new Dictionary<string, string>
{
    ["Theme"] = "Dark",
    ["Language"] = "en-US",
    ["TimeZone"] = "UTC"
};

// Adding elements
ages.Add("Alice", 30);
ages["Bob"] = 25;              // Also adds if key doesn't exist

// Accessing elements
int aliceAge = ages["Alice"];  // Throws if key not found

// Safe access
if (ages.TryGetValue("Charlie", out int charlieAge))
{
    Console.WriteLine($"Charlie is {charlieAge} years old");
}
else
{
    Console.WriteLine("Charlie not found");
}

// Updating values
ages["Alice"] = 31;           // Updates existing value

// Checking if key exists
bool hasAlice = ages.ContainsKey("Alice");
bool hasAge25 = ages.ContainsValue(25);

// Removing elements
ages.Remove("Bob");
ages.Clear(); // Remove all

// Iterating
foreach (KeyValuePair<string, int> kvp in ages)
{
    Console.WriteLine($"{kvp.Key}: {kvp.Value}");
}

// Iterate keys only
foreach (string name in ages.Keys)
{
    Console.WriteLine(name);
}

// Iterate values only
foreach (int age in ages.Values)
{
    Console.WriteLine(age);
}

// LINQ queries
var adults = ages.Where(kvp => kvp.Value >= 18)
                 .ToDictionary(kvp => kvp.Key, kvp => kvp.Value);

Performance Characteristics:

Operation          | Time Complexity
-------------------|----------------
Add                | O(1) average
Remove             | O(1) average
Access by key      | O(1) average
ContainsKey        | O(1) average
ContainsValue      | O(n)

Common Use Cases:

// Use Case 1: Configuration settings
Dictionary<string, string> config = new Dictionary<string, string>
{
    ["DatabaseConnection"] = "Server=localhost;Database=MyDb",
    ["ApiKey"] = "abc123xyz",
    ["MaxRetries"] = "3"
};

string GetSetting(string key, string defaultValue = "")
{
    return config.TryGetValue(key, out string value) ? value : defaultValue;
}

// Use Case 2: Caching expensive operations
Dictionary<string, byte[]> imageCache = new Dictionary<string, byte[]>();

byte[] GetImage(string url)
{
    if (!imageCache.TryGetValue(url, out byte[] image))
    {
        image = DownloadImage(url); // Expensive operation
        imageCache[url] = image;
    }
    return image;
}

// Use Case 3: Counting occurrences
string text = "hello world hello";
Dictionary<string, int> wordCount = new Dictionary<string, int>();

foreach (string word in text.Split(' '))
{
    if (wordCount.ContainsKey(word))
        wordCount[word]++;
    else
        wordCount[word] = 1;
}
// Result: { "hello": 2, "world": 1 }

// Use Case 4: Lookup tables for data transformation
Dictionary<string, string> countryCodeMap = new Dictionary<string, string>
{
    ["US"] = "United States",
    ["UK"] = "United Kingdom",
    ["FR"] = "France"
};

string GetCountryName(string code)
{
    return countryCodeMap.TryGetValue(code, out string name) 
        ? name 
        : "Unknown";
}

// Use Case 5: Index multiple objects by ID
Dictionary<int, Customer> customerIndex = customers.ToDictionary(c => c.Id);

Customer GetCustomer(int id)
{
    return customerIndex.TryGetValue(id, out Customer customer) 
        ? customer 
        : null;
}

// Use Case 6: Group data by category
var productsByCategory = products
    .GroupBy(p => p.Category)
    .ToDictionary(g => g.Key, g => g.ToList());

Best Practices:

// ✅ Good: Use TryGetValue instead of ContainsKey + indexer
if (dict.TryGetValue(key, out var value))
{
    // Use value
}

// ❌ Avoid: Checking then accessing (two lookups)
if (dict.ContainsKey(key))
{
    var value = dict[key]; // Second lookup!
}

// ✅ Good: Specify initial capacity if size is known
var largeDictionary = new Dictionary<string, int>(10000);

// ✅ Good: Use null-coalescing for default values
string value = dict.TryGetValue(key, out string result) ? result : "default";

// ✅ Good: Use StringComparer for case-insensitive keys
var caseInsensitive = new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase);
caseInsensitive["NAME"] = 1;
caseInsensitive["name"] = 2; // Updates the same key

Advanced Patterns:

// Pattern 1: GetOrAdd pattern
public class Cache<TKey, TValue>
{
    private Dictionary<TKey, TValue> _cache = new Dictionary<TKey, TValue>();
    
    public TValue GetOrAdd(TKey key, Func<TKey, TValue> factory)
    {
        if (!_cache.TryGetValue(key, out TValue value))
        {
            value = factory(key);
            _cache[key] = value;
        }
        return value;
    }
}

// Usage
var cache = new Cache<string, string>();
string result = cache.GetOrAdd("key", k => ExpensiveOperation(k));

// Pattern 2: Multi-level dictionary (nested lookup)
Dictionary<string, Dictionary<string, int>> multiLevel = new Dictionary<string, Dictionary<string, int>>();

void AddValue(string category, string item, int value)
{
    if (!multiLevel.ContainsKey(category))
        multiLevel[category] = new Dictionary<string, int>();
    
    multiLevel[category][item] = value;
}

// Pattern 3: Inverted index
Dictionary<string, List<int>> invertedIndex = new Dictionary<string, List<int>>();

void IndexDocument(int docId, string[] words)
{
    foreach (string word in words)
    {
        if (!invertedIndex.ContainsKey(word))
            invertedIndex[word] = new List<int>();
        
        invertedIndex[word].Add(docId);
    }
}

List<int> SearchDocuments(string word)
{
    return invertedIndex.TryGetValue(word, out var docs) ? docs : new List<int>();
}

ObservableCollection<T>

ObservableCollection<T> is a dynamic collection that provides notifications when items are added, removed, or the entire list is refreshed. It's essential for UI data binding in WPF, UWP, and other XAML frameworks.

Key Characteristics:

  • Implements INotifyCollectionChanged
  • Implements INotifyPropertyChanged
  • Automatically updates UI when collection changes
  • Best for: MVVM pattern, data binding, real-time UI updates

Basic Operations:

using System.Collections.ObjectModel;

// Creating and initializing
ObservableCollection<string> items = new ObservableCollection<string>();
ObservableCollection<Product> products = new ObservableCollection<Product>
{
    new Product { Id = 1, Name = "Laptop" },
    new Product { Id = 2, Name = "Mouse" }
};

// Subscribe to change notifications
products.CollectionChanged += (sender, e) =>
{
    switch (e.Action)
    {
        case NotifyCollectionChangedAction.Add:
            Console.WriteLine($"Added: {e.NewItems[0]}");
            break;
        case NotifyCollectionChangedAction.Remove:
            Console.WriteLine($"Removed: {e.OldItems[0]}");
            break;
        case NotifyCollectionChangedAction.Replace:
            Console.WriteLine($"Replaced: {e.OldItems[0]} with {e.NewItems[0]}");
            break;
        case NotifyCollectionChangedAction.Reset:
            Console.WriteLine("Collection cleared");
            break;
    }
};

// Adding elements (triggers notification)
products.Add(new Product { Id = 3, Name = "Keyboard" });

// Removing elements (triggers notification)
products.RemoveAt(0);
products.Remove(products.First(p => p.Id == 2));

// Replacing elements (triggers notification)
products[0] = new Product { Id = 4, Name = "Monitor" };

// Clearing (triggers notification)
products.Clear();

Common Use Cases:

// Use Case 1: MVVM ViewModel with ObservableCollection
public class ProductViewModel : INotifyPropertyChanged
{
    private ObservableCollection<Product> _products;
    
    public ObservableCollection<Product> Products
    {
        get => _products;
        set
        {
            _products = value;
            OnPropertyChanged(nameof(Products));
        }
    }
    
    public ProductViewModel()
    {
        Products = new ObservableCollection<Product>();
        LoadProducts();
    }
    
    public void AddProduct(Product product)
    {
        Products.Add(product); // UI updates automatically
    }
    
    public void RemoveProduct(Product product)
    {
        Products.Remove(product); // UI updates automatically
    }
    
    public event PropertyChangedEventHandler PropertyChanged;
    
    protected void OnPropertyChanged(string propertyName)
    {
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
    }
}

// Use Case 2: Real-time notification list
public class NotificationService
{
    public ObservableCollection<Notification> Notifications { get; }
    
    public NotificationService()
    {
        Notifications = new ObservableCollection<Notification>();
        
        // Subscribe to new notifications
        Notifications.CollectionChanged += (s, e) =>
        {
            if (e.Action == NotifyCollectionChangedAction.Add)
            {
                // Play sound, show popup, etc.
                ShowNotificationPopup((Notification)e.NewItems[0]);
            }
        };
    }
    
    public void AddNotification(string message)
    {
        Notifications.Insert(0, new Notification
        {
            Message = message,
            Timestamp = DateTime.Now
        });
        
        // Keep only last 50 notifications
        while (Notifications.Count > 50)
        {
            Notifications.RemoveAt(Notifications.Count - 1);
        }
    }
}

// Use Case 3: Live data feed
public class LiveDataFeed
{
    public ObservableCollection<StockPrice> StockPrices { get; }
    private Timer _updateTimer;
    
    public LiveDataFeed()
    {
        StockPrices = new ObservableCollection<StockPrice>();
        
        // Update prices every second
        _updateTimer = new Timer(UpdatePrices, null, 0, 1000);
    }
    
    private void UpdatePrices(object state)
    {
        var newPrices = FetchLatestPrices();
        
        foreach (var price in newPrices)
        {
            var existing = StockPrices.FirstOrDefault(s => s.Symbol == price.Symbol);
            if (existing != null)
            {
                var index = StockPrices.IndexOf(existing);
                StockPrices[index] = price; // Triggers UI update
            }
            else
            {
                StockPrices.Add(price);
            }
        }
    }
}

Best Practices:

// ✅ Good: Use ObservableCollection for UI-bound data
public ObservableCollection<TodoItem> TodoItems { get; set; }

// ❌ Avoid: Using List for UI binding (no automatic updates)
public List<TodoItem> TodoItems { get; set; } // UI won't update!

// ✅ Good: Initialize in constructor
public MyViewModel()
{
    Items = new ObservableCollection<string>();
}

// ✅ Good: Batch updates to minimize UI refreshes
public void AddMultipleItems(IEnumerable<Item> items)
{
    // Temporarily disable notifications
    foreach (var item in items)
    {
        Items.Add(item);
    }
}

// ⚠️ Note: ObservableCollection is not thread-safe
// Use Dispatcher for UI thread updates
Application.Current.Dispatcher.Invoke(() =>
{
    Items.Add(newItem);
});

Additional Collection Types

Queue<T> - First-In-First-Out (FIFO)

Queue<string> queue = new Queue<string>();

// Enqueue (add to end)
queue.Enqueue("First");
queue.Enqueue("Second");
queue.Enqueue("Third");

// Dequeue (remove from front)
string first = queue.Dequeue(); // "First"

// Peek (view front without removing)
string next = queue.Peek(); // "Second"

// Use Case: Task processing
Queue<Task> taskQueue = new Queue<Task>();

void ProcessTasks()
{
    while (taskQueue.Count > 0)
    {
        var task = taskQueue.Dequeue();
        task.Execute();
    }
}

Stack<T> - Last-In-First-Out (LIFO)

Stack<int> stack = new Stack<int>();

// Push (add to top)
stack.Push(1);
stack.Push(2);
stack.Push(3);

// Pop (remove from top)
int top = stack.Pop(); // 3

// Peek (view top without removing)
int nextTop = stack.Peek(); // 2

// Use Case: Undo functionality
Stack<ICommand> undoStack = new Stack<ICommand>();

void ExecuteCommand(ICommand command)
{
    command.Execute();
    undoStack.Push(command);
}

void Undo()
{
    if (undoStack.Count > 0)
    {
        var command = undoStack.Pop();
        command.Undo();
    }
}

LinkedList<T> - Doubly-Linked List

LinkedList<string> list = new LinkedList<string>();

// Add elements
var node1 = list.AddFirst("First");
var node2 = list.AddLast("Last");
var node3 = list.AddAfter(node1, "Middle");

// Navigate
LinkedListNode<string> current = list.First;
while (current != null)
{
    Console.WriteLine(current.Value);
    current = current.Next;
}

// Remove
list.Remove(node2);

// Use Case: LRU Cache
class LRUCache<TKey, TValue>
{
    private Dictionary<TKey, LinkedListNode<CacheItem>> _cache;
    private LinkedList<CacheItem> _lru;
    private int _capacity;
    
    private class CacheItem
    {
        public TKey Key { get; set; }
        public TValue Value { get; set; }
    }
    
    public TValue Get(TKey key)
    {
        if (_cache.TryGetValue(key, out var node))
        {
            // Move to front (most recently used)
            _lru.Remove(node);
            _lru.AddFirst(node);
            return node.Value.Value;
        }
        return default;
    }
    
    public void Put(TKey key, TValue value)
    {
        if (_cache.TryGetValue(key, out var node))
        {
            // Update and move to front
            node.Value.Value = value;
            _lru.Remove(node);
            _lru.AddFirst(node);
        }
        else
        {
            // Add new item
            if (_cache.Count >= _capacity)
            {
                // Remove least recently used
                var lru = _lru.Last;
                _cache.Remove(lru.Value.Key);
                _lru.RemoveLast();
            }
            
            var newNode = _lru.AddFirst(new CacheItem { Key = key, Value = value });
            _cache[key] = newNode;
        }
    }
}

SortedList<TKey, TValue> - Sorted Key-Value Pairs

SortedList<int, string> sortedList = new SortedList<int, string>
{
    { 3, "Three" },
    { 1, "One" },
    { 2, "Two" }
};

// Automatically sorted by key
foreach (var kvp in sortedList)
{
    Console.WriteLine($"{kvp.Key}: {kvp.Value}");
}
// Output: 1: One, 2: Two, 3: Three

// Use Case: Time-series data
SortedList<DateTime, double> temperatures = new SortedList<DateTime, double>();
temperatures[DateTime.Now] = 72.5;
temperatures[DateTime.Now.AddHours(1)] = 73.2;

SortedSet<T> - Sorted Unique Elements

SortedSet<int> numbers = new SortedSet<int> { 5, 2, 8, 1, 9 };

// Automatically sorted and unique
foreach (int num in numbers)
{
    Console.WriteLine(num); // 1, 2, 5, 8, 9
}

// Range operations
var subset = numbers.GetViewBetween(2, 8); // { 2, 5, 8 }

// Use Case: Leaderboard
SortedSet<Player> leaderboard = new SortedSet<Player>(
    Comparer<Player>.Create((a, b) => b.Score.CompareTo(a.Score))
);

Choosing the Right Collection

Decision Tree

Start Here
├─ Need key-value pairs?
│   ├─ YES → Dictionary<TKey, TValue>
│   │   ├─ Need sorted by key? → SortedDictionary<TKey, TValue>
│   │   └─ Need index access? → SortedList<TKey, TValue>
│   └─ NO ↓
│
├─ Need unique elements only?
│   ├─ YES → HashSet<T>
│   │   └─ Need sorted? → SortedSet<T>
│   └─ NO ↓
│
├─ Need specific order?
│   ├─ First-In-First-Out? → Queue<T>
│   ├─ Last-In-First-Out? → Stack<T>
│   └─ Custom order? → LinkedList<T>
│
├─ Need UI data binding?
│   └─ YES → ObservableCollection<T>
│
└─ Default choice → List<T>

Comparison Table

CollectionOrderedIndexedUniqueSortedUse Case
List<T>General-purpose sequential data
HashSet<T>Unique elements, fast lookup
Dictionary<TKey,TValue>Key✅ KeysKey-value pairs, fast lookup
ObservableCollection<T>UI data binding
Queue<T>✅ FIFOTask processing, buffering
Stack<T>✅ LIFOUndo/redo, parsing
LinkedList<T>Frequent insertions/deletions
SortedList<TKey,TValue>✅ KeysSorted key-value, index access
SortedSet<T>Sorted unique elements

Performance Comparison

Time Complexity:

OperationList<T>HashSet<T>Dictionary<TKey,TValue>
Add (end)O(1)*O(1)*O(1)*
Add (start)O(n)O(1)*N/A
RemoveO(n)O(1)*O(1)*
SearchO(n)O(1)*O(1)* (by key)
Access by indexO(1)N/AN/A
ContainsO(n)O(1)*O(1)* (by key)

*Amortized time complexity (average case)

Space Complexity:

CollectionMemory Overhead
List<T>Low - Contiguous array
HashSet<T>Medium - Hash table
Dictionary<TKey,TValue>Medium - Hash table
LinkedList<T>High - Node pointers

Common Operations Across Collections

LINQ Integration

All collections implement IEnumerable<T>, enabling powerful LINQ queries:

List<int> numbers = new List<int> { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };

// Filtering
var evenNumbers = numbers.Where(n => n % 2 == 0).ToList();

// Transformation
var doubled = numbers.Select(n => n * 2).ToList();

// Aggregation
int sum = numbers.Sum();
int max = numbers.Max();
double average = numbers.Average();

// Ordering
var descending = numbers.OrderByDescending(n => n).ToList();

// Grouping
var grouped = numbers.GroupBy(n => n % 3);

// Complex queries
List<Product> products = GetProducts();

var expensiveElectronics = products
    .Where(p => p.Category == "Electronics")
    .Where(p => p.Price > 500)
    .OrderByDescending(p => p.Price)
    .Take(10)
    .ToList();

// Joining collections
var customerOrders = customers
    .Join(orders,
          customer => customer.Id,
          order => order.CustomerId,
          (customer, order) => new { customer.Name, order.Total })
    .ToList();

Conversion Between Collections

// List to HashSet (remove duplicates)
List<int> listWithDuplicates = new List<int> { 1, 2, 2, 3, 3, 3 };
HashSet<int> uniqueSet = new HashSet<int>(listWithDuplicates);

// HashSet to List (for indexing)
List<int> listFromSet = uniqueSet.ToList();

// Dictionary to List of key-value pairs
Dictionary<string, int> dict = new Dictionary<string, int>
{
    ["A"] = 1,
    ["B"] = 2
};
List<KeyValuePair<string, int>> kvpList = dict.ToList();

// List to Dictionary (with key selector)
List<Product> products = GetProducts();
Dictionary<int, Product> productDict = products.ToDictionary(p => p.Id);

// Array to List
int[] array = { 1, 2, 3, 4, 5 };
List<int> list = array.ToList();

// List to Array
int[] backToArray = list.ToArray();

// ObservableCollection to List
ObservableCollection<string> observable = new ObservableCollection<string> { "A", "B" };
List<string> regularList = observable.ToList();

Copying Collections

// Shallow copy (references same objects)
List<Product> original = GetProducts();
List<Product> shallowCopy = new List<Product>(original);

// Alternative shallow copy
List<Product> anotherCopy = original.ToList();

// Deep copy (requires custom implementation)
List<Product> DeepCopy(List<Product> source)
{
    return source.Select(p => new Product
    {
        Id = p.Id,
        Name = p.Name,
        Price = p.Price
    }).ToList();
}

// Dictionary copy
Dictionary<string, int> originalDict = new Dictionary<string, int> { ["A"] = 1 };
Dictionary<string, int> dictCopy = new Dictionary<string, int>(originalDict);

Thread Safety and Concurrent Collections

Standard collections are not thread-safe. For multi-threaded scenarios, use concurrent collections from System.Collections.Concurrent:

ConcurrentBag<T>

using System.Collections.Concurrent;

ConcurrentBag<int> bag = new ConcurrentBag<int>();

// Thread-safe add
Parallel.For(0, 1000, i =>
{
    bag.Add(i); // Safe from multiple threads
});

// Thread-safe take
if (bag.TryTake(out int item))
{
    Console.WriteLine(item);
}

ConcurrentDictionary<TKey, TValue>

ConcurrentDictionary<string, int> concurrent = new ConcurrentDictionary<string, int>();

// Thread-safe add or update
concurrent.AddOrUpdate(
    key: "counter",
    addValue: 1,
    updateValueFactory: (key, oldValue) => oldValue + 1
);

// Thread-safe get or add
int value = concurrent.GetOrAdd("key", k => ExpensiveOperation(k));

// Thread-safe try update
concurrent.TryUpdate("key", newValue: 10, comparisonValue: 5);

ConcurrentQueue<T>

ConcurrentQueue<Task> taskQueue = new ConcurrentQueue<Task>();

// Thread-safe enqueue
Parallel.For(0, 100, i =>
{
    taskQueue.Enqueue(new Task(() => ProcessItem(i)));
});

// Thread-safe dequeue
while (taskQueue.TryDequeue(out Task task))
{
    task.Start();
}

Best Practices Summary

1. Choose the Right Collection

// ✅ Good: Use appropriate collection for the use case
HashSet<int> uniqueIds = new HashSet<int>();        // Need uniqueness
Dictionary<string, User> userCache = new Dictionary<string, User>(); // Fast lookup
List<Order> orders = new List<Order>();             // Sequential data

// ❌ Avoid: Using wrong collection type
List<int> shouldBeSet = new List<int>();
if (!shouldBeSet.Contains(id)) // O(n) - slow!
    shouldBeSet.Add(id);

2. Specify Capacity When Known

// ✅ Good: Pre-allocate to avoid resizing
List<int> largeList = new List<int>(10000);
Dictionary<string, int> largeDict = new Dictionary<string, int>(10000);

// ❌ Avoid: Multiple resizing operations
List<int> slowList = new List<int>(); // Default capacity is 4
for (int i = 0; i < 10000; i++)
{
    slowList.Add(i); // Multiple resize operations
}

3. Use Collection Initializers

// ✅ Good: Concise and readable
var colors = new List<string> { "Red", "Green", "Blue" };
var ages = new Dictionary<string, int>
{
    ["Alice"] = 30,
    ["Bob"] = 25
};

// ❌ Avoid: Verbose initialization
var colors2 = new List<string>();
colors2.Add("Red");
colors2.Add("Green");
colors2.Add("Blue");

4. Use TryGetValue for Dictionaries

// ✅ Good: Single lookup
if (dict.TryGetValue(key, out var value))
{
    Console.WriteLine(value);
}

// ❌ Avoid: Double lookup
if (dict.ContainsKey(key))
{
    Console.WriteLine(dict[key]); // Second lookup!
}

5. Consider Read-Only Collections

using System.Collections.ObjectModel;

// ✅ Good: Expose read-only view
private List<string> _items = new List<string>();
public IReadOnlyList<string> Items => _items.AsReadOnly();

// ✅ Good: Immutable initialization
public IReadOnlyCollection<string> ValidStatuses { get; } = 
    new ReadOnlyCollection<string>(new[] { "Active", "Pending", "Closed" });

6. Use Appropriate Comparison

// ✅ Good: Case-insensitive dictionary
var caseInsensitive = new Dictionary<string, int>(
    StringComparer.OrdinalIgnoreCase);

// ✅ Good: Custom comparer for complex types
var products = new HashSet<Product>(new ProductIdComparer());

7. Avoid Modifying During Iteration

// ❌ Avoid: Modifying during foreach (throws exception)
foreach (var item in list)
{
    if (ShouldRemove(item))
        list.Remove(item); // Exception!
}

// ✅ Good: Use ToList() for safe modification
foreach (var item in list.ToList())
{
    if (ShouldRemove(item))
        list.Remove(item);
}

// ✅ Better: Use RemoveAll
list.RemoveAll(item => ShouldRemove(item));

Common Patterns and Scenarios

Pattern 1: Grouping Data

List<Order> orders = GetOrders();

// Group by customer
var ordersByCustomer = orders
    .GroupBy(o => o.CustomerId)
    .ToDictionary(g => g.Key, g => g.ToList());

// Group and aggregate
var totalsByCustomer = orders
    .GroupBy(o => o.CustomerId)
    .ToDictionary(g => g.Key, g => g.Sum(o => o.Total));

Pattern 2: Caching with Lazy Loading

public class DataCache<TKey, TValue>
{
    private readonly Dictionary<TKey, TValue> _cache = new Dictionary<TKey, TValue>();
    private readonly Func<TKey, TValue> _loader;
    
    public DataCache(Func<TKey, TValue> loader)
    {
        _loader = loader;
    }
    
    public TValue Get(TKey key)
    {
        if (!_cache.TryGetValue(key, out TValue value))
        {
            value = _loader(key);
            _cache[key] = value;
        }
        return value;
    }
}

// Usage
var userCache = new DataCache<int, User>(id => Database.GetUser(id));
User user = userCache.Get(123); // Loads from DB first time only

Pattern 3: Building Lookup Indexes

List<Product> products = GetProducts();

// Single index by ID
Dictionary<int, Product> byId = products.ToDictionary(p => p.Id);

// Multiple indexes
var byName = products.ToDictionary(p => p.Name);
var byCategory = products
    .GroupBy(p => p.Category)
    .ToDictionary(g => g.Key, g => g.ToList());

// Composite key index
var byNameAndCategory = products.ToDictionary(
    p => (p.Name, p.Category),
    p => p
);

Pattern 4: Frequency Counting

string text = "hello world hello everyone";
string[] words = text.Split(' ');

// Count word frequency
Dictionary<string, int> frequency = new Dictionary<string, int>();
foreach (string word in words)
{
    if (frequency.ContainsKey(word))
        frequency[word]++;
    else
        frequency[word] = 1;
}

// LINQ alternative
var frequencyLinq = words
    .GroupBy(w => w)
    .ToDictionary(g => g.Key, g => g.Count());

Pattern 5: Set Operations for Business Logic

// Find users who have completed all required courses
HashSet<string> requiredCourses = new HashSet<string> { "C# 101", "SQL 101", "Azure 101" };
HashSet<string> completedCourses = user.CompletedCourses;

bool hasCompletedAll = requiredCourses.IsSubsetOf(completedCourses);

// Find missing courses
var missingCourses = new HashSet<string>(requiredCourses);
missingCourses.ExceptWith(completedCourses);

Pattern 6: Builder Pattern with Collections

public class ReportBuilder
{
    private List<Section> _sections = new List<Section>();
    private Dictionary<string, object> _parameters = new Dictionary<string, object>();
    
    public ReportBuilder AddSection(Section section)
    {
        _sections.Add(section);
        return this;
    }
    
    public ReportBuilder WithParameter(string key, object value)
    {
        _parameters[key] = value;
        return this;
    }
    
    public Report Build()
    {
        return new Report(_sections, _parameters);
    }
}

// Usage
var report = new ReportBuilder()
    .AddSection(new Section("Header"))
    .AddSection(new Section("Body"))
    .WithParameter("Title", "Sales Report")
    .WithParameter("Date", DateTime.Now)
    .Build();

Quick Reference

Common Methods for All Collections

// Check if empty
bool isEmpty = collection.Count == 0;
bool hasItems = collection.Any(); // LINQ

// Clear all elements
collection.Clear();

// Convert to array
var array = collection.ToArray();

// Convert to list
var list = collection.ToList();

// Count elements
int count = collection.Count;
int linqCount = collection.Count(); // LINQ

// Check existence
bool exists = collection.Contains(item);
bool anyMatch = collection.Any(predicate); // LINQ

List<T> Quick Reference

var list = new List<int>();
list.Add(1);                    // Add to end
list.Insert(0, 2);              // Insert at index
list.AddRange(new[] {3, 4});    // Add multiple
list.Remove(1);                 // Remove by value
list.RemoveAt(0);               // Remove by index
list.RemoveAll(x => x > 5);     // Remove matching
list.Clear();                   // Remove all
bool has = list.Contains(1);    // Check existence
int index = list.IndexOf(1);    // Find index
list.Sort();                    // Sort ascending
list.Reverse();                 // Reverse order
int[] array = list.ToArray();   // Convert to array

HashSet<T> Quick Reference

var set = new HashSet<int>();
set.Add(1);                     // Add element
set.Remove(1);                  // Remove element
bool has = set.Contains(1);     // Check existence
set.UnionWith(other);           // Union
set.IntersectWith(other);       // Intersection
set.ExceptWith(other);          // Difference
set.Clear();                    // Remove all

Dictionary<TKey, TValue> Quick Reference

var dict = new Dictionary<string, int>();
dict.Add("key", 1);             // Add
dict["key"] = 2;                // Add or update
dict.Remove("key");             // Remove
bool has = dict.ContainsKey("key");         // Check key
bool hasValue = dict.ContainsValue(1);      // Check value
dict.TryGetValue("key", out int value);     // Safe get
dict.Clear();                   // Remove all
var keys = dict.Keys;           // Get all keys
var values = dict.Values;       // Get all values

Key Takeaways:

  • Use List<T> for ordered, indexed sequences (default choice)
  • Use HashSet<T> for unique elements and fast membership testing
  • Use Dictionary<TKey, TValue> for fast key-based lookups
  • Use ObservableCollection<T> for UI data binding scenarios
  • Choose specialized collections (Queue<T>, Stack<T>, LinkedList<T>) when their specific ordering matters
  • Leverage LINQ for powerful data transformations
  • Consider thread-safety with Concurrent collections in multi-threaded scenarios
  • Always specify capacity when size is known to optimize performance
  • Use appropriate comparison strategies for custom types

Remember: The right collection can make the difference between O(n) and O(1) performance. Choose wisely based on your access patterns and requirements.


Additional Resources

Official Documentation

Performance and Best Practices

LINQ and Advanced Topics

Specialized Topics