Conventions I use regarding collections in C#
The C# language has various interfaces and collection types available. I've promoted the following conventions, mainly informed by the https://learn.microsoft.com/en-us/dotnet/standard/design-guidelines/guidelines-for-collections article and my own experience using these types. The conventions are mostly based on the semantics I aim to communicate with the type. If the semantics match, then I would consider using that type.
If multiple types could be applicable, I chose the use the simplest type instead. The below list is sorted in what I believe the be the simplest to most complex collection type.
Before I describe my list, why should you care? I believe there are some valuable lessons to be learnt from Domain Driven Design, where Intention Revealing Interfaces being the most relevant pattern from that theory applicable to what I'm writing about here. Communicating intent can be done through the class names you use, but also through the abstractions you are using. Another piece of theory incorporated into these conventions is the thinking behind the Interface Segregation Principle that aims to minimise coupling by limiting the exposed API of the abstractions, that you can recognise back from the 'tie-breaker rule' described in the previous section. I believe it has helped me in my design work and I believe it can be equally helpful to you.
Without further ado, here are the collection abstractions I typically work with in my work as Software Engineer and what aspects I attribute to them:
IReadOnlyCollection<T>
This represents a collection of information, that cannot be changed by the outside world.
Items within the collection are not expected to be ordered.
The collection has a finite size and retrieving elements from it can only be done by traversing the collection.
Semantics of the 'first' or 'last' items are irrelevant when dealing with this collection. It's typically all or nothing.
If returned from a property, it's the 'live' representation of the state and the elements within the collection can change over time if the owner decides. If returned from a method, a snapshot of the current state is returned and the elements within the collection are therefore unchanging.
The collection could contain duplicates. I would explicitly document if the absence of duplicates is guaranteed.
IReadOnlyList<T>
This represents a list of information, that cannot be changed by the outside world.
It's almost the same as IReadOnlyCollection<T>
, with the following notable exceptions:
- Semantics of the 'first' or 'last' items are actually important when dealing with this collection. It's also expected that items can be retrieved using an index, and that this happens in constant (O(1)) time.
IReadOnlySet<T>
This is a set of information, that cannot be changed by the outside world.
It's almost the same as IReadOnlyCollection<T>
, with the following notable exceptions:
This collection guarantees uniqueness among all it's elements.
Checking if an instance is 'part of the set' can be answered in constant time (O(1)).
IReadOnlyDictionary<Key, Value>
This is a 1:1 lookup of information, that cannot be changed by the outside world.
It's almost the same as IReadOnlySet<T>
, with the following notable exceptions:
Uniqueness is defined by the key, instead of by the element as a whole.
Checking if an instance is 'part of the lookup' can be answered in constant time (O(1)), only when using the key.
There is a strong relationship between the key, and the value. This could for example be enriching some Entity Object with additional metadata without intending to extend that object. This could also be linking a relationship from 1 thing to another.
Need a 1:many lookup, then use
ILookup<Key,Value>
instead.
ICollection<T>
\
IList<T>
\
ISet<T>
and
IDictionary<key, Value>
These are mutable versions of their IReadOnly*<T>
counterparts. Should only be exposed publicly if and only if the outside would should be capable of changing the contents of the collection.
IEnumerable<T>
This represents a stream of information.
The stream does not have be ordered, but it could be; I assume the data is not ordered in any way and I explicitly document if it is.
The stream may generate elements lazily or note; I should take into account that fetching an element from the stream is not guaranteed to be a constant time (O(1)) operation.
The stream may be infinite. Most are not. I should document if the stream is actually infinite.
The stream cannot be mutated by the outside world, only by the owner of the stream. Therefore only the owner can decide or more items are to be emitted or not.
Data retrieved from the stream is not guaranteed to be the same every single time the stream is being consumed. This really depends on what the stream is capturing. A history of temperature measurements probably always has the same elements at the start between multiple retrievals. A stream generating random numbers would not.
The stream could contain duplicates.