Skip to content

Commit 02d57e4

Browse files
CopilotBillWagnergewarren
authored
Update value equality article with records examples and detailed justifications (#48010)
* Initial plan * Update value equality article with records example and detailed justifications Co-authored-by: BillWagner <[email protected]> * Apply suggestions from code review * Address review comments: simplify records and fix formatting Co-authored-by: BillWagner <[email protected]> * Apply suggestions from code review Co-authored-by: Genevieve Warren <[email protected]> * Convert records recommendation to TIP block format Co-authored-by: BillWagner <[email protected]> --------- Co-authored-by: copilot-swe-agent[bot] <[email protected]> Co-authored-by: BillWagner <[email protected]> Co-authored-by: Bill Wagner <[email protected]> Co-authored-by: Genevieve Warren <[email protected]>
1 parent c6c53f1 commit 02d57e4

File tree

3 files changed

+136
-11
lines changed

3 files changed

+136
-11
lines changed

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

Lines changed: 27 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,8 @@ 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+
> [!TIP]
17+
> **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.
1718
1819
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.
1920

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

3435
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:
3536

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.)
37+
1. **Override the [virtual](../../language-reference/keywords/virtual.md) <xref:System.Object.Equals%28System.Object%29?displayProperty=nameWithType> method.** 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.)
3738

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:
39+
2. **Implement the <xref:System.IEquatable%601?displayProperty=nameWithType> interface by providing a type-specific `Equals` method.** 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:
3940

4041
* 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.)
4142

4243
* 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.
4344

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.
45+
3. **Optional but recommended: Overload the [==](../../language-reference/operators/equality-operators.md#equality-operator-) and [!=](../../language-reference/operators/equality-operators.md#inequality-operator-) operators.** 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.
4546

46-
4. Override <xref:System.Object.GetHashCode%2A?displayProperty=nameWithType> so that two objects that have value equality produce the same hash code.
47+
4. **Override <xref:System.Object.GetHashCode%2A?displayProperty=nameWithType> so that two objects that have value equality produce the same hash code.** 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.
4748

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.
49+
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.** 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.
4950

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

5367
## Class example
5468

55-
The following example shows how to implement value equality in a class (reference type).
69+
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:
5670

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

@@ -75,11 +89,13 @@ The `==` and `!=` operators can be used with classes even if the class does not
7589
7690
## Struct example
7791
78-
The following example shows how to implement value equality in a struct (value type):
92+
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:
7993
8094
:::code language="csharp" source="snippets/how-to-define-value-equality-for-a-type/ValueEqualityStruct/Program.cs":::
8195
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.
96+
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.
97+
98+
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.
8399
84100
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.
85101
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
namespace ValueEqualityRecord;
2+
3+
public record TwoDPoint(int X, int Y);
4+
5+
public record ThreeDPoint(int X, int Y, int Z) : TwoDPoint(X, Y);
6+
7+
class Program
8+
{
9+
static void Main(string[] args)
10+
{
11+
// Create some points
12+
TwoDPoint pointA = new TwoDPoint(3, 4);
13+
TwoDPoint pointB = new TwoDPoint(3, 4);
14+
TwoDPoint pointC = new TwoDPoint(5, 6);
15+
16+
ThreeDPoint point3D_A = new ThreeDPoint(3, 4, 5);
17+
ThreeDPoint point3D_B = new ThreeDPoint(3, 4, 5);
18+
ThreeDPoint point3D_C = new ThreeDPoint(3, 4, 7);
19+
20+
Console.WriteLine("=== Value Equality with Records ===");
21+
22+
// Value equality works automatically
23+
Console.WriteLine($"pointA.Equals(pointB) = {pointA.Equals(pointB)}"); // True
24+
Console.WriteLine($"pointA == pointB = {pointA == pointB}"); // True
25+
Console.WriteLine($"pointA.Equals(pointC) = {pointA.Equals(pointC)}"); // False
26+
Console.WriteLine($"pointA == pointC = {pointA == pointC}"); // False
27+
28+
Console.WriteLine("\n=== Hash Codes ===");
29+
30+
// Equal objects have equal hash codes automatically
31+
Console.WriteLine($"pointA.GetHashCode() = {pointA.GetHashCode()}");
32+
Console.WriteLine($"pointB.GetHashCode() = {pointB.GetHashCode()}");
33+
Console.WriteLine($"pointC.GetHashCode() = {pointC.GetHashCode()}");
34+
35+
Console.WriteLine("\n=== Inheritance with Records ===");
36+
37+
// Inheritance works correctly with value equality
38+
Console.WriteLine($"point3D_A.Equals(point3D_B) = {point3D_A.Equals(point3D_B)}"); // True
39+
Console.WriteLine($"point3D_A == point3D_B = {point3D_A == point3D_B}"); // True
40+
Console.WriteLine($"point3D_A.Equals(point3D_C) = {point3D_A.Equals(point3D_C)}"); // False
41+
42+
// Different types are not equal (unlike problematic class example)
43+
Console.WriteLine($"pointA.Equals(point3D_A) = {pointA.Equals(point3D_A)}"); // False
44+
45+
Console.WriteLine("\n=== Collections ===");
46+
47+
// Works seamlessly with collections
48+
var pointSet = new HashSet<TwoDPoint> { pointA, pointB, pointC };
49+
Console.WriteLine($"Set contains {pointSet.Count} unique points"); // 2 unique points
50+
51+
var pointDict = new Dictionary<TwoDPoint, string>
52+
{
53+
{ pointA, "First point" },
54+
{ pointC, "Different point" }
55+
};
56+
57+
// Demonstrate that equivalent points work as the same key
58+
var duplicatePoint = new TwoDPoint(3, 4);
59+
Console.WriteLine($"Dictionary contains key for {duplicatePoint}: {pointDict.ContainsKey(duplicatePoint)}"); // True
60+
Console.WriteLine($"Dictionary contains {pointDict.Count} entries"); // 2 entries
61+
62+
Console.WriteLine("\n=== String Representation ===");
63+
64+
// Automatic ToString implementation
65+
Console.WriteLine($"pointA.ToString() = {pointA}");
66+
Console.WriteLine($"point3D_A.ToString() = {point3D_A}");
67+
68+
Console.WriteLine("Press any key to exit.");
69+
Console.ReadKey();
70+
}
71+
}
72+
73+
/* Expected Output:
74+
=== Value Equality with Records ===
75+
pointA.Equals(pointB) = True
76+
pointA == pointB = True
77+
pointA.Equals(pointC) = False
78+
pointA == pointC = False
79+
80+
=== Hash Codes ===
81+
pointA.GetHashCode() = -1400834708
82+
pointB.GetHashCode() = -1400834708
83+
pointC.GetHashCode() = -148136000
84+
85+
=== Inheritance with Records ===
86+
point3D_A.Equals(point3D_B) = True
87+
point3D_A == point3D_B = True
88+
point3D_A.Equals(point3D_C) = False
89+
pointA.Equals(point3D_A) = False
90+
91+
=== Collections ===
92+
Set contains 2 unique points
93+
Dictionary contains key for TwoDPoint { X = 3, Y = 4 }: True
94+
Dictionary contains 2 entries
95+
96+
=== String Representation ===
97+
pointA.ToString() = TwoDPoint { X = 3, Y = 4 }
98+
point3D_A.ToString() = ThreeDPoint { X = 3, Y = 4, Z = 5 }
99+
*/
Lines changed: 10 additions & 0 deletions
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)