Lesson 4
Sorted Dictionaries and Custom Comparators in C#
Topic Overview

Welcome to our exploration of sorted dictionaries using custom classes and comparers in C#. In today's lesson, we'll learn how to use custom classes as keys in sorted dictionaries. This approach enhances data organization and access. With the addition of comparers, we can dictate the order in such dictionaries.

Quick Recap on Sorted Dictionaries

A sorted dictionary is a dictionary with its keys always in order. This arrangement makes operations like searching for keys within a range more efficient. In C#, we use the SortedDictionary class to create them:

C#
1using System; 2using System.Collections.Generic; 3 4public class Program { 5 public static void Main(string[] args) { 6 SortedDictionary<string, int> sMap = new SortedDictionary<string, int>(); 7 sMap["a"] = 1; 8 sMap["b"] = 2; 9 sMap["c"] = 3; 10 11 foreach(var kvp in sMap) { 12 Console.WriteLine($"{kvp.Key}={kvp.Value}"); // Outputs "a=1", "b=2", "c=3" 13 } 14 } 15}
Introduction to Custom Classes in C#

Custom classes enable us to create objects that fit our data — for instance, a Person class for employee information or a Book class for a library database. In C#, classes are the blueprints for creating objects.

Consider this simple class, for example:

C#
1using System; 2 3public class Person { 4 public string Name { get; private set; } 5 public int Age { get; private set; } 6 7 public Person(string name, int age) { 8 Name = name; 9 Age = age; 10 } 11} 12 13public class Program { 14 public static void Main(string[] args) { 15 Person person = new Person("John Doe", 30); 16 Console.WriteLine(person.Name); // Outputs "John Doe" 17 Console.WriteLine(person.Age); // Outputs 30 18 } 19}
Using Custom Classes as Keys in Sorted Dictionaries

Using custom classes as dictionary keys helps organize complex multivariate keys in a sorted dictionary. Consider the following example using the Person class as a key in a sorted dictionary (i.e., SortedDictionary). However, this will not work yet.

C#
1using System; 2using System.Collections.Generic; 3 4public class Person { 5 public string Name { get; private set; } 6 public int Age { get; private set; } 7 8 public Person(string name, int age) { 9 Name = name; 10 Age = age; 11 } 12 13 public override int GetHashCode() { 14 return HashCode.Combine(Name, Age); 15 } 16 17 // Rest of the class... 18} 19 20public class Program { 21 public static void Main(string[] args) { 22 SortedDictionary<Person, string> people = new SortedDictionary<Person, string>(); 23 24 Person john = new Person("John", 30); 25 Person alice = new Person("Alice", 25); 26 27 people[john] = "Programmer"; 28 people[alice] = "Designer"; 29 } 30}

We can see here that John is assigned the value "Programmer", and Alice is assigned the value "Designer." However, this code will produce an exception. The reason is that the SortedDictionary needs a way to compare the Person objects to maintain its order. This requires not only implementing GetHashCode and Equals (important for identifying objects) but also providing a means to compare objects, typically through the IComparable<T> interface or a custom IComparer<T>.

Comparers and Their Role in Sorted Dictionaries

C# uses a comparer to determine the order of two keys. To make this comparison, we add the IComparable interface or a custom IComparer to our class. Without these methods, SortedDictionary can't compare its Person class keys. Here’s how to modify the Person class to implement the IComparable interface:

C#
1using System; 2using System.Collections.Generic; 3 4public class Person : IComparable<Person> { 5 public string Name { get; private set; } 6 public int Age { get; private set; } 7 8 public Person(string name, int age) { 9 Name = name; 10 Age = age; 11 } 12 13 public int CompareTo(Person other) { 14 int ageCompare = this.Age.CompareTo(other.Age); 15 if (ageCompare != 0) { 16 return ageCompare; 17 } 18 return this.Name.CompareTo(other.Name); 19 } 20 21 public override bool Equals(object obj) { 22 if (obj == null || GetType() != obj.GetType()) 23 return false; 24 25 Person person = (Person)obj; 26 return Age == person.Age && Name == person.Name; 27 } 28 29 public override int GetHashCode() { 30 return HashCode.Combine(Name, Age); 31 } 32} 33 34public class Program { 35 public static void Main(string[] args) { 36 SortedDictionary<Person, string> people = new SortedDictionary<Person, string>(); 37 38 Person john = new Person("John", 30); 39 Person alice = new Person("Alice", 25); 40 41 people[john] = "Programmer"; 42 people[alice] = "Designer"; 43 44 foreach (var person in people.Keys) { 45 Console.WriteLine($"{person.Name} is a {people[person]}"); 46 } 47 // Output: 48 // Alice is a Designer 49 // John is a Programmer 50 } 51}

In the code above, we implement the IComparable interface and override the CompareTo, Equals, and GetHashCode methods. The CompareTo method ensures that Person objects are initially sorted by age, and if ages are the same, then by name. Overriding Equals and GetHashCode ensures that Person objects behave consistently when used in collections like SortedDictionary. The GetHashCode method returns an integer representation of an object for quick look-ups in hash-based collections.

Lesson Summary

We've explored how to use custom classes as keys in sorted dictionaries and how comparers work in this context using the IComparable interface. Implementing CompareTo is necessary to maintain the order within the SortedDictionary, while overriding Equals and GetHashCode ensures object consistency and identity in collections. Now, prepare for some hands-on exercises to reinforce these concepts.

Enjoy this lesson? Now it's time to practice with Cosmo!
Practice is how you turn knowledge into actual skills.