Есть ли какие-либо ограничения для ключа в словаре C#?

«Есть ли какие-либо ограничения для ключа в словаре C#?» — вопрос из категории C# Core, который задают на 25% собеседований C# Разработчик. Ниже — развёрнутый ответ с разбором ключевых моментов.

Ответ

Да, для корректной работы Dictionary<TKey, TValue> к типу ключа предъявляются строгие требования, вытекающие из его внутренней хеш-табличной реализации.

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

  1. Уникальность: Ключи в рамках одного словаря должны быть уникальными. Попытка добавить дубликат выбросит ArgumentException.
  2. Консистентность GetHashCode() и Equals(): Это самое важное требование.
    • GetHashCode() должен возвращать одно и то же значение для одного и того же объекта на протяжении всего времени его жизни (пока объект используется как ключ).
    • Equals() должен быть логически согласован с GetHashCode(): если Equals() возвращает true для двух объектов, то и GetHashCode() для них должен возвращать одинаковое значение. Обратное не обязательно.
  3. Неизменяемость (рекомендация): Ключ должен быть неизменяемым по значению после добавления в словарь. Если свойство, участвующее в GetHashCode()/Equals(), изменить, ключ "потеряется" в словаре, так как будет искаться по новому хеш-коду.

Пример проблем с изменяемым ключом:

public class MutableKey
{
    public int Id { get; set; }

    public override int GetHashCode() => Id.GetHashCode();
    public override bool Equals(object obj) => obj is MutableKey other && Id == other.Id;
}

var dict = new Dictionary<MutableKey, string>();
var key = new MutableKey { Id = 1 };
dict[key] = "Value 1";

// Меняем ключ ПОСЛЕ добавления в словарь — КАТАСТРОФА!
key.Id = 2;

// Попытка найти значение по старому ключу (Id=1) не сработает
bool found = dict.TryGetValue(new MutableKey { Id = 1 }, out var value); // found = false

// Попытка найти по новому ключу (Id=2) тоже, скорее всего, не сработает,
// потому что объект лежит в другой "корзине" хеш-таблицы.

Лучшие практики для ключей:

  • Используйте встроенные неизменяемые типы: string, int, Guid, DateTime и т.д.
  • Используйте record типы (начиная с C# 9), которые по умолчанию предоставляют корректную реализацию GetHashCode() и Equals() на основе значений.
  • Если создаёте свой класс для ключа, сделайте его неизменяемым (только для чтения свойства, init-аксессоры) и всегда корректно переопределяйте GetHashCode() и Equals().
// Идеальный ключ с использованием record (неизменяемый и корректный)
public record PersonKey(string Name, int BirthYear);

var dictionary = new Dictionary<PersonKey, string>();
dictionary[new PersonKey("Alice", 1990)] = "Developer";
// Безопасно и надёжно.