Что нужно сделать, чтобы использовать свой класс в качестве ключа в словаре?
Комментарии (3)
Ответ сгенерирован нейросетью и может содержать ошибки
Использование пользовательского класса в качестве ключа в словаре C#
Чтобы использовать собственный класс в качестве ключа в словаре (Dictionary<TKey, TValue>), необходимо корректно реализовать два критически важных метода: GetHashCode() и Equals(). Словарь в C# для быстрого поиска и доступа к элементам использует хеш-таблицу, которая опирается на эти методы.
Основные требования
- Переопределение метода
GetHashCode() - Переопределение метода
Equals() - Дополнительные рекомендации: реализация
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.