Является ли Словарь сломанным или должен ли GetHashCode() основываться только на неизменяемых членах?
Когда объект добавляется в класс .NET System.Collections.Generic.Dictionary, хэш-код ключа хранится внутри и используется для более поздние сравнения. Когда хэш-код изменяется после его первоначальной вставки в словарь, он часто становится "недоступным" и может удивить своих пользователей, когда проверка существования, даже используя ту же ссылку, возвращает false (пример кода ниже).
Документация GetHashCode гласит:
Метод GetHashCode для объекта должен последовательно возвращать один и тот же хэш-код, если не существует модификации состояния объекта, которая определяет возвращаемое значение метода Equals объекта.
Таким образом, согласно документам GetHashCode
хеш-код может меняться всякий раз, когда равенство -изменяет состояние, но реализация Dictionary
не поддерживает это.
Является ли реализация текущего словаря .NET сломанной в том, что она неправильно игнорирует хэш-коды? Должен ли GetHashCode()
быть основан только на неизменяемых членах? Или, есть ли что-то еще, чтобы сломать возможную ложную дихотомию?
class Hashable
{
public int PK { get; set; }
public override int GetHashCode()
{
if (PK != 0) return PK.GetHashCode();
return base.GetHashCode();
}
public override bool Equals(object obj)
{
return Equals(obj as Hashable);
}
public virtual bool Equals(Hashable other)
{
if (other == null) return false;
else if (ReferenceEquals(this, other)) return true;
else if (PK != 0 && other.PK != 0) return Equals(PK, other.PK);
return false;
}
public override string ToString()
{
return string.Format("Hashable {0}", PK);
}
}
class Test
{
static void Main(string[] args)
{
var dict = new Dictionary<Hashable, bool>();
var h = new Hashable();
dict.Add(h, true);
h.PK = 42;
if (!dict.ContainsKey(h)) // returns false, despite same reference
dict.Add(h, false);
}
}
Ответы
Ответ 1
Нет, вы просто не должны мутировать ключ (материальным способом) после вставки его в словарь. Это по дизайну и способ, которым каждый хеш-стол, который я когда-либо использовал, работает. Документы даже указывают это:
Пока объект используется как ключ в Dictionary<TKey, TValue>
, он не должен каким-либо образом изменять его хеш-значение. Каждый ключ в Dictionary<TKey, TValue>
должен быть уникальным в соответствии со сравнением словаря. Ключ не может быть нулевым, но может быть значение, если тип значения TValue является ссылочным типом.
Так что это только удивит пользователей, которые не читают документацию:)
Ответ 2
Чтобы добавить к Jon ответ, я бы просто добавил акцент на определенную часть документов, которые вы цитировали:
Метод GetHashCode для объекта должен последовательно возвращать один и тот же хеш кода, если нет изменение состояния объекта , который определяет возвращаемое значение метода Equals объекта.
Теперь, вы нарушили правила. Вы изменили PK
, что не влияет на результат Equals
(потому что у вас есть проверка ReferenceEquals
там), но результат вашего GetHashCode
меняется. Так что простой ответ.
Принимая более концептуальный подход, я думаю, вы можете посмотреть на это следующим образом: если вы переопределили поведение Equals
и GetHashCode
для вашего типа, то вы взяли на себя ответственность за концепцию того, что это означает для один экземпляр этого типа должен быть равен другому. И на самом деле вы определили его таким образом, что объект Hashable
можно изменить на нечто совершенно другое; то есть то, что больше не используется так же, как раньше (поскольку его хэш-код был изменен).
С этой точки зрения, после выполнения dict.Add(h, true)
, а затем вы измените h.PK
, словарь не содержит объект, на который ссылается h
. Он содержит что-то еще (что нигде не существует). Это похоже на тип, который вы определили, - это змея, которая пролила кожу.