Introduction
Starting with .NET 9, we now have a new interface for comparisons, IAlternateEqualityComparer<TAlternate, T>
, as well as a new method in hash-based types called GetAlternateLookup
. These, together with the new struct AlternateLookup<TAlternateKey>
, enable us to perform operations on hash-based types using an alternative key.
At first glance, using an alternative key might seem unnecessary since we can always convert our alternative type to the type used as the key. However, such conversions can come with drawbacks. For example, converting a Span<char>
to a string
involves creating a new string object, which generates unnecessary garbage. This overhead occurs solely for the purpose of using the generated string as a key in a hash-based collection.
With this new feature, we can now use an alternate key directly in existing hash-based types. Let’s explore how it works.
IAlternateEqualityComparer
The IAlternateEqualityComparer<TAlternate,T>
interface, is essentially like having an IEqualityComparer<TAlternative,T>
. It is an interface that is used for comparing two different types for equality, and its implementation needs three methods:
Create(TAlternate)
This method creates an instance of typeT
that is considered equal to the provided instance of typeTAlternate
. It is used when adding new entries to hash-based collections. During such operations, the runtime needs a way to produce aT
instance from theTAlternate
instance, so it can be used as a key.Equals(TAlternate, T)
This method checks for equality between instances ofTAlternate
andT
. It is primarily used for looking up values using the alternate key.GetHashCode(TAlternate)
This method computes a hash code for the alternate key type. It is used in the same way as theGetHashCode
method for the primary key type in hash-based collections.
Currently, C# provides a built-in implementation of this interface for string
and Span<char>
.
The AlternateLookup Struct And The GetAlternateLookup Method
The AlternateLookup Structs, are structs that exist nested inside the various hash based types, like the dictionary and offer methods that allow operations that this hash based type supports but with the usage of an alternative key.
The GetAlternateLookup Method, is the method that will give us an instance of that type.
Here’s an example in code:
var dict = new Dictionary<string, int> {
{ "One", 1 },
{ "Two", 2 },
{ "Three", 3 }
};
var altLookup = dict.GetAlternateLookup<ReadOnlySpan<char>>();
ReadOnlySpan<char> one = "One";
ReadOnlySpan<char> two = "Two";
ReadOnlySpan<char> three = "Three";
ReadOnlySpan<char> four = "Four";
Console.WriteLine(altLookup[one]); // Retrieve value
altLookup[four] = 4; // Add new value
Console.WriteLine(dict["Four"]);
Here our altLookup
variable is of type Dictionary<string, int>.AlternateLookup<ReadOnlySpan<char>>
.
Along with the GetAlternateLookup
, we have the TryGetAlternateLookup
that can be used like this:
if(dict.TryGetAlternateLookup<ReadOnlySpan<char>>(out var altLookup))
Console.WriteLine("Alternative lookup with the type ReadOnlySpan<char> is supported");
An Example Of Our Own Alternate Lookup
Let’s explore how to implement our own alternative lookup between two types. Suppose we have a Person
type with a unique ID, and we want to use a dictionary where the keys are Person
objects, but we also want to perform lookups using their IDs as alternative keys.
Since we don’t want to create a new Person
object when an ID doesn’t exist, we will skip implementing the Create
method of the IAlternateEqualityComparer
. In this case, our alternative keys will be used exclusively for lookups.
Here’s our Person
class:
public record Person(string firstName, string lastName, int id)
{
public string FirstName => firstName;
public string LastName => lastName;
public int Id => id;
}
And here’s a dictionary:
Person john = new("John", "Doe", 123);
Person bob = new("Bob", "Smith", 987);
Dictionary<Person, string> dict = new(new PersonEqualityComparer())
{
{ john, "Big street 11" },
{ bob, "Small street 99" }
};
Now we need to create a PersonEqualityComparer
class that will be used for our alternative lookups:
public class PersonEqualityComparer : IEqualityComparer<Person>, IAlternateEqualityComparer<int, Person>
{
public bool Equals(Person? x, Person? y) => x != null && y != null && x.Id == y.Id;
public int GetHashCode(Person person) => person.Id.GetHashCode();
public bool Equals(int id, Person person) => id == person.Id;
public int GetHashCode(int id) => id.GetHashCode();
public Person Create(int id) => throw new NotImplementedException();
}
Our comparer type must also implement the IEqualityComparer
interface. The first two methods are the required implementations of the IEqualityComparer
interface, while the last three methods belong to the IAlternateEqualityComparer
interface.
As mentioned earlier, we chose not to implement the Create
method because we don’t intend to create a new object each time. Additionally, we lack all the necessary information (such as the first and last name) to construct a complete Person
object. Our alternate lookup is designed exclusively for retrieving values, not for adding new ones.
The Equals
method is straightforward in this case, as our int
maps directly to the int id
field of the Person
record. Now, we can perform operations like this:
var altLookup = dict.GetAlternateLookup<int>();
Console.WriteLine(altLookup[123]); // Big street 11
Conclusion
The alternate lookup in our example isn’t strictly necessary, as we can retrieve the desired Person directly using its ID. It might be useful in scenarios where we don’t want to load multiple Person records into memory but still need to locate a specific record’s address using its ID. However, in such cases, it would be more efficient to design the dictionary with IDs as the primary keys from the start.
Starting with .NET 9, alternate lookups provide a convenient option. In particular, they are highly beneficial for scenarios involving Span<char>
and string
. By using alternate lookups, we avoid the performance cost of creating heap-allocated strings solely to retrieve values from a dictionary of strings.
As always, thank you for reading, and if you have any questions or comments you can use the comments section, or contact me directly via the contact form or by email. Also, if you don’t want to miss any of the new blog posts, you can subscribe to my newsletter or the RSS feed.