При чтении «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'ом и я осознал, что генерируемое значение не будет постоянным на протяжении жизни объекта при его изменениях.
Решил набросать примерчик для того, чтобы осознать масштаб проблемы, итак, предположим, у нас есть класс отражающий человека, а для уникальной идентификации будем использовать его номер СНИЛС:
Перегруженные методы сгенерированы ReSharper. На первый взгляд все хорошо. Поля используемые в проверке на равенство используются для генерации хэша. Равные объекты будут иметь равные хэш-коды. Вроде бы все замечательно.
Добавим некоей бизнес-логики:
И видим сообщение «True».
А что если в какой-то момент я решил поменять свой СНИЛС
И видим сообщение «False».
А что произошло?
Внутренне HashSet состоит из некоторого количества корзин. Корзина для объекта выбирается на основе значения, возвращаемого GetHashCode. Как только мы изменили номер СНИЛС, изменилось и значение, возвращаемое GetHashCode. HashSet, в свою очередь, на основе хэш-кода выбрал другую корзину для просмотра и, естественно, в этой корзине нашего объекта нет(с очень малой вероятностью он конечно мог там оказаться). В других корзинах HashSet смотреть не будет, т.к. равные объекты должны иметь равные значения GetHashCode. Вот и все дела. Объект не будет найден.
А как оно вообще работало?
Если вы не переопределяли Equals & GetHashCode, то у вашего объекта будет постоянный на протяжении жизни объекта GetHashCode независимо от изменений, сделанных вами в полях объекта. Но, в случае, если вы перегружаете эти методы, то необходимо в алгоритме генерации хэша использовать только неизменяемые поля, либо не изменять поля, использующиеся в алгоритме генерации, либо придумать свой костыль(как вариант, можно использовать подход реализованный в стандартной реализации класса Object).
Отсюда мораль:
Значение хэш-кода должно быть постоянным на протяжении жизни объекта, или вы должны четко осознавать что вы делаете, если в вашем случае оно может изменяться.
PS. Я понимаю, что тут описан далеко не Rocket Science. Все здесь написанное очевидно и вытекает из требований Майкрософт к упомянутым методам. Есть хорошее описание от Липперта тут тем не менее, с ходу, я напоролся и не поверил что HashSet вернет False. Надеюсь что вы теперь нет.
«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. Надеюсь что вы теперь нет.