generics
.NET
programming
data structures
List vs Collection

What is the difference between List of T and Collectionof T?

Data Structures & Algorithms practice on Codemia

Step through 300 algorithm problems with animated visualisers that show the data structure changing as the code runs.

Practice algorithms

Introduction

In .NET, both List<T> and Collection<T> represent ordered mutable collections, but they are designed for different responsibilities. List<T> is the default high performance container for most application code. Collection<T> is an extensibility base class used when mutation needs validation, policy checks, or domain side effects.

When List<T> Is the Right Choice

List<T> is backed by a dynamic array and is optimized for practical everyday operations. Index lookup is constant time, append is amortized constant time, and iteration is fast.

For internal business logic where you only need add, remove, and enumerate, List<T> is usually the simplest and most maintainable option.

csharp
1using System;
2using System.Collections.Generic;
3
4var users = new List<string> { "ava", "liam", "sofia" };
5users.Add("noah");
6users.Remove("liam");
7users.Insert(1, "mia");
8
9for (int i = 0; i < users.Count; i++)
10{
11    Console.WriteLine($"{i}: {users[i]}");
12}

Most framework APIs and examples accept IEnumerable<T> or IList<T>, so List<T> integrates naturally.

What Collection<T> Gives You

Collection<T> from System.Collections.ObjectModel wraps an internal list and exposes overridable mutation hooks:

  • 'InsertItem'
  • 'SetItem'
  • 'RemoveItem'
  • 'ClearItems'

Those hooks are the main reason to choose it. They let you centralize rules in one place instead of repeating checks across the codebase.

csharp
1using System;
2using System.Collections.ObjectModel;
3
4public sealed class UniqueSkuCollection : Collection<string>
5{
6    protected override void InsertItem(int index, string item)
7    {
8        if (string.IsNullOrWhiteSpace(item))
9            throw new ArgumentException("SKU is required", nameof(item));
10
11        if (Contains(item))
12            throw new InvalidOperationException("Duplicate SKU is not allowed");
13
14        base.InsertItem(index, item);
15    }
16}
17
18var skus = new UniqueSkuCollection();
19skus.Add("A-100");
20// skus.Add("A-100"); // throws

This pattern is useful for domain aggregates, UI binding models, and libraries that need controlled state transitions.

API Design Guidance

For public APIs, exposing concrete mutable collection types can leak implementation details. A common pattern is:

  • Store data internally in List<T>.
  • Expose read only interfaces such as IReadOnlyList<T>.
  • Use Collection<T> only when callers must interact with a mutable collection that enforces behavior.
csharp
1using System.Collections.Generic;
2using System.Collections.ObjectModel;
3
4var internalList = new List<int> { 1, 2, 3 };
5IReadOnlyList<int> view = new ReadOnlyCollection<int>(internalList);
6
7Console.WriteLine(view[0]); // 1

This keeps your internal performance characteristics while preserving contract safety.

Performance and Maintenance Tradeoffs

List<T> is typically faster for plain operations because it avoids virtual hook calls and custom logic overhead. Collection<T> adds small indirection, but that cost is usually insignificant compared with the value of centralized invariants.

The real decision should be driven by correctness and maintenance:

  • If no mutation policy is needed, prefer List<T>.
  • If rules must always run on mutation, Collection<T> makes that explicit.

A subtle benefit is testability. With Collection<T>, one test suite can validate mutation rules once, instead of checking every call site that modifies a list.

Choosing Between ICollection<T>, List<T>, and Collection<T>

Developers often confuse interface and implementation choices:

  • 'ICollection<T> is a capability contract, not a specific data structure.'
  • 'List<T> is an implementation tuned for speed and convenience.'
  • 'Collection<T> is an implementation base for policy aware mutation.'

You can accept ICollection<T> in method parameters for flexibility while storing data as List<T> internally.

Common Pitfalls

  • Choosing Collection<T> by default without any custom hooks. This adds abstraction with no payoff.
  • Exposing mutable List<T> directly from domain objects. External code can bypass invariants.
  • Repeating validation checks at call sites instead of centralizing mutation policy.
  • Assuming read only view means immutable underlying data. The owner can still mutate internally.
  • Micro optimizing collection choice before measuring real bottlenecks.

Summary

  • 'List<T> is the default practical choice for mutable ordered data.'
  • 'Collection<T> is best when you need overridable mutation behavior and domain rules.'
  • Public API design often benefits from internal List<T> plus read only exposure.
  • Choose based on invariants and ownership boundaries, not type popularity.
  • Keep collection contracts explicit so callers understand mutability expectations.

Related reading
Course
Intermediate
27 lessons
15 hours
DSA Fundamentals

Master algorithmic patterns and data structures through hands-on LeetCode-style problems - from arrays and hashing to dynamic programming and advanced graphs.

View the course
Track what you have practised

A free account saves your progress, solutions and study plan across every problem on Codemia.

Data Structures & Algorithms practice on Codemia

Step through 300 algorithm problems with animated visualisers that show the data structure changing as the code runs.

Practice algorithms