When we develop software applications, we may need to create, store and manipulate groups of related objects. In this article, we will discuss the various ways in which we can perform such operations using different collections in .NET.

To download the source code for this article, you can visit our GitHub repository.

Let’s get started.

About Collections in .NET?

A .NET collection is a class that we can use to create, store, manage and manipulate a collection of related objects. This manipulation involves operations like creating, updating, reading, and removal of an object from the collection.

When we use a collection, its size can dynamically increase or decrease depending on the operations carried out on the collection.

Different Types of Collections in .NET

There are three different types of collections in .NET that we can use to handle grouped data:

  • Generic collections
  • Non-Generic collections
  • Concurrent collections

Generic Collections in .NET

We use generic collections in .NET when we want members of our collection to be of one particular data type. Whenever we want to use a generic collection, we create an instance of a class in the System.Collections.Generic namespace. When we work with data in a generic collection, strong typing is enforced. This gives our application an extra layer of security.

List<T>

The List<T> is a strongly-typed class that represents a list of objects. These objects can be accessed using an integer index. It is a data structure that implements the IList interface. The size of the List<T> grows or shrinks dynamically depending on the application’s requirement. It is the generic variant of the ArrayList class:

public static List<string> CreateCountries()
{
    List<string> countries = new();
    countries.Add("Jamaica");
    countries.Add("Switzerland");
    countries.Add("Mexico");
    countries.Add("Malaysia");
    countries.Add("Russia");
    
    return countries;
}

Here, we create a countries list, add the names of various countries to it, and then, return the list. Now, we can enumerate the items in this list, and also access them using a zero-based index.

To remove elements from the list, let’s implement a RemoveFromCountries method:

public static List<String> RemoveFromCountries(List<string> countries)
{
    countries.Remove("Mexico");
    countries.RemoveAt(3);
    countries.RemoveAll(x => x.EndsWith('a'));
    
    return countries;
}

This method takes a List<string> object as an argument. Then, it invokes three built-in methods: Remove, RemoveAt, and RemoveAll.

First, the Remove method takes in an argument and removes the first occurrence of that argument in the collection if it exists. It returns False if such an item is not found in the list. 

Then, the RemoveAt takes in an integer as a parameter and removes the item at that index in the list.

Finally, we pass a predicate as a parameter to the RemoveAll method, and it removes all items in the collection that matches the condition specified by the predicate.

To learn more about the properties and methods of the List<T> data structure, please see our previous article on List in C#.

SortedList<TKey, TValue>

The SortedList<TKey, TValue> is a generic class available in the System.Collections.Generic namespace. It stores data as a collection of key-value pairs that are always sorted by their keys. This data structure implements the Hashtable and ArrayList classes under the hood. We can use either the keys or the index of the objects in the list to access them.

To illustrate, let’s create a CreateCountriesAndCapitals method:

public static SortedList<string, string> CreateCountriesAndCapitals()
{
    SortedList<string, string> countriesAndCapitals = new();
    countriesAndCapitals.Add("Jamaica", "Kingston");
    countriesAndCapitals.Add("Switzerland", "Zurich");
    countriesAndCapitals.Add("Malaysia", "Kuala Lumpur");
    countriesAndCapitals.Add("Russia", "Moscow");
    countriesAndCapitals.TryAdd("Jamaica", "Kingston");

    return countriesAndCapitals;
}

In this method, we create an object of the SortedList<string, string> type. Then, we invoke the Add, and TryAdd methods passing in key-value pair arguments to be added to the collection.

When we use the Add method to add an object with a key that already exists in the list, it throws an ArgumentException. But when we use TryAdd, if the key already exists, nothing is added to the collection and no exception is raised

Additionally, the SortedList<TKey, TValue> cannot contain a null key. On the other hand, a value can only be null, if the TValue type is a reference type.

To access elements in the sorted list, let’s implement a ReadFromCountriesAndCapitals method:

public static List<string> ReadFromCountriesAndCapitals(SortedList<string, string> countriesAndCapitals)
{
    var capitals = new List<string>
    {
        countriesAndCapitals.Values[0],
        countriesAndCapitals.Values[1]
    };
    if (countriesAndCapitals.TryGetValue("Russia", out string? value))
    {
        capitals.Add(value);
    }

    return capitals;
}

We use the built-in Values[] property to retrieve a specific value from the SortedList<TKey, TValue>

Also, we invoke the TryGetValue method, passing it a string key, and an out parameter to store the value associated with the specified key.

To go into detail about the SortedList<TKey, TValue> collection type in .NET, please refer to this article.

Dictionary<Tkey, TValue>

The Dictionary<TKey, TValue>  is an abstract data type defined in the System.Collections.Generic namespace. We use it to define a strongly-typed and unordered group of key-value pairs. Elements in a dictionary can be accessed by their keys. Although some differences exist, the Dictionary<TKey, TValue> datatype is very similar to the OrderedList<TKey, TValue> datatype:

public static Dictionary<int, string> CreateCountriesWithRank()
{
    Dictionary<int, string> countriesWithRank = new();
    countriesWithRank.Add(1, "Jamaica");
    countriesWithRank.Add(2, "Netherlands");
    countriesWithRank.Add(3, "Congo");
    countriesWithRank.TryAdd(4, "England");
    countriesWithRank.TryAdd(5, "India");
    countriesWithRank.TryAdd(5, "India");

    return countriesWithRank;
}

Here, we declare an instance of the Dictionary<int, string> class, then we use two built-in methods to populate our dictionary with data.

Initially, we use the Add method to add a key-value pair item to our collection. If we use it to add an object with a key that already exists in our dictionary, it throws an ArgumentException.

Finally, with the TryAdd method, if we try to add an element with a key that is already in the dictionary, nothing gets added to it.

Now, let’s implement a method to read items from our generic dictionary:

public static List<string> ReadFromCountriesWithRank(Dictionary<int, string> countriesWithRank)
{
    var rankedNations = new List<string>
    {
        countriesWithRank.First().Value,
        countriesWithRank[2],
        countriesWithRank[3]
    };
    if (countriesWithRank.TryGetValue(4, out string? country))
    {
        rankedNations.Add(country);
    }

    return rankedNations;
}

First, we define an empty list to represent a collection of values retrieved from the dictionary.

Next, we call the First extension method to return the first item in the dictionary, and then use the Value property to get the value of that item.

Also, we use an indexer to get more elements from our dictionary.

We can also use the TryGetValue method to retrieve the value of a specified key. Here, we pass an out parameter that will reference the returned value. If the key exists in our collection, its value is added to our list.

To learn more about a Dictionary, please refer to this article.

SortedSet<T>

The SortedSet<T> class is a generic .NET collection type available in the System.Collections.Generic namespace. We use this class to represent a collection of unique and sorted items. Items in a SortedSet<T> are sorted according to the IComparable implementation provided by the keys, or a user-implemented comparer:

public static SortedSet<int> CreateNumbersSortedSet()
{
    SortedSet<int> numbersSortedSet = new();

    numbersSortedSet.Add(3);
    numbersSortedSet.Add(2);
    numbersSortedSet.Add(1);
    numbersSortedSet.Add(3);

    return numbersSortedSet;
}

Here, we declare an object of type SortedSet<int>.

Then, we use the built-in Add method to insert elements into the SortedSet. We pass it the value we want to add to the SortedSet as a parameter. When we try to add a value that already exists in our SortedSet, nothing will be added.

To access elements from a SortedSet, we can simply iterate over it:

public static List<int> ReadFromNumbersSortedSet(SortedSet<int> numbersSortedSet)
{
    var sortedNumbersList = new List<int>();
    foreach(var num in numbersSortedSet)
    {
        sortedNumbersList.Add(num);
    }

    return sortedNumbersList;
}

To go into detail about the SortedSet<T> class in .NET, please refer to this article.

HashSet<T>

The HashSet<T> is a generic .NET collection type in the System.Collections.Generic namespace. We use this class to represent a collection of unique objects, without duplicates.

To illustrate working with the HashSet<T> class, let’s implement a new method:

public static HashSet<int> CreateNumbersHashSet()
{
    HashSet<int> numbersHashSet = new();
    numbersHashSet.Add(1);
    numbersHashSet.Add(2);
    numbersHashSet.Add(3);
    numbersHashSet.Add(3);

    return numbersHashSet;
}

First, we create an object of the HashSet<int> type. Then, we invoke the Add method and pass the number we want to add to the HashSet as a parameter. When we try to add a number that already exists in the collection, nothing is added.

It is important to note that items in a generic HashSet are unsorted. When we want a sorted collection with no duplicates, we should consider using the SortedSet<T> class.

To retrieve data from our HashSet, all we have to do is to iterate over it:

public static List<int> ReadFromNumbersHashSet(HashSet<int> numbersHashSet)
{
    var numbersList = new List<int>();
    foreach(var num in numbersHashSet)
    {
        numbersList.Add(num);
    }

    return numbersList;
}

To go into detail about the HashSet<T> collection type in .NET, please refer to this article.

Queue<T>

The Queue<T> is a generic data structure that we use to represent a collection of First In, First Out(FIFO) objects. T represents the type of objects in the queue:

public static Queue<int> CreateNumbersQueue()
{
    Queue<int> numbersQueue = new();
    numbersQueue.Enqueue(1);
    numbersQueue.Enqueue(2);
    numbersQueue.Enqueue(3);
    
    return numbersQueue;
}

Here, we use the Enqueue method to enter the integer argument to the end of the queue.

Now, let’s create another method that retrieves an item from our queue:

public static int ReadFromNumbersQueue(Queue<int> numbersQueue)
{
    var number = numbersQueue.Dequeue();
    
    return number;
}

We invoke the Dequeue method and store the return value in a number variable. The Dequeue method removes the first item added to the queue and returns it.

To go into detail about the Queue<T> data structure, please refer to this article.

Stack<T>

The Stack<T> is a generic collection type available in the System.Collections.Generic namespace. We use this data structure to represent a collection of Last In, First Out(LIFO) objects.

To illustrate, first, let’s implement a method that creates an empty stack and adds elements to the stack:

public static Stack<string> CreateWordsStack()
{
    Stack<string> wordsStack = new();
    wordsStack.Push("First");
    wordsStack.Push("Second");
    wordsStack.Push("Third");

    return wordsStack;
}

Here, we invoke the Push method of the Stack<T> class, to enter various strings into our stack. The Push method adds an item to the top of the stack.

Next, let’s create a method that retrieves an item from the top of our stack:

public static string ReadFromWordsStack(Stack<string> wordsStack)
{
    var word = wordsStack.Pop();

    return word;
}

This method calls the built-in Pop method, which removes, and returns the item at the top of the stack. We use the Stack<T> class when we want to implement scheduled operations.

For more details about the Stack<T> collection type, please refer to this article.

Non-Generic Collections in .NET

These are a set of data structures defined in the System.Collections namespace. These collection types are type agnostic, meaning their elements can be of any type.

ArrayList

An ArrayList is an array with a size that can increase or decrease at runtime. The ArrayList collection type can have any number of elements. Elements in an ArrayList can be of any type and can be accessed by an index:

public static ArrayList CreateDetailsList()
{
    ArrayList detailsList = new();
    detailsList.Add(null);
    detailsList.Add(1);
    detailsList.Add('a');
    detailsList.Add(true);

    return detailsList;
}

Here, we declare an instance of the ArrayList class.

Then, we invoke the Add method multiple times, passing arguments of different types to it. This method takes in only one parameter which is of type System.Object. All values passed into it are appended to the end of the collection.

To demonstrate data retrieval from an ArrayList, let’s create another method:

public static ArrayList ReadFromDetailsList(ArrayList detailsList)
{
    var details = new ArrayList
    {
        detailsList[1],
        detailsList[0],
    };

    return details;
}

Here, we use a zero-based indexer to access some elements in our collection. These elements are used to initialize an ArrayList instance that our method returns.

In practice, we use the ArrayList when we are not sure about the data types of the objects we wish to store in a collection.

Hashtable

The Hashtable is a non-generic collection type that is defined in the System.Collections namespace. This collection type represents a group of key-value pair objects. Although both the Hashtable and Dictionary<TKey, TValue> are associative collections, they are different. Unlike the dictionary, when we use a Hashtable, we don’t have to specify the data type of the key and the value.

To view this content, you must be a member of Code's Patreon at $1 or more

Let’s define a method that creates an instance of the Hashtable class and adds items to our collection:

public static Hashtable CreateDetailsHashTable()
{
    Hashtable detailsHashTable = new();
    detailsHashTable.Add("name", "John");
    detailsHashTable.Add("language", 'C');
    detailsHashTable.Add("isEligible", "No");
    detailsHashTable["age"] = 17;

    return detailsHashTable;
}

Here, we use the Add method from the Hashtable class to persist data to our Hashtable. Then, we use an indexer to add an item to our collection.

Now, let’s create a method that will access the elements in our hashtable:

public static ArrayList ReadFromDetailsHashTable(Hashtable detailsHashTable)
{
    var myList = new ArrayList
    {
        detailsHashTable["name"],
        detailsHashTable["age"]
    };

    return myList;
}

Here, we initialize an ArrayList object with the values we retrieve from our hashtable. We get these values by using their keys as our collection indexer.

To learn more about the Hashtable class in .NET, please refer to this article.

SortedList

The SortedList is a non-generic collection type available in the System.Collections namespace. It represents a collection of key-value pairs that are always sorted by their keys. The SortedList does not permit duplicate keys. We use either the keys or the index of the objects in the collection to access them:

public static SortedList CreateNumbersSortedList()
{
    SortedList numbersSortedList = new SortedList();
    numbersSortedList.Add("first", 1);
    numbersSortedList.Add("second", 2);
    numbersSortedList.Add("third", 3);

    return numbersSortedList;
}

Here, we create an object of the SortedList type. Then, we use the built-in Add method to persist data to our collection. All keys and values in the collection are of type System.Object.

Moreover, to demonstrate how we can retrieve data from a sorted list, let’s implement the ReadFromNumbersSortedList method:

public static ArrayList ReadFromNumbersSortedList(SortedList numbersSortedList)
{
    var numList = new ArrayList
    {
        numbersSortedList["first"],
        numbersSortedList["second"],
        numbersSortedList.GetByIndex(2)
    };

    return numList;
}

To retrieve a value from a SortedList, we use the key as an indexer.

Also, we can pass the index of that value to the GetByIndex method, and then add the retrieved value to our list.

To learn more about the SortedList class in .NET, please refer to this article.

Stack

This is a non-generic collection type available in the System.Collections namespace. We use this data structure to represent a collection of Last In, First Out(LIFO) objects. Items in this collection type can be of any data type.

To see how the Stack class works, let’s go through an example:

public static Stack CreateDetailsStack()
{
    Stack detailsStack = new Stack();
    detailsStack.Push("one");
    detailsStack.Push(1);
    detailsStack.Push(true);

    return detailsStack;
}

Here, we call the Push method of the Stack class, to enter various objects into our stack. This method inserts an item to the top of the stack.

Next, let’s create a method to retrieve an item from the top of our stack:

public static ArrayList ReadFromDetailsStack(Stack detailsStack)
{
    var detailList = new ArrayList
    {
        detailsStack.Pop()
    };

    return detailList;
}

Here, we invoke the built-in Pop method, which removes an object from the top of the stack and returns it.

For more details about the Stack collection type, please refer to this article on it.

Queue

The Queue is a non-generic collection type used to represent a collection of First In, First Out(FIFO) objects. This data structure is defined in the System.Collections namespace. Objects of any data type can be added to this collection.

To demonstrate working with the Queue class, let’s implement a CreateDetailsQueue method:

public static Queue CreateDetailsQueue()
{
    Queue detailsQueue = new Queue();
    detailsQueue.Enqueue("First");
    detailsQueue.Enqueue(2);
    detailsQueue.Enqueue(false);

    return detailsQueue;
}

In this method, we declare an instance of the Queue class. Then, we invoke the built-in Enqueue method and pass it a parameter of any type. This method adds the parameter to the end of the queue.

Now, let’s create another method that retrieves an item from our queue:

public static ArrayList ReadFromDetailsQueue(Queue detailsQueue)
{
    var detailList = new ArrayList
    {
        detailsQueue.Dequeue()
    };

    return detailList;
}

Here, we invoke the built-in Dequeue method, which removes the first item added to the queue and returns it.

To learn more about the Queue data structure, please refer to this article.

Concurrent Collections in .NET

When we develop multi-threaded applications, the generic and non-generic collection types are insufficient and produce inconsistent results. In this situation, we use the classes in the System.Collections.Concurrent namespace. These collection types allow us to perform thread-safe operations when a collection is accessed concurrently by multiple threads.

ConcurrentDictionary<TKey, TValue>

The ConcurrentDictionary<TKey, TValue> is a thread-safe alternative to the Dictionary<TKey, TValue>. The key-value pairs in this collection can be accessed by multiple threads concurrently:

public static ConcurrentDictionary<int, string> CreateConcurrentNumbersDictionary()
{
    ConcurrentDictionary<int, string> concurrentNumbersDictionary = new();
    concurrentNumbersDictionary.TryAdd(1, "One");
    concurrentNumbersDictionary.TryAdd(2, "Two");
    concurrentNumbersDictionary.TryAdd(3, "Three");

    return concurrentNumbersDictionary;
}

Here, we invoke the TryAdd method to add items to our dictionary. This method tries to add the specified key-value pair to our collection and returns true if succeeds.

Next, let’s create a method to retrieve a value from our dictionary:

public static string ReadFromConcurrentNumbersDictionary(ConcurrentDictionary<int, string> concurrentNumbersDictionary)
{
    var word = concurrentNumbersDictionary[1];

    return word;
}

Here, we use an indexer to get the value of the specified key from our collection and return the retrieved value.

ConcurrentQueue<T>

The ConcurrentQueue<T> is a concurrent collection type defined in the System.Collections.Concurrent namespace. It is a generic type used to represent a collection of First In, First Out(FIFO) objects. This collection supports concurrent access by multiple threads at runtime.

To demonstrate, let’s define a CreateConcurrentNumbersQueue method:

public static ConcurrentQueue<int> CreateConcurrentNumbersQueue()
{
    ConcurrentQueue<int> concurrentNumbersQueue = new();
    concurrentNumbersQueue.Enqueue(1);
    concurrentNumbersQueue.Enqueue(2);
    concurrentNumbersQueue.Enqueue(3);

    return concurrentNumbersQueue;
}

Here, we create an instance of the ConcurrentQueue<int> class. Then, we invoke the Enqueue method and pass in an integer argument which is appended to the end of the queue.

Now, let’s create another method that retrieves an item from our queue:

public static ArrayList ReadFromConcurrentNumbersQueue(ConcurrentQueue<int> concurrentNumbersQueue)
{
    var numberList = new ArrayList();
    if(concurrentNumbersQueue.TryDequeue(out int num))
    {
        numberList.Add(num);
    }

    return numberList;
}

We invoke the built-in TryDequeue method, which attempts to remove the first item from the queue and sets it to the num parameter. It returns true if the removal is successful.

ConcurrentStack<T>

The ConcurrentStack<T> is a concurrent collection type available in the System.Collections.Concurrent namespace. We use this data structure to represent a thread-safe collection of Last In, First Out(LIFO) objects.

To see how the ConcurrentStack<T> class works, let’s work through an example:

public static ConcurrentStack<string> CreateConcurrentOperationStack()
{
    ConcurrentStack<string> concurrentOperationStack = new();
    concurrentOperationStack.Push("Operation - 1");
    concurrentOperationStack.Push("Operation - 2");
    concurrentOperationStack.Push("Operation - 3");

    return concurrentOperationStack;
}

Here, we create an object of the ConcurrentStack<string> type. Then, we invoke the Push method to add various strings to the top of the stack.

Next, let’s create a method that retrieves an item from the top of our stack:

public static string? ReadFromConcurrentOperationStack(ConcurrentStack<string> concurrentOperationStack)
{
    string? operation = null;
    if(concurrentOperationStack.TryPop(out string? op))
    {
        operation = op;
    }

    return operation;
}

This method returns the item found at the top of the stack, or null if no item was found.

Inside it, we initialize a nullable string. Then, we invoke the built-in TryPop method, which tries to remove an item at the top of the stack. If an item was removed, we assign it to theoperationvariable and return it.

To learn more about concurrent collections in .NET, please refer to this article.

Conclusion

We have learned about the different collections in .NET. However, before we use a particular data structure, we should consider its advantages and its suitability for the operations we wish to perform on our collection.

This content is available exclusively to members of Code's Patreon at $0 or more.