Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ title: "How to define value equality for a class or struct"
description: Learn how to define value equality for a class or struct. See code examples and view available resources.
ms.topic: how-to
ms.date: 03/26/2021
ai-usage: ai-assisted
helpviewer_keywords:
- "overriding Equals method [C#]"
- "object equivalence [C#]"
Expand Down Expand Up @@ -56,14 +57,72 @@ The following example shows how records automatically implement value equality w

Records provide several advantages for value equality:

- **Automatic implementation**: Records automatically implement `IEquatable<T>` and override `Equals(object?)`, `GetHashCode()`, and the `==`/`!=` operators.
- **Correct inheritance behavior**: Unlike the class example shown earlier, records handle inheritance scenarios correctly.
- **Automatic implementation**: Records automatically implement <xref:System.IEquatable%601?displayProperty=nameWithType> and override <xref:System.Object.Equals%2A?displayProperty=nameWithType>, <xref:System.Object.GetHashCode%2A?displayProperty=nameWithType>, and the `==`/`!=` operators.
- **Correct inheritance behavior**: Records implement `IEquatable<T>` using virtual methods that check the runtime type of both operands, ensuring correct behavior in inheritance hierarchies and polymorphic scenarios.
- **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.

## Records with members that use reference equality

When records contain members that use reference equality, the automatic value equality behavior of records doesn't work as expected. This applies to collections like <xref:System.Collections.Generic.List%601?displayProperty=nameWithType>, arrays, and other reference types that don't implement value-based equality (with the notable exception of <xref:System.String?displayProperty=nameWithType>, which does implement value equality).

> [!IMPORTANT]
> While records provide excellent value equality for basic data types, they don't automatically solve value equality for members that use reference equality. If a record contains a <xref:System.Collections.Generic.List%601?displayProperty=nameWithType>, <xref:System.Array?displayProperty=nameWithType>, or other reference types that don't implement value equality, two record instances with identical content in those members will still not be equal because the members use reference equality.
>
> ```csharp
> public record PersonWithHobbies(string Name, List<string> Hobbies);
>
> var person1 = new PersonWithHobbies("Alice", new List<string> { "Reading", "Swimming" });
> var person2 = new PersonWithHobbies("Alice", new List<string> { "Reading", "Swimming" });
>
> Console.WriteLine(person1.Equals(person2)); // False - different List instances!
> ```
This is because records use the <xref:System.Object.Equals%2A?displayProperty=nameWithType> method of each member, and collection types typically use reference equality rather than comparing their contents.
The following shows the problem:
:::code language="csharp" source="snippets/how-to-define-value-equality-for-a-type/RecordCollectionsIssue/Program.cs" id="ProblemExample":::
Here's how this behaves when you run the code:
:::code language="csharp" source="snippets/how-to-define-value-equality-for-a-type/RecordCollectionsIssue/Program.cs" id="ProblemDemonstration":::
### Solutions for records with reference-equality members
- **Custom <xref:System.IEquatable%601?displayProperty=nameWithType> implementation**: Replace the compiler-generated equality with a hand-coded version that provides content-based comparison for reference-equality members. For collections, implement element-by-element comparison using <xref:System.Linq.Enumerable.SequenceEqual%2A?displayProperty=nameWithType> or similar methods.
- **Use value types where possible**: Consider if your data can be represented with value types or immutable structures that naturally support value equality, such as <xref:System.Numerics.Vector%601?displayProperty=nameWithType> or <xref:System.Numerics.Plane>.
- **Use types with value-based equality**: For collections, consider using types that implement value-based equality or implement custom collection types that override <xref:System.Object.Equals%2A?displayProperty=nameWithType> to provide content-based comparison, such as <xref:System.Collections.Immutable.ImmutableArray%601?displayProperty=nameWithType> or <xref:System.Collections.Immutable.ImmutableList%601?displayProperty=nameWithType>.
- **Design with reference equality in mind**: Accept that some members will use reference equality and design your application logic accordingly, ensuring that you reuse the same instances when equality is important.
Here's an example of implementing custom equality for records with collections:
:::code language="csharp" source="snippets/how-to-define-value-equality-for-a-type/RecordCollectionsIssue/Program.cs" id="SolutionExample":::
This custom implementation works correctly:
:::code language="csharp" source="snippets/how-to-define-value-equality-for-a-type/RecordCollectionsIssue/Program.cs" id="SolutionDemonstration":::
The same issue affects arrays and other collection types:
:::code language="csharp" source="snippets/how-to-define-value-equality-for-a-type/RecordCollectionsIssue/Program.cs" id="OtherTypes":::
Arrays also use reference equality, producing the same unexpected results:
:::code language="csharp" source="snippets/how-to-define-value-equality-for-a-type/RecordCollectionsIssue/Program.cs" id="ArrayExample":::
Even readonly collections exhibit this reference equality behavior:
:::code language="csharp" source="snippets/how-to-define-value-equality-for-a-type/RecordCollectionsIssue/Program.cs" id="ImmutableExample":::
The key insight is that records solve the *structural* equality problem but don't change the *semantic* equality behavior of the types they contain.
## Class example
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:
Expand All @@ -83,9 +142,60 @@ The `==` and `!=` operators can be used with classes even if the class does not
> Console.WriteLine(p1.Equals(p2)); // output: True
> ```
>
> This code reports that `p1` equals `p2` despite the difference in `z` values. The difference is ignored because the compiler picks the `TwoDPoint` implementation of `IEquatable` based on the compile-time type.
>
> The built-in value equality of `record` types handles scenarios like this correctly. If `TwoDPoint` and `ThreeDPoint` were `record` types, the result of `p1.Equals(p2)` would be `False`. For more information, see [Equality in `record` type inheritance hierarchies](../../language-reference/builtin-types/record.md#equality-in-inheritance-hierarchies).
> This code reports that `p1` equals `p2` despite the difference in `z` values. The difference is ignored because the compiler picks the `TwoDPoint` implementation of `IEquatable` based on the compile-time type. This is a fundamental issue with polymorphic equality in inheritance hierarchies.
## Polymorphic equality
When implementing value equality in inheritance hierarchies with classes, the standard approach shown in the class example can lead to incorrect behavior when objects are used polymorphically. The issue occurs because <xref:System.IEquatable%601?displayProperty=nameWithType> implementations are chosen based on compile-time type, not runtime type.
### The problem with standard implementations
Consider this problematic scenario:
```csharp
TwoDPoint p1 = new ThreeDPoint(1, 2, 3); // Declared as TwoDPoint
TwoDPoint p2 = new ThreeDPoint(1, 2, 4); // Declared as TwoDPoint
Console.WriteLine(p1.Equals(p2)); // True - but should be False!
```
The comparison returns `True` because the compiler selects `TwoDPoint.Equals(TwoDPoint)` based on the declared type, ignoring the `Z` coordinate differences.

The key to correct polymorphic equality is ensuring that all equality comparisons use the virtual <xref:System.Object.Equals%2A?displayProperty=nameWithType> method, which can check runtime types and handle inheritance correctly. This can be achieved by using explicit interface implementation for <xref:System.IEquatable%601?displayProperty=nameWithType> that delegates to the virtual method:

The base class demonstrates the key patterns:

:::code language="csharp" source="snippets/how-to-define-value-equality-for-a-type/ValueEqualityPolymorphic/Program.cs" id="TwoDPointClass":::

The derived class correctly extends the equality logic:

:::code language="csharp" source="snippets/how-to-define-value-equality-for-a-type/ValueEqualityPolymorphic/Program.cs" id="ThreeDPointClass":::

Here's how this implementation handles the problematic polymorphic scenarios:

:::code language="csharp" source="snippets/how-to-define-value-equality-for-a-type/ValueEqualityPolymorphic/Program.cs" id="PolymorphicTest":::

The implementation also correctly handles direct type comparisons:

:::code language="csharp" source="snippets/how-to-define-value-equality-for-a-type/ValueEqualityPolymorphic/Program.cs" id="DirectTest":::

The equality implementation also works properly with collections:

:::code language="csharp" source="snippets/how-to-define-value-equality-for-a-type/ValueEqualityPolymorphic/Program.cs" id="CollectionTest":::

The preceding code demonstrates key elements to implementing value based equality:

- **Virtual `Equals(object?)` override**: The main equality logic happens in the virtual <xref:System.Object.Equals%2A?displayProperty=nameWithType> method, which is called regardless of compile-time type.
- **Runtime type checking**: Using `this.GetType() != p.GetType()` ensures that objects of different types are never considered equal.
- **Explicit interface implementation**: The <xref:System.IEquatable%601?displayProperty=nameWithType> implementation delegates to the virtual method, preventing compile-time type selection issues.
- **Protected virtual helper method**: The `protected virtual Equals(TwoDPoint? p)` method allows derived classes to override equality logic while maintaining type safety.

Use this pattern when:

- You have inheritance hierarchies where value equality is important
- Objects might be used polymorphically (declared as base type, instantiated as derived type)
- You need reference types with value equality semantics

The preferred approach is to use `record` types to implement value based equality. This approach requires a more complex implementation than the standard approach and requires thorough testing of polymorphic scenarios to ensure correctness.

## Struct example

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
namespace RecordCollectionsIssue;

// <ProblemExample>
// Records with reference-equality members don't work as expected
public record PersonWithHobbies(string Name, List<string> Hobbies);
// </ProblemExample>

// <SolutionExample>
// A potential solution using IEquatable<T> with custom equality
public record PersonWithHobbiesFixed(string Name, List<string> Hobbies) : IEquatable<PersonWithHobbiesFixed>
{
public virtual bool Equals(PersonWithHobbiesFixed? other)
{
if (ReferenceEquals(null, other)) return false;
if (ReferenceEquals(this, other)) return true;

// Use SequenceEqual for List comparison
return Name == other.Name && Hobbies.SequenceEqual(other.Hobbies);
}

public override int GetHashCode()
{
// Create hash based on content, not reference
var hashCode = new HashCode();
hashCode.Add(Name);
foreach (var hobby in Hobbies)
{
hashCode.Add(hobby);
}
return hashCode.ToHashCode();
}
}
// </SolutionExample>

// <OtherTypes>
// These also use reference equality - the issue persists
public record PersonWithHobbiesArray(string Name, string[] Hobbies);

public record PersonWithHobbiesImmutable(string Name, IReadOnlyList<string> Hobbies);
// </OtherTypes>

// <MainProgram>
class Program
{
static void Main(string[] args)
{
// <ProblemDemonstration>
Console.WriteLine("=== Records with Collections - The Problem ===");

// Problem: Records with mutable collections use reference equality for the collection
var person1 = new PersonWithHobbies("Alice", [ "Reading", "Swimming" ]);
var person2 = new PersonWithHobbies("Alice", [ "Reading", "Swimming" ]);

Console.WriteLine($"person1: {person1}");
Console.WriteLine($"person2: {person2}");
Console.WriteLine($"person1.Equals(person2): {person1.Equals(person2)}"); // False! Different List instances
Console.WriteLine($"Lists have same content: {person1.Hobbies.SequenceEqual(person2.Hobbies)}"); // True
Console.WriteLine();
// </ProblemDemonstration>

// <SolutionDemonstration>
Console.WriteLine("=== Solution 1: Custom IEquatable Implementation ===");

var personFixed1 = new PersonWithHobbiesFixed("Bob", [ "Cooking", "Hiking" ]);
var personFixed2 = new PersonWithHobbiesFixed("Bob", [ "Cooking", "Hiking" ]);

Console.WriteLine($"personFixed1: {personFixed1}");
Console.WriteLine($"personFixed2: {personFixed2}");
Console.WriteLine($"personFixed1.Equals(personFixed2): {personFixed1.Equals(personFixed2)}"); // True! Custom equality
Console.WriteLine();
// </SolutionDemonstration>

// <ArrayExample>
Console.WriteLine("=== Arrays Also Use Reference Equality ===");

var personArray1 = new PersonWithHobbiesArray("Charlie", ["Gaming", "Music" ]);
var personArray2 = new PersonWithHobbiesArray("Charlie", ["Gaming", "Music" ]);

Console.WriteLine($"personArray1: {personArray1}");
Console.WriteLine($"personArray2: {personArray2}");
Console.WriteLine($"personArray1.Equals(personArray2): {personArray1.Equals(personArray2)}"); // False! Arrays use reference equality too
Console.WriteLine($"Arrays have same content: {personArray1.Hobbies.SequenceEqual(personArray2.Hobbies)}"); // True
Console.WriteLine();
// </ArrayExample>

// <ImmutableExample>
Console.WriteLine("=== Same Issue with IReadOnlyList ===");

var personImmutable1 = new PersonWithHobbiesImmutable("Diana", [ "Art", "Travel" ]);
var personImmutable2 = new PersonWithHobbiesImmutable("Diana", [ "Art", "Travel" ]);

Console.WriteLine($"personImmutable1: {personImmutable1}");
Console.WriteLine($"personImmutable2: {personImmutable2}");
Console.WriteLine($"personImmutable1.Equals(personImmutable2): {personImmutable1.Equals(personImmutable2)}"); // False! Reference equality
Console.WriteLine($"Content is the same: {personImmutable1.Hobbies.SequenceEqual(personImmutable2.Hobbies)}"); // True
Console.WriteLine();
// </ImmutableExample>

Console.WriteLine("=== Collection Behavior Summary ===");
Console.WriteLine("Type | Equals Result | Reason");
Console.WriteLine("----------------------------------|---------------|------------------");
Console.WriteLine($"Record with List<T> | {person1.Equals(person2),-13} | Reference equality");
Console.WriteLine($"Record with custom IEquatable<T> | {personFixed1.Equals(personFixed2),-13} | Custom equality logic");
Console.WriteLine($"Record with Array | {personArray1.Equals(personArray2),-13} | Reference equality");
Console.WriteLine($"Record with IReadOnlyList<T> | {personImmutable1.Equals(personImmutable2),-13} | Reference equality");

Console.WriteLine("\nPress any key to exit.");
Console.ReadKey();
}
}
// </MainProgram>

/* Expected Output:
=== Records with Collections - The Problem ===
person1: PersonWithHobbies { Name = Alice, Hobbies = System.Collections.Generic.List`1[System.String] }
person2: PersonWithHobbies { Name = Alice, Hobbies = System.Collections.Generic.List`1[System.String] }
person1.Equals(person2): False
Lists have same content: True

=== Solution 1: Custom IEquatable Implementation ===
personFixed1: PersonWithHobbiesFixed { Name = Bob, Hobbies = System.Collections.Generic.List`1[System.String] }
personFixed2: PersonWithHobbiesFixed { Name = Bob, Hobbies = System.Collections.Generic.List`1[System.String] }
personFixed1.Equals(personFixed2): True

=== Arrays Also Use Reference Equality ===
personArray1: PersonWithHobbiesArray { Name = Charlie, Hobbies = System.String[] }
personArray2: PersonWithHobbiesArray { Name = Charlie, Hobbies = System.String[] }
personArray1.Equals(personArray2): False
Arrays have same content: True

=== Same Issue with IReadOnlyList ===
personImmutable1: PersonWithHobbiesImmutable { Name = Diana, Hobbies = System.String[] }
personImmutable2: PersonWithHobbiesImmutable { Name = Diana, Hobbies = System.String[] }
personImmutable1.Equals(personImmutable2): False
Content is the same: True

=== Collection Behavior Summary ===
Type | Equals Result | Reason
----------------------------------|---------------|------------------
Record with List<T> | False | Reference equality
Record with custom IEquatable<T> | True | Custom equality logic
Record with Array | False | Reference equality
Record with IReadOnlyList<T> | False | Reference equality
*/
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>

</Project>
Loading