0Pricing
C# Academy · Урок

Переопределение 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 — локальная установка не требуется.

Все уроки этого курса

  1. Реализация IEquatable
  2. Переопределение GetHashCode
  3. Реализация IComparable
  4. IComparer для пользовательской сортировки
← Назад к C# Academy