В FxCop есть такое правило Override GetHashCode on overriding Equals, переопределяйте GetHashCode переопределяя Equals. Так вот с этим правилом связан подводный камень. В Rule Description там написано об этом но как на мой взгляд не совсем ясно.
Это связано с принципом работы HashTable и Dictionary в .NET, и для того чтобы сравнение производилось верно при переопределении Equals необходимо обязательно переопределить GetHashCode в зависимости от тех данных которые учувствуют в сравнении. Иначе Equals просто не будет вызван, хотя многие разработчики ожидают что он будет вызыватся всегда. Equals вызывается только тогда когда GetHashCode возвращает одинаковые значения, что как было сказано выше, связано с принципом работы словарей и хеш-таблиц, для того чтобы разрешить коллизии в хеш-таблице.
С другой стороны на такие неправильные выводы вполне возможно наталкивает поведение System.Object метод GetHashCode которого возвращает значения, которые не зависят от данных хранящихся в объекте и возвращает разные хеш-коды для одинаковых объектов.
И хотя такое поведение казалось бы, мелочь, но часто многие в свое время натыкаются на этот подводный камень.
Эту распространенную ошибку можно рассмотреть на примере:
Есть какой-тоCustomType, по сути в нем два поля Name и Age:
Есть два списка:
Найдем их пересечение через linq оператор Intersect. Так как мы работаем с составным не примитивным типом необходимо будет указать IEqualityComparer для этого типа:
В результате ожидаем получить в списке intersect два значения: reno- 10 и toyota – 4. Но мы их не получим, потому что GetHashCode в CustomType имплементирован не верно. Так как у каждого объекта в целом разные хеш коды то метод Equals даже не будет вызываться.
Правильной имплементацией в данном случае будет вариант:
а можно попробовать и такой вариант, он конечно не верный но пересечение отработает правильно, так как Equals будет вызываться всегда.
Следовательно при имплементации Equals обязательно необходимо имплементировать GetHashCode причем с учетом тех данных что принимают участие в сравнение в Equals. В противном случае Equals может даже и не вызываться.
Так как много людей попадается на этот подводный камень, я и решил написать об этом.
Из комментов plsc_Rover:
Важно также не перепутать имплементацию GetHashCode из IEqualityComparer, а не переопределение GetHashCode объекта, т.к. во втором случае словарь будет работать неправильно.
Это связано с принципом работы HashTable и Dictionary в .NET, и для того чтобы сравнение производилось верно при переопределении Equals необходимо обязательно переопределить GetHashCode в зависимости от тех данных которые учувствуют в сравнении. Иначе Equals просто не будет вызван, хотя многие разработчики ожидают что он будет вызыватся всегда. Equals вызывается только тогда когда GetHashCode возвращает одинаковые значения, что как было сказано выше, связано с принципом работы словарей и хеш-таблиц, для того чтобы разрешить коллизии в хеш-таблице.
С другой стороны на такие неправильные выводы вполне возможно наталкивает поведение System.Object метод GetHashCode которого возвращает значения, которые не зависят от данных хранящихся в объекте и возвращает разные хеш-коды для одинаковых объектов.
И хотя такое поведение казалось бы, мелочь, но часто многие в свое время натыкаются на этот подводный камень.
Эту распространенную ошибку можно рассмотреть на примере:
Есть какой-тоCustomType, по сути в нем два поля Name и Age:
public class NamesComparer : IEqualityComparer<CustomType>
{
#region IEqualityComparer<CustomType> Members
public bool Equals(CustomType x, CustomType y)
{
return string.Equals(x.Name, y.Name);
}
public int GetHashCode(CustomType obj)
{
// распространенная ошибка, неверно
return obj.GetHashCode();
}
#endregion
}
[DebuggerDisplay("Name: {Name}, age: {Age}")]
public class CustomType : IEqualityComparer<CustomType>
{
public CustomType()
{
}
public CustomType(string name, int age)
{
Name = name;
Age = age;
}
public int Age { get; set; }
public string Name { get; set; }
public override bool Equals(CustomType x, CustomType y)
{
return string.Equals(x.Name, y.Name);
}
public override int GetHashCode(CustomType obj)
{
// распространенная ошибка, неверно
return obj.GetHashCode();
}
}
Есть два списка:
CustomType[] customTypeShortList = new[] {
new CustomType("reno", 1),
new CustomType("toyota", 3) };
CustomType[] customTypeLongList = new[] {
new CustomType("audi", 5),
new CustomType("opel", 7),
new CustomType("reno", 10),
new CustomType("subaru", 5),
new CustomType("toyota", 4),
new CustomType("nissan", 3)};
Найдем их пересечение через linq оператор Intersect. Так как мы работаем с составным не примитивным типом необходимо будет указать IEqualityComparer для этого типа:
IEnumerable<CustomType> intersect = customTypeLongList
.Intersect(customTypeShortList, new NamesComparer());
В результате ожидаем получить в списке intersect два значения: reno- 10 и toyota – 4. Но мы их не получим, потому что GetHashCode в CustomType имплементирован не верно. Так как у каждого объекта в целом разные хеш коды то метод Equals даже не будет вызываться.
Правильной имплементацией в данном случае будет вариант:
public int GetHashCode(CustomType obj)
{
return obj.Name.GetHashCode();
}
а можно попробовать и такой вариант, он конечно не верный но пересечение отработает правильно, так как Equals будет вызываться всегда.
public int GetHashCode(CustomType obj)
{
// также неверно, из-за того что это ударит по производительности
return string.Empty.GetHashCode();
}
Следовательно при имплементации Equals обязательно необходимо имплементировать GetHashCode причем с учетом тех данных что принимают участие в сравнение в Equals. В противном случае Equals может даже и не вызываться.
Так как много людей попадается на этот подводный камень, я и решил написать об этом.
Из комментов plsc_Rover:
Важно также не перепутать имплементацию GetHashCode из IEqualityComparer, а не переопределение GetHashCode объекта, т.к. во втором случае словарь будет работать неправильно.