0Pricing
C# Academy · レッスン

GetHashCodeのオーバーライド

等価性とハッシュ処理の一貫性を保ちます。

「GetHashCodeのオーバーライド」はCoddyKit上の無料C# Academyレッスンです。 これはレッスン2/4です。 下記で完全なレッスンを無料で読むことができます。その後、ブラウザ内の組み込みコードエディタと24時間対応のAIチューターでハンズオン演習できます。 これはC# Academy学習パスの一部であり、ウェブとCoddyKitアプリ全体で進捗が同期されます。 C# Academyコースには全4レッスンが含まれています。

GetHashCode が重要な理由

Dictionary や HashSet のようなハッシュベースのコレクションは、項目をバケットに振り分けるために GetHashCode を使用します。Equals だけをオーバーライドして GetHashCode をオーバーライドしないと、これらのコレクションは等しい項目を見つけられなくなる可能性があります。

黄金律

契約は単純です。2つのオブジェクトが等しい場合、それらは同じハッシュコードを返さなければなりません。逆は必須ではなく、異なるオブジェクトが同じハッシュコードを共有すること(衝突)はあります。

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 に必要な理由

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 は安全に処理します。手動で計算する場合は、NullReferenceException を避けるために null を確認してください。

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)]);
    }
}

自分で試してみる

契約が守られることを確認しましょう。等しい2つのキーを作成し、等価性とハッシュコードの両方が一致することを確認してから、セットで使ってみてください。

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));
    }
}

クイックチェック

ハッシュに関する契約を思い出してください。

まとめ

正しいハッシュ処理によって、ハッシュベースのコレクションが正しく機能します。

  • 等しいオブジェクトは同じハッシュコードを返さなければなりません。
  • Equals と同じフィールドに対して HashCode.Combine を使います。
  • キーを検索可能な状態に保つため、不変なフィールドをハッシュします。
  • 衝突は通常起こります。衝突は Equals が解決します。
  • レコードは正しい実装を自動生成します。

よくある質問

「GetHashCodeのオーバーライド」レッスンは無料ですか?

はい。「GetHashCodeのオーバーライド」の完全なテキストはこのウェブで無料で読めます。インタラクティブに演習し(組み込みコードエディタと24時間対応のAIチューター)、C# Academyコースの残りをアンロックするには、CoddyKit PROにアップグレードしてください。 C# Academyコースには全4レッスンが含まれています。

「GetHashCodeのオーバーライド」で何を学びますか?

等価性とハッシュ処理の一貫性を保ちます。 ブラウザで直接実行するハンズオンコードでC# Academyを演習し、24時間対応のAIチューターがレッスンを進める中での質問に答えます。

C# Academyを始めるのに経験は必要ですか?

事前経験は必要ありません。CoddyKitのC# Academyは初級者から上級者向けに構成されているため、ここから始めるか最初から始めて、自分のペースで進むことができます。 これはレッスン2/4です。

「GetHashCodeのオーバーライド」レッスンにはどのくらい時間がかかりますか?

ほとんどのCoddyKitレッスンは約5~10分かかります。各レッスンはコンパクトでインタラクティブなので、着実に進歩し、ウェブとアプリ全体で正確に前回の場所から再開できます。

このC# Academyレッスンでコードを書いて実行できますか?

はい。すべてのC# Academyレッスンに組み込みコードエディタが含まれているため、ブラウザでリアルコードを書いて実行し、即座のAIフィードバックを取得できます。ローカル設定は不要です。

このコースのすべてのレッスン

  1. IEquatableの実装
  2. GetHashCodeのオーバーライド
  3. IComparableの実装
  4. カスタムソートのためのIComparer
← C# Academyに戻る