Почему Equals() не вызывается для всех объектов при добавлении в коллекцию
У меня есть тип, который я использую как ключ в IDictionary. Тип выглядит следующим образом:
public class Employee
{
public string Name { get; set; }
public int ID { get; set; }
public override bool Equals(object obj)
{
Employee emp = obj as Employee;
if (emp != null)
return emp.Name.Equals(this.Name);
return false;
}
public override int GetHashCode()
{
return this.Name.GetHashCode();
}
}
Теперь я создал словарь в качестве следующего в моей статье следующим образом
IDictionary<Employee, int> empCollection = new Dictionary<Employee, int>();
Employee emp1 = new Employee() { Name = "abhi", ID = 1 };
Employee emp2 = new Employee() { Name = "vikram", ID = 2 };
Employee emp3 = new Employee() { Name = "vikram", ID = 3 };
empCollection.Add(emp1, 1);
empCollection.Add(emp2, 2);
empCollection.Add(emp3, 3);
Теперь при отладке я узнал, что когда emp1 добавлен в коллекцию, только метод GetHashCode вызывается типа ключа, после чего, когда emp2 добавляется в коллекцию, метод GetHashCode снова вызывается, но в случае emp3 оба GetHashCode и методы Equals.
Может быть, это выглядит слишком наивно, задавая этот вопрос, но почему метод Equals не вызывается, когда объект eqImp2 добавляется в коллекцию. Что происходит внутри. Пожалуйста, объясните.
Ответы
Ответ 1
Словарь и все другие подобные контейнеры используют хэш-код в качестве быстрой и грязной проверки: разные хэш-коды означают, что два объекта определенно не равны; идентичные хэш-коды ничего не значат. Документация GetHashCode
определяет это поведение, говоря
Если два объекта сравниваются как равные, метод GetHashCode для каждого объект должен вернуть одно и то же значение. Однако, если два объекта не сравнить как равные, методы GetHashCode для двух объектов не должны возвращать разные значения.
Ваши emp1
и emp2
генерируют разные хэш-коды, поэтому словарю не нужно запускать Equals
; он уже знает, что они не равны. С другой стороны, emp2
и emp3
генерируют один и тот же хэш-код, поэтому словарь должен вызывать Equals
, чтобы определить, действительно ли они равны, или же идентичный хэш-код был просто результатом случайности.
Ответ 2
В вашем примере GetHashCode
просматривается хэш-код имени. emp3 имеет то же имя, что и emp2 ( "викрам" ). Они равны с хэш-кодом, поэтому он выглядит с помощью Equals
.
Ответ 3
emp2 и emp3 имеют один и тот же ключ. Это вызовет "ключевое столкновение" в словаре. Сначала он назывался GetHashCode() и определил, что хэш-коды были одинаковыми. Затем он гарантирует, что они одинаковы, вызывая Equals(). Код словаря:
int num = this.comparer.GetHashCode(key) & 2147483647;
...
if (this.entries[i].hashCode == num && this.comparer.Equals(this.entries[i].key, key))
Очевидно, что если хэш-коды не совпадают, ему никогда не нужно вызывать Equals.
Вы должны получить такой инструмент, как ILSpy, а затем вы можете посмотреть код и найти ответ самостоятельно.
Ответ 4
Если вы продолжите этот эксперимент, вы увидите некоторое поведение, специфичное для реализации Dictionary<TKey, TValue>
, и некоторое поведение, которое требуется из-за того, как вы реализовали GetHashCode
.
Во-первых, важно понять роль GetHashCode
и Equals
при сравнении объектов для равенства. Дополнительная информация доступна на этом вопросе, но я повторю основные правила здесь:
- Метод
Equals
устанавливает точно, какие объекты равны, а какие - нет. Все необходимые проверки должны быть выполнены в этом методе для окончательного определения перед возвратом.
- Хэш-код - это значение, вычисленное из значения вашего объекта. Обычно он намного меньше исходного объекта (в нашем случае хеш-код является 4-байтным целым) и не обязательно уникальным. Однако гораздо быстрее вычислять и сравнивать друг с другом, чем сами исходные объекты.
- Когда хэш-коды не обязательно должны быть уникальными, разные хеш-коды указывают разные объекты (т.е.
Equals
обязательно вернет false), но одинаковые хеш-коды ничего не означают (т.е. Equals
может вернуть true или false).
Коллекции, которые связывают значения с ключевым объектом (например, IDictionary<TKey, TValue>
в .NET или Map<K, V>
в Java), используют хэш-коды для повышения эффективности реализации. Однако, поскольку документация для Object.GetHashCode
специально не требует, чтобы результаты были уникальными, эти коллекции не могут полагаться только на хеш-коды для правильного функциональность. Если два объекта имеют один и тот же хэш-код, только вызов Equals
может различать их. В этом случае рассматривается случай, описанный для вставки emp3
: метод [IDictionary<TKey, TValue>.Add
] нужно выбросить ArgumentException
, если вы пытаетесь вставить одно и то же значение, и только вызов Equals
может определить, совпадает ли новый ключ emp3
с ранее вставленным emp3
.
Дополнительные характеристики реализации
Реализация конкретной коллекции может привести к большему количеству вызовов GetHashCode
, чем вы ожидаете. Например, при изменении размера внутреннего хранилища хеш-таблицы binary- или B-tree может только вызывать GetHashCode
один раз (если результаты кэшируются в древовидной структуре) или, возможно, потребуется вызвать GetHashCode
для нескольких объектов во время каждой операции вставки или поиска (если результаты не кэшируются).
Иногда реализация хеш-таблицы должна вызывать GetHashCode
для нескольких объектов или, возможно, даже Equals
для объектов с разными хэш-кодами из-за того, что они используют арифметику по модулю, чтобы поместить ключи в "ведра". Специфические характеристики этого варьируются от одной реализации к следующей.
Ответ 5
Это потому, что GetHashCode - это ярлык.
Сначала С# вызовет GetHashCode, который должен быстро выполняться.
Если два объекта имеют разные HashCodes, тогда нет необходимости вызывать, предположительно, более дорогой метод Equals.
Только если они имеют один и тот же HashCode, тогда он вызывается Equals. Это потому, что HashCode не гарантированно будет уникальным