DEV Community

mohamed Tayel
mohamed Tayel

Posted on

Using a Custom Type as a Dictionary Key in C#

In C#, dictionaries rely on keys behaving correctly when it comes to equality and hashing. Standard types like string, int, and float work seamlessly as dictionary keys because Microsoft has ensured they implement proper equality comparison and hashing. However, when using a custom type as a key, you need to manually define how equality and hashing should work.

In this article, we will:

  1. Create a custom type (CountryCode) and use it as a dictionary key.
  2. Understand why overriding Equals and GetHashCode is necessary.
  3. Implement a console application that demonstrates these concepts.

1. Creating a Custom Type as a Dictionary Key

Let's assume we want to store country details in a dictionary where the key is a custom type (CountryCode) instead of a string. The CountryCode type will wrap a string representing a country's ISO code (e.g., "USA", "FRA", "DZA").

Defining the CountryCode Class

using System;
using System.Collections.Generic;

public class CountryCode
{
    public string Code { get; }

    public CountryCode(string code)
    {
        if (string.IsNullOrWhiteSpace(code) || code.Length != 3)
        {
            throw new ArgumentException("Country code must be exactly 3 characters.", nameof(code));
        }

        Code = code.ToUpper(); // Normalize case
    }

    public override string ToString() => Code;

    // Override Equals for value comparison
    public override bool Equals(object obj)
    {
        if (obj is CountryCode other)
        {
            return string.Equals(this.Code, other.Code, StringComparison.OrdinalIgnoreCase);
        }
        return false;
    }

    // Override GetHashCode to match Equals behavior
    public override int GetHashCode()
    {
        return StringComparer.OrdinalIgnoreCase.GetHashCode(Code);
    }

    // Overload == and != for consistency
    public static bool operator ==(CountryCode left, CountryCode right)
    {
        if (left is null) return right is null;
        return left.Equals(right);
    }

    public static bool operator !=(CountryCode left, CountryCode right)
    {
        return !(left == right);
    }
}
Enter fullscreen mode Exit fullscreen mode

Key Takeaways:

  1. Constructor validation: Ensures the country code is always 3 characters long.
  2. Overriding ToString(): Makes CountryCode display its value properly.
  3. Overriding Equals(): Compares values instead of object references.
  4. Overriding GetHashCode(): Ensures correct dictionary behavior.
  5. Overloading == and !=: Makes equality checks consistent.

2. Using CountryCode as a Dictionary Key

Now, let's define a dictionary that maps country codes to country names.

Defining the Country Class

public class Country
{
    public string Name { get; }
    public CountryCode Code { get; }

    public Country(string name, string code)
    {
        Name = name;
        Code = new CountryCode(code);
    }

    public override string ToString() => $"{Name} ({Code})";
}
Enter fullscreen mode Exit fullscreen mode

This class represents a country with a name and an ISO country code.


3. Implementing a Dictionary with Custom Keys

Now, we will create a dictionary where the key is CountryCode and the value is a Country object.

Building the Dictionary

class Program
{
    static void Main()
    {
        // Dictionary with CountryCode as the key
        Dictionary<CountryCode, Country> countries = new Dictionary<CountryCode, Country>
        {
            { new CountryCode("USA"), new Country("United States", "USA") },
            { new CountryCode("FRA"), new Country("France", "FRA") },
            { new CountryCode("DZA"), new Country("Algeria", "DZA") }
        };

        Console.Write("Enter a country code: ");
        string userInput = Console.ReadLine();

        // Convert user input to CountryCode
        CountryCode inputCode = new CountryCode(userInput);

        // Lookup country
        if (countries.TryGetValue(inputCode, out Country foundCountry))
        {
            Console.WriteLine($"Country found: {foundCountry}");
        }
        else
        {
            Console.WriteLine("Country not found.");
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

4. Testing the Application

Example Run:

Enter a country code: dza
Country found: Algeria (DZA)
Enter fullscreen mode Exit fullscreen mode

This works even though we entered "dza" in lowercase because our Equals method ignores case.

What Happens if We Don't Override Equals and GetHashCode?

If we remove these overrides, the dictionary lookup will fail. The dictionary won't recognize "dza" and "DZA" as the same key, even though they contain the same value.


5. Summary

  1. Why Use a Custom Type as a Key?

    • Provides better control over data validation and formatting.
    • Helps prevent accidental mistakes (e.g., using "USA " instead of "USA").
  2. What Issues Can Occur?

    • Dictionaries compare keys using Equals() and GetHashCode().
    • Reference types compare object instances by default, not values.
  3. How to Fix These Issues?

    • Override Equals() to compare values.
    • Override GetHashCode() to ensure hashing is consistent.
    • Use a case-insensitive StringComparer to handle input variations.
    • Overload == and != to ensure equality checks work properly.

By following these steps, we ensure that our dictionary functions correctly even when using custom types as keys.

Top comments (0)