Collections
Collections
Section titled “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[]
)
Section titled “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
Section titled “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>
Section titled “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
Section titled “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>
Section titled “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
Section titled “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>
Section titled “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
Section titled “Example”using System;using System.Collections.Generic;
var dict = new Dictionary<string, int>();dict["Alice"] = 25;dict["Bob"] = 30;
Console.WriteLine(dict["Alice"]);
// iterateforeach (var kvp in dict){ Console.WriteLine($"{kvp.Key}: {kvp.Value}");}
// lookupvar age = dict["Alice"];
Excursus: Try Pattern
Section titled “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"]; // KeyNotFoundException
Of 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
out
Parameter instead. That is a special type of parameter that allows extracting information from the method call, similar to a return value.
Example
Section titled “Example”var dict = new Dictionary<string, int>();dict["Alice"] = 25;
// use the try patternint 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 patternif (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
Section titled “Collection Interfaces”IEnumerable<T>
Section titled “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
foreach
loop 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
Section titled “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>
Section titled “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
Section titled “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>
Section titled “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
Section titled “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>
Section titled “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
Section titled “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
Section titled “🧭 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.