Перейти к содержимому

Gethashcode c что это

  • автор:

Object. Get Hash Code Метод

Некоторые сведения относятся к предварительной версии продукта, в которую до выпуска могут быть внесены существенные изменения. Майкрософт не предоставляет никаких гарантий, явных или подразумеваемых, относительно приведенных здесь сведений.

Служит хэш-функцией по умолчанию.

public: virtual int GetHashCode();
public virtual int GetHashCode ();
abstract member GetHashCode : unit -> int override this.GetHashCode : unit -> int
Public Overridable Function GetHashCode () As Integer
Возвращаемое значение

Хэш-код для текущего объекта.

Примеры

Один из самых простых способов вычислить хэш-код для числового значения, которое имеет тот же или меньший диапазон, чем Int32 тип, — просто вернуть это значение. В следующем примере показана такая реализация структуры Number .

using System; public struct Number < private int n; public Number(int value) < n = value; >public int Value < get < return n; >> public override bool Equals(Object obj) < if (obj == null || ! (obj is Number)) return false; else return n == ((Number) obj).n; >public override int GetHashCode() < return n; >public override string ToString() < return n.ToString(); >> public class Example < public static void Main() < Random rnd = new Random(); for (int ctr = 0; ctr , hash code = ", n, n.GetHashCode()); > > > // The example displays output like the following: // n = -634398368, hash code = -634398368 // n = 2136747730, hash code = 2136747730 // n = -1973417279, hash code = -1973417279 // n = 1101478715, hash code = 1101478715 // n = 2078057429, hash code = 2078057429 // n = -334489950, hash code = -334489950 // n = -68958230, hash code = -68958230 // n = -379951485, hash code = -379951485 // n = -31553685, hash code = -31553685 // n = 2105429592, hash code = 2105429592 
open System [] type Number(value: int) = member _.Value = value override _.Equals(obj) = match obj with | :? Number as n -> n.Value = value | _ -> false override _.GetHashCode() = value override _.ToString() = string value let rnd = Random() for _ = 0 to 9 do let randomN = rnd.Next(Int32.MinValue, Int32.MaxValue) let n = Number randomN printfn $"n = , hash code = " // The example displays output like the following: // n = -634398368, hash code = -634398368 // n = 2136747730, hash code = 2136747730 // n = -1973417279, hash code = -1973417279 // n = 1101478715, hash code = 1101478715 // n = 2078057429, hash code = 2078057429 // n = -334489950, hash code = -334489950 // n = -68958230, hash code = -68958230 // n = -379951485, hash code = -379951485 // n = -31553685, hash code = -31553685 // n = 2105429592, hash code = 2105429592 
Public Structure Number Private n As Integer Public Sub New(value As Integer) n = value End Sub Public ReadOnly Property Value As Integer Get Return n End Get End Property Public Overrides Function Equals(obj As Object) As Boolean If obj Is Nothing OrElse Not TypeOf obj Is Number Then Return False Else Return n = CType(obj, Number).n End If End Function Public Overrides Function GetHashCode() As Integer Return n End Function Public Overrides Function ToString() As String Return n.ToString() End Function End Structure Module Example Public Sub Main() Dim rnd As New Random() For ctr As Integer = 0 To 9 Dim randomN As Integer = rnd.Next(Int32.MinValue, Int32.MaxValue) Dim n As New Number(randomN) Console.WriteLine("n = , hash code = ", n, n.GetHashCode()) Next End Sub End Module ' The example displays output like the following: ' n = -634398368, hash code = -634398368 ' n = 2136747730, hash code = 2136747730 ' n = -1973417279, hash code = -1973417279 ' n = 1101478715, hash code = 1101478715 ' n = 2078057429, hash code = 2078057429 ' n = -334489950, hash code = -334489950 ' n = -68958230, hash code = -68958230 ' n = -379951485, hash code = -379951485 ' n = -31553685, hash code = -31553685 ' n = 2105429592, hash code = 2105429592 

Часто тип содержит несколько полей данных, которые могут участвовать в создании хэш-кода. Одним из способов создания хэш-кода является объединение этих полей с помощью XOR (eXclusive OR) операции, как показано в следующем примере.

using System; // A type that represents a 2-D point. public struct Point < private int x; private int y; public Point(int x, int y) < this.x = x; this.y = y; >public override bool Equals(Object obj) < if (! (obj is Point)) return false; Point p = (Point) obj; return x == p.x & y == p.y; >public override int GetHashCode() < return x ^ y; >> public class Example < public static void Main() < Point pt = new Point(5, 8); Console.WriteLine(pt.GetHashCode()); pt = new Point(8, 5); Console.WriteLine(pt.GetHashCode()); >> // The example displays the following output: // 13 // 13 
// A type that represents a 2-D point. [] type Point(x: int, y: int) = member _.X = x member _.Y = y override _.Equals(obj) = match obj with | :? Point as p -> x = p.X && y = p.Y | _ -> false override _.GetHashCode() = x ^^^ y let pt = Point(5, 8) printfn $"" let pt2 = Point(8, 5) printfn $"" // The example displays the following output: // 13 // 13 
' A type that represents a 2-D point. Public Structure Point Private x As Integer Private y As Integer Public Sub New(x As Integer, y As Integer) Me.x = x Me.y = y End Sub Public Overrides Function Equals(obj As Object) As Boolean If Not TypeOf obj Is Point Then Return False Dim p As Point = CType(obj, Point) Return x = p.x And y = p.y End Function Public Overrides Function GetHashCode() As Integer Return x Xor y End Function End Structure Public Module Example Public Sub Main() Dim pt As New Point(5, 8) Console.WriteLine(pt.GetHashCode()) pt = New Point(8, 5) Console.WriteLine(pt.GetHashCode()) End Sub End Module 

В предыдущем примере возвращается тот же хэш-код для (n1, n2) и (n2, n1), поэтому может возникнуть больше конфликтов, чем желательно. Существует ряд решений, поэтому хэш-коды в таких случаях не идентичны. Один из них — возврат хэш-кода Tuple объекта, отражающего порядок каждого поля. В следующем примере показана возможная реализация, использующая Tuple класс . Обратите внимание, что издержки на производительность при создании экземпляра Tuple объекта могут значительно повлиять на общую производительность приложения, в котором хранится большое количество объектов в хэш-таблицах.

using System; public struct Point < private int x; private int y; public Point(int x, int y) < this.x = x; this.y = y; >public override bool Equals(Object obj) < if (obj is Point) < Point p = (Point) obj; return x == p.x & y == p.y; >else < return false; >> public override int GetHashCode() < return Tuple.Create(x, y).GetHashCode(); >> public class Example < public static void Main() < Point pt = new Point(5, 8); Console.WriteLine(pt.GetHashCode()); pt = new Point(8, 5); Console.WriteLine(pt.GetHashCode()); >> // The example displays the following output: // 173 // 269 
[] type Point(x: int, y: int) = member _.X = x member _.Y = y override _.Equals(obj) = match obj with | :? Point as p -> x = p.X && y = p.Y | _ -> false override _.GetHashCode() = (x, y).GetHashCode() let pt = Point(5, 8) printfn $"" let pt2 = Point(8, 5) printfn $"" // The example displays the following output: // 173 // 269 
Public Structure Point Private x As Integer Private y As Integer Public Sub New(x As Integer, y As Integer) Me.x = x Me.y = y End Sub Public Overrides Function Equals(obj As Object) As Boolean If Not TypeOf obj Is Point Then Return False Dim p As Point = CType(obj, Point) Return x = p.x And y = p.y End Function Public Overrides Function GetHashCode() As Integer Return Tuple.Create(x, y).GetHashCode() End Function End Structure Public Module Example Public Sub Main() Dim pt As New Point(5, 8) Console.WriteLine(pt.GetHashCode()) pt = New Point(8, 5) Console.WriteLine(pt.GetHashCode()) End Sub End Module ' The example displays the following output: ' 173 ' 269 

Второе альтернативное решение включает в себя взвешивание отдельных хэш-кодов путем сдвига хэш-кодов последовательных полей влево на два или более бита. В оптимальном случае биты, сдвинутые за бит 31, должны быть обтекаемы, а не отбрасываются. Так как биты удаляются операторами сдвига влево в C# и Visual Basic, для этого необходимо создать метод shift-and-wrap влево, как показано ниже:

public int ShiftAndWrap(int value, int positions) < positions = positions & 0x1F; // Save the existing bit pattern, but interpret it as an unsigned integer. uint number = BitConverter.ToUInt32(BitConverter.GetBytes(value), 0); // Preserve the bits to be discarded. uint wrapped = number >> (32 - positions); // Shift and wrap the discarded bits. return BitConverter.ToInt32(BitConverter.GetBytes((number
let shiftAndWrap (value: int) positions = let positions = positions &&& 0x1F // Save the existing bit pattern, but interpret it as an unsigned integer. let number = BitConverter.ToUInt32(BitConverter.GetBytes value, 0) // Preserve the bits to be discarded. let wrapped = number >>> (32 - positions) // Shift and wrap the discarded bits. BitConverter.ToInt32(BitConverter.GetBytes((number  
Public Function ShiftAndWrap(value As Integer, positions As Integer) As Integer positions = positions And &h1F ' Save the existing bit pattern, but interpret it as an unsigned integer. Dim number As UInteger = BitConverter.ToUInt32(BitConverter.GetBytes(value), 0) ' Preserve the bits to be discarded. Dim wrapped AS UInteger = number >> (32 - positions) ' Shift and wrap the discarded bits. Return BitConverter.ToInt32(BitConverter.GetBytes((number  

В следующем примере этот метод shift-and-wrap используется для вычисления хэш-кода структуры, используемой Point в предыдущих примерах.

using System; public struct Point < private int x; private int y; public Point(int x, int y) < this.x = x; this.y = y; >public override bool Equals(Object obj) < if (!(obj is Point)) return false; Point p = (Point) obj; return x == p.x & y == p.y; >public override int GetHashCode() < return ShiftAndWrap(x.GetHashCode(), 2) ^ y.GetHashCode(); >private int ShiftAndWrap(int value, int positions) < positions = positions & 0x1F; // Save the existing bit pattern, but interpret it as an unsigned integer. uint number = BitConverter.ToUInt32(BitConverter.GetBytes(value), 0); // Preserve the bits to be discarded. uint wrapped = number >> (32 - positions); // Shift and wrap the discarded bits. return BitConverter.ToInt32(BitConverter.GetBytes((number > public class Example < public static void Main() < Point pt = new Point(5, 8); Console.WriteLine(pt.GetHashCode()); pt = new Point(8, 5); Console.WriteLine(pt.GetHashCode()); >> // The example displays the following output: // 28 // 37 
open System [] type Point(x: int, y: int) = member _.X = x member _.Y = y override _.Equals(obj) = match obj with | :? Point as p -> x = p.X && y = p.Y | _ -> false override this.GetHashCode() = this.ShiftAndWrap(x.GetHashCode(), 2) ^^^ y.GetHashCode() member _.ShiftAndWrap(value, positions) = let positions = positions &&& 0x1F // Save the existing bit pattern, but interpret it as an unsigned integer. let number = BitConverter.ToUInt32(BitConverter.GetBytes value, 0) // Preserve the bits to be discarded. let wrapped = number >>> (32 - positions) // Shift and wrap the discarded bits. BitConverter.ToInt32(BitConverter.GetBytes((number " let pt2 = Point(8, 5) printfn $"" // The example displays the following output: // 28 // 37 
Public Structure Point Private x As Integer Private y As Integer Public Sub New(x As Integer, y As Integer) Me.x = x Me.y = y End Sub Public Overrides Function Equals(obj As Object) As Boolean If Not TypeOf obj Is Point Then Return False Dim p As Point = CType(obj, Point) Return x = p.x And y = p.y End Function Public Overrides Function GetHashCode() As Integer Return ShiftAndWrap(x.GetHashCode(), 2) XOr y.GetHashCode() End Function Private Function ShiftAndWrap(value As Integer, positions As Integer) As Integer positions = positions And &h1F ' Save the existing bit pattern, but interpret it as an unsigned integer. Dim number As UInteger = BitConverter.ToUInt32(BitConverter.GetBytes(value), 0) ' Preserve the bits to be discarded. Dim wrapped AS UInteger = number >> (32 - positions) ' Shift and wrap the discarded bits. Return BitConverter.ToInt32(BitConverter.GetBytes((number  

Комментарии

Хэш-код — это числовое значение, которое используется для вставки и идентификации объекта в хэш-коллекции, такой как Dictionary класс, Hashtable класс или тип, производный DictionaryBase от класса . Метод GetHashCode предоставляет этот хэш-код для алгоритмов, которым требуется быстрая проверка равенства объектов.

Сведения об использовании хэш-кодов в хэш-таблицах и о некоторых дополнительных алгоритмах хэш-кода см. в статье Хэш-функция в Википедии.

Два объекта, которые равны, возвращают одинаковые хэш-коды. Однако обратное не верно: одинаковые хэш-коды не подразумевают равенство объектов, так как разные (неравные) объекты могут иметь одинаковые хэш-коды. Кроме того, .NET не гарантирует реализацию GetHashCode метода по умолчанию, и значение, возвращаемое этим методом, может отличаться для реализаций .NET, таких как разные версии платформа .NET Framework и .NET Core, и платформ, таких как 32-разрядные и 64-разрядные платформы. По этим причинам не используйте реализацию этого метода по умолчанию в качестве уникального идентификатора объекта для целей хэширования. Отсюда следуют два последствия:

  • Не следует предполагать, что одинаковые хэш-коды подразумевают равенство объектов.
  • Никогда не следует сохранять или использовать хэш-код за пределами домена приложения, в котором он был создан, так как один и тот же объект может хэшировать в доменах приложений, процессах и платформах.

Хэш-код предназначен для эффективной вставки и поиска в коллекциях, основанных на хэш-таблице. Хэш-код не является постоянным значением. По этой причине:

  • Не сериализуйте значения хэш-кода и не храните их в базах данных.
  • Не используйте хэш-код в качестве ключа для получения объекта из коллекции с ключами.
  • Не отправляйте хэш-коды между доменами приложений или процессами. В некоторых случаях хэш-коды могут вычисляться для каждого процесса или домена приложения.
  • Не используйте хэш-код вместо значения, возвращаемого функцией криптографического хэширования, если требуется криптографически надежный хэш. Для криптографических хэшей используйте класс, производный System.Security.Cryptography.HashAlgorithm от класса или System.Security.Cryptography.KeyedHashAlgorithm .
  • Не проверяйте равенство хэш-кодов, чтобы определить, равны ли два объекта. (Неравные объекты могут иметь идентичные хэш-коды.) Чтобы проверить равенство, вызовите ReferenceEquals метод или Equals .

Метод GetHashCode может быть переопределен производным типом. Если GetHashCode параметр не переопределен, хэш-коды для ссылочных типов вычисляются путем вызова Object.GetHashCode метода базового класса, который вычисляет хэш-код на основе ссылки на объект. Дополнительные сведения см. в разделе RuntimeHelpers.GetHashCode. Иными словами, два объекта, для которых ReferenceEquals возвращает true метод, имеют одинаковые хэш-коды. Если типы значений не переопределяют GetHashCode, ValueType.GetHashCode метод базового класса использует отражение для вычисления хэш-кода на основе значений полей типа. Другими словами, типы значений, поля которых имеют одинаковые значения, имеют одинаковые хэш-коды. Дополнительные сведения о переопределении GetHashCodeсм. в разделе "Примечания к наследующим".

При переопределении GetHashCode метода следует также переопределить Equals, и наоборот. Если переопределенный Equals метод возвращает значение true , когда два объекта проверяются на равенство, переопределенный GetHashCode метод должен возвращать одинаковое значение для двух объектов.

Если объект, используемый в качестве ключа в хэш-таблице, не предоставляет полезную реализацию GetHashCode, можно указать поставщик хэш-кода, предоставив IEqualityComparer реализацию одной из перегрузок конструктора Hashtable класса.

Примечания для среда выполнения Windows

При вызове GetHashCode метода для класса в среда выполнения Windows он обеспечивает поведение по умолчанию для классов, которые не переопределяют GetHashCode. Это часть поддержки, которую платформа .NET Framework предоставляет для среда выполнения Windows (см. раздел Поддержка платформа .NET Framework приложений Магазина Windows и среда выполнения Windows). Классы в среда выполнения Windows не наследуют Objectи в настоящее время не реализуют GetHashCode. Однако они, как представляется, имеют ToStringметоды , Equals(Object)и GetHashCode при их использовании в коде C# или Visual Basic, а платформа .NET Framework обеспечивает поведение по умолчанию для этих методов.

среда выполнения Windows классы, написанные на C# или Visual Basic, могут переопределить GetHashCode метод .

Примечания для тех, кто наследует этот метод

Хэш-функция используется для быстрого создания числа (хэш-кода), соответствующего значению объекта. Хэш-функции обычно относятся к каждому типу и для уникальности должны использовать по крайней мере одно из полей экземпляра в качестве входных данных. Хэш-коды не должны вычисляться с использованием значений статических полей.

Для классов, производных от Object, GetHashCode метод может делегировать реализацию базового класса GetHashCode() только в том случае, если производный класс определяет равенство как равенство ссылок. Реализация GetHashCode() по умолчанию для ссылочных типов возвращает хэш-код, эквивалентный тому, который возвращается методом GetHashCode(Object) . Для неизменяемых ссылочных типов можно переопределить GetHashCode() . Как правило, для изменяемых ссылочных типов следует переопределять GetHashCode() только в следующих случаях:

  • Вы можете вычислить хэш-код из полей, которые не являются изменяемыми; Или
  • Вы можете убедиться, что хэш-код изменяемого объекта не изменяется, пока объект содержится в коллекции, которая зависит от его хэш-кода.

В противном случае можно подумать, что изменяемый объект теряется в хэш-таблице. Если вы решили переопределить GetHashCode() для изменяемого ссылочного типа, в документации должно быть ясно, что пользователи вашего типа не должны изменять значения объектов, пока объект хранится в хэш-таблице.

Для типов значений предоставляет реализацию хэш-кода по умолчанию, GetHashCode() которая использует отражение. Для повышения производительности рекомендуется переопределить его.

Дополнительные сведения и примеры вычисления хэш-кодов различными способами см. в разделе Примеры.

Хэш-функция должна иметь следующие свойства:

  • Если два объекта сравниваются как равные GetHashCode() , метод для каждого объекта должен возвращать одно и то же значение. Однако если два объекта не сравниваются как равные, GetHashCode() методы для этих двух объектов не должны возвращать разные значения.
  • Метод GetHashCode() для объекта должен постоянно возвращать один и тот же хэш-код, если не будет изменено состояние объекта, определяющее возвращаемое значение метода System.Object.Equals объекта. Обратите внимание, что это верно только для текущего выполнения приложения и что при повторном запуске приложения может быть возвращен другой хэш-код.
  • Для максимальной производительности хэш-функция должна генерировать равномерное распределение для всех входных данных, включая входные данные, которые сильно кластеризованы. Подразумевается, что небольшие изменения состояния объекта должны привести к большим изменениям результирующего хэш-кода для обеспечения оптимальной производительности хэш-таблицы.
  • Хэш-функции должны быть недорогими для вычислений.
  • Метод GetHashCode() не должен вызывать исключения.

Например, реализация метода, предоставляемого GetHashCode() классом String , возвращает идентичные хэш-коды для идентичных строковых значений. Таким образом, два String объекта возвращают один и тот же хэш-код, если они представляют одно и то же строковое значение. Кроме того, метод использует все символы в строке для создания разумного случайного распределения выходных данных, даже если входные данные кластеризованы в определенных диапазонах (например, у многих пользователей могут быть строки, содержащие только менее 128 символов ASCII, даже если строка может содержать любой из 65 535 символов Юникода).

Предоставление хорошей хэш-функции для класса может значительно повлиять на производительность добавления этих объектов в хэш-таблицу. В хэш-таблице с ключами, обеспечивающими хорошую реализацию хэш-функции, поиск элемента занимает постоянное время (например, операция O(1). В хэш-таблице с плохой реализацией хэш-функции производительность поиска зависит от количества элементов в хэш-таблице (например, операция O( n ), где n — количество элементов в хэш-таблице). Злоумышленник может вводить данные, которые увеличивают число конфликтов, что может значительно снизить производительность приложений, зависящих от хэш-таблиц, при следующих условиях:

  • Если хэш-функции создают частые конфликты.
  • Если большая часть объектов в хэш-таблице создает хэш-коды, равные или приблизительно равные друг другу.
  • Когда пользователи вводили данные, из которых вычисляется хэш-код.

Производные классы, которые переопределяют GetHashCode() , также должны переопределяться Equals(Object) , чтобы гарантировать, что два объекта, которые считаются равными, имеют одинаковый хэш-код; в Hashtable противном случае тип может работать неправильно.

Применяется к

См. также раздел

  • Hashtable
  • Equals(Object)
  • GetHashCode(Object)

GetHashCode() и философский камень, или краткий очерк о граблях

Казалось бы, что тема словарей, хэш-таблиц и всяческих хэш-кодов расписана вдоль и поперек, а каждый второй разработчик, будучи разбужен от ранней вечерней дремы примерно в 01:28am, быстренько набросает на листочке алгоритм балансировки Hashtable, попутно доказав все свойства в big-O нотации.

Возможно, такая хорошая осведомленность о предмете нашей беседы, может сослужить и плохую службу, вселяя ложное чувство уверенности: "Это ж так просто! Что тут может пойти не так?"

Как оказалось, может! Что именно может - в паре программистских пятничных баек, сразу после краткого ликбеза о том, что же такое хэш-таблица.

Так как статья все-таки пятничная, ликбез будет исключительно кратким и академически не строгим.

Хэш-таблица для самых маленьких

Наверняка, многие из вас ходили в поликлиники, ЖЭКи, паспортные столы и другие заведения повышенного уровня человеколюбия старого образца. Когда вы, нагибаясь к окошку, называете свою фамилию (адрес, номер паспорта и количество родимых пятен), бабушка-божий-одуванчик по ту сторону кивает, шаркающей походкой удаляется в недра конторы, и затем через не слишком-то продолжительное время приносит вашу бумажку: будь то медицинская карта, а то и новый паспорт.

Волшебство, позволяющее не самому быстрому в мире сотруднику найти нужный документ среди тысяч других, представляет собой ни что иное, как воплощенную в физическом мире хэш-таблицу:

Теплая ламповая хэш-таблица

При подобной организации данных каждому объекту соответствует какой-то хэш-код. В случае с поликлиникой хэш-кодом может быть ваша фамилия.

Сама же хэш-таблица представляет собой некий "комод" с ящиками, в каждом из которых лежат объекты, определенным образом сгруппированные по их хэш-кодам. Зачем, спрашивается, нужна эта особая группировка, и почему не использовать сами значения хэша в качестве надписи на ящиках? Ну, наверное, потому, что набор коробок под все возможные фамилии в мире не в каждую поликлинику влезет.

Поэтому поступают хитрее: от фамилии берут одну, две или три первые буквы. В результате нашему "Иванову" придется лежать в одном ящике с "Ивасенко", но специально обученный сотрудник с достаточно ненулевой вероятностью найдет нужный объект простым перебором.

Если же хэш числовой (как это обычно у нас бывает в IT), то просто берут остаток от его деления на количество коробок, что еще проще.

Так и живем, а чтобы все это хозяйство работало правильно, хэш-коды должны соответствовать некоторым весьма простым правилам:

  1. Хэш-код - это не первичный ключ, он совсем не обязан быть уникальным.
    Поликлиника вполне способна сносно функционировать даже в случае, когда у неё на учете стоят два пациента по фамиилии "Иванов".
  2. При этом хэш-коды должны быть более-менее равномерно распределены по пространству возможных значений.
    Можно, конечно, в качестве хэш-кода использовать количество глаз у пациента, только вот преимуществ такая картотека никаких не даст - двуглазые рулят, поэтому перебирать каждый раз придется почти все.
  3. Хэш-код - это не атрибут объекта, поэтому самостоятельной ценности он не несёт и хранить его не нужно (и даже вредно).
    В одной поликлинике хэш - это фамилия, в другой - имя, а креативный паспортный стол хэширует по дате рождения и цвету глаз. И кто их разберет, как они там внутри работают.
  4. Но для одного и того же объекта (или разных, но одинаковых объектов) хэш должен совпадать. Не должно происходить такого, что по понедельникам моя карточка лежит сверху и справа, по четвергам - по центру, а по субботам её вообще под ножку ставят, чтобы хэш-таблица не шаталась.

Ну а теперь перейдем к реальным (ну или почти реальным) примерам.

Хэш, кеш и EF

На коленке написанная подсистема по работе с документами. Документ - это такая простая штука вида

public class Document < public Int32 Id public String Name . >

Документы хранятся в базе посредством Entity Framework. А от бизнеса требование - чтобы в один момент времени документ мог редактироваться только одним пользователем.

В лучших традициях велосипедостроения это требование на самом нижнем уровне реализовано в виде хэш-таблицы:

HashSet _openDocuments;

И когда кто-то создает новый документ, сохраняет его в базу и продолжает редактировать, используется следующий код:

var newDocument = new Document(); // document is created _openDocuments.Add(newDocument); // document is open, nobody else can edit it. context.Documents.Add(newDocument); await context.SaveChangesAsync(); // so it's safe to write the document to the DB

Как вы думаете, чему равно значение переменной test в следующей строке, которая выполнится сразу после написанного выше кода?

Boolean test = _openDocuments.Contains(newDocument);

Разумеется, false, иначе бы этой статьи тут не было. Дьявол обычно кроется в деталях, а в нашем случае - в политике EF и в троеточиях объявления класса Document.

Для EF свойство Id выступает в роли первичного ключа, поэтому заботливая ORM по умолчанию мапит его на автоинкрементное поле базы данных. Таким образом, в момент создания объекта его Id равен 0, а сразу после записи в базу ему присваевается какое-то осмысленное значение:

var newDocument = new Document(); // newDocument.Id == 0 _openDocuments.Add(newDocument); context.Documents.Add(newDocument); await context.SaveChangesAsync(); // newDocument.Id == 42

Само по себе такое поведение, конечно, хэш-таблицу сломать неспособно, поэтому для того, чтобы красиво выстрелить в ногу, внутри класса Document надо написать так:

public class Document < public Int32 Id public String Name public override int GetHashCode() < return Id; >>

А вот теперь пазл складывается: записали мы в хэш-таблицу объект с хэш-кодом 0, а позже попросили объект с кодом 42.

Мораль сей басни такова: если вы закопались в отладке, и вам кажется, что либо вы, либо компилятор сошли с ума - проверьте, как у ваших объектов переопределены GetHashCode и Equals методы. Иногда бывает интересно.

Но если вы думаете, что только у написанных вашими коллегами классов бывают творческие реализации GetHashCode, то вот вам вторая история.

Квадратно-гнездовой метод

Как-то при работе над прототипом одной системы, обрабатывающей прямоугольники (а чаще квадраты) разного целочисленного размера, нужно было избавиться от дубликатов. То есть если на входе есть прямоугольники [20, 20], [30, 30] и [20, 20], то до выхода должны дойти [20, 20] и [30, 30]. Классическая задача, которая в лоб решается использованием хэш-таблицы:

private static IEnumerable FilterRectangles(IEnumerable rectangles) < HashSetresult = new HashSet(); foreach (var rectangle in rectangles) result.Add(rectangle); return result; >

Вроде бы и работает, но вовремя заметили, что производительность фильтрации как-то тяготеет к O(n^2), а не к более приятному O(n). Но постойте, классики Computer Science, ошибаться, конечно, могут, но не так фатально.

HashSet опять же самая обычная, да и Size - весьма тривиальная структура из FCL. Хорошо, что догадались проверить, какие же хэш-коды генерируются:

 var a = new Size(20,20).GetHashCode(); // a == 0 var b = new Size(30,30).GetHashCode(); // b == 0

Возможно, в этом есть какая-то непостижимая логика (если она существует, то, пожалуйста, отпишитесь в комментариях), но до тех пор я бы хотел взглянуть в глаза тому индусу, который придумал хэш-функцию, возвращающую одинаковое значение для любых квадратных размеров.

Хотя, подозреваю, я слишком строг к этому представителю великой народности: реализуя вычисление хэша для SizeF, он, по всей вероятности, учел допущенную ошибку проектирования:

var a = new SizeF(20,20).GetHashCode(); // a == 346948956 var b = new SizeF(30,30).GetHashCode(); // b == 346948956

Нет, a и b теперь не равны примитивному нулю! Теперь это истинно случайное значение 346948956.

Вместо заключения

Если вы думаете, что хэш-коды могут забавно вычисляться только в ваших собственных классах, ну и изредка в сущностях FCL, еще один забавный пример:

var a = Int64.MinValue.GetHashCode(); // a == 0 var b = Int64.MaxValue.GetHashCode(); // a == 0

Так что если вы ратуете за активное использование в ваших алгоритмах магических констант, и при этом поглядываете на хэширование. В общем, не говорите, что вас не предупреждали.

А будут ли выводы? Ну, давайте:

  1. Хорошо известные и изученные технологии могут преподносить любопытные сюрпризы на практике.
  2. При написании хэш-функции рекомендуется хорошенько подумать. либо использовать специальные кодогенераторы (см. в сторону Resharper).
  3. Верить никому нельзя. Мне - можно.

GetHashCode — вычисление хэш

a1 и a2, по логике вещей, совершенно идентичны. Но для C# нет. Их хэши различны. Конечно же, понятно, что это различие может быть оправдано, например, различием адресами в памяти, ну или временем создания, в конце концов :). Так вот хочется точно узнать, как же формируются хэши объектов и чем обосновывается их различие для идентичных пользовательских объектов.

Отслеживать
23.8k 3 3 золотых знака 47 47 серебряных знаков 61 61 бронзовый знак
задан 2 мар 2013 в 22:07
13.6k 13 13 золотых знаков 62 62 серебряных знака 122 122 бронзовых знака

1 ответ 1

Сортировка: Сброс на вариант по умолчанию

Для начала, поскольку ваш класс A не переопределяет GetHashCode() , эта функция наследуется от object .

Точный алгоритм вычисления GetHashCode() для object не специфицирован:

Реализация GetHashCode по умолчанию не гарантирует уникальные значения для различных объектов. Кроме того, .NET Framework не гарантирует, что реализация GetHashCode по умолчанию, в том числе значения, возвращаемые ей, не поменяются при смене версии фреймворка. Соответственно, значение функции GetHashCode по умолчанию не должно быть использована как уникальный идентификатор объекта с целью хеширования.

То, что вам два объекта кажутся идентичными, не гарантирует, что у них при реализации по умолчанию совпадут хеш-коды. С другой стороны, посудите сами: если у двух людей совпадает имя и фамилия, это ведь ещё не значит, что это один и тот же человек? Так и .NET framework, если у двух экземпляров класса A совпадают все поля, ещё не считает эти экземпляры одним и тем же объектом. Соответственно, по умолчанию используется что угодно -- например, адрес при создании или время создания, документация не говорит о точном методе. Если вас не устраивает поведение по умолчанию, вы должны его перекрыть.

Если ваш класс не использует операцию Equals и не является ключом в хеш-таблицах, в принципе можно ничего и не делать, вас должно устраивать и поведение по умолчанию. Но если ваш класс служит ключом, вам необходимо переопределить как Equals , так и GetHashCode() , причём согласованным образом: если x.Equals(y) возвращает true , то и хеш-коды у x и y обязаны совпадать.

Отсюда следует, что вы должны переопределять функцию GetHashCode() вместе с функцией Equals . Например, так:

public class A < public int b = 0; public int c; public override bool Equals(object obj) < return Equals(obj as A); >public bool Equals(A that) < if (that == null) return false; return this.b == that.b && this.c == that.c; >public override int GetHashCode() < return b.GetHashCode() * 13 + c.GetHashCode(); >> 

В C#, в отличие от многих других языков, есть объекты двух типов: объекты, полностью определяющиеся своими атрибутами (то есть, полями), и объекты, представляющие собой самостоятельную сущность, не сводящуюся лишь к значениям атрибутов.

Примером объекта первого типа являются, например, числа: если вы пишете

x = 5; y = 5; 

-- нет никакой разницы между первой и второй пятёркой, это одна и та же сущность. Подобные штуки называются в .NET типами-значениями и вводятся ключевым словом struct . Для них метод Equals по умолчанию сравнивает значения всех полей, и если они равны, заключает, что и структуры равны, одинаковы. Поэтому структуры можно копировать по значению, структуру можно безболезненно заменить на её копию. (Метод GetHashCode по умолчанию определён соответствующим образом: для равных структур он гарантированно возвращает одинаковый хеш.)

Примером другой сущности может служить, например, автомобиль, атрибутами которого являются марка, год выпуска, мощность двигателя и т. п. Два автомобиля с одинаковыми техническими параметрами -- это всё же разные автомобили. Такие объекты в C# называются ссылочными типами и вводятся ключевым словом class . Для классов, соответственно, реализация Equals по умолчанию не имеет права предполагать, что одинаковые значения полей автоматически означают равенство, каждый экземпляр класса считается уникальным.

Подумайте, не нужна ли вам на самом деле вместо класса A структура?

Вопрос о том, определяемся ли мы все, люди, лишь нашим набором параметров, или в нас есть что-то вне этого, не так уж прост.

Gethashcode c что это

Все классы в .NET, даже те, которые мы сами создаем, а также базовые типы, такие как System.Int32 , являются неявно производными от класса Object. Даже если мы не указываем класс Object в качестве базового, по умолчанию неявно класс Object все равно стоит на вершине иерархии наследования. Поэтому все типы и классы могут реализовать те методы, которые определены в классе System.Object. Рассмотрим эти методы.

ToString

Метод ToString служит для получения строкового представления данного объекта. Для базовых типов просто будет выводиться их строковое значение:

int i = 5; Console.WriteLine(i.ToString()); // выведет число 5 double d = 3.5; Console.WriteLine(d.ToString()); // выведет число 3,5

Для классов же этот метод выводит полное название класса с указанием пространства имен, в котором определен этот класс. И мы можем переопределить данный метод. Посмотрим на примере:

Person person = new Person < Name = "Tom" >; Console.WriteLine(person.ToString()); // выведет название класса Person Clock clock = new Clock < Hours = 15, Minutes = 34, Seconds = 53 >; Console.WriteLine(clock.ToString()); // выведет 15:34:53 class Clock < public int Hours < get; set; >public int Minutes < get; set; >public int Seconds < get; set; >public override string ToString() < return $"::"; > > class Person < public string Name < get; set; >= ""; >

Для переопределения метода ToString() в классе Clock, который представляет часы, используется ключевое слово override (как и при обычном переопределении виртуальных или абстрактных методов). В данном случае метод ToString() выводит в строке значения свойств Hours, Minutes, Seconds.

Класс Person не переопределяет метод ToString, поэтому для этого класса срабатывает стандартная реализация этого метода, которая выводит просто название класса.

Кстати в данном случае мы могли задействовать обе реализации:

Person tom = new Person < Name = "Tom" >; Console.WriteLine(tom.ToString()); // Tom Person undefined = new Person(); Console.WriteLine(undefined.ToString()); // Person class Person < public string Name < get; set; >= ""; public override string? ToString() < if (string.IsNullOrEmpty(Name)) return base.ToString(); return Name; >>

То есть если имя - свойство Name не имеет значения, оно представляет пустую строку, то возвращается базовая реализация - название класса. Стоит отметить, что базовая реализация возвращает не просто строку, а объект string? - то есть это может быть строка string, либо значение null , которое указывает на отсутствие значения. И в реальности в качестве возвращаемого типа для метода мы можем использовать как string , так и string?

Если же имя у объекта Person установлено, то возвращается значение свойства Name. Для проверки строки на наличие значения применяется метод String.IsNullOrEmpty() .

Стоит отметить, что различные технологии на платформе .NET активно используют метод ToString для разных целей. В частности, тот же метод Console.WriteLine() по умолчанию выводит именно строковое представление объекта. Поэтому, если нам надо вывести строковое представление объекта на консоль, то при передаче объекта в метод Console.WriteLine необязательно использовать метод ToString() - он вызывается неявно:

Person person = new Person < Name = "Tom" >; Console.WriteLine(person); // Tom Clock clock = new Clock < Hours = 15, Minutes = 34, Seconds = 53 >; Console.WriteLine(clock); // выведет 15:34:53

Метод GetHashCode

Метод GetHashCode позволяет возвратить некоторое числовое значение, которое будет соответствовать данному объекту или его хэш-код. По данному числу, например, можно сравнивать объекты. Можно определять самые разные алгоритмы генерации подобного числа или взять реализацию базового типа:

class Person < public string Name < get; set; >= ""; public override int GetHashCode() < return Name.GetHashCode(); >>

В данном случае метод GetHashCode возвращает хеш-код для значения свойства Name. То есть два объекта Person, которые имеют одно и то же имя, будут возвращать один и тот же хеш-код. Однако в реальности алгоритм может быть самым различным.

Получение типа объекта и метод GetType

Метод GetType позволяет получить тип данного объекта:

Person person = new Person < Name = "Tom" >; Console.WriteLine(person.GetType()); // Person

Этот метод возвращает объект Type , то есть тип объекта.

С помощью ключевого слова typeof мы получаем тип класса и сравниваем его с типом объекта. И если этот объект представляет тип Person, то выполняем определенные действия.

object person = new Person < Name = "Tom" >; if (person.GetType() == typeof(Person)) Console.WriteLine("Это реально класс Person");

Причем поскольку класс Object является базовым типом для всех классов, то мы можем переменной типа object присвоить объект любого типа. Однако для этой переменной метод GetType все равно вернет тот тип, на объект которого ссылается переменная. То есть в данном случае объект типа Person.

Стоит отметить, что проверку типа в примере выше можно сократить с помощью оператора is :

object person = new Person < Name = "Tom" >; if (person is Person) Console.WriteLine("Это реально класс Person");

В отличие от методов ToString, Equals, GetHashCode метод GetType() не переопределяется.

Метод Equals

Метод Equals позволяет сравнить два объекта на равенство. В качестве параметра он принимает объект для сравнения в виде типа object и возврашает true , если оба объекта равны:

public override bool Equals(object? obj)

Например, реализуем данный метод в классе Person:

class Person < public string Name < get; set; >= ""; public override bool Equals(object? obj) < // если параметр метода представляет тип Person // то возвращаем true, если имена совпадают if (obj is Person person) return Name == person.Name; return false; >// вместе с методом Equals следует реализовать метод GetHashCode public override int GetHashCode() => Name.GetHashCode(); >

Метод Equals принимает в качестве параметра объект любого типа, который мы затем приводим к текущему классу - классу Person.

Если переданный объект представляет тип Person, то возвращаем результат сравнения имен двух объектов Person. Если же объект представляет другой тип, то возвращается false.

В данном случае для примера применяется довольно простой алгоритм сравнения, однако при необходимости реализацию метода можно сделать более сложной, например, сравнивать по нескольким свойствам при их наличии.

Стоит отметить, что вместе с методом Equals следует реализовать метод GetHashCode.

var person1 = new Person < Name = "Tom" >; var person2 = new Person < Name = "Bob" >; var person3 = new Person < Name = "Tom" >; bool person1EqualsPerson2 = person1.Equals(person2); // false bool person1EqualsPerson3 = person1.Equals(person3); // true Console.WriteLine(person1EqualsPerson2); // false Console.WriteLine(person1EqualsPerson3); // true

И если следует сравнивать два сложных объекта, как в данном случае, то лучше использовать метод Equals, а не стандартную операцию ==.

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *