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.
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.
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 theoperation
variable 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.