воскресенье, 31 августа 2014 г.

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

При чтении "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. Надеюсь что вы теперь нет.

Комментариев нет:

Отправить комментарий