Переопределение GetHashCode и Equals в C#
Я проводил собеседование с кандидатом с опытом работы более 6 лет и спросил:
Что такое GetHashCode в C# .net. и где он используется?
Он ответил, что это адрес памяти, где хранится объект. Я сделал паузу, так как большинство его предыдущих ответов были очень впечатляющими, но почему-то этот ответ меня не очень удовлетворил. Опять же, я не был очень уверен в реализации по умолчанию, так как в своих предыдущих проектах я всегда использовал этот метод для переопределения. Итак, я сомневаюсь, что реализация по умолчанию возвращает адрес памяти. Итак, провел небольшое исследование и решил поделиться этим, так как большинство разработчиков здесь борются.
Тот факт, что метод GetHashCode возвращает адрес объекта в управляемой куче, является мифом. Это не может быть правдой из-за его непостоянства. Сборщик мусора при уплотнении бедра сдвигает объекты и тем самым меняет все их адреса.
Начнем с вопроса Зачем нам это нужно?
Метод GetHashCode предоставляет этот хэш-код для алгоритмов, которым требуется быстрая проверка равенства объектов. Хэш-код — это числовое значение, которое используется для вставки и идентификации объекта в коллекции на основе хэша, такой как класс Dictionary
Два одинаковых объекта возвращают одинаковые хеш-коды. Однако обратное неверно: одинаковые хеш-коды не означают равенства объектов, поскольку разные (неравные) объекты могут иметь одинаковые хеш-коды.
И что если хэш-коды двух объектов одинаковы, он использует метод Equals, чтобы проверить, совпадают ли они или нет. Давайте поймем это с помощью приведенного ниже кода —
class Program
{
static void Main(string[] args)
{
var obj1 = new AllowedItem("A-Key", "A-Value", true);
var obj2 = new AllowedItem("A-Key", "A-Value", true);
var dic = new Dictionary<AllowedItem, string>();
dic.Add(obj1, "obj1");
dic.Add(obj2, "obj2");
}
}
public class AllowedItem
{
public string Name { get; private set; }
public string Value { get; private set; }
public bool IsAllowed { get; private set; }
public AllowedItem(string name, string value, bool isAllowed)
{
Name = name;
Value = value;
IsAllowed = isAllowed;
}
public override bool Equals(object obj)
{
if (obj is AllowedItem other)
{
if (Name == other.Name && Value == other.Value && IsAllowed == other.IsAllowed)
return true;
}
return false;
}
public override int GetHashCode()
{
return Name.GetHashCode() ^
Value.GetHashCode() ^
IsAllowed.GetHashCode();
}
}
Мы пытаемся вставить 2 одинаковых объекта в качестве ключа в словарь. Здесь это вызовет исключение ниже, когда в Disctionay вставляется дубликат ключа.
System.ArgumentException: «Элемент с таким же ключом уже добавлен. Ключ: ConsoleApp2.AllowedItem’
Здесь важно отметить, что при добавлении первого элемента в Dictionary вызывается GetHasCode и для объекта сохраняется целое число хэш-кода. Теперь, когда 2-й объект вставлен, он снова вызывает GetHashCode и сравнивается со всеми существующими ключами hasCode, если он соответствует. Он вызывает переопределение Equals, которое также говорит то же самое, поэтому мы получаем ошибку как дублирующийся ключ.
Хэш-код также используется для HashSet
public interface IEqualityComparer<in T>
{
bool Equals(T x, T y);
int GetHashCode(T obj);
}
Теперь, когда мы знаем, почему мы используем GetHashCode, давайте ответим на еще один важный вопрос.
Какова реализация по умолчанию в случае типа значения и ссылочного типа?
В случае значения GetHashCode вернуть (т.е. вернуть это) то же значение, если байтовое представление умещается в 4 байта (целый размер). бывший —
int x = 16;
var intHash = x.GetHashCode(); //Result: 16
bool y = true;
var boolHash = y.GetHashCode(); //Result:1
Ссылочный тип немного сложен. Начиная с .NET 2.0 алгоритм хеширования изменен. Теперь он использует управляемый идентификатор потока, в котором выполняется метод, и метод выглядит так:
inline DWORD GetNewHashCode()
{
// Every thread has its own generator for hash codes so that we won't get into a situation
// where two threads consistently give out the same hash codes.
// Choice of multiplier guarantees period of 2**32 - see Knuth Vol 2 p16 (3.2.1.2 Theorem A)
DWORD multiplier = m_ThreadId*4 + 5;
m_dwHashCodeSeed = m_dwHashCodeSeed*multiplier + 1;
return m_dwHashCodeSeed;
}
Таким образом, у каждого потока есть свой генератор хеш-кодов, благодаря чему мы не можем попасть в ситуацию, когда два потока последовательно генерируют одинаковые хеш-коды.
При первом вызове метода GetHashCode CLR оценивает хэш-код и помещает его в поле SyncBlockIndex объекта. Если SyncBlock связан с объектом, т. е. используется поле SyncBlockIndex, CLR записывает хэш-код в сам SyncBlock. Как только SyncBlock освобождается, CLR копирует хэш-код из его тела в заголовок объекта SyncBlockIndex. Это все.