Updated on
A SortedList in C# is a collection of key-value pairs that must be sorted at any given point in time based on its keys. Whenever we add or remove an element, the list keeps the keys in order on its own, so we never have to sort it.
We’ll create both versions of the class, the generic and the non-generic one, read and remove elements, and then see what it costs next to a Dictionary and a SortedDictionary.
Let’s start.
What Is a SortedList in C#?
A SortedList<TKey,TValue> is a collection of key-value pairs that keeps its keys in sorted order at all times. It lives in System.Collections.Generic. Every key must be unique and can’t be null, while values can repeat.
Under the hood it holds two arrays, one for the keys and one for the values, both ordered by key.
Looking up a key is a binary search over the keys array, so it costs O(log n). Adding a key that belongs in the middle means every later element moves one slot to the right, so that insert costs O(n). Removing an element shifts the rest back the same way.
Because the data sits in arrays, we can also read it by position. GetKeyAtIndex(0) or Keys[0] gives us the smallest key.
There’s also an older, non-generic System.Collections.SortedList, which stores its keys and values as object. We’ll create both versions in the next section and see where they differ.
In fact, we can argue that the SortedList is a hybrid between a Dictionary<TKey, TValue> and a List<T>. This has some operational advantages, as well as disadvantages.
Let’s compare adding new elements into a List object, to adding elements into a SortedList:
When we add a new element to a List, our list class will simply append the element to the end of the collection in the order in which we added them. For the SortedList, however, our class runs a binary search over the keys to find the right position for the new element, and then moves every element after that position one slot down to make room.
How to Create a SortedList in C#
There are two versions of this class available in C#; the generic and non-generic versions. They differ in how we create them, and some nuances in how we access them.
Generic SortedList
The generic SortedList is available under the System.Collections.Generic namespace. As with all generics, when creating a new object, we have to define the types of the key and value that is, TKey and TValue respectively.
Let’s create a generic SortedList that takes int keys and string values:
var myList = new SortedList<int,string>();
This gives us a strongly typed collection, which means that every pair of elements we add to it must be of int key type and string value type. This structure is so similar to a Dictionary that we can even initialize a new sorted list by feeding a dictionary into its constructor:
var myDictionary = new Dictionary<int, string> { {1, "first things first"}, { 2, "second things second" } };
var myListFromDictionary = new SortedList<int, string>(myDictionary);
The initialized list will simply sort itself using the keys, and retain the contents of the supplied dictionary. There are four other constructor overloads available when creating a SortedList.
Let’s describe all the available overloads:
SortedList<TKey,TValue>(IComparer<TKey>) SortedList<TKey,TValue>(IDictionary<TKey,TValue>, IComparer<TKey>) SortedList<TKey,TValue>(Int32) SortedList<TKey,TValue>(Int32, IComparer<TKey>)
In whichever overload we use, we can add elements to our list, and expect the list to expand to accommodate the new addition. However, we can’t add values of any other types to the keys or values. In fact, attempting to do this will give us a compiler error.
Now, let’s add some elements to our first generic SortedList. The last three lines don’t compile, and the comments show the errors we get:
myList.Add(1, "This is my first string element");
myList.Add(2, "This is my second string element");
myList.Add("three", "This is my third string element"); // CS1503: Argument 1: cannot convert from 'string' to 'int'
myList.Add(null, "There is nothing to see here"); // CS1503: Argument 1: cannot convert from '<null>' to 'int'
myList.Add(5, 5); // CS1503: Argument 2: cannot convert from 'int' to 'string'
We get these errors because we are trying to insert the wrong types, either as a key or as a value. The situation is quite different for a non-generic SortedList.
Non-Generic SortedList
The non-generic SortedList is available under the System.Collections namespace. Here, we don’t have to declare the types that we will be adding to our collection. Keys and values are both stored as object, so value types such as int get boxed on the way in.
To keep the keys in order, the list compares every new key with the keys it already holds, through their IComparable implementation. So the keys we add still have to be comparable with each other, even though the compiler no longer checks it for us.
Microsoft’s documentation for this class is blunt: “We don’t recommend that you use the SortedList class for new development.” It points us to the generic SortedList<TKey,TValue> instead. We still show the non-generic version here, because we can meet it in older code.
Let’s add using System.Collections; at the top of our file and create a non-generic SortedList:
var myNonGenericList = new SortedList();
Once we create an empty SortedList, we can use the Add() method to insert new elements into the list. Unlike the generic SortedList, since the key and value types are evaluated at runtime, we do not get any compiler errors even when we try to add unwanted types to our collection. We will, however, get runtime exceptions.
Let’s add the same elements from our generic collection into this non-generic one. This time everything compiles, but the two lines in the middle throw at runtime:
myNonGenericList.Add(1, "This is my first string element");
myNonGenericList.Add(2, "This is my second string element");
myNonGenericList.Add("three", "This is my third string element"); // System.InvalidOperationException: Failed to compare two elements in the array.
myNonGenericList.Add(null, "There is nothing to see here"); // System.ArgumentNullException: ...
// This line does not give us any errors!
myNonGenericList.Add(5, 5);
The string key fails because the list has to compare "three" with the int keys already in it, and an int can’t compare itself with a string. The inner exception says so: “Object must be of type Int32.”
Did you notice that the myNonGenericList.Add(5, 5) line did not return any errors? Since the value is a basic object type, our program does not need to control what kind of values we input into our collection. We will have to take responsibility for handling the different types when accessing the SortedList within our code.
How to Access a SortedList in C#
There are many methods and properties through which we can access a SortedList. First, let’s look at the properties of our SortedList:
The Capacity property gives us the number of elements our SortedList can hold before it has to grow its internal arrays. The Count gives us the number of elements currently within our collection. The Keys property gives us a list of all the Keys currently within the collection, while the Values property gives us a list of the Values. Under the hood, the SortedList actually makes use of two parallel arrays for storing keys and values. The IComparer<T> which provides the logic for sorting our collection is also exposed using the Comparer property.
There are many methods that we can use to access the SortedList in C#. We have already demonstrated the Add() method which adds a new element into the collection with the specified key and value. Once we have elements in the collection, we want to be able to perform actions on the collection. To check if a key is part of a collection, we can use the ContainsKey(TKey) method. The IndexOfKey(TKey) method will give us the zero-based index of our key if it exists in the list.
Let’s modify myList to add in the 3 and 4 key-value pairs:
if(!myList.ContainsKey(3))
{
myList.Add(3, "This is my third value");
}
if(!myList.ContainsValue("This is my fourth value"))
{
myList.Add(4, "This is my fourth value");
}
For the fourth item, instead of using ContainsKey(TKey), we use ContainsValue(TValue).
When we have the values but not the keys, we can perform the reverse search by using ContainsValue(TValue) and IndexOfValue(TValue) respectively.
This search is slower, though. ContainsKey() and IndexOfKey() run a binary search, which is O(log n), while ContainsValue() and IndexOfValue() check the values one by one, which is O(n).
So far we’ve only checked what’s in the list. To read a value, we use the indexer with a key, or TryGetValue() when the key may be missing. And because the pairs sit in sorted arrays, we can read them by position too:
var third = myList[3]; // "This is my third value" myList.TryGetValue(5, out var fifth); // false, fifth is null var firstKey = myList.GetKeyAtIndex(0); // 1
The indexer throws a KeyNotFoundException for a key that isn’t there, which is why TryGetValue() is the safer choice when we aren’t sure.
How to Remove Elements From a SortedList
While working with our list, we may want to remove some elements from it. Using the RemoveAt(Int32) method, we can remove a single element at a zero-based index. If we have the key for this element, we could instead remove it using the Remove(TKey) function:
myList.RemoveAt(1); // removes the element with key 2, which sits at index 1 myList.Remove(4); // removes the element with key 4 myList.Clear(); // removes all the elements, Count is now 0
We should remember that the list never sorts itself again. When we remove an element, every element after it moves up one slot, so RemoveAt() and Remove() cost O(n), just like adding a key in the middle.
When Should We Use a SortedList in C#?
We use a SortedList when we need key-value pairs in key order, we read them far more often than we change them, and the keys mostly arrive already sorted or the collection stays small.
Its closest rival is SortedDictionary<TKey,TValue>, which keeps the same order in a tree of linked nodes. Microsoft’s documentation puts the trade-off in one line: “SortedList<TKey,TValue> uses less memory than SortedDictionary<TKey,TValue>.” The same remarks name the price: inserting and removing unsorted keys costs O(n) in a SortedList and O(log n) in a SortedDictionary.
We measured the memory side on .NET 10. With 100,000 int keys and values, the SortedList kept about 1.0 MB, the SortedDictionary about 4.8 MB, and a plain Dictionary about 3.1 MB.
So for data we load once and then query, or that grows at the end, the SortedList is the better pick. For a large collection fed keys in random order, we reach for a SortedDictionary. And when order doesn’t matter at all, a Dictionary does the job.
By its nature, a SortedList must always be sorted. Therefore, whenever we add a new element in the middle of the list or remove one, it has to move the elements after it to keep them in the right order. This becomes more expensive as we increase the number of elements in our list.
For keys in random order, we should only use SortedList when we want to handle smaller collections that need to be sorted at all times. When dealing with larger collections, it is more efficient to use a dictionary, HashSet, or even a regular list, which we sort by a property once, at the point where we need it.
Let’s check the memory claim ourselves. We fill each collection with the same 100,000 keys and compare the size of the managed heap before and after, with a method in a static MemoryComparison class:
public static long RetainedBytes(Func<object> fill)
{
var before = GC.GetTotalMemory(forceFullCollection: true);
var collection = fill();
var after = GC.GetTotalMemory(forceFullCollection: true);
GC.KeepAlive(collection);
return after - before;
}
GC.KeepAlive() stops the runtime from collecting our collection before the second measurement. Then we run it once per collection type, with a small Fill() helper in the same class that takes an IDictionary<int, int> and the keys, adds every key with itself as the value and returns the collection:
var keys = Enumerable.Range(0, 100_000).ToArray();
var sortedList = MemoryComparison.RetainedBytes(
() => MemoryComparison.Fill(new SortedList<int, int>(), keys));
var sortedDictionary = MemoryComparison.RetainedBytes(
() => MemoryComparison.Fill(new SortedDictionary<int, int>(), keys));
var dictionary = MemoryComparison.RetainedBytes(
() => MemoryComparison.Fill(new Dictionary<int, int>(), keys));
On our machine the SortedList kept 1,048,448 bytes, the SortedDictionary 4,785,952 and the Dictionary 3,128,872. Part of the SortedList figure is spare room: the arrays double in size when they fill up, so our 100,000 pairs sit in arrays sized for 131,072. Calling TrimExcess() after the last Add() brought it down to about 0.8 MB.
These are numbers from one machine, and GC.GetTotalMemory() is an approximation, so we read them as proportions rather than exact sizes.
| SortedList<TKey, TValue> | SortedDictionary<TKey, TValue> | Dictionary<TKey, TValue> | |
|---|---|---|---|
| Stores pairs in | Two sorted arrays | A tree of nodes | A hash table |
| Enumerates in key order | Yes | Yes | No, order is not guaranteed |
| Look up by key | O(log n) | O(log n) | Close to O(1) |
| Add or remove an unsorted key | O(n) | O(log n) | Close to O(1) |
| Add a key at the end | O(log n) | O(log n) | Close to O(1) |
| Read by index | Yes, GetKeyAtIndex(), Keys[i] | No | No |
| Memory for 100,000 int pairs (our run) | about 1.0 MB | about 4.8 MB | about 3.1 MB |
And if we only need sorted unique values with nothing attached to them, a SortedSet is the simpler choice.
Conclusion
The SortedList is a very useful and memory-efficient way to store sorted data. It gives us the flexibility of being able to index data, and also select data using keys. Once we have a mastery of the SortedList object, we can apply it in so many parts of our daily programming syntax.
Tested with .NET 10.0.10 and MSTest 4.5.1.


