https://learn.microsoft.com/en-us/dotnet/csharp/programming-guide/concepts/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:
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.
Generic collections provide compile-time type checking, preventing runtime errors and eliminating the need for casting.
Collections automatically grow and shrink as needed, unlike fixed-size arrays that require manual resizing.
Each collection type is optimized for specific operations, enabling efficient data manipulation based on your use case.
Built-in methods for searching, sorting, filtering, and transforming data reduce the need for custom implementations.
Collections seamlessly integrate with Language Integrated Query (LINQ) for powerful data querying capabilities.
Using standard collection types makes code more readable and maintainable across teams and projects.
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.
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:
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> is an unordered collection that stores unique elements with fast lookup, insertion, and deletion operations.
Key Characteristics:
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> stores key-value pairs with fast lookup by key. It's similar to a hash table or associative array.
Key Characteristics:
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> 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:
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);
});
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<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<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<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<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))
);
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>
| Collection | Ordered | Indexed | Unique | Sorted | Use Case |
|---|---|---|---|---|---|
| List<T> | ✅ | ✅ | ❌ | ❌ | General-purpose sequential data |
| HashSet<T> | ❌ | ❌ | ✅ | ❌ | Unique elements, fast lookup |
| Dictionary<TKey,TValue> | ❌ | Key | ✅ Keys | ❌ | Key-value pairs, fast lookup |
| ObservableCollection<T> | ✅ | ✅ | ❌ | ❌ | UI data binding |
| Queue<T> | ✅ FIFO | ❌ | ❌ | ❌ | Task processing, buffering |
| Stack<T> | ✅ LIFO | ❌ | ❌ | ❌ | Undo/redo, parsing |
| LinkedList<T> | ✅ | ❌ | ❌ | ❌ | Frequent insertions/deletions |
| SortedList<TKey,TValue> | ✅ | ✅ | ✅ Keys | ✅ | Sorted key-value, index access |
| SortedSet<T> | ✅ | ❌ | ✅ | ✅ | Sorted unique elements |
Time Complexity:
| Operation | List<T> | HashSet<T> | Dictionary<TKey,TValue> |
|---|---|---|---|
| Add (end) | O(1)* | O(1)* | O(1)* |
| Add (start) | O(n) | O(1)* | N/A |
| Remove | O(n) | O(1)* | O(1)* |
| Search | O(n) | O(1)* | O(1)* (by key) |
| Access by index | O(1) | N/A | N/A |
| Contains | O(n) | O(1)* | O(1)* (by key) |
*Amortized time complexity (average case)
Space Complexity:
| Collection | Memory Overhead |
|---|---|
| List<T> | Low - Contiguous array |
| HashSet<T> | Medium - Hash table |
| Dictionary<TKey,TValue> | Medium - Hash table |
| LinkedList<T> | High - Node pointers |
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();
// 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();
// 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);
Standard collections are not thread-safe. For multi-threaded scenarios, use concurrent collections from System.Collections.Concurrent:
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<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<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();
}
// ✅ 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);
// ✅ 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
}
// ✅ 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");
// ✅ 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!
}
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" });
// ✅ 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());
// ❌ 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));
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));
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
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
);
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());
// 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);
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();
// 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
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
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
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:
List<T> for ordered, indexed sequences (default choice)HashSet<T> for unique elements and fast membership testingDictionary<TKey, TValue> for fast key-based lookupsObservableCollection<T> for UI data binding scenariosQueue<T>, Stack<T>, LinkedList<T>) when their specific ordering mattersConcurrent collections in multi-threaded scenariosRemember: The right collection can make the difference between O(n) and O(1) performance. Choose wisely based on your access patterns and requirements.
Class Inheritance in C#
Class inheritance in C# - fundamentals, implementation, and best practices for building hierarchical class structures.
RFC 6749 - OAuth 2.0 Authorization Framework
RFC 6749 outlines the core roles, token types, grant flows (Authorization Code, Implicit, Resource Owner Password, Client Credentials), security considerations, and extensibility mechanisms used widely across modern identity and access management systems.