Как С# вычисляет хэш-код для объекта?
Этот вопрос выходит из обсуждения кортежей.
Я начал думать о хэш-коде, который должен иметь кортеж.
Что, если мы примем класс KeyValuePair как кортеж? Он не переопределяет метод GetHashCode(), поэтому, вероятно, он не будет знать о хэш-кодах его "детей"... Итак, время выполнения вызовет Object.GetHashCode(), который не знает о реальная структура объекта.
Затем мы можем сделать два экземпляра некоторого ссылочного типа, которые на самом деле равны, из-за перегруженных GetHashCode() и Equals(). И используйте их как "дети" в кортежах, чтобы "обмануть" словарь.
Но это не сработает! Время выполнения каким-то образом определяет структуру нашего кортежа и вызывает перегруженный GetHashCode нашего класса!
Как это работает? Что сделал анализ Object.GetHashCode()?
Может ли это повлиять на производительность в некотором плохом сценарии, когда мы используем некоторые сложные ключи? (возможно, невозможный сценарий... но все же)
Рассмотрим этот код как пример:
namespace csharp_tricks
{
class Program
{
class MyClass
{
int keyValue;
int someInfo;
public MyClass(int key, int info)
{
keyValue = key;
someInfo = info;
}
public override bool Equals(object obj)
{
MyClass other = obj as MyClass;
if (other == null) return false;
return keyValue.Equals(other.keyValue);
}
public override int GetHashCode()
{
return keyValue.GetHashCode();
}
}
static void Main(string[] args)
{
Dictionary<object, object> dict = new Dictionary<object, object>();
dict.Add(new KeyValuePair<MyClass,object>(new MyClass(1, 1), 1), 1);
//here we get the exception -- an item with the same key was already added
//but how did it figure out the hash code?
dict.Add(new KeyValuePair<MyClass,object>(new MyClass(1, 2), 1), 1);
return;
}
}
}
Обновление Я думаю, что нашел объяснение для этого, как указано ниже в моем ответе. Основные результаты этого:
- Будьте осторожны с вашими ключами и их хэш-кодами: -)
- Для сложных словарных клавиш вы должны правильно переопределить Equals() и GetHashCode().
Ответы
Ответ 1
Кажется, что у меня есть ключ.
Я думал, что KeyValuePair является ссылочным типом, но это не так, это структура. И поэтому он использует метод ValueType.GetHashCode(). MSDN для него говорит: "Для вычисления возвращаемого значения используется одно или несколько полей производного типа".
Если вы возьмете настоящий ссылочный тип как "кортеж-провайдер", вы обманете словарь (или себя...).
using System.Collections.Generic;
namespace csharp_tricks
{
class Program
{
class MyClass
{
int keyValue;
int someInfo;
public MyClass(int key, int info)
{
keyValue = key;
someInfo = info;
}
public override bool Equals(object obj)
{
MyClass other = obj as MyClass;
if (other == null) return false;
return keyValue.Equals(other.keyValue);
}
public override int GetHashCode()
{
return keyValue.GetHashCode();
}
}
class Pair<T, R>
{
public T First { get; set; }
public R Second { get; set; }
}
static void Main(string[] args)
{
var dict = new Dictionary<Pair<int, MyClass>, object>();
dict.Add(new Pair<int, MyClass>() { First = 1, Second = new MyClass(1, 2) }, 1);
//this is a pair of the same values as previous! but... no exception this time...
dict.Add(new Pair<int, MyClass>() { First = 1, Second = new MyClass(1, 3) }, 1);
return;
}
}
}
Ответ 2
Не переопределяйте GetHashcode() и Equals() на изменяемых классах, только переопределяйте его на неизменяемых классах или структурах, иначе, если вы измените объект, используемый в качестве ключа, хеш-таблица больше не будет функционировать должным образом (вы не будете быть в состоянии получить значение, связанное с ключом после изменения объекта ключа)
Кроме того, хэш-таблицы не используют хэш-коды для идентификации объектов, которые они используют в качестве идентификаторов объектов ключа, не требуется, чтобы все ключи, которые используются для добавления записей в хеш-таблицу, возвращают разные хэш-коды, но рекомендуется, чтобы они выполняли, иначе производительность сильно страдает.
Ответ 3
Это отличная статья о GetHashCode от Effective С#: http://www.awprofessional.com/content/images/0321245660/items/wagner_item10.pdf
Ответ 4
Вот правильные реализации хэша и равенства для кортежа Quad (содержит 4 кортежных компонента внутри). Этот код обеспечивает правильное использование этого конкретного кортежа в HashSets и словарях.
Подробнее об объекте (включая исходный код) здесь.
Примечание использование ключевого слова unchecked (во избежание переполнения) и бросания NullReferenceException, если obj равно null (как требуется базовым методом)
public override bool Equals(object obj)
{
if (ReferenceEquals(null, obj))
throw new NullReferenceException("obj is null");
if (ReferenceEquals(this, obj)) return true;
if (obj.GetType() != typeof (Quad<T1, T2, T3, T4>)) return false;
return Equals((Quad<T1, T2, T3, T4>) obj);
}
public bool Equals(Quad<T1, T2, T3, T4> obj)
{
if (ReferenceEquals(null, obj)) return false;
if (ReferenceEquals(this, obj)) return true;
return Equals(obj.Item1, Item1)
&& Equals(obj.Item2, Item2)
&& Equals(obj.Item3, Item3)
&& Equals(obj.Item4, Item4);
}
public override int GetHashCode()
{
unchecked
{
int result = Item1.GetHashCode();
result = (result*397) ^ Item2.GetHashCode();
result = (result*397) ^ Item3.GetHashCode();
result = (result*397) ^ Item4.GetHashCode();
return result;
}
}
public static bool operator ==(Quad<T1, T2, T3, T4> left, Quad<T1, T2, T3, T4> right)
{
return Equals(left, right);
}
public static bool operator !=(Quad<T1, T2, T3, T4> left, Quad<T1, T2, T3, T4> right)
{
return !Equals(left, right);
}
Ответ 5
Посмотрите сообщение от Брэда Абрамса, а также комментарий Брайана Грукмейера для получения дополнительной информации о том, как работает object.GetHashCode. Кроме того, посмотрите первый комментарий к блогу Ayande post. Я не знаю, соблюдают ли текущие версии Framework все эти правила или если они действительно изменили его, как предполагал Брэд.
Ответ 6
У меня больше нет ссылки на книгу, и мне придется найти ее только для подтверждения, но я думал, что базовый хэш по умолчанию просто объединил всех членов вашего объекта. Он получил доступ к ним из-за того, как работает CLR, поэтому вы не могли писать так же хорошо, как и они.
Это полностью из памяти того, что я кратко прочитал, поэтому возьмите его за то, что пожелаете.
Изменить: Книга была внутри С# из MS Press. Один с пилой на обложке. Автор потратил много времени, объясняя, как все было реализовано в CLR, как перевод языка на MSIL и т.д. ЭСТ. Если вы можете найти книгу, это неплохо читается.
Изменить: Создайте ссылку при условии, что она выглядит как
Object.GetHashCode() использует внутреннее поле в классе System.Object для генерации хеш-значения. каждый Созданный объект присваивается уникальный ключ объекта, который хранится как целое число, когда он создано. Эти ключи начинаются с 1 и увеличиваются каждый раз, когда новый объект создается любой тип.
Мне кажется, мне нужно написать несколько собственных хеш-кодов, если я ожидаю использовать объекты в качестве хеш-ключей.
Ответ 7
поэтому, вероятно, он не будет знать о хэш-кодах его "детей".
В вашем примере, похоже, обратное:-) Хэш-код для ключа MyClass
и значение 1
одинаково для обоих KeyValuePair
. Реализация KeyValuePair должна использовать как Key
, так и Value
для собственного хеш-кода
Перемещение вверх, класс словаря требует уникальных ключей. Он использует хэш-код, предоставляемый каждым ключом, чтобы понять, что происходит. Помните, что среда выполнения не вызывает Object.GetHashCode()
, но она вызывает реализацию GetHashCode(), предоставленную экземпляром, который вы ему даете.
Рассмотрим более сложный случай:
public class HappyClass
{
enum TheUnit
{
Points,
Picas,
Inches
}
class MyDistanceClass
{
int distance;
TheUnit units;
public MyDistanceClass(int theDistance, TheUnit unit)
{
distance = theDistance;
units = unit;
}
public static int ConvertDistance(int oldDistance, TheUnit oldUnit, TheUnit newUnit)
{
// insert real unit conversion code here :-)
return oldDistance * 100;
}
/// <summary>
/// Figure out if we are equal distance, converting into the same units of measurement if we have to
/// </summary>
/// <param name="obj">the other guy</param>
/// <returns>true if we are the same distance</returns>
public override bool Equals(object obj)
{
MyDistanceClass other = obj as MyDistanceClass;
if (other == null) return false;
if (other.units != this.units)
{
int newDistance = MyDistanceClass.ConvertDistance(other.distance, other.units, this.units);
return distance.Equals(newDistance);
}
else
{
return distance.Equals(other.distance);
}
}
public override int GetHashCode()
{
// even if the distance is equal in spite of the different units, the objects are not
return distance.GetHashCode() * units.GetHashCode();
}
}
static void Main(string[] args)
{
// these are the same distance... 72 points = 1 inch
MyDistanceClass distPoint = new MyDistanceClass(72, TheUnit.Points);
MyDistanceClass distInch = new MyDistanceClass(1, TheUnit.Inch);
Debug.Assert(distPoint.Equals(distInch), "these should be true!");
Debug.Assert(distPoint.GetHashCode() != distInch.GetHashCode(), "But yet they are fundimentally different values");
Dictionary<object, object> dict = new Dictionary<object, object>();
dict.Add(new KeyValuePair<MyDistanceClass, object>(distPoint, 1), 1);
//this should not barf
dict.Add(new KeyValuePair<MyDistanceClass, object>(distInch, 1), 1);
return;
}
}
код >
В принципе... в случае моего примера вам нужно, чтобы два объекта были одинаковым расстоянием, чтобы вернуть "true" для Equals, но при этом возвращать разные хэш-коды.