C#idiomatic C#
collectionsidioms

Idioms

Collections

Pick the collection for how the code uses the data: a list to grow, a dictionary to look up, an interface to hand it out.

The collection type is the data structure decision that outlives the method that made it. These entries cover what to store, what to expose to callers, and which call turns a scan or a copy into a lookup.

A `List<T>` when the sequence changes, an array when it does not

List<T> is the sequence to reach for: it keeps elements in order, grows and shrinks, and adding at the end is O(1) on average. An array fixes its length when it is created, so it fits data whose size is known. Span<T> is neither: it is a view over a sequence that already exists, and it exists so that a slice can be passed around without a copy.

Preferred

csharp
var backlog = new List<WorkItem> { first, second };
backlog.Add(third);

Avoid

csharp
var backlog = new WorkItem[3];
var larger = new WorkItem[backlog.Length + 1];
Array.Copy(backlog, larger, backlog.Length);

Corpus: Common collection types · Collections · The array reference type

Take and return interfaces, keep concrete types insideCA1002

A signature that names List<T> hands callers the whole class, including Add, Insert, and Remove, and it fixes what a future implementation must return. CA1002 names the types designed for exposure: IList<T>, ICollection<T>, Collection<T>, and ReadOnlyCollection<T>. Inside the type, keep the concrete List<T> field: CA1859 prefers concrete types for fields, locals, and private members, because the compiler then avoids interface dispatch and can inline the call.

Preferred

csharp
private readonly List<Order> _orders = [];

public IReadOnlyList<Order> Orders => _orders;

Avoid

csharp
public List<Order> Orders { get; } = [];

Corpus: CA1002: Do not expose generic lists · CA1859: Use concrete types when possible for improved performance

Look up with `TryGetValue`, not `ContainsKey` plus the indexerCA1854

The indexer already checks whether the key exists before it returns a value, so a ContainsKey guard followed by dict[key] performs the lookup twice. TryGetValue performs it once and reports the miss through its return value. The indexer stays the right call when the key must exist or when you are assigning a value.

Preferred

csharp
if (_byId.TryGetValue(id, out var order))
{
    return order.Total;
}

Avoid

csharp
if (_byId.ContainsKey(id))
{
    return _byId[id].Total;
}

Corpus: CA1854: Prefer the 'IDictionary.TryGetValue(TKey, out TValue)' method · Common collection types · Hashtable and Dictionary Collection Types

`HashSet<T>` for membership and set arithmetic

A HashSet<T> holds unique elements and answers Add and lookup in O(1), so a membership question does not need a scan of a list. Add returns false when the element was already present, which is what a "have I seen this?" loop needs. SortedSet<T> offers the same interface with the elements kept in order.

Preferred

csharp
if (!seen.Add(id))
{
    continue;
}

Avoid

csharp
if (ids.Contains(id))
{
    continue;
}
ids.Add(id);

Corpus: Collections and Data Structures · Hashtable and Dictionary Collection Types · Sorted Collection Types

Give the collection its capacity when you know the size

A collection that runs out of room reallocates and copies its elements into the larger store. List<T>, Dictionary<TKey,TValue>, and HashSet<T> all take an initial capacity, so pass the expected size when it is known.

Preferred

csharp
var items = new List<Item>(source.Count);
foreach (var row in source)
{
    items.Add(Project(row));
}

Avoid

csharp
var items = new List<Item>();
foreach (var row in source)
{
    items.Add(Project(row));
}

Corpus: Collections and Data Structures

Initialise with a collection expression or an initializerIDE0028

A collection expression converts to whatever the target needs: an array, a List<T>, a Span<T>, or one of the collection interfaces. It always creates the collection with every element present, unlike a LINQ query, which defers until something enumerates it. A collection initializer does the same work through Add, and through the indexer for key/value pairs, which is why IDE0028 prefers both over a run of Add calls.

Preferred

csharp
List<string> tags = ["release", ..defaults];
Dictionary<string, int> counts = new() { ["a"] = 1 };

Avoid

csharp
var tags = new List<string>();
tags.Add("release");
tags.AddRange(defaults);

Corpus: Collection expressions (Collection literals) · Object and collection initializers · IDE0028: Use collection initializers or expressions

Do not hand out the mutable collection you keepCA2227

A settable collection property lets a caller replace your collection with a different one, and a property that returns an array hands out the storage itself: an array is not write-protected even when the property is read-only. Expose a read-only view and let callers add through methods you control. Collection initializer syntax still fills a get-only collection property, because it calls Add on the instance the property returns.

Preferred

csharp
private readonly List<Item> _items = [];

public IReadOnlyList<Item> Items => _items;

Avoid

csharp
public List<Item> Items { get; set; } = [];

Corpus: CA2227: Collection properties should be read only · CA1819: Properties should not return arrays · Object and collection initializers

`ImmutableArray<T>` and `ImmutableList<T>` for state you share

An immutable collection is safe to hand to another thread because every change returns a new collection and leaves the one a reader holds untouched. ImmutableArray<T> wraps a fixed array, so indexing is O(1). ImmutableList<T> stores its elements in a tree, which makes an update O(log n) and the indexer O(log n) too, so enumerate it with foreach. When several changes are pending, batch them in a builder and freeze the result with ToImmutable.

Preferred

csharp
var next = _rows.Add(row);
_rows = next;

Avoid

csharp
private readonly List<Row> _rows = [];

void Append(Row row) => _rows.Add(row);

Corpus: Collections and Data Structures · When to Use Generic Collections · Selecting a Collection Class

Concurrent collections for a collection several threads mutate

The generic collections carry no synchronization of their own, and one lock around them serializes every operation. When threads add or remove items concurrently, use the System.Collections.Concurrent types: ConcurrentDictionary<TKey,TValue>, ConcurrentQueue<T> and ConcurrentStack<T>, which are lock-free, and ConcurrentBag<T> for unordered work. A collection that is only read once it is built belongs in a plain Dictionary<TKey,TValue>, which is faster without the synchronization.

Preferred

csharp
private readonly ConcurrentDictionary<int, Job> _jobs = new();

public bool Accept(Job job) => _jobs.TryAdd(job.Id, job);

Avoid

csharp
private readonly Dictionary<int, Job> _jobs = new();

public void Accept(Job job) => _jobs[job.Id] = job;

Corpus: When to use a thread-safe collection · Thread-safe collections

Let the access pattern pick the queue, stack, or linked list

Queue<T> is first in, first out, and Stack<T> is last in, first out, so both say in the type what the code intends. LinkedList<T> is the one to take when you insert and remove in the middle of a sequence, with O(1) insertion and removal once you hold the node. A List<T> shifts every later element on a front insert, which is O(n).

Preferred

csharp
var work = new Queue<Job>();
work.Enqueue(job);
if (work.TryDequeue(out var next))
{
    Run(next);
}

Avoid

csharp
var work = new List<Job>();
work.Add(job);

var next = work[0];
work.RemoveAt(0);

Corpus: Selecting a Collection Class · Commonly Used Collection Types · When to Use Generic Collections

Deconstruct the pair while you walk a dictionary

Each element of a Dictionary<TKey,TValue> enumeration is a KeyValuePair<TKey,TValue>, and that type provides Deconstruct, so a foreach unpacks both halves in one declaration. Name the halves after what they hold rather than reaching through Key and Value.

Preferred

csharp
foreach (var (symbol, price) in _prices)
{
    Console.WriteLine($"{symbol}: {price}");
}

Avoid

csharp
foreach (KeyValuePair<string, decimal> entry in _prices)
{
    Console.WriteLine($"{entry.Key}: {entry.Value}");
}

Corpus: Deconstructing tuples and other types · Common collection types

Use `Index` and `Range` for the end of a sequence and for slices

^n counts from the end of any countable type, so items[^1] is the last element. a..b builds a Range whose start is inclusive and whose end is exclusive, and either operand may count from either end: items[1..^1] drops the first and last elements. A range over an array allocates the slice, so reach for a span when the copy is the thing you are avoiding.

Preferred

csharp
string last = phases[^1];
string[] middle = phases[1..^1];

Avoid

csharp
string last = phases[phases.Length - 1];
var middle = new string[phases.Length - 2];
Array.Copy(phases, 1, middle, 0, middle.Length);

Corpus: Member access operators and expressions · Common collection types · Collections

Sorted collections: give the comparer, choose list or dictionary

A sorted collection orders its keys with the IComparer<T> supplied at construction, and a dictionary compares keys with the IEqualityComparer<T> from its constructor. Without one, string comparison follows the current culture, which makes the order depend on the machine, so pass StringComparer.Ordinal or StringComparer.OrdinalIgnoreCase for identifiers. SortedDictionary<TKey,TValue> inserts and removes in O(log n) and uses more memory; SortedList<TKey,TValue> uses less and allows indexed retrieval, while insertion and removal are generally O(n).

Preferred

csharp
var roster = new SortedDictionary<string, Employee>(
    StringComparer.OrdinalIgnoreCase);

Avoid

csharp
var roster = new SortedDictionary<string, Employee>();
roster.Add("ana", employee);

Corpus: Sorted Collection Types · Comparisons and sorts within collections · When to Use Generic Collections

Derive from `Collection<T>` when you publish a collection typeCA1010

List<T> is built for performance, not for inheritance: it has no virtual member an inheritor can hook. Collection<T> is not abstract, already implements the generic collection interfaces, and exposes InsertItem, SetItem, RemoveItem, and ClearItems for overrides. CA1002 names it as the type to expose, and CA1010 offers it as the fix for a type that implements only the non-generic interfaces. When each element carries its own key, KeyedCollection<TKey,TItem> adds lookup by that key on top.

Preferred

csharp
public sealed class OrderBook : Collection<Order>
{
    protected override void InsertItem(int index, Order order)
        => base.InsertItem(index, order);
}

Avoid

csharp
public sealed class OrderBook : List<Order>
{
}

Corpus: CA1010: Collections should implement generic interface · CA1002: Do not expose generic lists · When to Use Generic Collections