Переопределение GetHashCode
Согласуйте сравнение на равенство и хеширование.
«Переопределение GetHashCode» — бесплатный урок C# Academy на CoddyKit. Это урок 2 из 4. Ты можешь прочитать весь урок бесплатно ниже — а потом практиковать его прямо в браузере с встроенным редактором кода и ИИ-репетитором 24/7. Это часть пути обучения C# Academy, и твой прогресс синхронизируется между веб-версией и приложением CoddyKit. Курс C# Academy содержит 4 уроков всего.
Почему важен GetHashCode
Коллекции на основе хеширования, такие как Dictionary и HashSet, используют GetHashCode, чтобы распределять элементы по корзинам. Если переопределить Equals, но не GetHashCode, такие коллекции могут не найти равные элементы.
Золотое правило
Правило простое: если два объекта равны, они должны возвращать одинаковый хеш-код. Обратное не требуется: разные объекты могут иметь один хеш-код, что называется коллизией.
using System;
public struct Point
{
public int X, Y;
public Point(int x, int y) { X = x; Y = y; }
public override bool Equals(object obj) => obj is Point p && p.X == X && p.Y == Y;
public override int GetHashCode() => HashCode.Combine(X, Y);
}
public class Program
{
public static void Main()
{
var a = new Point(1, 2);
var b = new Point(1, 2);
Console.WriteLine(a.Equals(b));
Console.WriteLine(a.GetHashCode() == b.GetHashCode());
}
}Используйте HashCode.Combine
Вспомогательный метод HashCode.Combine объединяет значения нескольких полей в хорошо распределённый хеш. Это современный рекомендуемый способ реализации GetHashCode.
using System;
public struct Color
{
public int R, G, B;
public Color(int r, int g, int b) { R = r; G = g; B = b; }
public override bool Equals(object obj) => obj is Color c && c.R == R && c.G == G && c.B == B;
public override int GetHashCode() => HashCode.Combine(R, G, B);
}
public class Program
{
public static void Main()
{
Console.WriteLine(new Color(255, 0, 0).GetHashCode() == new Color(255, 0, 0).GetHashCode());
}
}Переопределяйте равенство и хеширование вместе
Всегда переопределяйте Equals и GetHashCode вместе, используя в обоих методах одни и те же поля. Использование разных полей приводит к разным хешам равных объектов.
using System;
public class Book
{
public string Title;
public int Year;
public Book(string title, int year) { Title = title; Year = year; }
public override bool Equals(object obj)
=> obj is Book b && b.Title == Title && b.Year == Year;
public override int GetHashCode() => HashCode.Combine(Title, Year);
}
public class Program
{
public static void Main()
{
var a = new Book("C#", 2020);
var b = new Book("C#", 2020);
Console.WriteLine(a.GetHashCode() == b.GetHashCode());
}
}Зачем это нужно словарям
Dictionary сначала вычисляет хеш ключа, чтобы найти корзину, а затем использует Equals внутри этой корзины. Неправильный хеш направляет поиск в другую корзину, и ключ никогда не будет найден.
using System;
using System.Collections.Generic;
public struct Coord
{
public int X, Y;
public Coord(int x, int y) { X = x; Y = y; }
public override bool Equals(object obj) => obj is Coord c && c.X == X && c.Y == Y;
public override int GetHashCode() => HashCode.Combine(X, Y);
}
public class Program
{
public static void Main()
{
var map = new Dictionary<Coord, string> { [new Coord(2, 3)] = "hit" };
Console.WriteLine(map[new Coord(2, 3)]);
}
}Хеширование неизменяемых полей
Хеш-коды объекта должны оставаться неизменными, пока объект находится в хеш-коллекции. Основывайте хеш на неизменяемых полях: если ключ изменится после добавления, коллекция может потерять его.
using System;
public class Account
{
public readonly int Id; // immutable, safe to hash
public string Nickname; // mutable, do not hash
public Account(int id, string nick) { Id = id; Nickname = nick; }
public override bool Equals(object obj) => obj is Account a && a.Id == Id;
public override int GetHashCode() => Id.GetHashCode();
}
public class Program
{
public static void Main()
{
var acc = new Account(42, "old");
int h1 = acc.GetHashCode();
acc.Nickname = "new"; // does not change the hash
Console.WriteLine(h1 == acc.GetHashCode());
}
}Обработка полей со значением null
Если поле может иметь значение null, HashCode.Combine безопасно его обработает. При вычислении вручную проверяйте значение на null, чтобы избежать NullReferenceException.
using System;
public class Tag
{
public string Name;
public Tag(string name) { Name = name; }
public override bool Equals(object obj) => obj is Tag t && t.Name == Name;
public override int GetHashCode() => HashCode.Combine(Name); // null-safe
}
public class Program
{
public static void Main()
{
Console.WriteLine(new Tag(null).GetHashCode() == new Tag(null).GetHashCode());
}
}Коллизии — это нормально
Хеш-коды имеют размер 32 бита, поэтому для больших наборов данных коллизии неизбежны. Хороший хеш лишь равномерно распределяет значения, делая коллизии редкими; Equals разрешает возникшие коллизии.
using System;
using System.Collections.Generic;
public class Program
{
public static void Main()
{
var set = new HashSet<int>();
for (int i = 0; i < 5; i++) set.Add(i);
// Equality still works perfectly even though hashing is imperfect in general
Console.WriteLine(set.Contains(3));
Console.WriteLine(set.Contains(99));
}
}Записи создают его автоматически
Запись record автоматически создаёт корректный GetHashCode на основе своих свойств, согласованный с основанным на значениях Equals. Это самый простой способ правильно реализовать хеширование.
using System;
using System.Collections.Generic;
public record Coord(int X, int Y);
public class Program
{
public static void Main()
{
var set = new HashSet<Coord> { new Coord(1, 1) };
Console.WriteLine(set.Contains(new Coord(1, 1)));
}
}Корректный тип с поддержкой равенства
Объединим всё вместе: одни и те же поля в Equals и GetHashCode, неизменяемые ключи и HashCode.Combine для их объединения. Такой тип безупречно работает в качестве ключа словаря.
using System;
using System.Collections.Generic;
public struct ProductKey
{
public string Sku;
public int Variant;
public ProductKey(string sku, int variant) { Sku = sku; Variant = variant; }
public override bool Equals(object obj)
=> obj is ProductKey k && k.Sku == Sku && k.Variant == Variant;
public override int GetHashCode() => HashCode.Combine(Sku, Variant);
}
public class Program
{
public static void Main()
{
var stock = new Dictionary<ProductKey, int>
{
[new ProductKey("ABC", 1)] = 10
};
Console.WriteLine(stock[new ProductKey("ABC", 1)]);
}
}Попробуйте сами
Проверьте выполнение контракта: создайте два равных ключа и убедитесь, что они равны и имеют одинаковые хеш-коды, а затем используйте их в set.
using System;
using System.Collections.Generic;
public struct Name
{
public string First, Last;
public Name(string first, string last) { First = first; Last = last; }
public override bool Equals(object obj) => obj is Name n && n.First == First && n.Last == Last;
public override int GetHashCode() => HashCode.Combine(First, Last);
}
public class Program
{
public static void Main()
{
var a = new Name("Ada", "Lovelace");
var b = new Name("Ada", "Lovelace");
Console.WriteLine(a.Equals(b) + " " + (a.GetHashCode() == b.GetHashCode()));
var people = new HashSet<Name> { a };
Console.WriteLine(people.Contains(b));
}
}Быстрая проверка
Вспомните контракт хеширования.
Итоги
Корректное хеширование обеспечивает работу коллекций на основе хеширования.
- Равные объекты должны возвращать одинаковый хеш-код.
- Используйте
HashCode.Combineдля тех же полей, что и вEquals. - Хешируйте неизменяемые поля, чтобы ключи оставались доступными для поиска.
- Коллизии нормальны;
Equalsразрешает их. - Записи автоматически создают корректную реализацию.
Часто задаваемые вопросы
Урок «Переопределение GetHashCode» бесплатный?
Да — полный текст урока «Переопределение GetHashCode» бесплатно доступен здесь в веб-версии. Чтобы практиковать его интерактивно (встроенный редактор кода и ИИ-репетитор 24/7) и разблокировать остальной курс C# Academy, подпишись на CoddyKit PRO. Курс C# Academy содержит 4 уроков всего.
Чему я научусь в уроке «Переопределение GetHashCode»?
Согласуйте сравнение на равенство и хеширование. Ты практикуешь C# Academy с помощью реального кода, который запускаешь прямо в браузере, и ИИ-репетитор 24/7 отвечает на твои вопросы во время урока.
Нужен ли мне опыт, чтобы начать C# Academy?
Предыдущий опыт не требуется. C# Academy на CoddyKit структурирован для всех уровней — от новичков до продвинутых, поэтому ты можешь начать отсюда или с самого начала и учиться в своем темпе. Это урок 2 из 4.
Сколько времени занимает урок «Переопределение GetHashCode»?
Большинство уроков CoddyKit занимают около 5–10 минут. Каждый из них компактный и интерактивный, поэтому ты постоянно делаешь прогресс и продолжаешь с того же места в веб-версии и приложении.
Можно ли писать и запускать код в этом уроке C# Academy?
Да. Каждый урок C# Academy включает встроенный редактор кода, поэтому ты пишешь и запускаешь реальный код прямо в браузере и получаешь моментальную обратную связь от AI — локальная установка не требуется.
Все уроки этого курса
- Реализация IEquatable
- Переопределение GetHashCode
- Реализация IComparable
- IComparer для пользовательской сортировки