Overriding ToString, Equals and GetHashCode
Every type inherits ToString, Equals and GetHashCode from object, and Equals and GetHashCode must agree or dictionaries break.
public override bool Equals(object? obj) / public override int GetHashCode()Every type in C# derives from object, so every type already has these members. Their declarations are public virtual string? ToString(), public virtual bool Equals(object? obj) and public virtual int GetHashCode(). All three are virtual, so any class can replace them.
The inherited versions are rarely what you want. ToString returns the type's full name. Equals compares identity: two objects are equal only if they are the same object in memory, so two separately created points holding 1 and 2 are not equal.
ToString
Override it to return a short readable description. Console.WriteLine and string interpolation both call it, so the override improves every log line and debugger view at once. Keep it cheap, never return null, and never let it throw.
Equals and GetHashCode travel together
A hash code is an int summary of an object's contents. A Dictionary or HashSet uses it to pick which bucket to look in, then uses Equals to confirm the match. So the rule is: if two objects are equal, they must return the same hash code. Breaking it means the dictionary looks in the wrong bucket and reports that a key it is holding is not there.
HashCode.Combine(X, Y) computes a good hash from your fields. The numbers it returns are not stable between runs of the program, so never store one.
What trips people up
Overriding Equals does not change ==. For a class, == still compares references unless the operator is defined too. A record, added in C# 9, generates all of this for you.
Example
var a = new Point(1, 2);
var b = new Point(1, 2);
Console.WriteLine(a); // uses the ToString override
Console.WriteLine(a.Equals(b));
Console.WriteLine(a.GetHashCode() == b.GetHashCode());
var counts = new Dictionary<Point, int>();
counts[a] = 10;
counts[b] += 5; // the same entry, because a and b are equal and hash alike
Console.WriteLine(counts.Count);
Console.WriteLine(counts[a]);
sealed class Point(int x, int y)
{
public int X { get; } = x;
public int Y { get; } = y;
public override string ToString() => $"({X}, {Y})";
public override bool Equals(object? obj) =>
obj is Point other && other.X == X && other.Y == Y;
// Must agree with Equals: equal objects return the same number
public override int GetHashCode() => HashCode.Combine(X, Y);
}
// Output:
// (1, 2)
// True
// True
// 1
// 15When to use it
- Make log lines readable by giving a domain type a ToString that prints its identifier.
- Use a composite key type in a Dictionary and have two separately built keys match.
- De-duplicate a list of imported records with a HashSet based on their business fields.
More examples
What breaks when GetHashCode disagrees with Equals
This is the bug report. The class looks correct, Equals returns true for the two tags, and the HashSet still says it has never seen one. Overriding Equals without GetHashCode produces a warning from the compiler for exactly this reason. Deleting the override of Equals or adding GetHashCode both fix it.
var set = new HashSet<Tag>();
set.Add(new Tag("urgent"));
// Equal by Equals, but the hash codes differ, so the lookup checks
// the wrong bucket and finds nothing.
Console.WriteLine(set.Contains(new Tag("urgent")));
sealed class Tag(string name)
{
public string Name { get; } = name;
public override bool Equals(object? obj) => obj is Tag other && other.Name == Name;
// Wrong on purpose: inherited from object, so it is identity based
// and two equal Tags get two different numbers.
}
// Output:
// FalseA record is the shorthand for all three
Use a record whenever a type is defined by its values rather than its identity, which covers most data carried between layers. Write the three overrides by hand only when the equality rule is unusual, for example when one field must be ignored or compared case-insensitively.
// A record, added in C# 9, generates ToString, Equals, GetHashCode and ==
// from its positional members. This one line replaces the hand-written class.
sealed record PointRecord(int X, int Y);
// The hand-written equivalent, for comparison
sealed class PointClass(int x, int y)
{
public int X { get; } = x;
public int Y { get; } = y;
public override string ToString() => $"PointClass {{ X = {X}, Y = {Y} }}";
public override bool Equals(object? obj) =>
obj is PointClass other && other.X == X && other.Y == Y;
public override int GetHashCode() => HashCode.Combine(X, Y);
}
// new PointRecord(1, 2).ToString() => "PointRecord { X = 1, Y = 2 }"
// new PointRecord(1, 2).Equals(new PointRecord(1, 2)) => True
// new PointRecord(1, 2) == new PointRecord(1, 2) => TrueOverriding Equals does not change ==
For a class, == asks whether two variables point at the same object, and an Equals override has no effect on it. That is why a test written with == can fail while the same test written with Equals passes. Records define == for you; plain classes do not.
var left = new Code("GB");
var right = new Code("GB");
Console.WriteLine(left.Equals(right)); // value comparison, from the override
Console.WriteLine(left == right); // reference comparison, still default
Console.WriteLine(ReferenceEquals(left, right));
sealed class Code(string value)
{
public string Value { get; } = value;
public override bool Equals(object? obj) => obj is Code other && other.Value == Value;
public override int GetHashCode() => Value.GetHashCode();
}
// Output:
// True
// False
// False
Discussion