Skip to content

Commit 25554b7

Browse files
CopilotBillWagner
andcommitted
Update value equality article with records example and detailed justifications
Co-authored-by: BillWagner <[email protected]>
1 parent f901d47 commit 25554b7

File tree

3 files changed

+145
-11
lines changed

3 files changed

+145
-11
lines changed

docs/csharp/programming-guide/statements-expressions-operators/how-to-define-value-equality-for-a-type.md

Lines changed: 26 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ ms.assetid: 4084581e-b931-498b-9534-cf7ef5b68690
1313
---
1414
# How to define value equality for a class or struct (C# Programming Guide)
1515

16-
[Records](../../fundamentals/types/records.md) automatically implement value equality. Consider defining a `record` instead of a `class` when your type models data and should implement value equality.
16+
**Consider using [records](../../fundamentals/types/records.md) first.** Records automatically implement value equality with minimal code, making them the recommended approach for most data-focused types. If you need custom value equality logic or cannot use records, continue with the manual implementation steps below.
1717

1818
When you define a class or struct, you decide whether it makes sense to create a custom definition of value equality (or equivalence) for the type. Typically, you implement value equality when you expect to add objects of the type to a collection, or when their primary purpose is to store a set of fields or properties. You can base your definition of value equality on a comparison of all the fields and properties in the type, or you can base the definition on a subset.
1919

@@ -33,26 +33,39 @@ Any struct that you define already has a default implementation of value equalit
3333

3434
The implementation details for value equality are different for classes and structs. However, both classes and structs require the same basic steps for implementing equality:
3535

36-
1. Override the [virtual](../../language-reference/keywords/virtual.md) <xref:System.Object.Equals%28System.Object%29?displayProperty=nameWithType> method. In most cases, your implementation of `bool Equals( object obj )` should just call into the type-specific `Equals` method that is the implementation of the <xref:System.IEquatable%601?displayProperty=nameWithType> interface. (See step 2.)
36+
1. **Override the [virtual](../../language-reference/keywords/virtual.md) <xref:System.Object.Equals%28System.Object%29?displayProperty=nameWithType> method.** *Justification: This provides polymorphic equality behavior, allowing your objects to be compared correctly when treated as `object` references. It ensures proper behavior in collections and when using polymorphism.* In most cases, your implementation of `bool Equals( object obj )` should just call into the type-specific `Equals` method that is the implementation of the <xref:System.IEquatable%601?displayProperty=nameWithType> interface. (See step 2.)
3737

38-
2. Implement the <xref:System.IEquatable%601?displayProperty=nameWithType> interface by providing a type-specific `Equals` method. This is where the actual equivalence comparison is performed. For example, you might decide to define equality by comparing only one or two fields in your type. Don't throw exceptions from `Equals`. For classes that are related by inheritance:
38+
2. **Implement the <xref:System.IEquatable%601?displayProperty=nameWithType> interface by providing a type-specific `Equals` method.** *Justification: This provides type-safe equality checking without boxing, resulting in better performance. It also avoids unnecessary casting and enables compile-time type checking.* This is where the actual equivalence comparison is performed. For example, you might decide to define equality by comparing only one or two fields in your type. Don't throw exceptions from `Equals`. For classes that are related by inheritance:
3939

4040
* This method should examine only fields that are declared in the class. It should call `base.Equals` to examine fields that are in the base class. (Don't call `base.Equals` if the type inherits directly from <xref:System.Object>, because the <xref:System.Object> implementation of <xref:System.Object.Equals%28System.Object%29?displayProperty=nameWithType> performs a reference equality check.)
4141

4242
* Two variables should be deemed equal only if the run-time types of the variables being compared are the same. Also, make sure that the `IEquatable` implementation of the `Equals` method for the run-time type is used if the run-time and compile-time types of a variable are different. One strategy for making sure run-time types are always compared correctly is to implement `IEquatable` only in `sealed` classes. For more information, see the [class example](#class-example) later in this article.
4343

44-
3. Optional but recommended: Overload the [==](../../language-reference/operators/equality-operators.md#equality-operator-) and [!=](../../language-reference/operators/equality-operators.md#inequality-operator-) operators.
44+
3. **Optional but recommended: Overload the [==](../../language-reference/operators/equality-operators.md#equality-operator-) and [!=](../../language-reference/operators/equality-operators.md#inequality-operator-) operators.** *Justification: This provides consistent and intuitive syntax for equality comparisons, matching user expectations from built-in types. It ensures that `obj1 == obj2` and `obj1.Equals(obj2)` behave the same way.*
4545

46-
4. Override <xref:System.Object.GetHashCode%2A?displayProperty=nameWithType> so that two objects that have value equality produce the same hash code.
46+
4. **Override <xref:System.Object.GetHashCode%2A?displayProperty=nameWithType> so that two objects that have value equality produce the same hash code.** *Justification: This is required for correct behavior in hash-based collections like `Dictionary<TKey,TValue>` and `HashSet<T>`. Objects that are equal must have equal hash codes, or these collections won't work correctly.*
4747

48-
5. Optional: To support definitions for "greater than" or "less than," implement the <xref:System.IComparable%601> interface for your type, and also overload the [<=](../../language-reference/operators/comparison-operators.md#less-than-or-equal-operator-) and [>=](../../language-reference/operators/comparison-operators.md#greater-than-or-equal-operator-) operators.
48+
5. **Optional: To support definitions for "greater than" or "less than," implement the <xref:System.IComparable%601> interface for your type, and also overload the [<=](../../language-reference/operators/comparison-operators.md#less-than-or-equal-operator-) and [>=](../../language-reference/operators/comparison-operators.md#greater-than-or-equal-operator-) operators.** *Justification: This enables sorting operations and provides a complete ordering relationship for your type, useful when adding objects to sorted collections or when sorting arrays or lists.*
4949

50-
> [!NOTE]
51-
> You can use records to get value equality semantics without any unnecessary boilerplate code.
50+
## Record example
51+
52+
The following example shows how records automatically implement value equality with minimal code:
53+
54+
:::code language="csharp" source="snippets/how-to-define-value-equality-for-a-type/ValueEqualityRecord/Program.cs":::
55+
56+
Records provide several advantages for value equality:
57+
58+
- **Automatic implementation**: Records automatically implement `IEquatable<T>`, override `Equals(object?)`, `GetHashCode()`, and the `==`/`!=` operators.
59+
- **Correct inheritance behavior**: Unlike the class example shown earlier, records handle inheritance scenarios correctly.
60+
- **Immutability by default**: Records encourage immutable design, which works well with value equality semantics.
61+
- **Concise syntax**: Positional parameters provide a compact way to define data types.
62+
- **Better performance**: The compiler-generated equality implementation is optimized and doesn't use reflection like the default struct implementation.
63+
64+
Use records when your primary goal is to store data and you need value equality semantics.
5265

5366
## Class example
5467

55-
The following example shows how to implement value equality in a class (reference type).
68+
The following example shows how to implement value equality in a class (reference type). This manual approach is needed when you can't use records or need custom equality logic:
5669

5770
:::code language="csharp" source="snippets/how-to-define-value-equality-for-a-type/ValueEqualityClass/Program.cs":::
5871

@@ -75,11 +88,13 @@ The `==` and `!=` operators can be used with classes even if the class does not
7588
7689
## Struct example
7790
78-
The following example shows how to implement value equality in a struct (value type):
91+
The following example shows how to implement value equality in a struct (value type). While structs have default value equality, a custom implementation can improve performance:
7992
8093
:::code language="csharp" source="snippets/how-to-define-value-equality-for-a-type/ValueEqualityStruct/Program.cs":::
8194
82-
For structs, the default implementation of <xref:System.Object.Equals%28System.Object%29?displayProperty=nameWithType> (which is the overridden version in <xref:System.ValueType?displayProperty=nameWithType>) performs a value equality check by using reflection to compare the values of every field in the type. When an implementer overrides the virtual `Equals` method in a struct, the purpose is to provide a more efficient means of performing the value equality check and optionally to base the comparison on some subset of the struct's fields or properties.
95+
For structs, the default implementation of <xref:System.Object.Equals%28System.Object%29?displayProperty=nameWithType> (which is the overridden version in <xref:System.ValueType?displayProperty=nameWithType>) performs a value equality check by using reflection to compare the values of every field in the type. Although this implementation produces correct results, it is relatively slow compared to a custom implementation that you write specifically for the type.
96+
97+
When you override the virtual `Equals` method in a struct, the purpose is to provide a more efficient means of performing the value equality check and optionally to base the comparison on some subset of the struct's fields or properties.
8398
8499
The [==](../../language-reference/operators/equality-operators.md#equality-operator-) and [!=](../../language-reference/operators/equality-operators.md#inequality-operator-) operators can't operate on a struct unless the struct explicitly overloads them.
85100
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
namespace ValueEqualityRecord;
2+
3+
// A simple record type that automatically implements value equality
4+
public record TwoDPoint(int X, int Y)
5+
{
6+
// Records automatically implement:
7+
// - IEquatable<TwoDPoint>
8+
// - Equals(object?)
9+
// - GetHashCode()
10+
// - operator == and !=
11+
// - ToString() with useful output
12+
}
13+
14+
// Records can be derived and still maintain proper value equality
15+
public record ThreeDPoint(int X, int Y, int Z) : TwoDPoint(X, Y);
16+
17+
class Program
18+
{
19+
static void Main(string[] args)
20+
{
21+
// Create some points
22+
TwoDPoint pointA = new TwoDPoint(3, 4);
23+
TwoDPoint pointB = new TwoDPoint(3, 4);
24+
TwoDPoint pointC = new TwoDPoint(5, 6);
25+
26+
ThreeDPoint point3D_A = new ThreeDPoint(3, 4, 5);
27+
ThreeDPoint point3D_B = new ThreeDPoint(3, 4, 5);
28+
ThreeDPoint point3D_C = new ThreeDPoint(3, 4, 7);
29+
30+
Console.WriteLine("=== Value Equality with Records ===");
31+
32+
// Value equality works automatically
33+
Console.WriteLine($"pointA.Equals(pointB) = {pointA.Equals(pointB)}"); // True
34+
Console.WriteLine($"pointA == pointB = {pointA == pointB}"); // True
35+
Console.WriteLine($"pointA.Equals(pointC) = {pointA.Equals(pointC)}"); // False
36+
Console.WriteLine($"pointA == pointC = {pointA == pointC}"); // False
37+
38+
Console.WriteLine("\n=== Hash Codes ===");
39+
40+
// Equal objects have equal hash codes automatically
41+
Console.WriteLine($"pointA.GetHashCode() = {pointA.GetHashCode()}");
42+
Console.WriteLine($"pointB.GetHashCode() = {pointB.GetHashCode()}");
43+
Console.WriteLine($"pointC.GetHashCode() = {pointC.GetHashCode()}");
44+
45+
Console.WriteLine("\n=== Inheritance with Records ===");
46+
47+
// Inheritance works correctly with value equality
48+
Console.WriteLine($"point3D_A.Equals(point3D_B) = {point3D_A.Equals(point3D_B)}"); // True
49+
Console.WriteLine($"point3D_A == point3D_B = {point3D_A == point3D_B}"); // True
50+
Console.WriteLine($"point3D_A.Equals(point3D_C) = {point3D_A.Equals(point3D_C)}"); // False
51+
52+
// Different types are not equal (unlike problematic class example)
53+
Console.WriteLine($"pointA.Equals(point3D_A) = {pointA.Equals(point3D_A)}"); // False
54+
55+
Console.WriteLine("\n=== Collections ===");
56+
57+
// Works seamlessly with collections
58+
var pointSet = new HashSet<TwoDPoint> { pointA, pointB, pointC };
59+
Console.WriteLine($"Set contains {pointSet.Count} unique points"); // 2 unique points
60+
61+
var pointDict = new Dictionary<TwoDPoint, string>
62+
{
63+
{ pointA, "First point" },
64+
{ pointC, "Different point" }
65+
};
66+
67+
// Demonstrate that equivalent points work as the same key
68+
var duplicatePoint = new TwoDPoint(3, 4);
69+
Console.WriteLine($"Dictionary contains key for {duplicatePoint}: {pointDict.ContainsKey(duplicatePoint)}"); // True
70+
Console.WriteLine($"Dictionary contains {pointDict.Count} entries"); // 2 entries
71+
72+
Console.WriteLine("\n=== String Representation ===");
73+
74+
// Automatic ToString implementation
75+
Console.WriteLine($"pointA.ToString() = {pointA}");
76+
Console.WriteLine($"point3D_A.ToString() = {point3D_A}");
77+
78+
Console.WriteLine("Press any key to exit.");
79+
Console.ReadKey();
80+
}
81+
}
82+
83+
/* Expected Output:
84+
=== Value Equality with Records ===
85+
pointA.Equals(pointB) = True
86+
pointA == pointB = True
87+
pointA.Equals(pointC) = False
88+
pointA == pointC = False
89+
90+
=== Hash Codes ===
91+
pointA.GetHashCode() = -1400834708
92+
pointB.GetHashCode() = -1400834708
93+
pointC.GetHashCode() = -148136000
94+
95+
=== Inheritance with Records ===
96+
point3D_A.Equals(point3D_B) = True
97+
point3D_A == point3D_B = True
98+
point3D_A.Equals(point3D_C) = False
99+
pointA.Equals(point3D_A) = False
100+
101+
=== Collections ===
102+
Set contains 2 unique points
103+
Dictionary contains key for TwoDPoint { X = 3, Y = 4 }: True
104+
Dictionary contains 2 entries
105+
106+
=== String Representation ===
107+
pointA.ToString() = TwoDPoint { X = 3, Y = 4 }
108+
point3D_A.ToString() = ThreeDPoint { X = 3, Y = 4, Z = 5 }
109+
*/
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<PropertyGroup>
4+
<OutputType>Exe</OutputType>
5+
<TargetFramework>net8.0</TargetFramework>
6+
<Nullable>enable</Nullable>
7+
<ImplicitUsings>enable</ImplicitUsings>
8+
</PropertyGroup>
9+
10+
</Project>

0 commit comments

Comments
 (0)