Некоторые тонкости GetHashCode

в 15:11, , рубрики: .net

При чтении «Framework Design Guidelines: Conventions, Idioms, and Patterns for Reusable .NET Libraries» натолкнулся на такую фразу:

«Ensure that GetHashCode returns exactly the same value regardless of any changes that are made to the object».

Хм… подумал я, о чем это они? Перед глазами всплыла стандартная реализация, которая генерируется ReSharper'ом и я осознал, что генерируемое значение не будет постоянным на протяжении жизни объекта при его изменениях.

Решил набросать примерчик для того, чтобы осознать масштаб проблемы, итак, предположим, у нас есть класс отражающий человека, а для уникальной идентификации будем использовать его номер СНИЛС:

public class Employee
    {
        public string FirstName { get; set; }
        public string SecondName { get; set; }
        public string Snils { get; set; }

        protected bool Equals(Employee other)
        {
            return string.Equals(Snils, other.Snils);
        }

        public override bool Equals(object obj)
        {
            if (ReferenceEquals(null, obj)) return false;
            if (ReferenceEquals(this, obj)) return true;
            if (obj.GetType() != this.GetType()) return false;
            return Equals((Employee) obj);
        }

        public override int GetHashCode()
        {
            return (Snils != null ? Snils.GetHashCode() : 0);
        }
    }

Перегруженные методы сгенерированы ReSharper. На первый взгляд все хорошо. Поля используемые в проверке на равенство используются для генерации хэша. Равные объекты будут иметь равные хэш-коды. Вроде бы все замечательно.
Добавим некоей бизнес-логики:

var employees = new HashSet<Employee>();
var employee = new Employee()
                           {
                               FirstName = "Sergei",
                               SecondName = "Popov",
                               Snils = "123456"
                           };
employees.Add(employee);
Console.WriteLine(employees.Contains(employee));

И видим сообщение «True».
А что если в какой-то момент я решил поменять свой СНИЛС

var employees = new HashSet<Employee>()
var employee = new Employee()
                           {
                               FirstName = "Sergei",
                               SecondName = "Popov",
                               Snils = "123456"
                           };
employees.Add(employee);

// решил я поменять свой СНИЛС
employee.Snils = "654321";
Console.WriteLine(employees.Contains(employee));

И видим сообщение «False».

А что произошло?
Внутренне HashSet состоит из некоторого количества корзин. Корзина для объекта выбирается на основе значения, возвращаемого GetHashCode. Как только мы изменили номер СНИЛС, изменилось и значение, возвращаемое GetHashCode. HashSet, в свою очередь, на основе хэш-кода выбрал другую корзину для просмотра и, естественно, в этой корзине нашего объекта нет(с очень малой вероятностью он конечно мог там оказаться). В других корзинах HashSet смотреть не будет, т.к. равные объекты должны иметь равные значения GetHashCode. Вот и все дела. Объект не будет найден.

А как оно вообще работало?
Если вы не переопределяли Equals & GetHashCode, то у вашего объекта будет постоянный на протяжении жизни объекта GetHashCode независимо от изменений, сделанных вами в полях объекта. Но, в случае, если вы перегружаете эти методы, то необходимо в алгоритме генерации хэша использовать только неизменяемые поля, либо не изменять поля, использующиеся в алгоритме генерации, либо придумать свой костыль(как вариант, можно использовать подход реализованный в стандартной реализации класса Object).

Отсюда мораль:
Значение хэш-кода должно быть постоянным на протяжении жизни объекта, или вы должны четко осознавать что вы делаете, если в вашем случае оно может изменяться.

PS. Я понимаю, что тут описан далеко не Rocket Science. Все здесь написанное очевидно и вытекает из требований Майкрософт к упомянутым методам. Есть хорошее описание от Липперта тут тем не менее, с ходу, я напоролся и не поверил что HashSet вернет False. Надеюсь что вы теперь нет.

Автор: f0bos

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js