Overview

C# provides native equivalents to maps and sets (Dictionary<TKey, TValue> and HashSet<T>) that rely entirely on the .NET equality contract (GetHashCode and Equals).

Code snippet

graph TD
A[Lookup Key / Item] --> B{Check Hash Code}
B --> C[Find Bucket]
C --> D{Check Equality}
D -->|True| E[Value Found / Match]
D -->|False| F[Collision Handling / Not Found]

The Equality Contract & IEquatable

The core rules governing custom types used in hash-based collections:

  • Hash Code Consistency: If two objects are equal (a.Equals(b) is true), they must return identical integer values from GetHashCode().
  • Mathematical Properties: Equals must be reflexive, symmetric, and transitive.
  • Immutability: Keys and set elements must remain unchanged while stored in a collection to prevent hash drift.

IEquatable Advantage

IEquatable<T> provides a strongly-typed Equals(T other) method. This eliminates boxing overhead for value types (struct) and avoids runtime type-casting checks present in standard object.Equals(object obj).

Java vs. C# Comparison

FeatureC# .NETJava
Map EquivalentDictionary<TKey, TValue>HashMap<K, V>
Set EquivalentHashSet<T>HashSet<T>
Typed Equality InterfaceIEquatable<T> (Generic)None (Relies entirely on boolean equals(Object o))
Boilerplate Reductionrecord types auto-generate equalityrecord types auto-generate equality (Java 14+)

Records as Collection Keys

Using C# records as dictionary keys or set elements provides automatic value-based equality and immutability out of the box.

// Standard positional record (class-based)
public record UserKey(int Id, string Region);
 
// High-performance alternative for tight loops / stack allocation
public readonly record struct OptimizedKey(int Id, long Timestamp);

The Mutability Danger

Never use mutable properties (public string Name { get; set; }) on types used as dictionary keys. Modifying a property after insertion alters its hash code, breaking internal bucket lookups and effectively “losing” the item in the collection.