← Назад к вопросам

Что нужно сделать, чтобы использовать свой класс в качестве ключа в словаре?

2.0 Middle🔥 133 комментариев
#Основы C# и .NET

Комментарии (3)

🐱
deepseek-v3.2PrepBro AI6 апр. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Использование пользовательского класса в качестве ключа в словаре C#

Чтобы использовать собственный класс в качестве ключа в словаре (Dictionary<TKey, TValue>), необходимо корректно реализовать два критически важных метода: GetHashCode() и Equals(). Словарь в C# для быстрого поиска и доступа к элементам использует хеш-таблицу, которая опирается на эти методы.

Основные требования

  1. Переопределение метода GetHashCode()
  2. Переопределение метода Equals()
  3. Дополнительные рекомендации: реализация IEquatable<T> и обеспечение неизменяемости ключа.

Подробное объяснение

1. Переопределение GetHashCode()

Метод GetHashCode() возвращает целочисленный хеш-код объекта. Для корректной работы словаря два равных объекта (по логике Equals) должны возвращать одинаковый хеш-код. Обратное не обязательно: разные объекты могут иметь одинаковый хеш-код (коллизия), но это снижает производительность.

Важные правила:

  • Хеш-код должен быть стабильным: если объект не изменяется, хеш-код должен оставаться тем же.
  • Вычисление должно быть быстрым.
  • Хеш-коды должны быть как можно более уникальными, равномерно распределяясь по диапазону int.

Пример реализации:

public class PersonKey
{
    public string FirstName { get; }
    public string LastName { get; }
    public int BirthYear { get; }

    public PersonKey(string firstName, string lastName, int birthYear)
    {
        FirstName = firstName;
        LastName = lastName;
        BirthYear = birthYear;
    }

    public override int GetHashCode()
    {
        // Используем HashCode.Combine (доступен с C# 8.0/.NET Core 2.1+)
        // Это самый правильный и производительный способ
        return HashCode.Combine(FirstName, LastName, BirthYear);
    }
}

Для версий до C# 8.0 можно использовать вычисление вручную:

public override int GetHashCode()
{
    int hash = 17;
    hash = hash * 23 + (FirstName?.GetHashCode() ?? 0);
    hash = hash * 23 + (LastName?.GetHashCode() ?? 0);
    hash = hash * 23 + BirthYear.GetHashCode();
    return hash;
}

2. Переопределение Equals(object obj)

Метод определяет логическое равенство объектов. Без корректной реализации Equals словарь может считать два идентичных по данным объекта разными ключами.

public override bool Equals(object obj)
{
    // Проверка на null и сравнение типов
    if (obj is null || GetType() != obj.GetType())
        return false;

    // Приведение типа и покомпонентное сравнение
    var other = (PersonKey)obj;
    return FirstName == other.FirstName &&
           LastName == other.LastName &&
           BirthYear == other.BirthYear;
}

3. Реализация интерфейса IEquatable<T> (рекомендуется)

Это улучшает производительность за счёт избежания упаковки (boxing) и проверок типов.

public class PersonKey : IEquatable<PersonKey>
{
    // ... свойства и конструктор ...

    public override bool Equals(object obj) => Equals(obj as PersonKey);
    
    public bool Equals(PersonKey other)
    {
        if (other is null) return false;
        // Оптимизация для сравнения ссылок на один объект
        if (ReferenceEquals(this, other)) return true;
        
        return FirstName == other.FirstName &&
               LastName == other.LastName &&
               BirthYear == other.BirthYear;
    }

    public override int GetHashCode()
    {
        return HashCode.Combine(FirstName, LastName, BirthYear);
    }
}

Полный пример класса-ключа

public class PersonKey : IEquatable<PersonKey>
{
    public string FirstName { get; }
    public string LastName { get; }
    public int BirthYear { get; }

    public PersonKey(string firstName, string lastName, int birthYear)
    {
        FirstName = firstName;
        LastName = lastName;
        BirthYear = birthYear;
    }

    public bool Equals(PersonKey other)
    {
        if (other is null) return false;
        if (ReferenceEquals(this, other)) return true;
        
        return FirstName == other.FirstName &&
               LastName == other.LastName &&
               BirthYear == other.BirthYear;
    }

    public override bool Equals(object obj) => Equals(obj as PersonKey);
    
    public override int GetHashCode() => HashCode.Combine(FirstName, LastName, BirthYear);
    
    // Переопределение ToString() для удобства отладки
    public override string ToString() => $"{FirstName} {LastName} ({BirthYear})";
}

Пример использования в словаре

var dict = new Dictionary<PersonKey, string>();

var key1 = new PersonKey("Иван", "Петров", 1990);
var key2 = new PersonKey("Иван", "Петров", 1990); // Другой объект, но те же данные

dict[key1] = "Менеджер проектов";
Console.WriteLine(dict.ContainsKey(key2)); // Вывод: True
Console.WriteLine(dict[key2]); // Вывод: "Менеджер проектов"

Критически важное правило: Неизменяемость ключей

Если объект-ключ изменяется после добавления в словарь, его хеш-код тоже может измениться. Это приводит к невозможности найти этот объект в словаре, так как поиск будет выполняться по новому хеш-коду в неправильной корзине хеш-таблицы. Объект-ключ становится "потерянным" в словаре, вызывая утечки памяти и некорректное поведение.

Решение: Делайте ключи неизменяемыми (immutable):

  • Объявляйте свойства только с getter'ами ({ get; })
  • Инициализируйте все значения только в конструкторе
  • Используйте записи (record), которые по умолчанию предоставляют правильную реализацию равенства и неизменяемость

Современный подход с использованием record (C# 9.0+)

Начиная с C# 9.0, можно использовать типы record, которые автоматически генерируют корректные реализации GetHashCode() и Equals() на основе всех свойств:

public record PersonKey(string FirstName, string LastName, int BirthYear);

// Использование
var dict = new Dictionary<PersonKey, string>();
dict[new PersonKey("Иван", "Петров", 1990)] = "Менеджер";
// Все работает корректно без ручного переопределения методов

Вывод: Для использования пользовательского класса в качестве ключа словаря необходимо гарантировать корректную работу хеш-таблицы через правильную реализацию GetHashCode() и Equals(), а также обеспечить неизменяемость ключевых свойств после создания объекта. Современный и лаконичный способ — использование типов record.

Что нужно сделать, чтобы использовать свой класс в качестве ключа в словаре? | PrepBro