Are you still using List<T>
like it’s 2005? What if I told you that a few tweaks could save you memory, improve performance, and prevent subtle bugs? Buckle up—this guide will transform how you use Lists in your C# projects.
Understanding Lists in C#
What is a List in C#?
List<T>
is a generic collection provided by .NET’s System.Collections.Generic
namespace. Think of it as a dynamic array: it grows and shrinks automatically as you add or remove elements. Unlike arrays, List<T>
doesn’t require you to define the size up front.
Use cases include:
- Holding dynamic sets of items (e.g., search results, UI elements).
- Storing data with frequent additions/removals.
- Working with LINQ for powerful data manipulation.
How Lists Differ from Arrays and Other Collections
Feature | List<T> | Array | LinkedList<T> | HashSet<T> |
---|---|---|---|---|
Dynamic sizing | ✅ | ❌ | ✅ | ✅ |
Index access | ✅ | ✅ | ❌ | ❌ |
Fast insert/delete | ❌ | ❌ | ✅ | ✅ (no duplicates) |
Allows duplicates | ✅ | ✅ | ✅ | ❌ |
List<T>
offers a great balance between performance and flexibility, especially for indexed operations.
Best Practices for Using Lists in C#
Use Type Parameters Efficiently
Always specify the type explicitly. Avoid using List<object>
unless absolutely necessary—boxing and unboxing kills performance and type safety.
List<int> numbers = new List<int>();
Initializing Lists the Right Way
Initialize Lists with an estimated capacity when you know the expected size to reduce internal array reallocations:
List<string> logs = new List<string>(1000);
Avoiding Excessive List Reallocations
Each time your List exceeds capacity, it doubles its size. This is expensive. If you know you’re done adding:
logs.TrimExcess();
Use it wisely; premature trimming might lead to more reallocations later.
Iterating Safely and Efficiently
- Use
foreach
for readability. - Use
for
if you plan to modify items. - Avoid LINQ for hot loops due to allocation cost.
// Safe for read
foreach (var item in items) { ... }
// Better for modifying
for (int i = 0; i < items.Count; i++) { items[i] = Update(items[i]); }
Removing Elements Without Breaking Things
Removing while iterating can crash your loop. Avoid this:
foreach (var item in list)
{
if (ShouldRemove(item))
list.Remove(item); // This will throw!
}
Instead, use:
list.RemoveAll(ShouldRemove);
Or iterate backwards:
for (int i = list.Count - 1; i >= 0; i--)
{
if (ShouldRemove(list[i]))
list.RemoveAt(i);
}
Advanced Tips & Performance Considerations
When to Use ReadOnlyCollection or ImmutableList
Use ReadOnlyCollection<T>
to prevent accidental modifications:
ReadOnlyCollection<string> readOnlyNames = names.AsReadOnly();
Or ImmutableList<T>
from System.Collections.Immutable
when immutability is needed for multi-threading or safety.
Minimizing Memory Footprint
- Reuse Lists from a pool when possible.
- Use
Clear()
instead of creating new Lists. - Avoid storing large Lists in static variables unless absolutely needed.
Sorting and Searching Optimally
For large datasets:
list.Sort();
int index = list.BinarySearch(target);
Custom comparer:
list.Sort((a, b) => a.Name.CompareTo(b.Name));
Thread Safety with Lists
List<T>
is not thread-safe. Use locks:
lock(syncRoot)
{
sharedList.Add(item);
}
Or switch to concurrent alternatives:
ConcurrentBag<T> bag = new ConcurrentBag<T>();
Real-World Code Examples
Common List Operations
var fruits = new List<string> { "apple", "banana", "cherry" };
fruits.Add("date"); // Adds 'date' to the end of the list
fruits.Remove("banana"); // Removes the element 'banana' from the list
var cherry = fruits.Find(f => f.Contains("cherry")); // Finds an element containing 'cherry'
These examples cover the most common List operations: adding, removing, and finding elements using a predicate.
Combining Lists Like a Pro
var all = list1.Concat(list2).ToList(); // Combines list1 and list2 into a single list
var onlyFirst = list1.Except(list2).ToList(); // Gets elements in list1 that are not in list2
var common = list1.Intersect(list2).ToList(); // Gets elements common to both lists
These examples show how to merge and filter Lists using LINQ set operations, great for managing distinct or overlapping datasets.
Performance Benchmark Example
var sw = Stopwatch.StartNew();
for (int i = 0; i < 1000000; i++)
list.Add(i);
sw.Stop();
Console.WriteLine($"Time: {sw.ElapsedMilliseconds}ms");
This snippet demonstrates a basic way to benchmark the performance of List operations, particularly addition in large quantities.
FAQ: Using Lists Like a Pro
List<T>
or ObservableCollection<T>
for UI apps?Use ObservableCollection<T>
if you need UI change notifications (e.g., WPF binding).
List<T>
truly read-only?Wrap it in ReadOnlyCollection<T>
or use ImmutableList<T>
.
Probably due to capacity doubling or not trimming excess memory.
Conclusion: Smarter Lists, Faster Code
List<T>
is a power tool in C#, but like any tool, misuse can backfire. Whether it’s preventing memory waste, improving iteration performance, or enforcing immutability, smart List usage makes your code better. Try these tips in your next feature or refactor sprint. And if you learned something new, leave a comment or check out my other deep dives into .NET collections.