Является ли Словарь сломанным или должен ли 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. Он содержит что-то еще (что нигде не существует). Это похоже на тип, который вы определили, - это змея, которая пролила кожу.