Unpacking Collections in C#

Admir Mujkic
11 min readApr 10

--

Managing collections of objects is a frequent task that .NET developers encounter. In a recent publication “Discovering the Power of Collections in .NET with Categorization with Examples in C# — Part 1,” we delved into the diverse collection types available to .NET developers and their unique features.

We emphasized the importance of selecting the appropriate collection type for each software project.

The objective of this article is to provide practical examples of some of the most commonly used collection types in C#, and to examine their usage more closely. Our focus will be on effectively leveraging .NET collections to optimize the programming workflow.

From Unsplash.com by Jason Leung

Demystifying ICollection<T> and ICollection

When we speak about ICollection<T> we can say is like a interface that helps you work with groups of things, like a box of toys. It can tell you how many toys are in the box, if a specific toy is in the box, and even give you a list of all the toys. If the box is not read-only, meaning you can add or take out toys, you can also use ICollection<T> to add new toys, remove old ones, or clear out the entire box. And, you can use it with the foreach statement.

public interface ICollection<T> : IEnumerable<T>, IEnumerable
{
int Count { get; }
bool Contains (T item);
void CopyTo (T[] array, int arrayIndex);
bool IsReadOnly { get; }
void Add(T item);
bool Remove (T item);
void Clear();
}

In another hand the non generic ICollection is like a countable collection, but you can’t add or remove items from it, and you can’t check if a certain item is in the collection. It’s mostly just for counting the number of items in a collection.

public interface ICollection : IEnumerable
{
int Count { get; }
bool IsSynchronized { get; }
object SyncRoot { get; }
void CopyTo (Array array, int index);
}

If you’re making a read-only ICollection<T>, you should throw a NotSupportedException when trying to Add, Remove, or Clear items. These interfaces are usually used with either the IList or IDictionary interface.

IList<T> vs IList: Exploring the Differences and Similarities in C# Collections

One of the most popular interface is IList<T>. This is another interface for collections, but it’s specifically designed for collections that can be accessed by position. In addition to the features provided by ICollection<T> and IEnumerable<T>, IList<T> also allows you to read or write an element at a specific position using an index. You can also add or remove elements at a specific position in the collection.

public interface IList<T> : ICollection<T>, IEnumerable<T>, IEnumerable
{
T this [int index] { get; set; }
int IndexOf (T item);
void Insert (int index, T item);
void RemoveAt (int index);
}

When you have a list of items and you want to find a particular item in the list, you can use the IndexOf method in IList<T>. It searches the list from the beginning and tells you where the item is located in the list. If the item is not found in the list, it returns -1. This method checks each item in the list one by one until it finds the item or reaches the end of the list. This type of search is called a linear search.

Also keep on mind, that the performance of the IndexOf method in IList<T> can be slower for large collections because it has to check each item in the list one by one until it finds the item it’s looking for or reaches the end of the list. This is called a linear search. For large collections, this can take a long time and may not be very efficient. In such cases, it may be better to use a different data structure or algorithm that can perform the search more quickly, such as a hash table or binary search.

The Power of IReadOnlyCollection<T> and IReadOnlyList<T>

In .NET, there are interfaces called IReadOnlyCollection<T> and IReadOnlyList<T> that are similar to ICollection<T> and IList<T>, respectively. However, these interfaces only provide members for read-only operations, meaning you can’t modify the collection or list. They are useful if you want to provide a way for other parts of your code to access your collection or list without the risk of accidentally changing it.

public interface IReadOnlyCollection<out T> : IEnumerable<T>, IEnumerable
{
int Count { get; }
}
public interface IReadOnlyList<out T> : IReadOnlyCollection<T>, IEnumerable<T>, IEnumerable
{
T this[int index] { get; }
}

It’s pretty common for writable (mutable) collections to have both read-only and read/write interfaces.

The read-only interfaces not only enable working with collections covariantly, but they also provide a way to expose a private writable collection as a read-only view publicly. This can be achieved by implementing the read-only interface for the collection and returning it instead of the actual collection. This way, the collection can be modified internally, but the external users can only read from it.

Unleashing the Power of C# Arrays

I believe everybody heard about arrays. Arrays are a fundamental concept in programming that are widely used in various languages and frameworks, including .NET. In .NET, the Array class serves as the base class for all single and multidimensional arrays in C#.

Arrays are a type of data structure that can store a collection of elements. In C#, the Array class provides a standard set of methods that can be used with any array, no matter how it was created or what kind of elements it holds.

When we create an array in C#, we can do so using specific syntax. The .NET runtime then creates a special type for the array based on its dimensions and element types, which can be used with interfaces like IList<string> that work with collections.

However, it’s important to remember that once an array is created, it can’t be resized. It occupies a fixed amount of memory in a contiguous block, which makes it easy to access individual elements quickly, but prevents us from adding or removing elements dynamically. If we need a collection that can change in size, we should consider using other types like lists or dictionaries.

When an array in C# contains reference type elements, each element occupies only as much space in the array as a reference, which is 4 bytes in a 32-bit environment or 8 bytes in a 64-bit environment.

This is because the reference only stores the memory address where the actual object data is stored.

Using an example in C#, let’s see how this works in memory.

using System.Text;
namespace AdmirLiveDotNetCollection;
internal static class Program
{
private static void Main(string[] args)
{
var builders = new StringBuilder [5];
builders[0] = new StringBuilder("Penzle: Flexibility");
builders[1] = new StringBuilder("Penzle: Increased protection");
builders[2] = new StringBuilder("Penzle: User-friendly");
var numbers = new long [3];
numbers[0] = 12345;
numbers[1] = 67891;
}
}
Arrays in memory

Important to remember that In C#, arrays are always reference types, regardless of the type of elements they contain. This means that when one array is assigned to another using the statement arrayB = arrayA, both variables reference the same array in memory. Any changes made to one variable or the array will be reflected in the other variable or array as well.

When we compare two arrays in C#, if they are two separate arrays with different memory locations, then they will not be considered equal, even if they contain the same elements. This is because arrays are compared based on their memory references, not their actual element values.

To compare two arrays based on their elements and not just their memory references, we need to use a structural equality comparer that compares every element of the arrays. This will ensure that two arrays with the same elements are considered equal, regardless of whether they have the same memory location or not.

using System.Collections;

namespace AdmirLiveDotNetCollection;

internal static class Program
{
private static void Main(string[] args)
{
object[] a1 = { "Penzle", 123, true };
object[] a2 = { "Penzle", 123, true };
Console.WriteLine (a1 == a2); // This is false
Console.WriteLine (a1.Equals (a2)); // This is false
IStructuralEquatable se1 = a1;
Console.WriteLine (se1.Equals (a2, StructuralComparisons.StructuralEqualityComparer)); // This is true because the arrays have the same contents
}
}
The output of the above program.

Secrets of C# Dictionaries

I believe you are worked with dictionaries at some point in your career. The best part is that you can easily access the values by their keys, which makes working with dictionaries a breeze. Here is signature of generic version of IDictionary as IDictionary<TKey, TValue>.

public interface IDictionary<TKey, TValue> : ICollection<KeyValuePair<TKey, TValue>>, IEnumerable
{
bool ContainsKey (TKey key);
bool TryGetValue (TKey key, out TValue value);
void Add (TKey key, TValue value);
bool Remove (TKey key);
TValue this [TKey key] { get; set; }
ICollection <TKey> Keys { get; }
ICollection <TValue> Values { get; }
}

Please remember that in C#, there’s an interface called IReadOnlyDictionary<TKey,TValue> for read-only access to dictionary members. It’s useful when you don’t need to modify data and want to prevent accidental modifications to the collection.

You can add items to a dictionary using the Add method or the index’s set accessor. The set accessor adds a new item to the dictionary if it’s not already there, or updates it if it’s already there. However, duplicate keys aren’t allowed, so calling Add twice with the same key will throw an exception.

You can use the indexer or the TryGetValue method to get items from a dictionary. The indexer will throw an exception if the key doesn’t exist, but TryGetValue will return false. You can also check if a key exists in the dictionary by calling the ContainsKey method. If you want to retrieve an item after checking its membership, you’ll have to do two lookups, which is more expensive.

Directly enumerating over an IDictionary<TKey,TValue> returns a sequence of Structs for KeyValuePairs:

public struct KeyValuePair <TKey, TValue>
{
public TKey Key { get; }
public TValue Value { get; }
}

The imporant part here is that you can access a dictionarys keys or values using the Keys/Values properties. These properties allow you to enumerate over just the keys or values in the dictionary, respectively.

IDictionary

The non generic IDictionary interface is similar to the IDictionary<TKey,TValue>, but with two key differences. It’s important to know these differences since IDictionary is used in legacy code, including the .NET Base Class Library:

  1. When retrieving a nonexistent key using the indexer, null is returned instead of throwing an exception.
  2. Contains tests for membership instead of ContainsKey.

When you enumerate over a non generic IDictionary, you get a sequence of DictionaryEntry structures such as:

public struct DictionaryEntry
{
public object Key { get; set; }
public object Value { get; set; }
}

Immutable Collections in C#

When we speak about ReadOnlyCollection<T>, it is important to keep in mind that it is designed to give us a read-only view of a collection, which simplifies software and reduces software bugs by restricting the ability to modify the collection.

The concept of immutable collections extends this concept by providing collections that, once they are created, cannot be modified at all after they are created. If you want to change an immutable collection, you must create a new one.

It simplifies parallelism and multithreading, and makes code easier to reason about. Immutability is a characteristics of functional programming. The disadvantage of immutability is that creating a new object is slow. There are ways to mitigate this problem, such as using portions of the original structure in order to mitigate this problem.

Performance

We have to keep on mind that most immutable collections use an AVL tree internally, which allows add/remove operations to reuse portions of the original structure, reducing the overhead of these operations. However, this comes at the cost of making read operations slower.

AVL tree is a self-balancing binary search tree that maintains its height at O(log n) by performing rotations. It was the first data structure to be invented to ensure worst-case time for search, insertion, and deletion operations is O(log n).

As a result, immutable collections are generally slower than their mutable counterparts for both reading and writing. ImmutableList<T> is the most affected, being 10 to 200 times slower than List<T>, depending on the size of the list. ImmutableArray<T> exists to avoid the overhead for read operations by using an array, but it’s slower than ImmutableList<T> for add operations because none of the original structure can be reused.

ImmutableList<T> has slow performance for both read and add operations, while ImmutableArray<T> has very fast read performance but very slow add performance.

In another hand, ImmutableArray<T> is a better choice if you require fast read operations but can tolerate slower add operations. ImmutableList<T> is not recommended if you require high performance for either read or write operations.

Please keep on mind when removing an element, ImmutableArray is more expensive than List<T> due to the allocation of a new collection which places additional load on the garbage collector.

Finally

When you are selecting a .NET collection, there are a few things to think about. The most important thing is what your project needs. But here are some things that are generally worth considering:

  • If your app needs to be fast, look for a collection that can get data quickly. For example, if you’re always adding and removing things from a collection, try using LinkedList<T>. It’s really fast at those operations.
  • If you have a lot of data and not much memory, choose a collection that uses memory efficiently. You could use HashSet<T> or a Dictionary<TKey, TValue> with a special comparer to save space.
  • If your app needs to have multiple threads working on the collection at once, use a collection that’s thread-safe. For example, you could use ConcurrentDictionary<TKey, TValue> or ConcurrentQueue<T>.
  • If you need to store a certain type of data, pick a collection that’s designed for that data. For example, if you need to store key-value pairs, go with Dictionary<TKey, TValue>.
  • Finally, think about how easy the collection is to use. If you’re new to .NET, you might want to start with something simple like List<T>.

Based on my experience, think about what your app needs, and then select a collection that will work well for those needs. After 15 years in the software engineering field, I have learned that everything must be traded off for the best possible result for your application, design, architecture, or whatever you are working on. Good luck!

P.S. If you believe I can help with something, don’t hesitate to contact me, and I will reach you as soon as possible. admir.m@penzle.com

Cheers! 👋

--

--

Admir Mujkic

Admir combined engineering expertise with business acumen to make a positive impact & share knowledge. Dedicated to educating the next generation of leaders.

Recommended from Medium

Lists

See more recommendations