Учитывая следующий класс
public class Foo
{
public int FooId { get; set; }
public string FooName { get; set; }
public override bool Equals(object obj)
{
Foo fooItem = obj as Foo;
return fooItem.FooId == this.FooId;
}
public override int GetHashCode()
{
// Which is preferred?
return base.GetHashCode();
//return this.FooId.GetHashCode();
}
}
Я переопределил метод Equals
, потому что Foo
представляет строку для таблицы Foo
. Какой предпочтительный метод для переопределения GetHashCode
?
Почему важно переопределить GetHashCode
?
Да, важно, чтобы ваш элемент использовался в качестве словаря словаря или HashSet<T>
и т.д., поскольку он используется (при отсутствии пользовательского IEqualityComparer<T>
) для группировки элементов в ведра. Если хэш-код для двух элементов не соответствует, они никогда не могут считаться равными (Equals
просто никогда не будет вызываться).
Метод GetHashCode()
должен отражать логику Equals
; Правила:
Equals(...) == true
), тогда они должны возвратить одно и то же значение для GetHashCode()
GetHashCode()
равно, для них не обязательно быть одинаковым; это столкновение, и Equals
вызывается, чтобы убедиться, что это реальное равенство или нет.В этом случае выглядит "return FooId;
" - подходящая реализация GetHashCode()
. Если вы тестируете несколько свойств, обычно их объединяют с использованием кода, как показано ниже, для уменьшения диагональных коллизий (т.е. Для new Foo(3,5)
используется другой хеш-код для new Foo(5,3)
):
int hash = 13;
hash = (hash * 7) + field1.GetHashCode();
hash = (hash * 7) + field2.GetHashCode();
...
return hash;
Oh - для удобства вы также можете рассмотреть возможность предоставления операторов ==
и !=
при переопределении Equals
и GetHashCode
.
Демонстрация того, что происходит, когда вы ошибаетесь, здесь.
На самом деле очень сложно реализовать GetHashCode()
правильно, потому что, помимо уже упомянутых правил Marc, хэш-код не должен меняться в течение всего жизненного цикла объекта. Поэтому поля, которые используются для вычисления хэш-кода, должны быть неизменными.
Наконец-то я нашел решение этой проблемы, когда я работал с NHibernate. Мой подход заключается в вычислении хеш-кода из идентификатора объекта. Идентификатор может быть установлен только с помощью конструктора, поэтому, если вы хотите изменить ID, что очень маловероятно, вам нужно создать новый объект с новым идентификатором и, следовательно, новый хэш-код. Этот подход лучше всего работает с GUID, потому что вы можете предоставить конструктор без параметров, который произвольно генерирует идентификатор.
Отвергая Equals, вы в основном заявляете, что именно тот, кто лучше знает, как сравнивать два экземпляра данного типа, так что вы, вероятно, будете лучшим кандидатом для предоставления лучшего хеш-кода.
Это пример того, как ReSharper пишет для вас функцию GetHashCode():
public override int GetHashCode()
{
unchecked
{
var result = 0;
result = (result * 397) ^ m_someVar1;
result = (result * 397) ^ m_someVar2;
result = (result * 397) ^ m_someVar3;
result = (result * 397) ^ m_someVar4;
return result;
}
}
Как вы можете видеть, он просто пытается угадать хороший хеш-код, основанный на всех полях в классе, но так как вы знаете свой домен или диапазон значений, вы все равно можете обеспечить лучший.
Пожалуйста, не забудьте проверить параметр obj против null
при переопределении Equals()
.
А также сравните тип.
public override bool Equals(object obj)
{
if (obj == null || GetType() != obj.GetType())
return false;
Foo fooItem = obj as Foo;
return fooItem.FooId == this.FooId;
}
Причиной этого является: Equals
должен возвращать значение false при сравнении с null
. См. Также http://msdn.microsoft.com/en-us/library/bsc2ak47.aspx
Как насчет:
public override int GetHashCode()
{
return string.Format("{0}_{1}_{2}", prop1, prop2, prop3).GetHashCode();
}
Предполагая, что производительность не является проблемой:)
Это связано с тем, что фреймворк требует, чтобы два объекта, которые являются одинаковыми, должны иметь один и тот же хэш-код. Если вы переопределите метод equals, чтобы выполнить специальное сравнение двух объектов, и оба объекта считаются одинаковыми с помощью метода, то хэш-код двух объектов также должен быть одинаковым. (Словари и Hashtables полагаются на этот принцип).
Просто добавьте ответы выше:
Если вы не переопределяете Equals, то по умолчанию используется сравнение ссылок объектов. То же самое относится к hashcode - приведение по умолчанию обычно основано на адресе памяти ссылки. Поскольку вы переопределили Equals, это означает, что правильное поведение заключается в сравнении того, что вы внедрили в Equals, а не в ссылках, поэтому вы должны сделать то же самое для hashcode.
Клиенты вашего класса ожидают, что хэш-код будет иметь схожую логику с методом equals, например, методы linq, которые используют IEqualityComparer, сначала сравнивают хэш-коды и только если они равны, они будут сравнивать метод Equals(), который может быть более дорогостоящим для запуска, если бы мы не реализовали hashcode, у равного объекта, вероятно, будут разные хэш-коды (потому что они имеют другой адрес памяти) и будут ошибочно определены как не равные (Equals() даже не попадет).
Кроме того, кроме проблемы, что вы не сможете найти свой объект, если вы использовали его в словаре (потому что он был вставлен одним хэш-кодом, и когда вы его ищете, хэш-код по умолчанию, вероятно, будет другим, и снова Equals() даже не будет вызван, как объясняет Марк Гравелл в своем ответе, вы также вводите нарушение словаря или концепции hashset, которые не должны допускать идентичные ключи - вы уже заявили, что эти объекты по сути являются одинаковыми, когда вы перегружаете Equals, поэтому вы не хотите, чтобы оба они были разными ключами в структуре данных, которые предполагают наличие уникального ключа. Но поскольку у них есть другой хэш-код, "тот же" ключ будет вставлен как другой.
У нас есть две проблемы.
Вы не можете обеспечить разумный GetHashCode()
, если какое-либо поле в
объект может быть изменен. Также часто объект никогда не будет использоваться в
которая зависит от GetHashCode()
. Таким образом, стоимость
реализация GetHashCode()
часто не стоит того, или это не
возможно.
Если кто-то помещает ваш объект в коллекцию, которая вызывает
GetHashCode()
, и вы отменили Equals()
, не делая
GetHashCode()
вести себя правильно, этот человек может проводить дни
устраняя проблему.
Поэтому по умолчанию я делаю.
public class Foo
{
public int FooId { get; set; }
public string FooName { get; set; }
public override bool Equals(object obj)
{
Foo fooItem = obj as Foo;
return fooItem.FooId == this.FooId;
}
public override int GetHashCode()
{
// Some comment to explain if there is a real problem with providing GetHashCode()
// or if I just don't see a need for it for the given class
throw new Exception("Sorry I don't know what GetHashCode should do for this class");
}
}
GetHashCode
, чтобы любые два равных объекта возвращали один и тот же хэш-код; return 24601;
и return 8675309;
оба будут действительными реализациями GetHashCode
. Производительность Dictionary
будет приличной только тогда, когда количество элементов невелико, и станет очень плохой, если количество элементов увеличится, но в любом случае будет работать правильно.
Хэш-код используется для коллекций на основе хэша, таких как Dictionary, Hashtable, HashSet и т.д. Цель этого кода - очень быстро предварительно сортировать конкретный объект, помещая его в определенную группу (ведро). Эта предварительная сортировка очень помогает в поиске этого объекта, когда вам нужно вернуть его из коллекции хешей, потому что код должен искать ваш объект только в одном ведро, а не во всех его объектах. Лучшее распределение хэш-кодов (лучшая уникальность) - более быстрое извлечение. В идеальной ситуации, когда каждый объект имеет уникальный хеш-код, поиск его является операцией O (1). В большинстве случаев он приближается к O (1).
Это не обязательно важно; это зависит от размера ваших коллекций и требований к производительности, и будет ли ваш класс использоваться в библиотеке, где вы не можете знать требования к производительности. Я часто знаю, что размеры моей коллекции не очень большие, и мое время более ценно, чем несколько микросекунд производительности, достигнутых благодаря созданию идеального хеш-кода; поэтому (чтобы избавиться от раздражающего предупреждения компилятором) я просто использую:
public override int GetHashCode()
{
return base.GetHashCode();
}
(Конечно, я мог бы использовать #pragma, чтобы выключить предупреждение, но я предпочитаю этот способ.)
Если вы находитесь в положении, в котором вы нуждаетесь, производительность, чем все проблемы, упомянутые другими, применимы, конечно. Самый важный - в противном случае вы получите неправильные результаты при извлечении элементов из хеш-набора или словаря: хеш-код не должен меняться в зависимости от времени жизни объекта (точнее, в течение времени, когда требуется хэш-код, например, в качестве ключа в словаре): например, следующее неверно, поскольку значение является общедоступным и поэтому может быть изменено извне в класс во время жизни экземпляра, поэтому вы не должны использовать его в качестве основы для хеш-кода:
class A
{
public int Value;
public override int GetHashCode()
{
return Value.GetHashCode(); //WRONG! Value is not constant during the instance life time
}
}
С другой стороны, если значение не может быть изменено, можно использовать:
class A
{
public readonly int Value;
public override int GetHashCode()
{
return Value.GetHashCode(); //OK Value is read-only and can't be changed during the instance life time
}
}
Я понимаю, что исходный GetHashCode() возвращает адрес памяти объекта, поэтому необходимо переопределить его, если вы хотите сравнить два разных объекта.
Редакция: Это было неправильно, исходный метод GetHashCode() не может обеспечить равенство двух значений. Хотя объекты, которые равны, возвращают один и тот же хэш-код.
Ниже, используя отражение, мне кажется, что лучше использовать общедоступные свойства, так как вам не нужно беспокоиться о добавлении/удалении свойств (хотя это не так распространенный сценарий). Это, как мне показалось, лучше работает. (По сравнению с секундомером).
public int getHashCode()
{
PropertyInfo[] theProperties = this.GetType().GetProperties();
int hash = 31;
foreach (PropertyInfo info in theProperties)
{
if (info != null)
{
var value = info.GetValue(this,null);
if(value != null)
unchecked
{
hash = 29 * hash ^ value.GetHashCode();
}
}
}
return hash;
}