Mastering Lists in C#: 10x Your Collection Skills

The Ultimate Guide to C# List: Best Practices and Code Examples

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

FeatureList<T>ArrayLinkedList<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

Should I use List<T> or ObservableCollection<T> for UI apps?

Use ObservableCollection<T> if you need UI change notifications (e.g., WPF binding).

How can I make a List<T> truly read-only?

Wrap it in ReadOnlyCollection<T> or use ImmutableList<T>.

Why is my List consuming so much memory?

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.

Leave a Reply

Your email address will not be published. Required fields are marked *