diff --git a/docs/csharp/programming-guide/statements-expressions-operators/how-to-define-value-equality-for-a-type.md b/docs/csharp/programming-guide/statements-expressions-operators/how-to-define-value-equality-for-a-type.md index cf2143ee8b7e3..058d6671edfac 100644 --- a/docs/csharp/programming-guide/statements-expressions-operators/how-to-define-value-equality-for-a-type.md +++ b/docs/csharp/programming-guide/statements-expressions-operators/how-to-define-value-equality-for-a-type.md @@ -13,7 +13,8 @@ ms.assetid: 4084581e-b931-498b-9534-cf7ef5b68690 --- # How to define value equality for a class or struct (C# Programming Guide) -[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. +> [!TIP] +> **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. 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. @@ -33,26 +34,39 @@ Any struct that you define already has a default implementation of value equalit 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: -1. Override the [virtual](../../language-reference/keywords/virtual.md) 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 interface. (See step 2.) +1. **Override the [virtual](../../language-reference/keywords/virtual.md) 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 interface. (See step 2.) -2. Implement the 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: +2. **Implement the 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: * 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 , because the implementation of performs a reference equality check.) * 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. -3. Optional but recommended: Overload the [==](../../language-reference/operators/equality-operators.md#equality-operator-) and [!=](../../language-reference/operators/equality-operators.md#inequality-operator-) operators. +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. -4. Override so that two objects that have value equality produce the same hash code. +4. **Override 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` and `HashSet`. Objects that are equal must have equal hash codes, or these collections won't work correctly. -5. Optional: To support definitions for "greater than" or "less than," implement the 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. +5. **Optional: To support definitions for "greater than" or "less than," implement the 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. -> [!NOTE] -> You can use records to get value equality semantics without any unnecessary boilerplate code. +## Record example + +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: + +:::code language="csharp" source="snippets/how-to-define-value-equality-for-a-type/ValueEqualityRecord/Program.cs"::: + +Records provide several advantages for value equality: + +- **Automatic implementation**: Records automatically implement `IEquatable` and override `Equals(object?)`, `GetHashCode()`, and the `==`/`!=` operators. +- **Correct inheritance behavior**: Unlike the class example shown earlier, records handle inheritance scenarios correctly. +- **Immutability by default**: Records encourage immutable design, which works well with value equality semantics. +- **Concise syntax**: Positional parameters provide a compact way to define data types. +- **Better performance**: The compiler-generated equality implementation is optimized and doesn't use reflection like the default struct implementation. + +Use records when your primary goal is to store data and you need value equality semantics. ## Class example -The following example shows how to implement value equality in a class (reference type). +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: :::code language="csharp" source="snippets/how-to-define-value-equality-for-a-type/ValueEqualityClass/Program.cs"::: @@ -75,11 +89,13 @@ The `==` and `!=` operators can be used with classes even if the class does not ## Struct example -The following example shows how to implement value equality in a struct (value type): +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: :::code language="csharp" source="snippets/how-to-define-value-equality-for-a-type/ValueEqualityStruct/Program.cs"::: -For structs, the default implementation of (which is the overridden version in ) 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. +For structs, the default implementation of (which is the overridden version in ) 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. + +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. 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. diff --git a/docs/csharp/programming-guide/statements-expressions-operators/snippets/how-to-define-value-equality-for-a-type/ValueEqualityRecord/Program.cs b/docs/csharp/programming-guide/statements-expressions-operators/snippets/how-to-define-value-equality-for-a-type/ValueEqualityRecord/Program.cs new file mode 100644 index 0000000000000..f9041b9ce482d --- /dev/null +++ b/docs/csharp/programming-guide/statements-expressions-operators/snippets/how-to-define-value-equality-for-a-type/ValueEqualityRecord/Program.cs @@ -0,0 +1,99 @@ +namespace ValueEqualityRecord; + +public record TwoDPoint(int X, int Y); + +public record ThreeDPoint(int X, int Y, int Z) : TwoDPoint(X, Y); + +class Program +{ + static void Main(string[] args) + { + // Create some points + TwoDPoint pointA = new TwoDPoint(3, 4); + TwoDPoint pointB = new TwoDPoint(3, 4); + TwoDPoint pointC = new TwoDPoint(5, 6); + + ThreeDPoint point3D_A = new ThreeDPoint(3, 4, 5); + ThreeDPoint point3D_B = new ThreeDPoint(3, 4, 5); + ThreeDPoint point3D_C = new ThreeDPoint(3, 4, 7); + + Console.WriteLine("=== Value Equality with Records ==="); + + // Value equality works automatically + Console.WriteLine($"pointA.Equals(pointB) = {pointA.Equals(pointB)}"); // True + Console.WriteLine($"pointA == pointB = {pointA == pointB}"); // True + Console.WriteLine($"pointA.Equals(pointC) = {pointA.Equals(pointC)}"); // False + Console.WriteLine($"pointA == pointC = {pointA == pointC}"); // False + + Console.WriteLine("\n=== Hash Codes ==="); + + // Equal objects have equal hash codes automatically + Console.WriteLine($"pointA.GetHashCode() = {pointA.GetHashCode()}"); + Console.WriteLine($"pointB.GetHashCode() = {pointB.GetHashCode()}"); + Console.WriteLine($"pointC.GetHashCode() = {pointC.GetHashCode()}"); + + Console.WriteLine("\n=== Inheritance with Records ==="); + + // Inheritance works correctly with value equality + Console.WriteLine($"point3D_A.Equals(point3D_B) = {point3D_A.Equals(point3D_B)}"); // True + Console.WriteLine($"point3D_A == point3D_B = {point3D_A == point3D_B}"); // True + Console.WriteLine($"point3D_A.Equals(point3D_C) = {point3D_A.Equals(point3D_C)}"); // False + + // Different types are not equal (unlike problematic class example) + Console.WriteLine($"pointA.Equals(point3D_A) = {pointA.Equals(point3D_A)}"); // False + + Console.WriteLine("\n=== Collections ==="); + + // Works seamlessly with collections + var pointSet = new HashSet { pointA, pointB, pointC }; + Console.WriteLine($"Set contains {pointSet.Count} unique points"); // 2 unique points + + var pointDict = new Dictionary + { + { pointA, "First point" }, + { pointC, "Different point" } + }; + + // Demonstrate that equivalent points work as the same key + var duplicatePoint = new TwoDPoint(3, 4); + Console.WriteLine($"Dictionary contains key for {duplicatePoint}: {pointDict.ContainsKey(duplicatePoint)}"); // True + Console.WriteLine($"Dictionary contains {pointDict.Count} entries"); // 2 entries + + Console.WriteLine("\n=== String Representation ==="); + + // Automatic ToString implementation + Console.WriteLine($"pointA.ToString() = {pointA}"); + Console.WriteLine($"point3D_A.ToString() = {point3D_A}"); + + Console.WriteLine("Press any key to exit."); + Console.ReadKey(); + } +} + +/* Expected Output: +=== Value Equality with Records === +pointA.Equals(pointB) = True +pointA == pointB = True +pointA.Equals(pointC) = False +pointA == pointC = False + +=== Hash Codes === +pointA.GetHashCode() = -1400834708 +pointB.GetHashCode() = -1400834708 +pointC.GetHashCode() = -148136000 + +=== Inheritance with Records === +point3D_A.Equals(point3D_B) = True +point3D_A == point3D_B = True +point3D_A.Equals(point3D_C) = False +pointA.Equals(point3D_A) = False + +=== Collections === +Set contains 2 unique points +Dictionary contains key for TwoDPoint { X = 3, Y = 4 }: True +Dictionary contains 2 entries + +=== String Representation === +pointA.ToString() = TwoDPoint { X = 3, Y = 4 } +point3D_A.ToString() = ThreeDPoint { X = 3, Y = 4, Z = 5 } +*/ \ No newline at end of file diff --git a/docs/csharp/programming-guide/statements-expressions-operators/snippets/how-to-define-value-equality-for-a-type/ValueEqualityRecord/ValueEqualityRecord.csproj b/docs/csharp/programming-guide/statements-expressions-operators/snippets/how-to-define-value-equality-for-a-type/ValueEqualityRecord/ValueEqualityRecord.csproj new file mode 100644 index 0000000000000..fd4dd4565750e --- /dev/null +++ b/docs/csharp/programming-guide/statements-expressions-operators/snippets/how-to-define-value-equality-for-a-type/ValueEqualityRecord/ValueEqualityRecord.csproj @@ -0,0 +1,10 @@ + + + + Exe + net8.0 + enable + enable + + + \ No newline at end of file