One step beyond by using .NET Collections to its fullest
In .NET, we can create and manage collections (i.e., groups of related objects) that can dynamically grow and shrink based on our needs. Alternatively, we could use arrays of objects. However, arrays can be used when we need a fixed number of strongly typed objects.
As we can understand, collections can provide great flexibility when working with groups of objects. So let’s take it from the start. The .NET offers several kinds of collections. Each collection is a class that we should instantiate, and then we can manage (e.g., add, remove, filter, etc.) its items using the provided methods.
For example, we may want items of the same data type (e.g., string) or a collection with key/value pairs (e.g., an integer key for a string value). In such a case, we can use the
System.Collections.Generic namespace (e.g.
In this article, we will learn about the following collection namespaces, their most frequently used classes, when we can use them, make some comparisons and learn about the yield keyword.
System.Collections: Include the legacy types of ArrayList, Hashtable, Queue, and Stack that stores elements as
System.Collections.Generic: To create generic (
<T>) collections for a specific data type.
System.Collections.Concurrent: Provide thread-safe collection classes to access the collection items from multiple threads (concurrently).
System.Collections.Immutable: Provide immutable collection interfaces and classes (thread-safe collections that cannot be changed).
The System.Collections namespace contains interfaces and classes that define various collections of objects. The defined types of this namespace are legacy and, thus, are not recommended to be used for new development. The following table shows the frequently used classes and their recommended alternative in the
|Legacy Class||Description||Recommended Class|
|ArrayList||An array of objects whose size is dynamically increased as required (implements the
|Hashtable||A collection of key/value pairs that are organized based on the key’s hash code. A
|Queue||A first-in, first-out (FIFO) collection of objects.||Queue|
|Stack||A last-in, first-out (LIFO) collection of objects.||Stack|
The System.Collections.Generic namespace provides interfaces and generic collection classes to create collections with the same data type (by enforcing strong typing). So, when creating an instance of a generic collection class, such as
Dictionary<TKey, TValue>, etc., we should replace the
T parameter with the type of our objects.
For example, we could keep a list of string values (
List<string>), a list of custom
User objects (
List< User>), a dictionary of integer keys with string values (
Dictionary<int, string>), etc.
Each generic collection class has its purpose and usage. For example, in a
Dictionary<TKey, TValue>, we can add items (objects or value types) paired with a unique key and quickly retrieve the item by using the key. In the following section, we can see some frequently used generic collection classes.
A Dictionary provide a collection of a paired key to value items.
- Each key must be unique in the collection (no duplicate keys).
- It is implemented as a hash table. Thus, retrieving a value by using its key is very fast (close to O(1)).
- A key cannot be
null, but a value can be.
- For enumeration purposes, each item is represented as a KeyValuePair structure.
- For an immutable dictionary class, see ImmutableDictionary.
- For a read-only dictionary, see the ReadOnlyDictionary.
A List is a strongly typed list of objects that can be accessed by its index (see the code example below).
- We can add items using the
- It is not guaranteed to be sorted.
- We can access an item using an integer index (zero-based).
- Allows duplicate items.
- For an immutable list class, see ImmutableList.
- For a read-only list, see the IReadOnlyCollection.
Queues and stacks are maybe one of the first things you learn when learning a computer language in computer science class. In general, queues and stacks are used to temporarily store information to be used (accessed) in a specific order.
Pushmethod becomes an O(n) operation).
- Queues are used to access the information in the same order stored in the collection (Figure 1), i.e., first-in, first-out (FIFO).
- To easily remember the queue concept, imagine people waiting in a line to get their coffee (i.e., a queue of people). The one that is first in the line will be the first that will be served.
- Use the
Enqueuemethod to add a
Titem at the end of the
- Use the
Dequeuemethod to get and remove the oldest
Titem from the start of the queue.
- Use the
Peekmethod to get (peek) the next
Titem to be dequeued.
- Stacks are used to access the information in the reverse order that is stored (Figure 2), i.e., last-in, first-out (LIFO).
- To easily remember the stack concept, imagine a pile of boxes (on top of each other). If you need to move them, you will probably get one on top (i.e., the last you stored).
- Use the
Pushmethod to add a
Titem at the top of the
- Use the
Popmethod to get and remove the
Titem from the top of the
- Use the
Peekmethod to get (peek) the next
Titem that would be popped.
In .NET 6.0, the PriorityQueue collection was introduced in the
System.Collections.Generic namespace, in which we can add (i.e. Enqueue) new items with a value and a priority. On dequeuing, the item with the lowest priority value will be removed. The .NET documentation notes that first-in-first-out semantics are not guaranteed in cases of equal priority.
The System.Collections.Concurrent namespace provides several thread-safe collection classes that we should use instead of the corresponding types in the
System.Collections namespaces whenever multiple threads access the collection concurrently. In the following table, we can see some frequently used concurrent collection classes.
|Concurrent Class Collection||Description|
|ConcurrentDictionary||Provides a thread-safe collection of paired
|ConcurrentQueue||Provides thread-safe Queues, i.e., first-in, first-out (FIFO).|
|ConcurrentStack||Provides thread-safe Stacks, i.e., last-in, first-out (LIFO).|
The System.Collections.Immutable namespace provides immutable collections that can assure its consumer that the collection never changes. In addition, they provide implicit thread safety. Thus, we do not need locks to access the collections. In the following table, we can see some frequently used immutable collection classes.
|Immutable Class Collection||Description|
|ImmutableDictionary||Provides an immutable collection of paired
|ImmutableList||Provides an immutable list (of strongly typed objects accessed by index).|
|ImmutableArray||Provides methods to create immutable arrays (cannot be changed after they are created).|
As the .NET documentation states, yield is a contextual keyword used in a statement to indicate an iterator (for example, when used in a method). By using
yield, we can define an iterator (e.g., an
IEnumerable<T>) without creating a temporary collection (e.g., a
List<T>) to hold the state of the enumerator.
In the following code examples, we can see that we do not need a temporary collection when using
yield return. Okay, the example functions may be silly, but you get the point of the
yield keyword 😉. If we want to state the end of the iteration, we can use the
yield break statement.
In previous sections, we saw that we could “convert” our collections to
Immutable. From their names, we can assume that we will not be able to perform changes in the collections in both cases. So, what’s their difference? Let’s start with the following code examples to learn how we can “convert” our Dictionary or List collections to either ReadOnly or Immutable.
In C#, a read-only collection is just a wrapper of the actual collection (e.g., the
aList in the example), which prevents being modified by not providing the related methods. However, if the actual collection is modified, the ReadOnlyCollection will also be changed. In addition, it’s important to note that read-only collections are not thread-safe.
The System.Collections.Immutable namespace contains interfaces and classes that define immutable collections, such as
ImmutableDictionary<TKey,TValue>, etc. We can assure our consumers that the collection never changes by using immutable collections. In addition, they provide implicit thread safety. Thus, we do not need locks to access the collections. It’s important to notice that the immutable collections provide modification methods (e.g., Add), but they will not make any modifications and create a new set instance.
So, as we can understand, ReadOnly and Immutable collections are quite different.
.NET provides several collections namespaces that provide great flexibility when working with groups of objects. However, the
System.Collections namespace is considered a legacy, and thus, it’s not recommended for new development. The
System.Collections.Generic namespace can be used as an alternative.
System.Collections.Generic namespace provides interfaces and generic collection classes to create collections with the same data type (by enforcing strong typing). It’s the most used collection namespace that provides among others, dictionaries, lists, queues, stacks, and priority queues.
An essential (must-know) namespace when working with concurrent requests (multiple threads) is the
System.Collections.Concurrent namespace. It provides a concurrent (thread-safe) version of the known dictionaries, queues, and stacks.
ReadOnly and Immutable collections are quite different, but both provide necessary functionality when we need collections that cannot be changed.
.NET provides awesome collections namespaces (tools) that we can use based on our needs 😃.