Collections
Collections are data types that provide structures for storing and accessing multiple elements. Here are the most important collection types in C#.
Arrays (T[])
- Namespace:
System - Fixed size: yes
- Ordering: Elements have a well-defined position (called Index)
- Index access: ✅ Fast (O(1))
- When tu use: When you know the exact number of elements in advance, and performance is critical.
✅ Pros: Fast access, memory-efficient.
⚠️ Cons: Size cannot be changed once created.
Example
int[] numbers = new int[3];
numbers[0] = 10;
numbers[1] = 20;
numbers[2] = 30;
foreach (int n in numbers)
{
Console.WriteLine(n);
}List<T>
- Namespace:
System.Collections.Generic - Fixed size: No (can grow when elements are inserted)
- Ordering: Maintains insertion order. (Elements are stored in array under the hood)
- Index access: ✅ Fast (O(1))
- When to use: Default choice for most dynamic collections.
✅ Pros: Easy to use, fast lookup by index, good performance for adding/removing at the end.
⚠️ Cons: Inserting/removing in the middle can be costly (O(n)).
Example
using System;
using System.Collections.Generic;
var list = new List<int> { 1, 2, 3 };
list.Add(4);
list.Remove(2);
foreach (int n in list)
{
Console.WriteLine(n);
}HashSet<T>
- Namespace:
System.Collections.Generic - Fixed size: No
- Ordering: Unordered (no guaranteed order).
- Duplicates: ❌ Not allowed.
- Access: Fast membership checks (O(1)).
- When to use: When you need a collection of unique elements and fast lookup, but order doesn’t matter.
✅ Pros: Very fast Add, Remove, and Contains.
⚠️ Cons: No index access, no guaranteed order.
Example
using System;
using System.Collections.Generic;
var set = new HashSet<string>();
set.Add("apple");
set.Add("banana");
set.Add("apple"); // ignored, duplicate
foreach (var item in set)
{
Console.WriteLine(item);
}Dictionary<TKey, TValue>
- Namespace:
System.Collections.Generic - Fixed size: No
- Ordering: No guaranteed order
- Access: Fast key lookup (O(1))
- When to use: When you want to associate keys with values for fast lookup.
✅ Pros: Very fast lookup by key.
⚠️ Cons: No indexing by position.
Example
using System;
using System.Collections.Generic;
var dict = new Dictionary<string, int>();
dict["Alice"] = 25;
dict["Bob"] = 30;
Console.WriteLine(dict["Alice"]);
// iterate
foreach (var kvp in dict)
{
Console.WriteLine($"{kvp.Key}: {kvp.Value}");
}
// lookup
var age = dict["Alice"];Excursus: Try Pattern
When accessing a specific value in a dictionary you need to know its key. Note, that the lookup might fail, if there is no value for a specific key:
var dict = new Dictionary<string, int>();
dict["Alice"] = 25;
var result = dict["Bob"]; // KeyNotFoundExceptionOf course you can wrap the lookup in a try-catch block, but using exception handling for such cases is not recommended, because it adds a lot of overhead and can slow down your application.
What you can often use instead is the Try Pattern.
Some types offer methods that are named Try....
For example the Dictionary<T> type offers a method called TryGetValue.
Usually these methods follow the same pattern:
- They perform the action specified after the Try, for example getting the value from the dictionary.
- Instead of offering the result as a return value, the return a boolean value indicating, if the action was successful.
- The result value is offered as an
outParameter instead. That is a special type of parameter that allows extracting information from the method call, similar to a return value.
Example
var dict = new Dictionary<string, int>();
dict["Alice"] = 25;
// use the try pattern
int result;
bool keyExists = TryGetValue("Bob", out result); // the keyword out is needed
// if the call was successful:
// - keyExists will be true
// - result will hold the value corresponding to the key
if (keyExists)
{
// you can access result here
}
else
{
// no corresponding value found
}or shorter:
var dict = new Dictionary<string, int>();
dict["Alice"] = 25;
// use the try pattern
if (TryGetValue("Bob", out var result)) // you can create a new variable for the result inline
{
// you can access result here
}
else
{
// no corresponding value found
}Collection Interfaces
IEnumerable<T>
- Namespace:
System.Collections.Generic - Purpose: Provides read-only iteration over a collection.
- Index access: No (just iteration)
- When to use: When you only need to read data in a
foreachloop and don’t need modification.
✅ Pros: Works with foreach, LINQ, and is the lowest common denominator for collections.
⚠️ Cons: No add/remove/index operations.
Example
using System;
using System.Collections.Generic;
IEnumerable<int> enumerable = new List<int> { 1, 2, 3 };
foreach (var n in enumerable)
{
Console.WriteLine(n);
}ICollection<T>
- Namespace:
System.Collections.Generic - Extends:
IEnumerable<T> - Purpose: Adds count, Add, Remove, and Contains methods.
- When to use: When you need a modifiable collection, but don’t need index-based access. Often used when accessing database tables from code.
✅ Pros: More capabilities than IEnumerable.
⚠️ Cons: No direct indexing.
Example
using System;
using System.Collections.Generic;
ICollection<string> names = new List<string>();
names.Add("Alice");
names.Add("Bob");
Console.WriteLine(names.Count);
foreach (var name in names)
{
Console.WriteLine(name);
}IList<T>
- Namespace:
System.Collections.Generic - Extends:
ICollection<T>andIEnumerable<T> - Purpose: Adds indexing, Insert, and RemoveAt.
- When to use: When you need the flexibility of a list and a common interface type.
✅ Pros: Full list functionality via interface.
⚠️ Cons: No dictionary-style key lookup.
Example
using System;
using System.Collections.Generic;
IList<int> numbers = new List<int>();
numbers.Add(10);
numbers.Add(20);
numbers.Insert(1, 15); // Insert at index 1
foreach (var n in numbers)
{
Console.WriteLine(n);
}IQueryable<T>
- Namespace:
System.Linq - Extends:
IEnumerable<T> - Purpose: Supports deferred execution and building Linq queries that can be translated (e.g., to SQL).
- When to use: When querying data from external sources like databases, where the query is not executed in memory.
✅ Pros: Can build complex queries; executed by the data provider (e.g., SQL server).
⚠️ Cons: Only makes sense with LINQ providers; don’t use for in-memory lists.
Example
using System;
using System.Linq;
using System.Collections.Generic;
IQueryable<int> queryable = new List<int> { 1, 2, 3, 4, 5 }.AsQueryable();
var evenNumbers = queryable.Where(n => n % 2 == 0);
foreach (var n in evenNumbers)
{
Console.WriteLine(n);
}🧭 Summary Table
| Type / Interface | Fixed Size | Modifiable | Indexed | Key Lookup | Deferred Query | Typical Use Case |
|---|---|---|---|---|---|---|
T[] (Array) | ✅ Yes | ❌ No | ✅ Yes | ❌ No | ❌ No | Fast fixed-size storage |
List<T> | ❌ No | ✅ Yes | ✅ Yes | ❌ No | ❌ No | General-purpose dynamic list |
HashSet<T> | ❌ No | ✅ Yes | ❌ No | ✅ Yes | ❌ No | Unique elements |
Dictionary<TKey,TValue> | ❌ No | ✅ Yes | ❌ No | ✅ Yes | ❌ No | Key-value storage |
IEnumerable<T> | N/A | ❌ No | ❌ No | ❌ No | ✅ LINQ | Read-only iteration |
ICollection<T> | N/A | ✅ Yes | ❌ No | ❌ No | ❌ No | Abstract collection manipulation |
IList<T> | N/A | ✅ Yes | ✅ Yes | ❌ No | ❌ No | Generic list handling |
IQueryable<T> | N/A | ❌ Usually | ❌ No | ❌ No | ✅ Yes | Database and LINQ provider queries |
👉 Practical rule of thumb:
- Use
List<T>for most in-memory dynamic lists. - Use
Dictionary<TKey, TValue>for key-value lookups. - Use
IEnumerable<T>for read-only or LINQ pipelines. - Use
IQueryable<T>when querying databases. - Use arrays only when the size is fixed or performance is critical.
- Use
ICollection<T>/IList<T>as abstractions to make your APIs more flexible.