При чтении «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