One step beyond by using .NET Collections to its fullest
Introduction
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. List<string>
, Dictionary<int, string>
).
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 asObject
type.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).
.NET Collections
System.Collections
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 System.Collections.Generic
namespace.
Legacy Class | Description | Recommended Class |
---|---|---|
ArrayList | An array of objects whose size is dynamically increased as required (implements the IList interface). |
List |
Hashtable | A collection of key/value pairs that are organized based on the key’s hash code. A Hashtable is slower than a dictionary because it requires boxing and unboxing. |
Dictionary |
Queue | A first-in, first-out (FIFO) collection of objects. | Queue |
Stack | A last-in, first-out (LIFO) collection of objects. | Stack |
System.Collections.Generic
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 List<T>
, 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.
Dictionary
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.
Code Example:
// Initialize the Dictionary
Dictionary<int, string> myDictionary = new Dictionary<int, string>();
// Add items into the Dictionary
myDictionary.Add(2, "A string value-2");
myDictionary.Add(1, "A string value-1");
myDictionary.Add(3, "A string value-3");
// Try to add an item by first checking if it exists.
if(!myDictionary.TryAdd(3, "A new value"))
{
Console.WriteLine("The provided key (3) already exists in the dictionary.");
}
// Check if a key already exists.
if (!myDictionary.ContainsKey(4))
{
myDictionary.Add(4, "A string value-4");
}
// Modify the value of the key with value: 1 (not the index).
myDictionary[1] = "A string value-1: Modified!";
// Get and show the value of a specific key.
Console.WriteLine($"The value of key:3 is: {myDictionary[3]}");
// Enumerate the items in the dictionary.
foreach (KeyValuePair<int, string> keyValueItem in myDictionary)
{
Console.WriteLine($"Key:{keyValueItem.Key}. Value:{keyValueItem.Value}");
}
// Output:
// The provided key (3) already exists in the dictionary.
// The value of key:3 is: A string value-3
// Key:2. Value:A string value-2
// Key:1. Value:A string value-1: Modified!
// Key:3. Value:A string value-3
// Key:4. Value:A string value-4
List
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
Add
orAddRange
methods. - It is not guaranteed to be sorted.
- We can access an item using an integer index (zero-based).
- Accepts
null
values. - Allows duplicate items.
- For an immutable list class, see ImmutableList.
- For a read-only list, see the IReadOnlyCollection.
Code Example:
// Initialize the List (with some items)
List<string> aList = new List<string>() { "Item-1", "Item-2" };
// Add Items in the list
aList.Add("Item-3");
aList.Add("Item-3");
// Access an item by using its index
aList[3] = "Item-4";
// Check if an item already exists.
if (aList.Contains("Item-2"))
{
Console.WriteLine("The provided item already exists in the list.");
}
// Remove an item from the list
aList.Remove("Item-2");
// Enumerate the items in the list.
foreach (string listItem in aList)
{
Console.WriteLine($"List Item:{listItem}");
}
// Output:
// The provided item already exists in the list.
// List Item:Item-1
// List Item:Item-3
// List Item:Item-4
Queue and Stack
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.
Push
method 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
Enqueue
method to add aT
item at the end of theQueue<T>
. - Use the
Dequeue
method to get and remove the oldestT
item from the start of the queue. - Use the
Peek
method to get (peek) the nextT
item 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
Push
method to add aT
item at the top of theStack<T>
. - Use the
Pop
method to get and remove theT
item from the top of theStack<T>
. - Use the
Peek
method to get (peek) the nextT
item that would be popped.

Priority Queue
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.

System.Collections.Concurrent
The System.Collections.Concurrent namespace provides several thread-safe collection classes that we should use instead of the corresponding types in the System.Collections.Generic
and 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 TKey to TValue items. |
ConcurrentQueue | Provides thread-safe Queues, i.e., first-in, first-out (FIFO). |
ConcurrentStack | Provides thread-safe Stacks, i.e., last-in, first-out (LIFO). |
System.Collections.Immutable
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 TKey to TValue items. |
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). |
One Step Beyond
Yield Contextual Keyword
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.
public IEnumerable<int> GetNumbers(int from, int to)
{
List<int> numbers = new List<int>();
for (int i = from; i <= to; i++)
{
numbers.Add(i);
}
return numbers;
}
// By using the Yield keyword, we do not need a temporary collection.
public IEnumerable<int> GetNumbersUsingYield(int from, int to)
{
for (int i = from; i <= to; i++)
{
yield return i;
}
}
Immutable VS ReadOnly Collections
In previous sections, we saw that we could “convert” our collections to ReadOnly
or 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.
// Create a normal dictionary
Dictionary<int, string> aDictionary = new Dictionary<int, string>();
aDictionary.Add(1, "A value");
aDictionary.Add(2, "Another value");
// Create an Immutable dictionary.
ImmutableDictionary<int,string> anImmutableDictionary = aDictionary.ToImmutableDictionary();
// Wrap the dictionary as ReadOnly.
ReadOnlyDictionary<int, string> aReadOnlyDictionary = new ReadOnlyDictionary<int, string>(aDictionary);
// Create a normal list
List<string> aList = new List<string>() { "Value1", "Value2" };
// Create an Immutable list from a normal list.
ImmutableList<string> anImmutableList = aList.ToImmutableList();
// Wrap a normal list as ReadOnly.
ReadOnlyCollection<string> aReadOnlyList = aList.AsReadOnly();
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 ImmutableList<T>
, 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.
Summary
.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.
The 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 😃.
If you liked this article (or not), do not hesitate to leave comments, questions, suggestions, complaints, or just say Hi in the section below. Don't be a stranger 😉!
Dont't forget to follow my feed and be a .NET Nakama. Have a nice day 😁.