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
var backlog = new List<WorkItem> { first, second };
backlog.Add(third);Avoid
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
private readonly List<Order> _orders = [];
public IReadOnlyList<Order> Orders => _orders;Avoid
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
if (_byId.TryGetValue(id, out var order))
{
return order.Total;
}Avoid
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
if (!seen.Add(id))
{
continue;
}Avoid
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
var items = new List<Item>(source.Count);
foreach (var row in source)
{
items.Add(Project(row));
}Avoid
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
List<string> tags = ["release", ..defaults];
Dictionary<string, int> counts = new() { ["a"] = 1 };Avoid
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
private readonly List<Item> _items = [];
public IReadOnlyList<Item> Items => _items;Avoid
public List<Item> Items { get; set; } = [];Corpus: CA2227: Collection properties should be read only · CA1819: Properties should not return arrays · Object and collection initializers
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
private readonly ConcurrentDictionary<int, Job> _jobs = new();
public bool Accept(Job job) => _jobs.TryAdd(job.Id, job);Avoid
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
var work = new Queue<Job>();
work.Enqueue(job);
if (work.TryDequeue(out var next))
{
Run(next);
}Avoid
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
foreach (var (symbol, price) in _prices)
{
Console.WriteLine($"{symbol}: {price}");
}Avoid
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
string last = phases[^1];
string[] middle = phases[1..^1];Avoid
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
var roster = new SortedDictionary<string, Employee>(
StringComparer.OrdinalIgnoreCase);Avoid
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
public sealed class OrderBook : Collection<Order>
{
protected override void InsertItem(int index, Order order)
=> base.InsertItem(index, order);
}Avoid
public sealed class OrderBook : List<Order>
{
}Corpus: CA1010: Collections should implement generic interface · CA1002: Do not expose generic lists · When to Use Generic Collections