.NET Nakama

Improving your .NET skills

One step beyond by using .NET Collections to its fullest

May 04, 2022 (~16 Minute Read)
C# BASICS COLLECTIONS CONCURRENT IMMUTABLE READ-ONLY PRIORITY QUEUE YIELD KEYWORD

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 as Object 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 or AddRange 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.

Queue

  • 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 a T item at the end of the Queue<T>.
  • Use the Dequeue method to get and remove the oldest T item from the start of the queue.
  • Use the Peek method to get (peek) the next T item to be dequeued.
The queue commands illustration.
Figure 1. - The queue commands illustration.

Stack

  • 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 a T item at the top of the Stack<T>.
  • Use the Pop method to get and remove the T item from the top of the Stack<T>.
  • Use the Peek method to get (peek) the next T item that would be popped.
The stack commands illustration.
Figure 2. - The stack commands illustration.

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.

The Priority Queue commands illustration.
Figure 3. - The Priority Queue commands illustration.

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 😁.