From 5e15be85bb81557413af371384e542a1729ec7f7 Mon Sep 17 00:00:00 2001 From: Bill Wagner Date: Tue, 18 Aug 2026 11:28:55 -0400 Subject: [PATCH 01/11] Add Expressions: operators article (issue #55334) Create docs/csharp/fundamentals/expressions/operators.md covering arithmetic, unary, increment/decrement, relational, equality survey, conditional-logical, conditional (?:), simple and compound assignment. - Add operators snippets project (net10.0, nullable, implicit usings) with 9 region-marked examples; 0 warnings, 0 errors - Add operators.md TOC node under Expressions and statements - Add reciprocal link in expressions/index.md - Add reciprocal link in expressions/equality.md - Link excluded operators (shift/bitwise, checked/unchecked) to existing Language Reference pages Closes #55334 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: f5acfb15-adff-4860-a307-213196efda1c --- .../fundamentals/expressions/equality.md | 1 + docs/csharp/fundamentals/expressions/index.md | 1 + .../fundamentals/expressions/operators.md | 152 ++++++++++++++++++ .../expressions/snippets/operators/Program.cs | 152 ++++++++++++++++++ .../snippets/operators/operators.csproj | 10 ++ docs/csharp/toc.yml | 2 + 6 files changed, 318 insertions(+) create mode 100644 docs/csharp/fundamentals/expressions/operators.md create mode 100644 docs/csharp/fundamentals/expressions/snippets/operators/Program.cs create mode 100644 docs/csharp/fundamentals/expressions/snippets/operators/operators.csproj diff --git a/docs/csharp/fundamentals/expressions/equality.md b/docs/csharp/fundamentals/expressions/equality.md index efbb6325469d7..dd20b71286498 100644 --- a/docs/csharp/fundamentals/expressions/equality.md +++ b/docs/csharp/fundamentals/expressions/equality.md @@ -119,3 +119,4 @@ A common use is inside an `Equals` override to short-circuit the full comparison - [Records](../types/records.md) - [Tuples and deconstruction](../types/tuples.md) - [Equality operators (language reference)](../../language-reference/operators/equality-operators.md) +- [Arithmetic, comparison, logical, and assignment operators](operators.md) — the equality operator survey alongside arithmetic, logical, and assignment operators diff --git a/docs/csharp/fundamentals/expressions/index.md b/docs/csharp/fundamentals/expressions/index.md index d36c29dae0f52..a8eb292482c94 100644 --- a/docs/csharp/fundamentals/expressions/index.md +++ b/docs/csharp/fundamentals/expressions/index.md @@ -100,6 +100,7 @@ For a broader look at null-safe operators, see [C# null operators](../null-safet ## See also - [C# operators and expressions (language reference)](../../language-reference/operators/index.md) — full precedence table and every operator +- [Arithmetic, comparison, logical, and assignment operators](operators.md) — the everyday operators in depth - [Equality comparisons](equality.md) — how `==`, `!=`, and `Equals` work - [C# null operators](../null-safety/null-operators.md) — `?.`, `??`, and `??=` - [Boolean logical operators](../../language-reference/operators/boolean-logical-operators.md) diff --git a/docs/csharp/fundamentals/expressions/operators.md b/docs/csharp/fundamentals/expressions/operators.md new file mode 100644 index 0000000000000..7f582cd4def96 --- /dev/null +++ b/docs/csharp/fundamentals/expressions/operators.md @@ -0,0 +1,152 @@ +--- +title: "C# arithmetic, comparison, logical, and assignment operators" +description: Learn how C# arithmetic, relational, equality, logical, conditional, and assignment operators work, including integer division, short-circuit evaluation, and compound assignment. +ms.date: 08/18/2026 +ms.topic: concept-article +ai-usage: ai-assisted +--- + +# C# operators + +> [!TIP] +> This article is part of the **Fundamentals** section for developers who already know at least one programming language and are learning C#. If you're new to programming, start with the [Get started](../../tour-of-csharp/tutorials/index.md) tutorials first. +> +> **Coming from another language?** Most operators in this article (`+`, `-`, `*`, `/`, `%`, `&&`, `||`, `!`, `==`, `!=`, `<`, `>`, comparison operators, and `=`) work the same as in Java, C++, and JavaScript. The main surprises for newcomers are integer division behavior, the prefix/postfix distinction for `++`/`--`, and the way compound assignment converts back to the left-hand-side type. + +An *operator* combines one or more *operands* into a single value. You already know about expressions and operator precedence from [C# expressions](index.md); this article goes deeper into the specific operators you'll use every day. + +## Arithmetic operators + +The five arithmetic operators perform numeric calculations. + +| Operator | Name | Example | Result | +|----------|------|---------|--------| +| `+` | Addition | `10 + 3` | `13` | +| `-` | Subtraction | `10 - 3` | `7` | +| `*` | Multiplication | `10 * 3` | `30` | +| `/` | Division | `10 / 3` | `3` | +| `%` | Remainder | `10 % 3` | `1` | + +:::code language="csharp" source="snippets/operators/Program.cs" ID="ArithmeticOps"::: + +**Integer division truncates toward zero.** When both operands are integers, `/` discards the fractional part: `7 / 2` is `3`, not `3.5`. To get a decimal result, make at least one operand a floating-point type: `7.0 / 2` is `3.5`. This differs from some languages where `/` always produces a floating-point result. + +**Remainder (`%`) returns what's left over** after integer division: `10 % 3` is `1` because `10 = 3 × 3 + 1`. It's useful for cycling through a fixed range (`index % length`), testing divisibility (`n % 2 == 0`), and extracting digits. + +## Unary operators + +Unary operators act on a single operand. + +:::code language="csharp" source="snippets/operators/Program.cs" ID="UnaryOps"::: + +- `+x` (unary plus) — leaves the value unchanged; rarely written explicitly but valid. +- `-x` (unary minus) — negates the value. +- `!x` (logical NOT) — flips `true` to `false` and `false` to `true`. You'll use `!` often: `if (!list.Contains(item))`. + +## Increment and decrement + +`++` adds 1 and `--` subtracts 1. Both have a *prefix* form and a *postfix* form that differ in which value is returned: + +:::code language="csharp" source="snippets/operators/Program.cs" ID="IncrementDecrement"::: + +- **Prefix** (`++i`, `--i`): increments or decrements the variable first, then returns the *new* value. +- **Postfix** (`i++`, `i--`): returns the *current* value first, then increments or decrements the variable. + +When `++` or `--` appears as a standalone statement (not part of a larger expression), prefix and postfix have the same effect. The distinction matters only when the result is used — for example, in an assignment or as a method argument. + +## Relational operators + +Relational operators compare two values and return a `bool`. + +| Operator | Meaning | Example | +|----------|---------|---------| +| `<` | Less than | `speed < limit` | +| `>` | Greater than | `speed > limit` | +| `<=` | Less than or equal | `score <= 100` | +| `>=` | Greater than or equal | `score >= 0` | + +:::code language="csharp" source="snippets/operators/Program.cs" ID="RelationalOps"::: + +Relational operators work on all numeric types and `char`. For `char`, comparison is based on the numeric Unicode code point. + +## Equality operators + +`==` and `!=` check whether two values are equal or not. + +:::code language="csharp" source="snippets/operators/Program.cs" ID="EqualityOps"::: + +For numeric types and `string`, equality tests the values. For reference types, the default is identity (whether two variables point to the same object), but many types including `string` and `record` override this to compare content. For the full picture — how equality works across value types, reference types, records, and structs — see [Equality comparisons](equality.md). + +> [!NOTE] +> A common source of bugs is accidentally writing `=` (assignment) where you intended `==` (equality check). The C# compiler catches the most common forms of this mistake and issues an error or warning, but it pays to double-check any `if` condition that contains `=`. + +## Conditional-logical operators + +`&&` (AND) and `||` (OR) combine `bool` expressions. + +:::code language="csharp" source="snippets/operators/Program.cs" ID="LogicalOps"::: + +Both operators *short-circuit*: they skip evaluating the right operand when the result is already determined. + +- `&&` returns `false` as soon as the left side is `false`. The right side is never evaluated. +- `||` returns `true` as soon as the left side is `true`. The right side is never evaluated. + +Short-circuit behavior has a practical benefit: you can safely guard an operation on the right side with a null check on the left side, as the example above shows. If `items` is `null`, the `&&` stops there — `items.Count` is never called, so no `NullReferenceException` is thrown. + +## Conditional operator `?:` + +The conditional operator (also called the *ternary* operator) evaluates one of two expressions based on a condition: + +``` +condition ? value-when-true : value-when-false +``` + +:::code language="csharp" source="snippets/operators/Program.cs" ID="ConditionalOp"::: + +The `?:` operator always evaluates exactly one branch — the side that doesn't match the condition is never evaluated. This makes it safe to use an expression on one side that would fail for other inputs, as long as the condition properly guards it. + +Use `?:` for simple, inline choices. For multi-way conditions or blocks of code, an `if`/`else` statement is usually clearer. + +## Assignment operators + +The simple assignment operator `=` stores a value in a variable: + +```csharp +int level = 1; // declaration + initialization +level = 5; // reassignment +``` + +Assignment in C# is *right-associative*, which means `a = b = c = 0` evaluates right to left: `c` gets `0`, then `b` gets `0`, then `a` gets `0`. + +### Compound assignment + +Compound assignment operators combine a binary operation with assignment: + +| Operator | Equivalent to | +|----------|---------------| +| `x += y` | `x = x + y` | +| `x -= y` | `x = x - y` | +| `x *= y` | `x = x * y` | +| `x /= y` | `x = x / y` | +| `x %= y` | `x = x % y` | + +:::code language="csharp" source="snippets/operators/Program.cs" ID="AssignmentOps"::: + +Compound assignment is more than just a shorthand. It evaluates the left-hand side **exactly once** and then converts the result back to the left-hand-side type. This matters when the left side has side effects (like an array indexer), and it's why compound assignment on a `byte` variable compiles without an explicit cast while the expanded form does not: + +:::code language="csharp" source="snippets/operators/Program.cs" ID="AssignmentChain"::: + +`small += 10` compiles because the compiler inserts the narrowing conversion automatically. `small = small + 10` would require an explicit `(byte)` cast, because the arithmetic promotes both operands to `int`. + +## Operators not covered here + +This article covers the operators you'll encounter most in everyday code. The C# language includes additional operators that are useful in specific scenarios: + +- **Shift operators** (`<<`, `>>`, `>>>`) and **bitwise/integer logical operators** (`&`, `|`, `^`, `~`) — for bit-level manipulation: [Bitwise and shift operators](../../language-reference/operators/bitwise-and-shift-operators.md) +- **`checked` and `unchecked`** — for controlling integer overflow behavior: [Checked and unchecked](../../language-reference/statements/checked-and-unchecked.md) + +## See also + +- [C# expressions](index.md) — how expressions form and how operator precedence works +- [Equality comparisons](equality.md) — how `==`, `!=`, and `Equals` work across different types +- [C# operators and expressions (language reference)](../../language-reference/operators/index.md) — full precedence table and every operator diff --git a/docs/csharp/fundamentals/expressions/snippets/operators/Program.cs b/docs/csharp/fundamentals/expressions/snippets/operators/Program.cs new file mode 100644 index 0000000000000..7f044924f4f92 --- /dev/null +++ b/docs/csharp/fundamentals/expressions/snippets/operators/Program.cs @@ -0,0 +1,152 @@ +// +int apples = 10; +int oranges = 3; + +Console.WriteLine(apples + oranges); // => 13 (addition) +Console.WriteLine(apples - oranges); // => 7 (subtraction) +Console.WriteLine(apples * oranges); // => 30 (multiplication) +Console.WriteLine(apples / oranges); // => 3 (integer division: truncates toward zero) +Console.WriteLine(apples % oranges); // => 1 (remainder) + +// Integer division always truncates toward zero — the fractional part is discarded +int result = 7 / 2; +Console.WriteLine(result); // => 3, not 3.5 + +// To get a decimal result, at least one operand must be a double or float +double precise = 7.0 / 2; +Console.WriteLine(precise); // => 3.5 +// + +// +int temperature = 20; +int windChill = -5; + +int heatIndex = +temperature; // unary +: value unchanged (rarely needed) +int coldFactor = -windChill; // unary -: negates the value → 5 + +Console.WriteLine(heatIndex); // => 20 +Console.WriteLine(coldFactor); // => 5 + +bool isRaining = false; +bool isSunny = !isRaining; // logical NOT: flips true/false +Console.WriteLine(isSunny); // => True +// + +// +int counter = 5; + +// Prefix: increment first, then use the new value +int a = ++counter; +Console.WriteLine(a); // => 6 +Console.WriteLine(counter); // => 6 + +// Postfix: use the current value first, then increment +int b = counter++; +Console.WriteLine(b); // => 6 (value before increment) +Console.WriteLine(counter); // => 7 (incremented after) + +// Decrement works the same way +int score = 10; +Console.WriteLine(score--); // => 10 (current value; score becomes 9) +Console.WriteLine(score); // => 9 +// + +// +int speed = 75; +int limit = 60; + +Console.WriteLine(speed > limit); // => True (greater than) +Console.WriteLine(speed < limit); // => False (less than) +Console.WriteLine(speed >= limit); // => True (greater than or equal) +Console.WriteLine(speed <= limit); // => False (less than or equal) + +// Relational operators work on all numeric types and char +char grade = 'B'; +Console.WriteLine(grade >= 'A' && grade <= 'C'); // => True +// + +// +int expected = 42; +int actual = 42; + +Console.WriteLine(actual == expected); // => True (values match) +Console.WriteLine(actual != expected); // => False (values differ) + +string name = "Alice"; +Console.WriteLine(name == "Alice"); // => True (string content matches) +Console.WriteLine(name == "alice"); // => False (case-sensitive) + +// A common mistake: assignment (=) instead of equality (==) +// The following line assigns 10 to x, not a comparison: +// bool wrong = (x = 10); // compiler error: can't convert int to bool directly +// Use == to compare +int x = 5; +Console.WriteLine(x == 10); // => False +// + +// +int age = 20; +bool hasTicket = true; + +// && (AND): both sides must be true +bool canEnter = age >= 18 && hasTicket; +Console.WriteLine(canEnter); // => True + +// || (OR): at least one side must be true +bool freeEntry = age < 5 || age >= 65; +Console.WriteLine(freeEntry); // => False + +// Short-circuit: right side is skipped when the result is already determined +// Here, items.Count is never called if items is null +List? items = null; +bool hasItems = items != null && items.Count > 0; +Console.WriteLine(hasItems); // => False (short-circuits; no NullReferenceException) +// + +// +int temperature2 = 35; + +// condition ? value-when-true : value-when-false +string weather = temperature2 > 30 ? "hot" : "comfortable"; +Console.WriteLine(weather); // => hot + +// Only the matching branch evaluates — the other branch is never run +int divisor = 0; +// The division 10 / divisor is never evaluated because divisor == 0 is true +int safe = divisor == 0 ? -1 : 10 / divisor; +Console.WriteLine(safe); // => -1 + +// Nested ?: is possible but use sparingly — an if/else is often clearer +int points = 85; +string grade2 = points >= 90 ? "A" : points >= 70 ? "B" : "C"; +Console.WriteLine(grade2); // => B +// + +// +int level = 1; +level = 5; // simple assignment: replaces the value +Console.WriteLine(level); // => 5 + +// Compound assignment: short form of binary operation + assignment +int hp = 100; +hp += 20; // same as: hp = hp + 20 +hp -= 10; // same as: hp = hp - 10 +hp *= 2; // same as: hp = hp * 2 +hp /= 3; // same as: hp = hp / 3 (integer division) +hp %= 7; // same as: hp = hp % 7 + +// Trace: 100 +20→ 120 -10→ 110 *2→ 220 /3→ 73 (integer division) %7→ 3 +Console.WriteLine(hp); // => 3 +// + +// +// Assignment is right-associative: evaluated right to left +int a2, b2, c2; +a2 = b2 = c2 = 0; // c2 = 0 first, then b2 = 0, then a2 = 0 +Console.WriteLine($"{a2} {b2} {c2}"); // => 0 0 0 + +// Compound assignment evaluates the left side once and converts back to the LHS type +byte small = 200; +small += 10; // equivalent to: small = (byte)(small + 10); result wraps to 210 +Console.WriteLine(small); // => 210 +// diff --git a/docs/csharp/fundamentals/expressions/snippets/operators/operators.csproj b/docs/csharp/fundamentals/expressions/snippets/operators/operators.csproj new file mode 100644 index 0000000000000..dfb40caafcf9a --- /dev/null +++ b/docs/csharp/fundamentals/expressions/snippets/operators/operators.csproj @@ -0,0 +1,10 @@ + + + + Exe + net10.0 + enable + enable + + + diff --git a/docs/csharp/toc.yml b/docs/csharp/toc.yml index 3e5ee8035ff78..fc597d7f78412 100644 --- a/docs/csharp/toc.yml +++ b/docs/csharp/toc.yml @@ -119,6 +119,8 @@ items: href: fundamentals/expressions/index.md - name: Equality href: fundamentals/expressions/equality.md + - name: Operators + href: fundamentals/expressions/operators.md - name: Selection statements href: fundamentals/statements/selection.md - name: Iteration statements From 4aa28fed8a9da8f583ffe089fc6d1c4a6ef479a2 Mon Sep 17 00:00:00 2001 From: Bill Wagner Date: Tue, 18 Aug 2026 11:41:14 -0400 Subject: [PATCH 02/11] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- .../fundamentals/expressions/snippets/operators/Program.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/csharp/fundamentals/expressions/snippets/operators/Program.cs b/docs/csharp/fundamentals/expressions/snippets/operators/Program.cs index 7f044924f4f92..783f7158300fd 100644 --- a/docs/csharp/fundamentals/expressions/snippets/operators/Program.cs +++ b/docs/csharp/fundamentals/expressions/snippets/operators/Program.cs @@ -147,6 +147,6 @@ // Compound assignment evaluates the left side once and converts back to the LHS type byte small = 200; -small += 10; // equivalent to: small = (byte)(small + 10); result wraps to 210 +small += 10; // equivalent to: small = (byte)(small + 10); result is 210 Console.WriteLine(small); // => 210 // From f62259f7948d63ccf07164917c371797025f0010 Mon Sep 17 00:00:00 2001 From: Bill Wagner Date: Tue, 18 Aug 2026 14:09:27 -0400 Subject: [PATCH 03/11] 2nd draft - Add negative integer division example (-7/2 = -3) to clarify truncation toward zero - Add negative-operand remainder examples (-7%3 = -1, 7%-3 = 1) with sign rule explanation - Explain char relational comparison uses Unicode code point values (tied to grade example) - Update != comment to explicitly say 'true when values are not equal' - Remove invalid commented-out code from EqualityOps snippet; move === example to NOTE callout in article prose as fenced code block - Remove nested conditional example and related prose from ConditionalOp section - Add Console.WriteLine after each compound assignment step (hp progression: 100 -> 120 -> 110 -> 220 -> 73 -> 3) - Rename 'Operators not covered here' to 'Other C# operators'; expand bullets with concise definitions - Add displayName to toc.yml entry for operators.md with all covered operators Copilot-Session: f5acfb15-adff-4860-a307-213196efda1c Co-Authored-By: Copilot <223556219+Copilot@users.noreply.github.com> --- .../fundamentals/expressions/operators.md | 29 +++++++++++------ .../expressions/snippets/operators/Program.cs | 31 ++++++++++--------- docs/csharp/toc.yml | 1 + 3 files changed, 38 insertions(+), 23 deletions(-) diff --git a/docs/csharp/fundamentals/expressions/operators.md b/docs/csharp/fundamentals/expressions/operators.md index 7f582cd4def96..b95c55e817111 100644 --- a/docs/csharp/fundamentals/expressions/operators.md +++ b/docs/csharp/fundamentals/expressions/operators.md @@ -29,9 +29,9 @@ The five arithmetic operators perform numeric calculations. :::code language="csharp" source="snippets/operators/Program.cs" ID="ArithmeticOps"::: -**Integer division truncates toward zero.** When both operands are integers, `/` discards the fractional part: `7 / 2` is `3`, not `3.5`. To get a decimal result, make at least one operand a floating-point type: `7.0 / 2` is `3.5`. This differs from some languages where `/` always produces a floating-point result. +**Integer division truncates toward zero.** When both operands are integers, `/` discards the fractional part: `7 / 2` is `3`, not `3.5`. Truncation is toward zero, not toward the smaller number: `-7 / 2` is `-3` (not `-4`). To get a decimal result, make at least one operand a floating-point type: `7.0 / 2` is `3.5`. This differs from some languages where `/` always produces a floating-point result. -**Remainder (`%`) returns what's left over** after integer division: `10 % 3` is `1` because `10 = 3 × 3 + 1`. It's useful for cycling through a fixed range (`index % length`), testing divisibility (`n % 2 == 0`), and extracting digits. +**Remainder (`%`) returns what's left over** after integer division: `10 % 3` is `1` because `10 = 3 × 3 + 1`. It's useful for cycling through a fixed range (`index % length`), testing divisibility (`n % 2 == 0`), and extracting digits. With negative operands, the sign of the result matches the sign of the *dividend* (the left operand): `-7 % 3` is `-1` and `7 % -3` is `1`. ## Unary operators @@ -67,18 +67,25 @@ Relational operators compare two values and return a `bool`. :::code language="csharp" source="snippets/operators/Program.cs" ID="RelationalOps"::: -Relational operators work on all numeric types and `char`. For `char`, comparison is based on the numeric Unicode code point. +Relational operators work on all numeric types and `char`. For `char`, comparison uses the character's numeric Unicode code point value, not any alphabetical or domain-specific ordering. In the grade example above, `'B'` is greater than or equal to `'A'` because `'B'` has Unicode value 66 and `'A'` has Unicode value 65 — the *numbers* determine the comparison, not the meaning of the letter grades. ## Equality operators -`==` and `!=` check whether two values are equal or not. +`==` and `!=` check whether two values are equal or not. `!=` is `true` when the operands are **not** equal, and `false` when they are. :::code language="csharp" source="snippets/operators/Program.cs" ID="EqualityOps"::: For numeric types and `string`, equality tests the values. For reference types, the default is identity (whether two variables point to the same object), but many types including `string` and `record` override this to compare content. For the full picture — how equality works across value types, reference types, records, and structs — see [Equality comparisons](equality.md). > [!NOTE] -> A common source of bugs is accidentally writing `=` (assignment) where you intended `==` (equality check). The C# compiler catches the most common forms of this mistake and issues an error or warning, but it pays to double-check any `if` condition that contains `=`. +> C# doesn't have a `===` operator. Writing `===` is a compile-time error: +> +> ```csharp +> // This does not compile — C# has no === operator +> bool same = (x === 10); +> ``` +> +> If you're coming from JavaScript, use `==` for value comparison (C# `==` already compares by value for primitive types and strings). A common related bug is accidentally writing `=` (assignment) where you meant `==` (equality check). The compiler catches the most common forms, but double-check any `if` condition that contains `=`. ## Conditional-logical operators @@ -138,12 +145,16 @@ Compound assignment is more than just a shorthand. It evaluates the left-hand si `small += 10` compiles because the compiler inserts the narrowing conversion automatically. `small = small + 10` would require an explicit `(byte)` cast, because the arithmetic promotes both operands to `int`. -## Operators not covered here +## Other C# operators -This article covers the operators you'll encounter most in everyday code. The C# language includes additional operators that are useful in specific scenarios: +This article covers the operators you'll encounter most in everyday code. The C# language includes more operators useful in specific scenarios: -- **Shift operators** (`<<`, `>>`, `>>>`) and **bitwise/integer logical operators** (`&`, `|`, `^`, `~`) — for bit-level manipulation: [Bitwise and shift operators](../../language-reference/operators/bitwise-and-shift-operators.md) -- **`checked` and `unchecked`** — for controlling integer overflow behavior: [Checked and unchecked](../../language-reference/statements/checked-and-unchecked.md) +- **Shift operators** (`<<`, `>>`, `>>>`) — shift the bits of an integer value left or right by a specified number of positions. **Bitwise and integer logical operators** (`&`, `|`, `^`, `~`) — combine or invert integer values one bit at a time, useful in flags, masks, and low-level code: [Bitwise and shift operators](../../language-reference/operators/bitwise-and-shift-operators.md) +- **`checked` and `unchecked`** — control whether integer overflow throws an exception (`checked`) or wraps silently (`unchecked`): [Checked and unchecked](../../language-reference/statements/checked-and-unchecked.md) +- **Null operators** (`??`, `??=`, `?.`, `?[]`) — safely handle `null` values by providing defaults or short-circuiting member access: [Null operators](../null-safety/null-operators.md) +- **Type-test and conversion operators** (`is`, `as`, `typeof`, cast `(T)`) — check or convert a value's runtime type: [Type-testing and cast operators](../../language-reference/operators/type-testing-and-cast.md) +- **Range and index operators** (`..`, `^`) — create ranges and end-relative indexes for slicing arrays and spans: [Member access and null-conditional operators](../../language-reference/operators/member-access-operators.md) +- **Deconstruction assignment** — unpack a tuple or type into individual variables in a single expression: [Deconstructing tuples and other types](../../fundamentals/functional/deconstruct.md) ## See also diff --git a/docs/csharp/fundamentals/expressions/snippets/operators/Program.cs b/docs/csharp/fundamentals/expressions/snippets/operators/Program.cs index 783f7158300fd..963c8f34e4615 100644 --- a/docs/csharp/fundamentals/expressions/snippets/operators/Program.cs +++ b/docs/csharp/fundamentals/expressions/snippets/operators/Program.cs @@ -12,9 +12,17 @@ int result = 7 / 2; Console.WriteLine(result); // => 3, not 3.5 +// Truncation applies to negative results too: -7 / 2 is -3, not -4 +int negResult = -7 / 2; +Console.WriteLine(negResult); // => -3 + // To get a decimal result, at least one operand must be a double or float double precise = 7.0 / 2; Console.WriteLine(precise); // => 3.5 + +// Remainder with negative operands: the sign of the result matches the dividend +Console.WriteLine(-7 % 3); // => -1 (-7 = 3 × -2 + (-1)) +Console.WriteLine(7 % -3); // => 1 ( 7 = -3 × -2 + 1) // // @@ -61,25 +69,23 @@ Console.WriteLine(speed <= limit); // => False (less than or equal) // Relational operators work on all numeric types and char +// char comparison uses the character's numeric Unicode code point, not alphabetical position +// 'B' (U+0042, value 66) is less than 'A' (U+0041, value 65)? No — 'A' (65) < 'B' (66) char grade = 'B'; -Console.WriteLine(grade >= 'A' && grade <= 'C'); // => True +Console.WriteLine(grade >= 'A' && grade <= 'C'); // => True ('A'=65 <= 'B'=66 <= 'C'=67) // // int expected = 42; int actual = 42; -Console.WriteLine(actual == expected); // => True (values match) -Console.WriteLine(actual != expected); // => False (values differ) +Console.WriteLine(actual == expected); // => True (values are equal) +Console.WriteLine(actual != expected); // => False (true when values are not equal) string name = "Alice"; Console.WriteLine(name == "Alice"); // => True (string content matches) Console.WriteLine(name == "alice"); // => False (case-sensitive) -// A common mistake: assignment (=) instead of equality (==) -// The following line assigns 10 to x, not a comparison: -// bool wrong = (x = 10); // compiler error: can't convert int to bool directly -// Use == to compare int x = 5; Console.WriteLine(x == 10); // => False // @@ -115,11 +121,6 @@ // The division 10 / divisor is never evaluated because divisor == 0 is true int safe = divisor == 0 ? -1 : 10 / divisor; Console.WriteLine(safe); // => -1 - -// Nested ?: is possible but use sparingly — an if/else is often clearer -int points = 85; -string grade2 = points >= 90 ? "A" : points >= 70 ? "B" : "C"; -Console.WriteLine(grade2); // => B // // @@ -130,12 +131,14 @@ // Compound assignment: short form of binary operation + assignment int hp = 100; hp += 20; // same as: hp = hp + 20 +Console.WriteLine(hp); // => 120 hp -= 10; // same as: hp = hp - 10 +Console.WriteLine(hp); // => 110 hp *= 2; // same as: hp = hp * 2 +Console.WriteLine(hp); // => 220 hp /= 3; // same as: hp = hp / 3 (integer division) +Console.WriteLine(hp); // => 73 hp %= 7; // same as: hp = hp % 7 - -// Trace: 100 +20→ 120 -10→ 110 *2→ 220 /3→ 73 (integer division) %7→ 3 Console.WriteLine(hp); // => 3 // diff --git a/docs/csharp/toc.yml b/docs/csharp/toc.yml index fc597d7f78412..eba9d52ac5ffd 100644 --- a/docs/csharp/toc.yml +++ b/docs/csharp/toc.yml @@ -120,6 +120,7 @@ items: - name: Equality href: fundamentals/expressions/equality.md - name: Operators + displayName: "+, -, *, /, %, unary +, unary -, !, ++, --, <, >, <=, >=, ==, !=, &&, ||, ?:, =, +=, -=, *=, /=, %=" href: fundamentals/expressions/operators.md - name: Selection statements href: fundamentals/statements/selection.md From 79f6e6471f9a18d8b8a627f306b2d5a1c8985097 Mon Sep 17 00:00:00 2001 From: Bill Wagner Date: Tue, 18 Aug 2026 14:20:25 -0400 Subject: [PATCH 04/11] A few small repairs Co-Authored-By: Copilot <223556219+Copilot@users.noreply.github.com> --- .../fundamentals/expressions/operators.md | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/docs/csharp/fundamentals/expressions/operators.md b/docs/csharp/fundamentals/expressions/operators.md index b95c55e817111..1aff58a2764f6 100644 --- a/docs/csharp/fundamentals/expressions/operators.md +++ b/docs/csharp/fundamentals/expressions/operators.md @@ -19,13 +19,13 @@ An *operator* combines one or more *operands* into a single value. You already k The five arithmetic operators perform numeric calculations. -| Operator | Name | Example | Result | -|----------|------|---------|--------| -| `+` | Addition | `10 + 3` | `13` | -| `-` | Subtraction | `10 - 3` | `7` | -| `*` | Multiplication | `10 * 3` | `30` | -| `/` | Division | `10 / 3` | `3` | -| `%` | Remainder | `10 % 3` | `1` | +| Operator | Name | Example | Result | +|----------|----------------|----------|--------| +| `+` | Addition | `10 + 3` | `13` | +| `-` | Subtraction | `10 - 3` | `7` | +| `*` | Multiplication | `10 * 3` | `30` | +| `/` | Division | `10 / 3` | `3` | +| `%` | Remainder | `10 % 3` | `1` | :::code language="csharp" source="snippets/operators/Program.cs" ID="ArithmeticOps"::: @@ -131,11 +131,11 @@ Compound assignment operators combine a binary operation with assignment: | Operator | Equivalent to | |----------|---------------| -| `x += y` | `x = x + y` | -| `x -= y` | `x = x - y` | -| `x *= y` | `x = x * y` | -| `x /= y` | `x = x / y` | -| `x %= y` | `x = x % y` | +| `x += y` | `x = x + y` | +| `x -= y` | `x = x - y` | +| `x *= y` | `x = x * y` | +| `x /= y` | `x = x / y` | +| `x %= y` | `x = x % y` | :::code language="csharp" source="snippets/operators/Program.cs" ID="AssignmentOps"::: From ab90c9885022bfceca8aee9a56875f04bd0c8cab Mon Sep 17 00:00:00 2001 From: Bill Wagner Date: Tue, 18 Aug 2026 14:42:22 -0400 Subject: [PATCH 05/11] Retire Programming Guide equality articles; migrate to Fundamentals (#55334) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Retire the three Programming Guide equality articles and preserve their unique content in docs/csharp/fundamentals/expressions/equality.md: - equality-comparisons.md - how-to-test-for-reference-equality-identity.md - how-to-define-value-equality-for-a-type.md Content migrated into equality.md: - Equivalence contract (5 rules: reflexive, symmetric, transitive, consistent, null behavior) added to the manual-implementation section. - New section: 'Records with reference-type members' — explains that synthesized record equality uses each member's own equality semantics, so List/array members compare by reference; shows custom IEquatable override with SequenceEqual as the recommended fix. - New section: 'Polymorphic equality in unsealed class hierarchies' — explains the compile-time dispatch hazard with IEquatable, the GetType() guard and virtual Equals pattern for correct unsealed-class equality, and notes that sealed classes and records avoid the problem. Snippet additions to snippets/equality/Program.cs: - RecordWithCollectionProblem / RecordWithCollectionFixed regions - PlaylistFixedDefinition type (custom IEquatable record) - PolymorphicEqualityDefinition (Shape/Circle hierarchy with GetType() guard) - PolymorphicEqualityUsage region Intentionally omitted: string-interning note (per Bill's explicit decision). Retirement wiring: - 3 redirects added to .openpublishing.redirection.csharp.json - TOC entries and empty parent node removed from toc.yml - All 5 inbound links updated: objects.md, how-to/index.md, overloaded-operator-errors.md, record-declaration-errors.md, equality-operators.md - Orphaned snippet projects deleted Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: f5acfb15-adff-4860-a307-213196efda1c --- .openpublishing.redirection.csharp.json | 12 + .../fundamentals/expressions/equality.md | 68 ++++- .../expressions/snippets/equality/Program.cs | 80 ++++++ .../fundamentals/object-oriented/objects.md | 4 +- docs/csharp/how-to/index.md | 4 +- .../overloaded-operator-errors.md | 2 +- .../record-declaration-errors.md | 4 +- .../operators/equality-operators.md | 2 +- .../equality-comparisons.md | 53 ---- ...how-to-define-value-equality-for-a-type.md | 214 --------------- ...to-test-for-reference-equality-identity.md | 32 --- .../RecordCollectionsIssue/Program.cs | 144 ---------- .../RecordCollectionsIssue.csproj | 10 - .../ValueEqualityClass/Program.cs | 175 ------------- .../ValueEqualityClass.csproj | 10 - .../ValueEqualityPolymorphic/Program.cs | 247 ------------------ .../ValueEqualityPolymorphic.csproj | 10 - .../ValueEqualityRecord/Program.cs | 99 ------- .../ValueEqualityRecord.csproj | 10 - .../ValueEqualityStruct/Program.cs | 97 ------- .../ValueEqualityStruct.csproj | 10 - .../Program.cs | 103 -------- .../TestingReferenceEquality.csproj | 11 - docs/csharp/toc.yml | 8 - 24 files changed, 166 insertions(+), 1243 deletions(-) delete mode 100644 docs/csharp/programming-guide/statements-expressions-operators/equality-comparisons.md delete mode 100644 docs/csharp/programming-guide/statements-expressions-operators/how-to-define-value-equality-for-a-type.md delete mode 100644 docs/csharp/programming-guide/statements-expressions-operators/how-to-test-for-reference-equality-identity.md delete mode 100644 docs/csharp/programming-guide/statements-expressions-operators/snippets/how-to-define-value-equality-for-a-type/RecordCollectionsIssue/Program.cs delete mode 100644 docs/csharp/programming-guide/statements-expressions-operators/snippets/how-to-define-value-equality-for-a-type/RecordCollectionsIssue/RecordCollectionsIssue.csproj delete mode 100644 docs/csharp/programming-guide/statements-expressions-operators/snippets/how-to-define-value-equality-for-a-type/ValueEqualityClass/Program.cs delete mode 100644 docs/csharp/programming-guide/statements-expressions-operators/snippets/how-to-define-value-equality-for-a-type/ValueEqualityClass/ValueEqualityClass.csproj delete mode 100644 docs/csharp/programming-guide/statements-expressions-operators/snippets/how-to-define-value-equality-for-a-type/ValueEqualityPolymorphic/Program.cs delete mode 100644 docs/csharp/programming-guide/statements-expressions-operators/snippets/how-to-define-value-equality-for-a-type/ValueEqualityPolymorphic/ValueEqualityPolymorphic.csproj delete mode 100644 docs/csharp/programming-guide/statements-expressions-operators/snippets/how-to-define-value-equality-for-a-type/ValueEqualityRecord/Program.cs delete mode 100644 docs/csharp/programming-guide/statements-expressions-operators/snippets/how-to-define-value-equality-for-a-type/ValueEqualityRecord/ValueEqualityRecord.csproj delete mode 100644 docs/csharp/programming-guide/statements-expressions-operators/snippets/how-to-define-value-equality-for-a-type/ValueEqualityStruct/Program.cs delete mode 100644 docs/csharp/programming-guide/statements-expressions-operators/snippets/how-to-define-value-equality-for-a-type/ValueEqualityStruct/ValueEqualityStruct.csproj delete mode 100644 docs/csharp/programming-guide/statements-expressions-operators/snippets/how-to-test-for-reference-equality-identity/Program.cs delete mode 100644 docs/csharp/programming-guide/statements-expressions-operators/snippets/how-to-test-for-reference-equality-identity/TestingReferenceEquality.csproj diff --git a/.openpublishing.redirection.csharp.json b/.openpublishing.redirection.csharp.json index b3feedae1279f..11ec525b066cc 100644 --- a/.openpublishing.redirection.csharp.json +++ b/.openpublishing.redirection.csharp.json @@ -5784,6 +5784,18 @@ { "source_path_from_root": "/redirections/proposals/csharp-9.0/nullable-reference-types-specification.md", "redirect_url": "/dotnet/csharp/language-reference/language-specification/types#893-nullable-reference-types" + }, + { + "source_path_from_root": "/docs/csharp/programming-guide/statements-expressions-operators/equality-comparisons.md", + "redirect_url": "/dotnet/csharp/fundamentals/expressions/equality" + }, + { + "source_path_from_root": "/docs/csharp/programming-guide/statements-expressions-operators/how-to-test-for-reference-equality-identity.md", + "redirect_url": "/dotnet/csharp/fundamentals/expressions/equality#use-objectreferenceequals-to-test-identity-directly" + }, + { + "source_path_from_root": "/docs/csharp/programming-guide/statements-expressions-operators/how-to-define-value-equality-for-a-type.md", + "redirect_url": "/dotnet/csharp/fundamentals/expressions/equality#implement-equality-yourself-when-a-type-cant-be-a-record" } ] } diff --git a/docs/csharp/fundamentals/expressions/equality.md b/docs/csharp/fundamentals/expressions/equality.md index dd20b71286498..8c600ff54ee1a 100644 --- a/docs/csharp/fundamentals/expressions/equality.md +++ b/docs/csharp/fundamentals/expressions/equality.md @@ -1,9 +1,18 @@ --- title: "C# Equality comparisons" -description: Learn how C# compares values and references with ==, !=, Equals, GetHashCode, and ReferenceEquals for classes, structs, records, and tuples. -ms.date: 07/22/2026 +description: Learn how C# compares values and references with ==, !=, Equals, GetHashCode, and ReferenceEquals for classes, structs, records, and tuples. Covers the equivalence contract, polymorphic equality in class hierarchies, and records with collection members. +ms.date: 08/18/2026 ms.topic: concept-article ai-usage: ai-assisted +helpviewer_keywords: + - "object equality [C#]" + - "value equality [C#]" + - "reference equality [C#]" + - "object identity [C#]" + - "object equivalence [C#]" + - "overriding Equals method [C#]" + - "Equals method [C#], overriding" + - "equivalence [C#]" --- # C# Equality comparisons @@ -90,6 +99,16 @@ In a complete manual implementation, provide these members: - An `override` of . Objects that are equal must return the same hash code. Without this pairing, the type behaves incorrectly in hash-based collections such as `Dictionary` or `HashSet`. See for guidance on a correct implementation. - Optionally, a typed `Equals` method by implementing . You often see this written as `Equals(T?)` in docs: `T` is a [type parameter](../types/generics.md), a placeholder for the current type, and `?` is a [nullable annotation](../null-safety/index.md) that says the argument can be `null`. This typed method can avoid extra conversions when callers already have the same type, but it's a secondary optimization. +A correct implementation also satisfies the *equivalence contract*. The following rules assume `x`, `y`, and `z` are not null: + +1. **Reflexive**: `x.Equals(x)` returns `true`. +2. **Symmetric**: `x.Equals(y)` returns the same value as `y.Equals(x)`. +3. **Transitive**: if `x.Equals(y)` and `y.Equals(z)` are both `true`, then `x.Equals(z)` must be `true`. +4. **Consistent**: successive calls to `x.Equals(y)` return the same value as long as neither object changes. +5. **Null behavior**: `x.Equals(null)` returns `false`; `x.Equals(y)` must not throw when called on a non-null `x`. + +The symmetric and transitive rules are easy to violate in inheritance hierarchies. See [Polymorphic equality in unsealed class hierarchies](#polymorphic-equality-in-unsealed-class-hierarchies) for guidance. + The following example starts with the and overrides, plus the optional typed `Equals` member, so you can see their effect before the `==` and `!=` operators are added. `HashCode.Combine` is a library helper that builds one hash code from the same values used by `Equals`: :::code language="csharp" source="snippets/equality/Program.cs" ID="ColorDefinition"::: @@ -111,6 +130,51 @@ A common use is inside an `Equals` override to short-circuit the full comparison > [!NOTE] > Advanced detail: when variables are typed as an [interface](../types/interfaces.md), `==` checks whether the interface variables refer to the same object. A call to `Equals` still runs the underlying object's implementation. +> [!NOTE] +> always returns `false` when comparing value types, even if both arguments contain the same values. This is because each value-type argument is independently *boxed* into a separate heap object when passed to `ReferenceEquals`. + +## Records with reference-type members + +Record equality is synthesized from the members' own equality. Each property or field is compared using its own `Equals` method. For most scalar values—`int`, `string`, `DateTime`, and similar types—that works exactly as you'd expect. The subtlety arises with common mutable collections such as `List` or `T[]`: these types compare by reference, so two record instances that contain *different list objects with the same content* are **not** considered equal by the synthesized record equality. + +:::code language="csharp" source="snippets/equality/Program.cs" ID="RecordWithCollectionProblem"::: + +`playlist1` and `playlist2` are separate `List` instances. Even though their contents match, `Equals` returns `false`. + +When you need two-record equality to reflect collection *contents*, you have a few options: + +- **Custom `IEquatable` override**: Implement `IEquatable` on the record and use (or an appropriate comparison) for the collection members. + + :::code language="csharp" source="snippets/equality/Program.cs" ID="PlaylistFixedDefinition"::: + + :::code language="csharp" source="snippets/equality/Program.cs" ID="RecordWithCollectionFixed"::: + +- **Use collection types with value equality**: doesn't override equality either, but a record that wraps a `ReadOnlySpan` or uses `SequenceEqual` in a custom `Equals` achieves the same goal. The key insight is to pick the right abstraction rather than fighting the defaults. + +- **Design around identity**: If the record represents an entity rather than a value—and the collection members are logically shared—then reference equality for those members may be intentional. Design the type to reuse the same list instance where equality matters. + +## Polymorphic equality in unsealed class hierarchies + +Implementing value equality in an unsealed class hierarchy requires extra care. The hazard is that `IEquatable.Equals(T? other)` is dispatched at compile time based on the *declared type* of the variable, not the runtime type. If `TwoDPoint` declares a non-virtual `Equals(TwoDPoint? other)`, then a variable declared as `TwoDPoint` but holding a `ThreeDPoint` at runtime calls `TwoDPoint.Equals`, silently ignoring the extra dimension. The result is that two points with different `Z` values incorrectly compare as equal. + +**The fix**: make the typed `Equals` method `virtual` and add a `GetType() == other.GetType()` guard. This ensures that objects of different runtime types are never considered equal, regardless of the declared type of the variable. + +:::code language="csharp" source="snippets/equality/Program.cs" ID="PolymorphicEqualityDefinition"::: + +Usage with a variable declared as the base type: + +:::code language="csharp" source="snippets/equality/Program.cs" ID="PolymorphicEqualityUsage"::: + +Key points for unsealed class hierarchies: + +- **`GetType()` guard**: including `GetType() == other.GetType()` in the base class `Equals` prevents a `Circle` from comparing equal to a `Square` with the same color, and prevents a `Circle` from comparing equal to a `Shape` base with the same color. +- **`virtual` on the typed `Equals`**: lets each derived class augment the comparison with its own fields by calling `base.Equals(other)`. +- **`GetHashCode` must include `GetType()`**: two objects are only considered equal when their runtime types match, so `GetHashCode` must reflect that. `HashCode.Combine(GetType(), ...)` achieves this. +- **Sealed classes are simpler**: a `sealed` class can't be subclassed, so compile-time and runtime types always agree. The standard `IEquatable` pattern shown for `Color` earlier in this article is correct and complete for sealed classes without any virtual dispatch. + +> [!TIP] +> Records handle inheritance correctly out of the box. When a base record and a derived record are both compared using `==` or `Equals`, the compiler-generated equality checks both the runtime type and all declared properties. Prefer `record` over a manual unsealed-class hierarchy when value equality is your goal. + ## See also - [Type system overview](../types/index.md) diff --git a/docs/csharp/fundamentals/expressions/snippets/equality/Program.cs b/docs/csharp/fundamentals/expressions/snippets/equality/Program.cs index 670765639cd50..b1ca0b1c64164 100644 --- a/docs/csharp/fundamentals/expressions/snippets/equality/Program.cs +++ b/docs/csharp/fundamentals/expressions/snippets/equality/Program.cs @@ -58,6 +58,30 @@ Console.WriteLine(ReferenceEquals(doc1, doc3)); // => True // +// +var playlist1 = new Playlist("Chill", new List { "Song A", "Song B" }); +var playlist2 = new Playlist("Chill", new List { "Song A", "Song B" }); + +Console.WriteLine(playlist1.Equals(playlist2)); // => False (different List instances) +Console.WriteLine(playlist1.Tracks.SequenceEqual(playlist2.Tracks)); // => True +// + +// +var fixed1 = new PlaylistFixed("Chill", new List { "Song A", "Song B" }); +var fixed2 = new PlaylistFixed("Chill", new List { "Song A", "Song B" }); + +Console.WriteLine(fixed1.Equals(fixed2)); // => True +// + +// +Shape circle1 = new Circle("red", 5.0); +Shape circle2 = new Circle("red", 7.0); +Shape circle3 = new Circle("red", 5.0); + +Console.WriteLine(circle1.Equals(circle2)); // => False (Radius differs) +Console.WriteLine(circle1.Equals(circle3)); // => True +// + // ── Type declarations ──────────────────────────────────────────────────────── class Order(int id, string name) @@ -103,3 +127,59 @@ class Document(string title) public string Title { get; } = title; } +// +// Unsealed class hierarchy — make the typed Equals virtual and guard with GetType() +// so a derived instance is never equal to an instance of a different runtime type. +class Shape : IEquatable +{ + public string Color { get; } + public Shape(string color) => Color = color; + + public override bool Equals(object? obj) => Equals(obj as Shape); + + // virtual so derived classes can override the comparison logic + public virtual bool Equals(Shape? other) => + other is not null && + GetType() == other.GetType() && // reject different runtime types + Color == other.Color; + + public override int GetHashCode() => HashCode.Combine(GetType(), Color); + + public static bool operator ==(Shape? l, Shape? r) => l?.Equals(r) ?? r is null; + public static bool operator !=(Shape? l, Shape? r) => !(l == r); +} + +class Circle : Shape +{ + public double Radius { get; } + public Circle(string color, double radius) : base(color) => Radius = radius; + + public override bool Equals(object? obj) => Equals(obj as Shape); + + public override bool Equals(Shape? other) => + other is Circle c && base.Equals(c) && Radius == c.Radius; + + public override int GetHashCode() => HashCode.Combine(Color, Radius); +} +// + +record Playlist(string Name, List Tracks); + +// +record PlaylistFixed(string Name, List Tracks) : IEquatable +{ + public virtual bool Equals(PlaylistFixed? other) => + other is not null && + Name == other.Name && + Tracks.SequenceEqual(other.Tracks); + + public override int GetHashCode() + { + var hc = new HashCode(); + hc.Add(Name); + foreach (var t in Tracks) hc.Add(t); + return hc.ToHashCode(); + } +} +// + diff --git a/docs/csharp/fundamentals/object-oriented/objects.md b/docs/csharp/fundamentals/object-oriented/objects.md index ed0e19f474ef8..33de713cf229c 100644 --- a/docs/csharp/fundamentals/object-oriented/objects.md +++ b/docs/csharp/fundamentals/object-oriented/objects.md @@ -39,9 +39,9 @@ When you compare two objects for equality, you must first distinguish whether yo :::code language="csharp" source="./snippets/objects/Equality.cs" ID="Snippet32"::: - The default implementation of `Equals` uses boxing and reflection in some cases. For information about how to provide an efficient equality algorithm that's specific to your type, see [How to define value equality for a type](../../programming-guide/statements-expressions-operators/how-to-define-value-equality-for-a-type.md). Records are reference types that use value semantics for equality. + The default implementation of `Equals` uses boxing and reflection in some cases. For information about how to provide an efficient equality algorithm that's specific to your type, see [Implement equality yourself when a type can't be a record](../expressions/equality.md#implement-equality-yourself-when-a-type-cant-be-a-record). Records are reference types that use value semantics for equality. -- To determine whether the values of the fields in two class instances are equal, you might be able to use the method or the [== operator](../../language-reference/operators/equality-operators.md#equality-operator-). However, only use them if the class has overridden or overloaded them to provide a custom definition of what "equality" means for objects of that type. The class might also implement the interface or the interface. Both interfaces provide methods that can be used to test value equality. When designing your own classes that override `Equals`, make sure to follow the guidelines stated in [How to define value equality for a type](../../programming-guide/statements-expressions-operators/how-to-define-value-equality-for-a-type.md) and . +- To determine whether the values of the fields in two class instances are equal, you might be able to use the method or the [== operator](../../language-reference/operators/equality-operators.md#equality-operator-). However, only use them if the class has overridden or overloaded them to provide a custom definition of what "equality" means for objects of that type. The class might also implement the interface or the interface. Both interfaces provide methods that can be used to test value equality. When designing your own classes that override `Equals`, make sure to follow the guidelines stated in [Implement equality yourself when a type can't be a record](../expressions/equality.md#implement-equality-yourself-when-a-type-cant-be-a-record) and . ## Related Sections diff --git a/docs/csharp/how-to/index.md b/docs/csharp/how-to/index.md index 9966fb8880776..78854d1f07a03 100644 --- a/docs/csharp/how-to/index.md +++ b/docs/csharp/how-to/index.md @@ -67,8 +67,8 @@ You may need to convert an object to a different type. You may create types that define their own rules for equality or define a natural ordering among objects of that type. -- [Test for reference-based equality](../programming-guide/statements-expressions-operators/how-to-test-for-reference-equality-identity.md). -- [Define value-based equality for a type](../programming-guide/statements-expressions-operators/how-to-define-value-equality-for-a-type.md). +- [Test for reference-based equality](../fundamentals/expressions/equality.md#use-objectreferenceequals-to-test-identity-directly). +- [Define value-based equality for a type](../fundamentals/expressions/equality.md#implement-equality-yourself-when-a-type-cant-be-a-record). ## Exception handling diff --git a/docs/csharp/language-reference/compiler-messages/overloaded-operator-errors.md b/docs/csharp/language-reference/compiler-messages/overloaded-operator-errors.md index ccb5aa518b072..efb8bf7917df3 100644 --- a/docs/csharp/language-reference/compiler-messages/overloaded-operator-errors.md +++ b/docs/csharp/language-reference/compiler-messages/overloaded-operator-errors.md @@ -283,7 +283,7 @@ The compiler enforces strict matching between operator declarations and the inte - **CS0660**: *Type defines operator == or operator != but doesn't override Object.Equals(object o)* - **CS0661**: *Type defines operator == or operator != but doesn't override Object.GetHashCode()* -The compiler requires that equality-related overrides and operator definitions stay in sync. When you override or define `operator ==` / `operator !=`, you must also provide the related overrides. For the full rules, see [How to define value equality for a type](../../programming-guide/statements-expressions-operators/how-to-define-value-equality-for-a-type.md) and [Equality operators](../operators/equality-operators.md). +The compiler requires that equality-related overrides and operator definitions stay in sync. When you override or define `operator ==` / `operator !=`, you must also provide the related overrides. For the full rules, see [Implement equality yourself when a type can't be a record](../../fundamentals/expressions/equality.md#implement-equality-yourself-when-a-type-cant-be-a-record) and [Equality operators](../operators/equality-operators.md). - Add an override of when you override (**CS0659**). Hash-based collections like and rely on the contract that two objects that are equal must return the same hash code. Without a matching `GetHashCode` override, objects that compare as equal might hash to different buckets, causing lookups and deduplication to fail silently. - Add an override of when you define `operator ==` or `operator !=` (**CS0660**). Code that calls `Equals` directly—including many framework APIs, LINQ methods, and collection operations—won't use your custom operator. Without a consistent `Equals` override, the same two objects might be considered equal by `==` but not by `Equals`, leading to unpredictable behavior. diff --git a/docs/csharp/language-reference/compiler-messages/record-declaration-errors.md b/docs/csharp/language-reference/compiler-messages/record-declaration-errors.md index 4ab09d24a9d5a..f1e0dbb31e978 100644 --- a/docs/csharp/language-reference/compiler-messages/record-declaration-errors.md +++ b/docs/csharp/language-reference/compiler-messages/record-declaration-errors.md @@ -134,11 +134,11 @@ To correct these errors, apply the following changes to your positional record d - **CS8857**: *The receiver of a `with` expression must have a non-void type.* - **CS8858**: *The receiver type 'type' is not a valid record type and is not a struct type.* -[Record types](../builtin-types/record.md) provide built-in [value-based equality](../builtin-types/record.md#value-equality). These diagnostics arise when your declarations conflict with the equality contract. For the complete rules on equality, see [equality comparisons](../../programming-guide/statements-expressions-operators/equality-comparisons.md). +[Record types](../builtin-types/record.md) provide built-in [value-based equality](../builtin-types/record.md#value-equality). These diagnostics arise when your declarations conflict with the equality contract. For the complete rules on equality, see [C# equality comparisons](../../fundamentals/expressions/equality.md). To correct these errors, apply the following changes: -- Add a `GetHashCode` method whenever you define an `Equals` method. The [equality contract](../../programming-guide/statements-expressions-operators/equality-comparisons.md) requires that objects considered equal produce the same hash code, so the compiler enforces that these two methods are always defined together (**CS8851**). +- Add a `GetHashCode` method whenever you define an `Equals` method. The [equivalence contract](../../fundamentals/expressions/equality.md#implement-equality-yourself-when-a-type-cant-be-a-record) requires that objects considered equal produce the same hash code, so the compiler enforces that these two methods are always defined together (**CS8851**). - Change the receiver of a `with` expression so that it's a [record type](../builtin-types/record.md) or a [struct type](../builtin-types/struct.md). The `with` expression creates a modified copy by using the `record` copy constructor, or value copy semantics for `struct` types (**CS8858**). - Ensure the receiver of a [`with` expression](../operators/with-expression.md) has a non-void type. The `with` expression produces a new copy of the receiver, so the receiver must evaluate to a value that can be copied (**CS8857**). diff --git a/docs/csharp/language-reference/operators/equality-operators.md b/docs/csharp/language-reference/operators/equality-operators.md index 0014853addcad..e8cd85d42b96e 100644 --- a/docs/csharp/language-reference/operators/equality-operators.md +++ b/docs/csharp/language-reference/operators/equality-operators.md @@ -114,5 +114,5 @@ For more information about equality of record types, see the [Equality members]( - - - -- [Equality comparisons](../../programming-guide/statements-expressions-operators/equality-comparisons.md) +- [Equality comparisons](../../fundamentals/expressions/equality.md) - [Comparison operators](comparison-operators.md) diff --git a/docs/csharp/programming-guide/statements-expressions-operators/equality-comparisons.md b/docs/csharp/programming-guide/statements-expressions-operators/equality-comparisons.md deleted file mode 100644 index 2f0c6a42cb514..0000000000000 --- a/docs/csharp/programming-guide/statements-expressions-operators/equality-comparisons.md +++ /dev/null @@ -1,53 +0,0 @@ ---- -title: "Equality Comparisons" -description: Learn about equality comparisons. See descriptions of 'value equality' and 'reference equality', and view additional resources. -ms.date: 07/20/2015 -helpviewer_keywords: - - "object equality [C#]" -ms.assetid: 10b865ea-4e7b-4127-9242-c9b8f57d9f04 ---- -# Equality comparisons (C# Programming Guide) - -It is sometimes necessary to compare two values for equality. In some cases, you are testing for *value equality*, also known as *equivalence*, which means that the values that are contained by the two variables are equal. In other cases, you have to determine whether two variables refer to the same underlying object in memory. This type of equality is called *reference equality*, or *identity*. This topic describes these two kinds of equality and provides links to other topics for more information. - -## Reference equality - - Reference equality means that two object references refer to the same underlying object. This can occur through simple assignment, as shown in the following example. - - [!code-csharp[csProgGuideStatements#18](~/samples/snippets/csharp/VS_Snippets_VBCSharp/csProgGuideStatements/CS/Statements.cs#18)] - - In this code, two objects are created, but after the assignment statement, both references refer to the same object. Therefore they have reference equality. Use the method to determine whether two references refer to the same object. - -The concept of reference equality applies only to reference types. Value type objects cannot have reference equality because when an instance of a value type is assigned to a variable, a copy of the value is made. Therefore you can never have two unboxed structs that refer to the same location in memory. Furthermore, if you use to compare two value types, the result will always be `false`, even if the values that are contained in the objects are all identical. This is because each variable is boxed into a separate object instance. For more information, see [How to test for reference equality (Identity)](./how-to-test-for-reference-equality-identity.md). - -## Value equality - - Value equality means that two objects contain the same value or values. For primitive value types such as [int](../../language-reference/builtin-types/integral-numeric-types.md) or [bool](../../language-reference/builtin-types/bool.md), tests for value equality are straightforward. You can use the [==](../../language-reference/operators/equality-operators.md#equality-operator-) operator, as shown in the following example. - -```csharp -int a = GetOriginalValue(); -int b = GetCurrentValue(); - -// Test for value equality. -if (b == a) -{ - // The two integers are equal. -} -``` - - For most other types, testing for value equality is more complex because it requires that you understand how the type defines it. For classes and structs that have multiple fields or properties, value equality is often defined to mean that all fields or properties have the same value. For example, two `Point` objects might be defined to be equivalent if pointA.X is equal to pointB.X and pointA.Y is equal to pointB.Y. For records, value equality means that two variables of a record type are equal if the types match and all property and field values match. - -However, there is no requirement that equivalence be based on all the fields in a type. It can be based on a subset. When you compare types that you do not own, you should make sure to understand specifically how equivalence is defined for that type. For more information about how to define value equality in your own classes and structs, see [How to define value equality for a type](./how-to-define-value-equality-for-a-type.md). - -### Value equality for floating-point values - - Equality comparisons of floating-point values ([double](../../language-reference/builtin-types/floating-point-numeric-types.md) and [float](../../language-reference/builtin-types/floating-point-numeric-types.md)) are problematic because of the imprecision of floating-point arithmetic on binary computers. For more information, see the remarks in the topic . - -## Related topics - -|Title|Description| -|-----------|-----------------| -|[How to test for reference equality (Identity)](./how-to-test-for-reference-equality-identity.md)|Describes how to determine whether two variables have reference equality.| -|[How to define value equality for a type](./how-to-define-value-equality-for-a-type.md)|Describes how to provide a custom definition of value equality for a type.| -|[Types](../../fundamentals/types/index.md)|Provides information about the C# type system and links to additional information.| -|[Records](../../fundamentals/types/records.md)|Provides information about record types, which test for value equality by default.| 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 deleted file mode 100644 index c81749a2866d2..0000000000000 --- a/docs/csharp/programming-guide/statements-expressions-operators/how-to-define-value-equality-for-a-type.md +++ /dev/null @@ -1,214 +0,0 @@ ---- -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#]" - - "Equals method [C#], overriding" - - "value equality [C#]" - - "equivalence [C#]" -ms.assetid: 4084581e-b931-498b-9534-cf7ef5b68690 ---- -# How to define value equality for a class or struct (C# Programming Guide) - -> [!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. - -In either case, and in both classes and structs, your implementation should follow the five guarantees of equivalence (for the following rules, assume that `x`, `y` and `z` are not null): - -1. The reflexive property: `x.Equals(x)` returns `true`. - -2. The symmetric property: `x.Equals(y)` returns the same value as `y.Equals(x)`. - -3. The transitive property: if `(x.Equals(y) && y.Equals(z))` returns `true`, then `x.Equals(z)` returns `true`. - -4. Successive invocations of `x.Equals(y)` return the same value as long as the objects referenced by x and y aren't modified. - -5. Any non-null value isn't equal to null. However, `x.Equals(y)` throws an exception when `x` is null. That breaks rules 1 or 2, depending on the argument to `Equals`. - -Any struct that you define already has a default implementation of value equality that it inherits from the override of the method. This implementation uses reflection to examine all the fields and properties 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. - -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.** 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 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.** 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.** 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.** 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. - -## 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 and override , , and the `==`/`!=` operators. -- **Correct inheritance behavior**: Records implement `IEquatable` 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 , arrays, and other reference types that don't implement value-based equality (with the notable exception of , 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 , , 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 Hobbies); -> -> var person1 = new PersonWithHobbies("Alice", new List { "Reading", "Swimming" }); -> var person2 = new PersonWithHobbies("Alice", new List { "Reading", "Swimming" }); -> -> Console.WriteLine(person1.Equals(person2)); // False - different List instances! -> ``` - -This is because records use the 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 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 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 or . - -- **Use types with value-based equality**: For collections, consider using types that implement value-based equality or implement custom collection types that override to provide content-based comparison, such as or . - -- **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: - -:::code language="csharp" source="snippets/how-to-define-value-equality-for-a-type/ValueEqualityClass/Program.cs"::: - -On classes (reference types), the default implementation of both methods performs a reference equality comparison, not a value equality check. When an implementer overrides the virtual method, the purpose is to give it value equality semantics. - -The `==` and `!=` operators can be used with classes even if the class does not overload them. However, the default behavior is to perform a reference equality check. In a class, if you overload the `Equals` method, you should overload the `==` and `!=` operators, but it is not required. - -> [!IMPORTANT] -> The preceding example code may not handle every inheritance scenario the way you expect. Consider the following code: -> -> ```csharp -> TwoDPoint p1 = new ThreeDPoint(1, 2, 3); -> TwoDPoint p2 = new ThreeDPoint(1, 2, 4); -> 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. 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 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 method, which can check runtime types and handle inheritance correctly. This can be achieved by using explicit interface implementation for 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 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 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 - -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. 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. - -## See also - -- [Equality comparisons](equality-comparisons.md) diff --git a/docs/csharp/programming-guide/statements-expressions-operators/how-to-test-for-reference-equality-identity.md b/docs/csharp/programming-guide/statements-expressions-operators/how-to-test-for-reference-equality-identity.md deleted file mode 100644 index 45cd03178ccd6..0000000000000 --- a/docs/csharp/programming-guide/statements-expressions-operators/how-to-test-for-reference-equality-identity.md +++ /dev/null @@ -1,32 +0,0 @@ ---- -title: "How to test for reference equality (Identity)" -description: Learn how to test for reference equality (Identity). See a code example and view additional available resources. -ms.date: 07/20/2015 -ms.topic: how-to -helpviewer_keywords: - - "object identity [C#]" - - "reference equality [C#]" -ms.assetid: 91307fda-267b-4fd2-a338-2aada39ee791 ---- -# How to test for reference equality (Identity) (C# Programming Guide) - -You do not have to implement any custom logic to support reference equality comparisons in your types. This functionality is provided for all types by the static method. - - The following example shows how to determine whether two variables have *reference equality*, which means that they refer to the same object in memory. - -The example also shows why always returns `false` for value types. This is due to **boxing**, which creates separate object instances for each value type argument. Additionally, you should not use to determine string equality. - -## Example - - [!code-csharp[TestingReferenceEquality](snippets/how-to-test-for-reference-equality-identity/Program.cs)] - - The implementation of `Equals` in the universal base class also performs a reference equality check, but it is best not to use this because, if a class happens to override the method, the results might not be what you expect. The same is true for the `==` and `!=` operators. When they are operating on reference types, the default behavior of `==` and `!=` is to perform a reference equality check. However, derived classes can overload the operator to perform a value equality check. To minimize the potential for error, it is best to always use when you have to determine whether two objects have reference equality. - - Constant strings within the same assembly are always interned by the runtime. That is, only one instance of each unique literal string is maintained. However, the runtime does not guarantee that strings created at run time are interned, nor does it guarantee that two equal constant strings in different assemblies are interned. - -> [!NOTE] -> `ReferenceEquals` returns `false` for value types due to **boxing**, as each argument is independently boxed into a separate object. - -## See also - -- [Equality Comparisons](./equality-comparisons.md) diff --git a/docs/csharp/programming-guide/statements-expressions-operators/snippets/how-to-define-value-equality-for-a-type/RecordCollectionsIssue/Program.cs b/docs/csharp/programming-guide/statements-expressions-operators/snippets/how-to-define-value-equality-for-a-type/RecordCollectionsIssue/Program.cs deleted file mode 100644 index 7c0639eb0a90d..0000000000000 --- a/docs/csharp/programming-guide/statements-expressions-operators/snippets/how-to-define-value-equality-for-a-type/RecordCollectionsIssue/Program.cs +++ /dev/null @@ -1,144 +0,0 @@ -namespace RecordCollectionsIssue; - -// -// Records with reference-equality members don't work as expected -public record PersonWithHobbies(string Name, List Hobbies); -// - -// -// A potential solution using IEquatable with custom equality -public record PersonWithHobbiesFixed(string Name, List Hobbies) : IEquatable -{ - 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(); - } -} -// - -// -// These also use reference equality - the issue persists -public record PersonWithHobbiesArray(string Name, string[] Hobbies); - -public record PersonWithHobbiesImmutable(string Name, IReadOnlyList Hobbies); -// - -// -class Program -{ - static void Main(string[] args) - { - // - 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(); - // - - // - 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(); - // - - // - 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(); - // - - // - 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(); - // - - Console.WriteLine("=== Collection Behavior Summary ==="); - Console.WriteLine("Type | Equals Result | Reason"); - Console.WriteLine("----------------------------------|---------------|------------------"); - Console.WriteLine($"Record with List | {person1.Equals(person2),-13} | Reference equality"); - Console.WriteLine($"Record with custom IEquatable | {personFixed1.Equals(personFixed2),-13} | Custom equality logic"); - Console.WriteLine($"Record with Array | {personArray1.Equals(personArray2),-13} | Reference equality"); - Console.WriteLine($"Record with IReadOnlyList | {personImmutable1.Equals(personImmutable2),-13} | Reference equality"); - - Console.WriteLine("\nPress any key to exit."); - Console.ReadKey(); - } -} -// - -/* 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 | False | Reference equality -Record with custom IEquatable | True | Custom equality logic -Record with Array | False | Reference equality -Record with IReadOnlyList | False | Reference equality -*/ \ 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/RecordCollectionsIssue/RecordCollectionsIssue.csproj b/docs/csharp/programming-guide/statements-expressions-operators/snippets/how-to-define-value-equality-for-a-type/RecordCollectionsIssue/RecordCollectionsIssue.csproj deleted file mode 100644 index fd4dd4565750e..0000000000000 --- a/docs/csharp/programming-guide/statements-expressions-operators/snippets/how-to-define-value-equality-for-a-type/RecordCollectionsIssue/RecordCollectionsIssue.csproj +++ /dev/null @@ -1,10 +0,0 @@ - - - - Exe - net8.0 - enable - enable - - - \ 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/ValueEqualityClass/Program.cs b/docs/csharp/programming-guide/statements-expressions-operators/snippets/how-to-define-value-equality-for-a-type/ValueEqualityClass/Program.cs deleted file mode 100644 index a9d497c3526ac..0000000000000 --- a/docs/csharp/programming-guide/statements-expressions-operators/snippets/how-to-define-value-equality-for-a-type/ValueEqualityClass/Program.cs +++ /dev/null @@ -1,175 +0,0 @@ -namespace ValueEqualityClass; - -class TwoDPoint : IEquatable -{ - public int X { get; private set; } - public int Y { get; private set; } - - public TwoDPoint(int x, int y) - { - if (x is (< 1 or > 2000) || y is (< 1 or > 2000)) - { - throw new ArgumentException("Point must be in range 1 - 2000"); - } - this.X = x; - this.Y = y; - } - - public override bool Equals(object obj) => this.Equals(obj as TwoDPoint); - - public bool Equals(TwoDPoint p) - { - if (p is null) - { - return false; - } - - // Optimization for a common success case. - if (Object.ReferenceEquals(this, p)) - { - return true; - } - - // If run-time types are not exactly the same, return false. - if (this.GetType() != p.GetType()) - { - return false; - } - - // Return true if the fields match. - // Note that the base class is not invoked because it is - // System.Object, which defines Equals as reference equality. - return (X == p.X) && (Y == p.Y); - } - - public override int GetHashCode() => (X, Y).GetHashCode(); - - public static bool operator ==(TwoDPoint lhs, TwoDPoint rhs) - { - if (lhs is null) - { - if (rhs is null) - { - return true; - } - - // Only the left side is null. - return false; - } - // Equals handles case of null on right side. - return lhs.Equals(rhs); - } - - public static bool operator !=(TwoDPoint lhs, TwoDPoint rhs) => !(lhs == rhs); -} - -// For the sake of simplicity, assume a ThreeDPoint IS a TwoDPoint. -class ThreeDPoint : TwoDPoint, IEquatable -{ - public int Z { get; private set; } - - public ThreeDPoint(int x, int y, int z) - : base(x, y) - { - if ((z < 1) || (z > 2000)) - { - throw new ArgumentException("Point must be in range 1 - 2000"); - } - this.Z = z; - } - - public override bool Equals(object obj) => this.Equals(obj as ThreeDPoint); - - public bool Equals(ThreeDPoint p) - { - if (p is null) - { - return false; - } - - // Optimization for a common success case. - if (Object.ReferenceEquals(this, p)) - { - return true; - } - - // Check properties that this class declares. - if (Z == p.Z) - { - // Let base class check its own fields - // and do the run-time type comparison. - return base.Equals((TwoDPoint)p); - } - else - { - return false; - } - } - - public override int GetHashCode() => (X, Y, Z).GetHashCode(); - - public static bool operator ==(ThreeDPoint lhs, ThreeDPoint rhs) - { - if (lhs is null) - { - if (rhs is null) - { - // null == null = true. - return true; - } - - // Only the left side is null. - return false; - } - // Equals handles the case of null on right side. - return lhs.Equals(rhs); - } - - public static bool operator !=(ThreeDPoint lhs, ThreeDPoint rhs) => !(lhs == rhs); -} - -class Program -{ - static void Main(string[] args) - { - ThreeDPoint pointA = new ThreeDPoint(3, 4, 5); - ThreeDPoint pointB = new ThreeDPoint(3, 4, 5); - ThreeDPoint pointC = null; - int i = 5; - - Console.WriteLine($"pointA.Equals(pointB) = {pointA.Equals(pointB)}"); - Console.WriteLine($"pointA == pointB = {pointA == pointB}"); - Console.WriteLine($"null comparison = {pointA.Equals(pointC)}"); - Console.WriteLine($"Compare to some other type = {pointA.Equals(i)}"); - - TwoDPoint pointD = null; - TwoDPoint pointE = null; - - Console.WriteLine($"Two null TwoDPoints are equal: {pointD == pointE}"); - - pointE = new TwoDPoint(3, 4); - Console.WriteLine($"(pointE == pointA) = {pointE == pointA}"); - Console.WriteLine($"(pointA == pointE) = {pointA == pointE}"); - Console.WriteLine($"(pointA != pointE) = {pointA != pointE}"); - - System.Collections.ArrayList list = new System.Collections.ArrayList(); - list.Add(new ThreeDPoint(3, 4, 5)); - Console.WriteLine($"pointE.Equals(list[0]): {pointE.Equals(list[0])}"); - - // Keep the console window open in debug mode. - Console.WriteLine("Press any key to exit."); - Console.ReadKey(); - } -} - -/* Output: - pointA.Equals(pointB) = True - pointA == pointB = True - null comparison = False - Compare to some other type = False - Two null TwoDPoints are equal: True - (pointE == pointA) = False - (pointA == pointE) = False - (pointA != pointE) = True - pointE.Equals(list[0]): False -*/ diff --git a/docs/csharp/programming-guide/statements-expressions-operators/snippets/how-to-define-value-equality-for-a-type/ValueEqualityClass/ValueEqualityClass.csproj b/docs/csharp/programming-guide/statements-expressions-operators/snippets/how-to-define-value-equality-for-a-type/ValueEqualityClass/ValueEqualityClass.csproj deleted file mode 100644 index f704bf4988fa6..0000000000000 --- a/docs/csharp/programming-guide/statements-expressions-operators/snippets/how-to-define-value-equality-for-a-type/ValueEqualityClass/ValueEqualityClass.csproj +++ /dev/null @@ -1,10 +0,0 @@ - - - - Exe - net8.0 - enable - enable - - - diff --git a/docs/csharp/programming-guide/statements-expressions-operators/snippets/how-to-define-value-equality-for-a-type/ValueEqualityPolymorphic/Program.cs b/docs/csharp/programming-guide/statements-expressions-operators/snippets/how-to-define-value-equality-for-a-type/ValueEqualityPolymorphic/Program.cs deleted file mode 100644 index 8ef4dc9eb9358..0000000000000 --- a/docs/csharp/programming-guide/statements-expressions-operators/snippets/how-to-define-value-equality-for-a-type/ValueEqualityPolymorphic/Program.cs +++ /dev/null @@ -1,247 +0,0 @@ -namespace ValueEqualityPolymorphic; - -// -// Safe polymorphic equality implementation using explicit interface implementation -class TwoDPoint : IEquatable -{ - public int X { get; private set; } - public int Y { get; private set; } - - public TwoDPoint(int x, int y) - { - if (x is (< 1 or > 2000) || y is (< 1 or > 2000)) - { - throw new ArgumentException("Point must be in range 1 - 2000"); - } - this.X = x; - this.Y = y; - } - - public override bool Equals(object? obj) => Equals(obj as TwoDPoint); - - // Explicit interface implementation prevents compile-time type issues - bool IEquatable.Equals(TwoDPoint? p) => Equals((object?)p); - - protected virtual bool Equals(TwoDPoint? p) - { - if (p is null) - { - return false; - } - - // Optimization for a common success case. - if (Object.ReferenceEquals(this, p)) - { - return true; - } - - // If run-time types are not exactly the same, return false. - if (this.GetType() != p.GetType()) - { - return false; - } - - // Return true if the fields match. - // Note that the base class is not invoked because it is - // System.Object, which defines Equals as reference equality. - return (X == p.X) && (Y == p.Y); - } - - public override int GetHashCode() => (X, Y).GetHashCode(); - - public static bool operator ==(TwoDPoint? lhs, TwoDPoint? rhs) - { - if (lhs is null) - { - if (rhs is null) - { - return true; - } - - // Only the left side is null. - return false; - } - // Equals handles case of null on right side. - return lhs.Equals(rhs); - } - - public static bool operator !=(TwoDPoint? lhs, TwoDPoint? rhs) => !(lhs == rhs); -} -// - -// -// For the sake of simplicity, assume a ThreeDPoint IS a TwoDPoint. -class ThreeDPoint : TwoDPoint, IEquatable -{ - public int Z { get; private set; } - - public ThreeDPoint(int x, int y, int z) - : base(x, y) - { - if ((z < 1) || (z > 2000)) - { - throw new ArgumentException("Point must be in range 1 - 2000"); - } - this.Z = z; - } - - public override bool Equals(object? obj) => Equals(obj as ThreeDPoint); - - // Explicit interface implementation prevents compile-time type issues - bool IEquatable.Equals(ThreeDPoint? p) => Equals((object?)p); - - protected override bool Equals(TwoDPoint? p) - { - if (p is null) - { - return false; - } - - // Optimization for a common success case. - if (Object.ReferenceEquals(this, p)) - { - return true; - } - - // Runtime type check happens in the base method - if (p is ThreeDPoint threeD) - { - // Check properties that this class declares. - if (Z != threeD.Z) - { - return false; - } - - return base.Equals(p); - } - - return false; - } - - public override int GetHashCode() => (X, Y, Z).GetHashCode(); - - public static bool operator ==(ThreeDPoint? lhs, ThreeDPoint? rhs) - { - if (lhs is null) - { - if (rhs is null) - { - // null == null = true. - return true; - } - - // Only the left side is null. - return false; - } - // Equals handles the case of null on right side. - return lhs.Equals(rhs); - } - - public static bool operator !=(ThreeDPoint? lhs, ThreeDPoint? rhs) => !(lhs == rhs); -} -// - -// -class Program -{ - static void Main(string[] args) - { - // - Console.WriteLine("=== Safe Polymorphic Equality ==="); - - // Test polymorphic scenarios that were problematic before - TwoDPoint p1 = new ThreeDPoint(1, 2, 3); - TwoDPoint p2 = new ThreeDPoint(1, 2, 4); - TwoDPoint p3 = new ThreeDPoint(1, 2, 3); - TwoDPoint p4 = new TwoDPoint(1, 2); - - Console.WriteLine("Testing polymorphic equality (declared as TwoDPoint):"); - Console.WriteLine($"p1 = ThreeDPoint(1, 2, 3) as TwoDPoint"); - Console.WriteLine($"p2 = ThreeDPoint(1, 2, 4) as TwoDPoint"); - Console.WriteLine($"p3 = ThreeDPoint(1, 2, 3) as TwoDPoint"); - Console.WriteLine($"p4 = TwoDPoint(1, 2)"); - Console.WriteLine(); - - Console.WriteLine($"p1.Equals(p2) = {p1.Equals(p2)}"); // False - different Z values - Console.WriteLine($"p1.Equals(p3) = {p1.Equals(p3)}"); // True - same values - Console.WriteLine($"p1.Equals(p4) = {p1.Equals(p4)}"); // False - different types - Console.WriteLine($"p4.Equals(p1) = {p4.Equals(p1)}"); // False - different types - Console.WriteLine(); - // - - // - // Test direct type comparisons - var point3D_A = new ThreeDPoint(3, 4, 5); - var point3D_B = new ThreeDPoint(3, 4, 5); - var point3D_C = new ThreeDPoint(3, 4, 7); - var point2D_A = new TwoDPoint(3, 4); - - Console.WriteLine("Testing direct type comparisons:"); - Console.WriteLine($"point3D_A.Equals(point3D_B) = {point3D_A.Equals(point3D_B)}"); // True - Console.WriteLine($"point3D_A.Equals(point3D_C) = {point3D_A.Equals(point3D_C)}"); // False - Console.WriteLine($"point3D_A.Equals(point2D_A) = {point3D_A.Equals(point2D_A)}"); // False - Console.WriteLine($"point2D_A.Equals(point3D_A) = {point2D_A.Equals(point3D_A)}"); // False - Console.WriteLine(); - // - - // - // Test operators - Console.WriteLine("Testing operators:"); - Console.WriteLine($"p1 == p2: {p1 == p2}"); // False - Console.WriteLine($"p1 == p3: {p1 == p3}"); // True - Console.WriteLine($"point3D_A == point3D_B: {point3D_A == point3D_B}"); // True - Console.WriteLine(); - // - - // - // Test with collections - Console.WriteLine("Testing with collections:"); - var hashSet = new HashSet { p1, p2, p3, p4 }; - Console.WriteLine($"HashSet contains {hashSet.Count} unique points"); // Should be 3: one ThreeDPoint(1,2,3), one ThreeDPoint(1,2,4), one TwoDPoint(1,2) - - var dictionary = new Dictionary - { - { p1, "First 3D point" }, - { p2, "Second 3D point" }, - { p4, "2D point" } - }; - - Console.WriteLine($"Dictionary contains {dictionary.Count} entries"); - Console.WriteLine($"Dictionary lookup for equivalent point: {dictionary.ContainsKey(new ThreeDPoint(1, 2, 3))}"); // True - // - - Console.WriteLine("Press any key to exit."); - Console.ReadKey(); - } -} -// - -/* Expected Output: -=== Safe Polymorphic Equality === -Testing polymorphic equality (declared as TwoDPoint): -p1 = ThreeDPoint(1, 2, 3) as TwoDPoint -p2 = ThreeDPoint(1, 2, 4) as TwoDPoint -p3 = ThreeDPoint(1, 2, 3) as TwoDPoint -p4 = TwoDPoint(1, 2) - -p1.Equals(p2) = False -p1.Equals(p3) = True -p1.Equals(p4) = False -p4.Equals(p1) = False - -Testing direct type comparisons: -point3D_A.Equals(point3D_B) = True -point3D_A.Equals(point3D_C) = False -point3D_A.Equals(point2D_A) = False -point2D_A.Equals(point3D_A) = False - -Testing operators: -p1 == p2: False -p1 == p3: True -point3D_A == point3D_B: True - -Testing with collections: -HashSet contains 3 unique points -Dictionary contains 3 entries -Dictionary lookup for equivalent point: True -*/ \ 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/ValueEqualityPolymorphic/ValueEqualityPolymorphic.csproj b/docs/csharp/programming-guide/statements-expressions-operators/snippets/how-to-define-value-equality-for-a-type/ValueEqualityPolymorphic/ValueEqualityPolymorphic.csproj deleted file mode 100644 index fd4dd4565750e..0000000000000 --- a/docs/csharp/programming-guide/statements-expressions-operators/snippets/how-to-define-value-equality-for-a-type/ValueEqualityPolymorphic/ValueEqualityPolymorphic.csproj +++ /dev/null @@ -1,10 +0,0 @@ - - - - Exe - net8.0 - enable - enable - - - \ 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/Program.cs b/docs/csharp/programming-guide/statements-expressions-operators/snippets/how-to-define-value-equality-for-a-type/ValueEqualityRecord/Program.cs deleted file mode 100644 index f9041b9ce482d..0000000000000 --- a/docs/csharp/programming-guide/statements-expressions-operators/snippets/how-to-define-value-equality-for-a-type/ValueEqualityRecord/Program.cs +++ /dev/null @@ -1,99 +0,0 @@ -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 deleted file mode 100644 index fd4dd4565750e..0000000000000 --- a/docs/csharp/programming-guide/statements-expressions-operators/snippets/how-to-define-value-equality-for-a-type/ValueEqualityRecord/ValueEqualityRecord.csproj +++ /dev/null @@ -1,10 +0,0 @@ - - - - Exe - net8.0 - enable - enable - - - \ 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/ValueEqualityStruct/Program.cs b/docs/csharp/programming-guide/statements-expressions-operators/snippets/how-to-define-value-equality-for-a-type/ValueEqualityStruct/Program.cs deleted file mode 100644 index aa4a81f1620a4..0000000000000 --- a/docs/csharp/programming-guide/statements-expressions-operators/snippets/how-to-define-value-equality-for-a-type/ValueEqualityStruct/Program.cs +++ /dev/null @@ -1,97 +0,0 @@ -namespace ValueEqualityStruct -{ - struct TwoDPoint : IEquatable - { - public int X { get; private set; } - public int Y { get; private set; } - - public TwoDPoint(int x, int y) - : this() - { - if (x is (< 1 or > 2000) || y is (< 1 or > 2000)) - { - throw new ArgumentException("Point must be in range 1 - 2000"); - } - X = x; - Y = y; - } - - public override bool Equals(object? obj) => obj is TwoDPoint other && this.Equals(other); - - public bool Equals(TwoDPoint p) => X == p.X && Y == p.Y; - - public override int GetHashCode() => (X, Y).GetHashCode(); - - public static bool operator ==(TwoDPoint lhs, TwoDPoint rhs) => lhs.Equals(rhs); - - public static bool operator !=(TwoDPoint lhs, TwoDPoint rhs) => !(lhs == rhs); - } - - class Program - { - static void Main(string[] args) - { - TwoDPoint pointA = new TwoDPoint(3, 4); - TwoDPoint pointB = new TwoDPoint(3, 4); - int i = 5; - - // True: - Console.WriteLine($"pointA.Equals(pointB) = {pointA.Equals(pointB)}"); - // True: - Console.WriteLine($"pointA == pointB = {pointA == pointB}"); - // True: - Console.WriteLine($"object.Equals(pointA, pointB) = {object.Equals(pointA, pointB)}"); - // False: - Console.WriteLine($"pointA.Equals(null) = {pointA.Equals(null)}"); - // False: - Console.WriteLine($"(pointA == null) = {pointA == null}"); - // True: - Console.WriteLine($"(pointA != null) = {pointA != null}"); - // False: - Console.WriteLine($"pointA.Equals(i) = {pointA.Equals(i)}"); - // CS0019: - // Console.WriteLine($"pointA == i = {pointA == i}"); - - // Compare unboxed to boxed. - System.Collections.ArrayList list = new System.Collections.ArrayList(); - list.Add(new TwoDPoint(3, 4)); - // True: - Console.WriteLine($"pointA.Equals(list[0]): {pointA.Equals(list[0])}"); - - // Compare nullable to nullable and to non-nullable. - TwoDPoint? pointC = null; - TwoDPoint? pointD = null; - // False: - Console.WriteLine($"pointA == (pointC = null) = {pointA == pointC}"); - // True: - Console.WriteLine($"pointC == pointD = {pointC == pointD}"); - - TwoDPoint temp = new TwoDPoint(3, 4); - pointC = temp; - // True: - Console.WriteLine($"pointA == (pointC = 3,4) = {pointA == pointC}"); - - pointD = temp; - // True: - Console.WriteLine($"pointD == (pointC = 3,4) = {pointD == pointC}"); - - Console.WriteLine("Press any key to exit."); - Console.ReadKey(); - } - } - - /* Output: - pointA.Equals(pointB) = True - pointA == pointB = True - Object.Equals(pointA, pointB) = True - pointA.Equals(null) = False - (pointA == null) = False - (pointA != null) = True - pointA.Equals(i) = False - pointE.Equals(list[0]): True - pointA == (pointC = null) = False - pointC == pointD = True - pointA == (pointC = 3,4) = True - pointD == (pointC = 3,4) = True - */ -} diff --git a/docs/csharp/programming-guide/statements-expressions-operators/snippets/how-to-define-value-equality-for-a-type/ValueEqualityStruct/ValueEqualityStruct.csproj b/docs/csharp/programming-guide/statements-expressions-operators/snippets/how-to-define-value-equality-for-a-type/ValueEqualityStruct/ValueEqualityStruct.csproj deleted file mode 100644 index f704bf4988fa6..0000000000000 --- a/docs/csharp/programming-guide/statements-expressions-operators/snippets/how-to-define-value-equality-for-a-type/ValueEqualityStruct/ValueEqualityStruct.csproj +++ /dev/null @@ -1,10 +0,0 @@ - - - - Exe - net8.0 - enable - enable - - - diff --git a/docs/csharp/programming-guide/statements-expressions-operators/snippets/how-to-test-for-reference-equality-identity/Program.cs b/docs/csharp/programming-guide/statements-expressions-operators/snippets/how-to-test-for-reference-equality-identity/Program.cs deleted file mode 100644 index 8d8bdcaf9118f..0000000000000 --- a/docs/csharp/programming-guide/statements-expressions-operators/snippets/how-to-test-for-reference-equality-identity/Program.cs +++ /dev/null @@ -1,103 +0,0 @@ -using System.Text; - -namespace TestReferenceEquality -{ - struct TestStruct - { - public int Num { get; private set; } - public string Name { get; private set; } - - public TestStruct(int i, string s) : this() - { - Num = i; - Name = s; - } - } - - class TestClass - { - public int Num { get; set; } - public string? Name { get; set; } - } - - class Program - { - static void Main() - { - // Demonstrate reference equality with reference types. - #region ReferenceTypes - - // Create two reference type instances that have identical values. - TestClass tcA = new TestClass() { Num = 1, Name = "New TestClass" }; - TestClass tcB = new TestClass() { Num = 1, Name = "New TestClass" }; - - Console.WriteLine($"ReferenceEquals(tcA, tcB) = {Object.ReferenceEquals(tcA, tcB)}"); // false - - // After assignment, tcB and tcA refer to the same object. - // They now have reference equality. - tcB = tcA; - Console.WriteLine($"After assignment: ReferenceEquals(tcA, tcB) = {Object.ReferenceEquals(tcA, tcB)}"); // true - - // Changes made to tcA are reflected in tcB. Therefore, objects - // that have reference equality also have value equality. - tcA.Num = 42; - tcA.Name = "TestClass 42"; - Console.WriteLine($"tcB.Name = {tcB.Name} tcB.Num: {tcB.Num}"); - #endregion - - // Demonstrate that two value type instances never have reference equality. - #region ValueTypes - - TestStruct tsC = new TestStruct( 1, "TestStruct 1"); - - // Value types are boxed into separate objects when passed to ReferenceEquals. - // Even if the same variable is used twice, boxing ensures they are different instances. - TestStruct tsD = tsC; - Console.WriteLine($"After assignment: ReferenceEquals(tsC, tsD) = {Object.ReferenceEquals(tsC, tsD)}"); // false - #endregion - - #region stringRefEquality - // Constant strings within the same assembly are always interned by the runtime. - // This means they are stored in the same location in memory. Therefore, - // the two strings have reference equality although no assignment takes place. - string strA = "Hello world!"; - string strB = "Hello world!"; - Console.WriteLine($"ReferenceEquals(strA, strB) = {Object.ReferenceEquals(strA, strB)}"); // true - - // After a new string is assigned to strA, strA and strB - // are no longer interned and no longer have reference equality. - strA = "Goodbye world!"; - Console.WriteLine($"strA = '{strA}' strB = '{strB}'"); - - Console.WriteLine("After strA changes, ReferenceEquals(strA, strB) = {0}", - Object.ReferenceEquals(strA, strB)); // false - - // A string that is created at runtime cannot be interned. - StringBuilder sb = new StringBuilder("Hello world!"); - string stringC = sb.ToString(); - // False: - Console.WriteLine($"ReferenceEquals(stringC, strB) = {Object.ReferenceEquals(stringC, strB)}"); - - // The string class overloads the == operator to perform an equality comparison. - Console.WriteLine($"stringC == strB = {stringC == strB}"); // true - - #endregion - - // Keep the console open in debug mode. - Console.WriteLine("Press any key to exit."); - Console.ReadKey(); - } - } -} - -/* Output: - ReferenceEquals(tcA, tcB) = False - After assignment: ReferenceEquals(tcA, tcB) = True - tcB.Name = TestClass 42 tcB.Num: 42 - After assignment: ReferenceEquals(tsC, tsD) = False - ReferenceEquals(strA, strB) = True - strA = "Goodbye world!" strB = "Hello world!" - After strA changes, ReferenceEquals(strA, strB) = False - ReferenceEquals(stringC, strB) = False - stringC == strB = True -*/ diff --git a/docs/csharp/programming-guide/statements-expressions-operators/snippets/how-to-test-for-reference-equality-identity/TestingReferenceEquality.csproj b/docs/csharp/programming-guide/statements-expressions-operators/snippets/how-to-test-for-reference-equality-identity/TestingReferenceEquality.csproj deleted file mode 100644 index 116202dc2c2bd..0000000000000 --- a/docs/csharp/programming-guide/statements-expressions-operators/snippets/how-to-test-for-reference-equality-identity/TestingReferenceEquality.csproj +++ /dev/null @@ -1,11 +0,0 @@ - - - - Exe - net8.0 - enable - enable - TestingReferenceEquality - - - diff --git a/docs/csharp/toc.yml b/docs/csharp/toc.yml index eba9d52ac5ffd..194b6d7dd4156 100644 --- a/docs/csharp/toc.yml +++ b/docs/csharp/toc.yml @@ -544,14 +544,6 @@ items: href: programming-guide/statements-expressions-operators/statements.md - name: Expression-bodied members href: programming-guide/statements-expressions-operators/expression-bodied-members.md - - name: Equality and equality comparisons - items: - - name: Equality comparisons - href: programming-guide/statements-expressions-operators/equality-comparisons.md - - name: "How to define value equality for a type" - href: programming-guide/statements-expressions-operators/how-to-define-value-equality-for-a-type.md - - name: "How to test for reference equality (identity)" - href: programming-guide/statements-expressions-operators/how-to-test-for-reference-equality-identity.md - name: Types items: - name: Casting and Type Conversions From 879228e229412cec6cd066b80a251e48279c6b7a Mon Sep 17 00:00:00 2001 From: Bill Wagner Date: Tue, 18 Aug 2026 15:20:34 -0400 Subject: [PATCH 06/11] restructure equality article. Co-Authored-By: Copilot <223556219+Copilot@users.noreply.github.com> --- .../fundamentals/expressions/equality.md | 70 +++++++++---------- 1 file changed, 35 insertions(+), 35 deletions(-) diff --git a/docs/csharp/fundamentals/expressions/equality.md b/docs/csharp/fundamentals/expressions/equality.md index 8c600ff54ee1a..c61227bca98f2 100644 --- a/docs/csharp/fundamentals/expressions/equality.md +++ b/docs/csharp/fundamentals/expressions/equality.md @@ -51,6 +51,20 @@ Tuples are value types too. Two tuples are equal when every element value matche For more information about tuple syntax and deconstruction, see [Tuples and deconstruction](../types/tuples.md). +## Use `Object.ReferenceEquals` to test identity directly + + always tests identity regardless of how a type overrides or overloads `==`. Use it as an identity diagnostic when you need to confirm whether two variables point to the exact same object: + +:::code language="csharp" source="snippets/equality/Program.cs" ID="ReferenceEqualsDemo"::: + +A common use is inside an `Equals` override to short-circuit the full comparison: when both arguments are the same reference, they're always equal without checking individual fields. + +> [!NOTE] +> Advanced detail: when variables are typed as an [interface](../types/interfaces.md), `==` checks whether the interface variables refer to the same object. A call to `Equals` still runs the underlying object's implementation. + +> [!NOTE] +> always returns `false` when comparing value types, even if both arguments contain the same values. This is because each value-type argument is independently *boxed* into a separate heap object when passed to `ReferenceEquals`. + ## Types can define different equality semantics Defaults aren't destiny. Some types define equality semantics that differ from the type-kind default, and your own types can do the same when their data should determine equality. @@ -85,10 +99,30 @@ The same compiler generation applies to `record struct` types: Record types generate the whole equality set for their own type. Both `record class` and `record struct` types override and . They also generate `==` and `!=` operators, plus a typed `Equals` method for the record type. Unlike a plain `struct`, a `record struct` therefore supports `==` and `!=` automatically. For more information about record types and their equality semantics, see [Records](../types/records.md#value-equality). +## Records with reference-type members + +Record equality is synthesized from the members' own equality. Each property or field is compared using its own `Equals` method. For most scalar values—`int`, `string`, `DateTime`, and similar types—that works exactly as you'd expect. The subtlety arises with common mutable collections such as `List` or `T[]`: these types compare by reference, so two record instances that contain *different list objects with the same content* are **not** considered equal by the synthesized record equality. + +:::code language="csharp" source="snippets/equality/Program.cs" ID="RecordWithCollectionProblem"::: + +`playlist1` and `playlist2` are separate `List` instances. Even though their contents match, `Equals` returns `false`. + +When you need two-record equality to reflect collection *contents*, you have a few options: + +- **Custom `IEquatable` override**: Implement `IEquatable` on the record and use (or an appropriate comparison) for the collection members. + + :::code language="csharp" source="snippets/equality/Program.cs" ID="PlaylistFixedDefinition"::: + + :::code language="csharp" source="snippets/equality/Program.cs" ID="RecordWithCollectionFixed"::: + +- **Use collection types with value equality**: doesn't override equality either, but a record that wraps a `ReadOnlySpan` or uses `SequenceEqual` in a custom `Equals` achieves the same goal. The key insight is to pick the right abstraction rather than fighting the defaults. + +- **Design around identity**: If the record represents an entity rather than a value—and the collection members are logically shared—then reference equality for those members may be intentional. Design the type to reuse the same list instance where equality matters. + ## Implement equality yourself when a type can't be a record > [!IMPORTANT] -> This section shows how to implement by hand the equality behavior that the compiler generates when you add `record` to a type. If your type can be a record, use `record` instead. It generates all these members for you. Implement them manually only when your type can't be a record. +> This section shows how to implement by hand the equality behavior that the compiler generates when you add `record` to a type. If your type can be a record, use `record` instead. It generates all these members for you. Implement them manually only when your type can't be a record. Correctly implementing all the requirements for equality requires you to understand the expectations for these operations. This section contains all those rules. While those seem complicated remember that you can almost always use a `record` type and have the language automatically create implementations that comply with all these requirements. When a class or struct represents a value, such as a color or a measurement, the equality members for that type must agree. The easiest way to achieve this consistency is to declare the type as a `record`. If the type can't be a record, such as when it must derive from a non-record class, implement the equality members yourself. The language enforces that user-defined `==` and `!=` operators must be declared as a pair. If you provide those operators, compiler warning [CS0660](../../language-reference/compiler-messages/overloaded-operator-errors.md#equality-operators) means the type also needs an override. Warning [CS0661](../../language-reference/compiler-messages/overloaded-operator-errors.md#equality-operators) means the type also needs an override. @@ -119,40 +153,6 @@ At this point, `Equals` reflects value equality, but `==` still tests identity f Adding `==` and `!=` operators is the remaining step when you need operator comparisons. This article intentionally stops before the full operator implementation so the first pass can focus on the equality contract. The operator-focused follow-up shows the completed shape. For the operator syntax, see [Equality operators](../../language-reference/operators/equality-operators.md) in the language reference. -## Use `Object.ReferenceEquals` to test identity directly - - always tests identity regardless of how a type overrides or overloads `==`. Use it as an identity diagnostic when you need to confirm whether two variables point to the exact same object: - -:::code language="csharp" source="snippets/equality/Program.cs" ID="ReferenceEqualsDemo"::: - -A common use is inside an `Equals` override to short-circuit the full comparison: when both arguments are the same reference, they're always equal without checking individual fields. - -> [!NOTE] -> Advanced detail: when variables are typed as an [interface](../types/interfaces.md), `==` checks whether the interface variables refer to the same object. A call to `Equals` still runs the underlying object's implementation. - -> [!NOTE] -> always returns `false` when comparing value types, even if both arguments contain the same values. This is because each value-type argument is independently *boxed* into a separate heap object when passed to `ReferenceEquals`. - -## Records with reference-type members - -Record equality is synthesized from the members' own equality. Each property or field is compared using its own `Equals` method. For most scalar values—`int`, `string`, `DateTime`, and similar types—that works exactly as you'd expect. The subtlety arises with common mutable collections such as `List` or `T[]`: these types compare by reference, so two record instances that contain *different list objects with the same content* are **not** considered equal by the synthesized record equality. - -:::code language="csharp" source="snippets/equality/Program.cs" ID="RecordWithCollectionProblem"::: - -`playlist1` and `playlist2` are separate `List` instances. Even though their contents match, `Equals` returns `false`. - -When you need two-record equality to reflect collection *contents*, you have a few options: - -- **Custom `IEquatable` override**: Implement `IEquatable` on the record and use (or an appropriate comparison) for the collection members. - - :::code language="csharp" source="snippets/equality/Program.cs" ID="PlaylistFixedDefinition"::: - - :::code language="csharp" source="snippets/equality/Program.cs" ID="RecordWithCollectionFixed"::: - -- **Use collection types with value equality**: doesn't override equality either, but a record that wraps a `ReadOnlySpan` or uses `SequenceEqual` in a custom `Equals` achieves the same goal. The key insight is to pick the right abstraction rather than fighting the defaults. - -- **Design around identity**: If the record represents an entity rather than a value—and the collection members are logically shared—then reference equality for those members may be intentional. Design the type to reuse the same list instance where equality matters. - ## Polymorphic equality in unsealed class hierarchies Implementing value equality in an unsealed class hierarchy requires extra care. The hazard is that `IEquatable.Equals(T? other)` is dispatched at compile time based on the *declared type* of the variable, not the runtime type. If `TwoDPoint` declares a non-virtual `Equals(TwoDPoint? other)`, then a variable declared as `TwoDPoint` but holding a `ThreeDPoint` at runtime calls `TwoDPoint.Equals`, silently ignoring the extra dimension. The result is that two points with different `Z` values incorrectly compare as equal. From 9a73934de586908fad3dd5818b60364186c087af Mon Sep 17 00:00:00 2001 From: Bill Wagner Date: Wed, 19 Aug 2026 11:21:58 -0400 Subject: [PATCH 07/11] Add 'Equality in class hierarchies' section to Language Reference operators. Implements the persisted plan from PR #55469 equality restructuring: - Add ## Equality in class hierarchies before ## Operator overloadability in equality-operators.md; covers declared/runtime-type dispatch hazards, GetType() guard, virtual Equals, derived-class augmentation, GetHashCode with GetType(), sealed-class simplification, and records guidance. - Create net10.0 snippet project at docs/csharp/language-reference/operators/snippets/EqualityHierarchies/ with HierarchyShapeDefinition, HierarchyCircleDefinition, HierarchyUsage regions (all build-verified, 0 warnings/errors). - Update metadata: description, ms.date, helpviewer_keywords. - Add reciprocal Fundamentals link in new section. Cray item 3 (relocate polymorphic equality to Language Reference) implemented. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: f5acfb15-adff-4860-a307-213196efda1c --- .../operators/equality-operators.md | 45 +++++++++++++++- .../EqualityHierarchies.csproj | 10 ++++ .../snippets/EqualityHierarchies/Program.cs | 52 +++++++++++++++++++ 3 files changed, 105 insertions(+), 2 deletions(-) create mode 100644 docs/csharp/language-reference/operators/snippets/EqualityHierarchies/EqualityHierarchies.csproj create mode 100644 docs/csharp/language-reference/operators/snippets/EqualityHierarchies/Program.cs diff --git a/docs/csharp/language-reference/operators/equality-operators.md b/docs/csharp/language-reference/operators/equality-operators.md index e8cd85d42b96e..8f584ff834489 100644 --- a/docs/csharp/language-reference/operators/equality-operators.md +++ b/docs/csharp/language-reference/operators/equality-operators.md @@ -1,7 +1,7 @@ --- title: "Equality operators - test if two objects are equal or not equal" -description: "C# equality operators test if two objects are equal or not equal. You can define equality operators for your types for custom comparisons for equality" -ms.date: 01/20/2026 +description: "C# equality operators test if two objects are equal or not equal. You can define equality operators for your types for custom comparisons for equality. Learn how to implement value equality correctly in sealed types and unsealed class hierarchies." +ms.date: 08/19/2026 author: pkulikov f1_keywords: - "==_CSharpKeyword" @@ -15,6 +15,8 @@ helpviewer_keywords: - "inequality operator [C#]" - "not equals operator [C#]" - "!= operator [C#]" + - "equality in class hierarchies [C#]" + - "polymorphic equality [C#]" --- # Equality operators - test if two objects are equal or not @@ -92,6 +94,45 @@ The following example demonstrates how to use the `!=` operator: :::code language="csharp" source="snippets/shared/EqualityOperators.cs" id="NonEquality"::: +## Equality in class hierarchies + +Value equality in an unsealed class hierarchy requires more care than in a sealed class. The hazard is that `IEquatable.Equals(T? other)` dispatch follows the *declared type* of the variable, not its runtime type. If `Shape` declares a non-`virtual` `Equals(Shape? other)`, a variable typed as `Shape` that holds a `Circle` at runtime invokes `Shape.Equals`—silently ignoring `Circle`-specific fields. Two `Circle` objects with different radii can compare as equal when accessed through a `Shape` variable. + +The correct pattern requires two cooperating requirements: make the typed `Equals` method `virtual` so each derived class can extend the comparison, and add a `GetType() == other.GetType()` guard in the base-class implementation so objects of different runtime types are never considered equal. + +### Base class implementation + +:::code language="csharp" source="snippets/EqualityHierarchies/Program.cs" id="HierarchyShapeDefinition"::: + +Key points: + +- **`virtual` typed `Equals`**: each derived class overrides this method to augment the comparison with its own fields. +- **`GetType()` guard**: `GetType() == other.GetType()` prevents a `Circle` from equaling a `Shape` with the same color, and prevents objects of different derived types from equaling each other. +- **`GetHashCode` includes `GetType()`**: because two objects are equal only when their runtime types match, `GetHashCode` must hash the runtime type as well as the data fields. Omitting `GetType()` here causes incorrect behavior in `Dictionary` and `HashSet`. +- **`==` delegates to `Equals`**: keeps operator and method equality consistent. + +### Derived class implementation + +A derived class that adds fields overrides the typed `Equals`, casts to its own type, calls `base.Equals`, then compares its own fields: + +:::code language="csharp" source="snippets/EqualityHierarchies/Program.cs" id="HierarchyCircleDefinition"::: + +`base.Equals(c)` enforces the `GetType()` guard and checks the shared fields. The cast via `other is Circle c` fails fast when the argument is a `Shape` of any other derived type. + +### Usage through a base-type variable + +:::code language="csharp" source="snippets/EqualityHierarchies/Program.cs" id="HierarchyUsage"::: + +### Sealed classes are simpler + +A `sealed` class cannot be subclassed, so compile-time and runtime types always agree. The `GetType()` guard and `virtual` dispatch are unnecessary. The `IEquatable` pattern shown in [Implement equality yourself](../../fundamentals/expressions/equality.md#implement-equality-yourself-when-a-type-cant-be-a-record) is correct and complete for a sealed class. + +### Prefer records for value equality in hierarchies + +Records handle inheritance correctly without manual work. The compiler-generated equality checks both runtime type and all declared properties, satisfying the symmetry and transitivity requirements automatically. Prefer `record` over a manual unsealed hierarchy when value equality is the goal. + +For an introduction to equality semantics across C# type kinds, see [Equality comparisons](../../fundamentals/expressions/equality.md). + ## Operator overloadability You can [overload](operator-overloading.md) the `==` and `!=` operators in a user-defined type. If you overload one of these two operators, you must also overload the other operator. diff --git a/docs/csharp/language-reference/operators/snippets/EqualityHierarchies/EqualityHierarchies.csproj b/docs/csharp/language-reference/operators/snippets/EqualityHierarchies/EqualityHierarchies.csproj new file mode 100644 index 0000000000000..5c0a78df5ac6b --- /dev/null +++ b/docs/csharp/language-reference/operators/snippets/EqualityHierarchies/EqualityHierarchies.csproj @@ -0,0 +1,10 @@ + + + + Exe + net10.0 + enable + enable + + + \ No newline at end of file diff --git a/docs/csharp/language-reference/operators/snippets/EqualityHierarchies/Program.cs b/docs/csharp/language-reference/operators/snippets/EqualityHierarchies/Program.cs new file mode 100644 index 0000000000000..71f49ebbc2e01 --- /dev/null +++ b/docs/csharp/language-reference/operators/snippets/EqualityHierarchies/Program.cs @@ -0,0 +1,52 @@ +// +Shape circle1 = new Circle("red", 5.0); +Shape circle2 = new Circle("red", 7.0); +Shape circle3 = new Circle("red", 5.0); +Shape shape1 = new Shape("red"); + +Console.WriteLine(circle1.Equals(circle2)); // => False (Radius differs) +Console.WriteLine(circle1.Equals(circle3)); // => True +Console.WriteLine(circle1.Equals(shape1)); // => False (different runtime types) +// + +// ── Type declarations ──────────────────────────────────────────────────────── + +// +// Shape is an unsealed base class. Making Equals virtual and guarding with GetType() +// ensures a derived instance is never equal to an instance of a different runtime type. +class Shape : IEquatable +{ + public string Color { get; } + public Shape(string color) => Color = color; + + public override bool Equals(object? obj) => Equals(obj as Shape); + + // virtual so derived classes can override and augment the comparison + public virtual bool Equals(Shape? other) => + other is not null && + GetType() == other.GetType() && // reject different runtime types + Color == other.Color; + + // GetType() is included because equality requires matching runtime types + public override int GetHashCode() => HashCode.Combine(GetType(), Color); + + public static bool operator ==(Shape? l, Shape? r) => l?.Equals(r) ?? r is null; + public static bool operator !=(Shape? l, Shape? r) => !(l == r); +} +// + +// +class Circle : Shape +{ + public double Radius { get; } + public Circle(string color, double radius) : base(color) => Radius = radius; + + public override bool Equals(object? obj) => Equals(obj as Shape); + + // Calls base.Equals to verify Color and runtime type, then adds Radius + public override bool Equals(Shape? other) => + other is Circle c && base.Equals(c) && Radius == c.Radius; + + public override int GetHashCode() => HashCode.Combine(GetType(), Color, Radius); +} +// \ No newline at end of file From 86435ea4773425351d47e9f527144b98e04eccb3 Mon Sep 17 00:00:00 2001 From: Bill Wagner Date: Wed, 19 Aug 2026 11:22:19 -0400 Subject: [PATCH 08/11] Simplify Fundamentals equality article; add byte-range note to operators. Implements remaining Cray review items for PR #55469: Cray item 1 (Records with reference-type members): - Remove PlaylistFixed/IEquatable implementation code; replace rendered code blocks with brief named-strategy list; keep RecordWithCollectionProblem surprise example intact. Cray item 2 (Implement equality yourself): - Lead with code (ColorDefinition first); shorten IMPORTANT callout to 2 sentences; consolidate member-list description into commentary after the code; compact equivalence contract intro; demote IEquatable footnote. - Retain IEquatableUsage region to show identity-vs-value contrast. Cray item 3 (Polymorphic section bridge): - Replace full polymorphic implementation in Fundamentals with a 3-sentence hazard summary + link to Language Reference ## Equality in class hierarchies. - Preserve ## Polymorphic equality in unsealed class hierarchies heading. Cray item 4 (operators.md byte-range): - Add byte-range clarification: 'the result, 210, fits within the byte range of 0-255'; beginner-safe, no checked/unchecked discussion. Cray item 5 (hash-loop): auto-resolved by PlaylistFixed removal. Snippets: - Remove PlaylistFixedDefinition, RecordWithCollectionFixed regions and PlaylistFixed type from Fundamentals Program.cs. - Remove PolymorphicEqualityDefinition, PolymorphicEqualityUsage regions (Shape/Circle now live in LR EqualityHierarchies project). - Fundamentals snippet builds 0 warnings/errors (net10.0). Links/redirects: - All three Programming Guide redirect targets unchanged (anchors verified). - Add reciprocal LR link in equality.md See also. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: f5acfb15-adff-4860-a307-213196efda1c --- .../fundamentals/expressions/equality.md | 65 +++++------------ .../fundamentals/expressions/operators.md | 2 +- .../expressions/snippets/equality/Program.cs | 69 ------------------- 3 files changed, 20 insertions(+), 116 deletions(-) diff --git a/docs/csharp/fundamentals/expressions/equality.md b/docs/csharp/fundamentals/expressions/equality.md index c61227bca98f2..3bc03416cb56e 100644 --- a/docs/csharp/fundamentals/expressions/equality.md +++ b/docs/csharp/fundamentals/expressions/equality.md @@ -107,33 +107,30 @@ Record equality is synthesized from the members' own equality. Each property or `playlist1` and `playlist2` are separate `List` instances. Even though their contents match, `Equals` returns `false`. -When you need two-record equality to reflect collection *contents*, you have a few options: +When you need record equality to reflect collection *contents*, you have a few options: -- **Custom `IEquatable` override**: Implement `IEquatable` on the record and use (or an appropriate comparison) for the collection members. +- **Implement `IEquatable`** on the record and override `Equals` to use for the collection members. +- **Use a collection type with value equality** — for example, a custom `IEqualityComparer` or a type whose own `Equals` compares elements. +- **Design around identity**: if the record represents an entity rather than a pure value, reference equality for its collection members may be intentional. - :::code language="csharp" source="snippets/equality/Program.cs" ID="PlaylistFixedDefinition"::: - - :::code language="csharp" source="snippets/equality/Program.cs" ID="RecordWithCollectionFixed"::: +## Implement equality yourself when a type can't be a record -- **Use collection types with value equality**: doesn't override equality either, but a record that wraps a `ReadOnlySpan` or uses `SequenceEqual` in a custom `Equals` achieves the same goal. The key insight is to pick the right abstraction rather than fighting the defaults. +> [!IMPORTANT] +> Use `record` whenever possible — the compiler generates all required equality members for you. Manual implementation is only needed when your type must derive from a non-record class or has other constraints that prevent `record`. -- **Design around identity**: If the record represents an entity rather than a value—and the collection members are logically shared—then reference equality for those members may be intentional. Design the type to reuse the same list instance where equality matters. +Here is a minimal manual implementation for a value type that can't be a record: -## Implement equality yourself when a type can't be a record +:::code language="csharp" source="snippets/equality/Program.cs" ID="ColorDefinition"::: -> [!IMPORTANT] -> This section shows how to implement by hand the equality behavior that the compiler generates when you add `record` to a type. If your type can be a record, use `record` instead. It generates all these members for you. Implement them manually only when your type can't be a record. Correctly implementing all the requirements for equality requires you to understand the expectations for these operations. This section contains all those rules. While those seem complicated remember that you can almost always use a `record` type and have the language automatically create implementations that comply with all these requirements. +The implementation provides three required members: `Equals(Color?)` as the core comparison, `override Equals(object?)` for object-level calls, and `override GetHashCode()` so hash-based collections work correctly. `HashCode.Combine` is a library helper that builds one hash from the same values used by `Equals`. Implementing (the `Equals(Color?)` overload) is optional but avoids boxing when callers already have the concrete type. -When a class or struct represents a value, such as a color or a measurement, the equality members for that type must agree. The easiest way to achieve this consistency is to declare the type as a `record`. If the type can't be a record, such as when it must derive from a non-record class, implement the equality members yourself. The language enforces that user-defined `==` and `!=` operators must be declared as a pair. If you provide those operators, compiler warning [CS0660](../../language-reference/compiler-messages/overloaded-operator-errors.md#equality-operators) means the type also needs an override. Warning [CS0661](../../language-reference/compiler-messages/overloaded-operator-errors.md#equality-operators) means the type also needs an override. +When you also define `==` and `!=`, the language requires them as a pair; warnings [CS0660](../../language-reference/compiler-messages/overloaded-operator-errors.md#equality-operators) and [CS0661](../../language-reference/compiler-messages/overloaded-operator-errors.md#equality-operators) remind you to keep all four members consistent. -In a complete manual implementation, provide these members: +With the three members above in place, `Equals` reflects value equality, but `==` still tests identity because no `==` operator has been declared yet: -- `==` and `!=` operators. Add them as a pair because the compiler requires a type that overloads one to overload the other. -- An `override` of . This override changes equality semantics for the type and keeps object-level equality consistent. -- An `override` of . Objects that are equal must return the same hash code. Without this pairing, the type behaves incorrectly in hash-based collections such as `Dictionary` or `HashSet`. See for guidance on a correct implementation. -- Optionally, a typed `Equals` method by implementing . You often see this written as `Equals(T?)` in docs: `T` is a [type parameter](../types/generics.md), a placeholder for the current type, and `?` is a [nullable annotation](../null-safety/index.md) that says the argument can be `null`. This typed method can avoid extra conversions when callers already have the same type, but it's a secondary optimization. +:::code language="csharp" source="snippets/equality/Program.cs" ID="IEquatableUsage"::: -A correct implementation also satisfies the *equivalence contract*. The following rules assume `x`, `y`, and `z` are not null: +A correct implementation must also satisfy the *equivalence contract* (assume `x`, `y`, and `z` are non-null): 1. **Reflexive**: `x.Equals(x)` returns `true`. 2. **Symmetric**: `x.Equals(y)` returns the same value as `y.Equals(x)`. @@ -141,39 +138,14 @@ A correct implementation also satisfies the *equivalence contract*. The followin 4. **Consistent**: successive calls to `x.Equals(y)` return the same value as long as neither object changes. 5. **Null behavior**: `x.Equals(null)` returns `false`; `x.Equals(y)` must not throw when called on a non-null `x`. -The symmetric and transitive rules are easy to violate in inheritance hierarchies. See [Polymorphic equality in unsealed class hierarchies](#polymorphic-equality-in-unsealed-class-hierarchies) for guidance. - -The following example starts with the and overrides, plus the optional typed `Equals` member, so you can see their effect before the `==` and `!=` operators are added. `HashCode.Combine` is a library helper that builds one hash code from the same values used by `Equals`: - -:::code language="csharp" source="snippets/equality/Program.cs" ID="ColorDefinition"::: - -At this point, `Equals` reflects value equality, but `==` still tests identity for the class because the type hasn't declared `==` and `!=` operators. Plain structs likewise still don't have a predefined `==` operator unless you declare one: - -:::code language="csharp" source="snippets/equality/Program.cs" ID="IEquatableUsage"::: - -Adding `==` and `!=` operators is the remaining step when you need operator comparisons. This article intentionally stops before the full operator implementation so the first pass can focus on the equality contract. The operator-focused follow-up shows the completed shape. For the operator syntax, see [Equality operators](../../language-reference/operators/equality-operators.md) in the language reference. +The symmetric and transitive rules require extra care in unsealed hierarchies — see [Equality in class hierarchies](../../language-reference/operators/equality-operators.md#equality-in-class-hierarchies) in the language reference. +For the complete `==` and `!=` operator syntax, see [Equality operators](../../language-reference/operators/equality-operators.md) in the language reference. ## Polymorphic equality in unsealed class hierarchies -Implementing value equality in an unsealed class hierarchy requires extra care. The hazard is that `IEquatable.Equals(T? other)` is dispatched at compile time based on the *declared type* of the variable, not the runtime type. If `TwoDPoint` declares a non-virtual `Equals(TwoDPoint? other)`, then a variable declared as `TwoDPoint` but holding a `ThreeDPoint` at runtime calls `TwoDPoint.Equals`, silently ignoring the extra dimension. The result is that two points with different `Z` values incorrectly compare as equal. - -**The fix**: make the typed `Equals` method `virtual` and add a `GetType() == other.GetType()` guard. This ensures that objects of different runtime types are never considered equal, regardless of the declared type of the variable. - -:::code language="csharp" source="snippets/equality/Program.cs" ID="PolymorphicEqualityDefinition"::: +Implementing value equality in an unsealed class hierarchy is error-prone. The key hazard: `IEquatable.Equals(T?)` dispatches on the *declared* type of the variable, not the runtime type, so a base-class implementation can silently ignore fields added by derived classes. The fix requires a `virtual` typed `Equals` and a `GetType() == other.GetType()` guard. Records handle this correctly out of the box — prefer `record` whenever value equality is the goal. -Usage with a variable declared as the base type: - -:::code language="csharp" source="snippets/equality/Program.cs" ID="PolymorphicEqualityUsage"::: - -Key points for unsealed class hierarchies: - -- **`GetType()` guard**: including `GetType() == other.GetType()` in the base class `Equals` prevents a `Circle` from comparing equal to a `Square` with the same color, and prevents a `Circle` from comparing equal to a `Shape` base with the same color. -- **`virtual` on the typed `Equals`**: lets each derived class augment the comparison with its own fields by calling `base.Equals(other)`. -- **`GetHashCode` must include `GetType()`**: two objects are only considered equal when their runtime types match, so `GetHashCode` must reflect that. `HashCode.Combine(GetType(), ...)` achieves this. -- **Sealed classes are simpler**: a `sealed` class can't be subclassed, so compile-time and runtime types always agree. The standard `IEquatable` pattern shown for `Color` earlier in this article is correct and complete for sealed classes without any virtual dispatch. - -> [!TIP] -> Records handle inheritance correctly out of the box. When a base record and a derived record are both compared using `==` or `Equals`, the compiler-generated equality checks both the runtime type and all declared properties. Prefer `record` over a manual unsealed-class hierarchy when value equality is your goal. +For the complete pattern, hazard explanation, and worked examples, see [Equality in class hierarchies](../../language-reference/operators/equality-operators.md#equality-in-class-hierarchies) in the language reference. ## See also @@ -183,4 +155,5 @@ Key points for unsealed class hierarchies: - [Records](../types/records.md) - [Tuples and deconstruction](../types/tuples.md) - [Equality operators (language reference)](../../language-reference/operators/equality-operators.md) +- [Equality in class hierarchies](../../language-reference/operators/equality-operators.md#equality-in-class-hierarchies) — advanced guidance on polymorphic equality - [Arithmetic, comparison, logical, and assignment operators](operators.md) — the equality operator survey alongside arithmetic, logical, and assignment operators diff --git a/docs/csharp/fundamentals/expressions/operators.md b/docs/csharp/fundamentals/expressions/operators.md index 1aff58a2764f6..f859e6e2594f8 100644 --- a/docs/csharp/fundamentals/expressions/operators.md +++ b/docs/csharp/fundamentals/expressions/operators.md @@ -143,7 +143,7 @@ Compound assignment is more than just a shorthand. It evaluates the left-hand si :::code language="csharp" source="snippets/operators/Program.cs" ID="AssignmentChain"::: -`small += 10` compiles because the compiler inserts the narrowing conversion automatically. `small = small + 10` would require an explicit `(byte)` cast, because the arithmetic promotes both operands to `int`. +`small += 10` compiles because the compiler inserts the narrowing conversion automatically — the result, `210`, fits within the `byte` range of 0–255. `small = small + 10` would require an explicit `(byte)` cast, because the arithmetic promotes both operands to `int`. ## Other C# operators diff --git a/docs/csharp/fundamentals/expressions/snippets/equality/Program.cs b/docs/csharp/fundamentals/expressions/snippets/equality/Program.cs index b1ca0b1c64164..2e10b33a99d16 100644 --- a/docs/csharp/fundamentals/expressions/snippets/equality/Program.cs +++ b/docs/csharp/fundamentals/expressions/snippets/equality/Program.cs @@ -66,21 +66,6 @@ Console.WriteLine(playlist1.Tracks.SequenceEqual(playlist2.Tracks)); // => True // -// -var fixed1 = new PlaylistFixed("Chill", new List { "Song A", "Song B" }); -var fixed2 = new PlaylistFixed("Chill", new List { "Song A", "Song B" }); - -Console.WriteLine(fixed1.Equals(fixed2)); // => True -// - -// -Shape circle1 = new Circle("red", 5.0); -Shape circle2 = new Circle("red", 7.0); -Shape circle3 = new Circle("red", 5.0); - -Console.WriteLine(circle1.Equals(circle2)); // => False (Radius differs) -Console.WriteLine(circle1.Equals(circle3)); // => True -// // ── Type declarations ──────────────────────────────────────────────────────── @@ -127,59 +112,5 @@ class Document(string title) public string Title { get; } = title; } -// -// Unsealed class hierarchy — make the typed Equals virtual and guard with GetType() -// so a derived instance is never equal to an instance of a different runtime type. -class Shape : IEquatable -{ - public string Color { get; } - public Shape(string color) => Color = color; - - public override bool Equals(object? obj) => Equals(obj as Shape); - - // virtual so derived classes can override the comparison logic - public virtual bool Equals(Shape? other) => - other is not null && - GetType() == other.GetType() && // reject different runtime types - Color == other.Color; - - public override int GetHashCode() => HashCode.Combine(GetType(), Color); - - public static bool operator ==(Shape? l, Shape? r) => l?.Equals(r) ?? r is null; - public static bool operator !=(Shape? l, Shape? r) => !(l == r); -} - -class Circle : Shape -{ - public double Radius { get; } - public Circle(string color, double radius) : base(color) => Radius = radius; - - public override bool Equals(object? obj) => Equals(obj as Shape); - - public override bool Equals(Shape? other) => - other is Circle c && base.Equals(c) && Radius == c.Radius; - - public override int GetHashCode() => HashCode.Combine(Color, Radius); -} -// record Playlist(string Name, List Tracks); - -// -record PlaylistFixed(string Name, List Tracks) : IEquatable -{ - public virtual bool Equals(PlaylistFixed? other) => - other is not null && - Name == other.Name && - Tracks.SequenceEqual(other.Tracks); - - public override int GetHashCode() - { - var hc = new HashCode(); - hc.Add(Name); - foreach (var t in Tracks) hc.Add(t); - return hc.ToHashCode(); - } -} -// - From 7864e5b4ea3ef6acc25826ccff98511b2282d068 Mon Sep 17 00:00:00 2001 From: Bill Wagner Date: Wed, 19 Aug 2026 11:26:36 -0400 Subject: [PATCH 09/11] Polish: improve equality documentation formatting and clarity - Add blank line before 'Polymorphic equality in unsealed class hierarchies' section heading in equality.md - Clarify 'declared type' with parenthetical '(the type written in the variable declaration)' in equality-operators.md Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: f5acfb15-adff-4860-a307-213196efda1c --- docs/csharp/fundamentals/expressions/equality.md | 1 + docs/csharp/language-reference/operators/equality-operators.md | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/csharp/fundamentals/expressions/equality.md b/docs/csharp/fundamentals/expressions/equality.md index 3bc03416cb56e..58bf9360dc638 100644 --- a/docs/csharp/fundamentals/expressions/equality.md +++ b/docs/csharp/fundamentals/expressions/equality.md @@ -141,6 +141,7 @@ A correct implementation must also satisfy the *equivalence contract* (assume `x The symmetric and transitive rules require extra care in unsealed hierarchies — see [Equality in class hierarchies](../../language-reference/operators/equality-operators.md#equality-in-class-hierarchies) in the language reference. For the complete `==` and `!=` operator syntax, see [Equality operators](../../language-reference/operators/equality-operators.md) in the language reference. + ## Polymorphic equality in unsealed class hierarchies Implementing value equality in an unsealed class hierarchy is error-prone. The key hazard: `IEquatable.Equals(T?)` dispatches on the *declared* type of the variable, not the runtime type, so a base-class implementation can silently ignore fields added by derived classes. The fix requires a `virtual` typed `Equals` and a `GetType() == other.GetType()` guard. Records handle this correctly out of the box — prefer `record` whenever value equality is the goal. diff --git a/docs/csharp/language-reference/operators/equality-operators.md b/docs/csharp/language-reference/operators/equality-operators.md index 8f584ff834489..00793f5ef5512 100644 --- a/docs/csharp/language-reference/operators/equality-operators.md +++ b/docs/csharp/language-reference/operators/equality-operators.md @@ -96,7 +96,7 @@ The following example demonstrates how to use the `!=` operator: ## Equality in class hierarchies -Value equality in an unsealed class hierarchy requires more care than in a sealed class. The hazard is that `IEquatable.Equals(T? other)` dispatch follows the *declared type* of the variable, not its runtime type. If `Shape` declares a non-`virtual` `Equals(Shape? other)`, a variable typed as `Shape` that holds a `Circle` at runtime invokes `Shape.Equals`—silently ignoring `Circle`-specific fields. Two `Circle` objects with different radii can compare as equal when accessed through a `Shape` variable. +Value equality in an unsealed class hierarchy requires more care than in a sealed class. The hazard is that `IEquatable.Equals(T? other)` dispatch follows the *declared type* (the type written in the variable declaration) of the variable, not its runtime type. If `Shape` declares a non-`virtual` `Equals(Shape? other)`, a variable typed as `Shape` that holds a `Circle` at runtime invokes `Shape.Equals`—silently ignoring `Circle`-specific fields. Two `Circle` objects with different radii can compare as equal when accessed through a `Shape` variable. The correct pattern requires two cooperating requirements: make the typed `Equals` method `virtual` so each derived class can extend the comparison, and add a `GetType() == other.GetType()` guard in the base-class implementation so objects of different runtime types are never considered equal. From 398814d36011d439245adab3900a3cb0e53a52c6 Mon Sep 17 00:00:00 2001 From: Bill Wagner Date: Wed, 19 Aug 2026 16:42:13 -0400 Subject: [PATCH 10/11] Final review Do a final review pass of all the changed content. --- .../fundamentals/expressions/equality.md | 75 +++++-------------- .../fundamentals/expressions/operators.md | 12 +-- .../expressions/snippets/equality/Program.cs | 30 -------- .../fundamentals/object-oriented/objects.md | 28 +++---- .../overloaded-operator-errors.md | 8 +- .../record-declaration-errors.md | 2 +- .../operators/equality-operators.md | 37 ++++++--- .../snippets/EqualityHierarchies/Program.cs | 32 +++++++- 8 files changed, 103 insertions(+), 121 deletions(-) diff --git a/docs/csharp/fundamentals/expressions/equality.md b/docs/csharp/fundamentals/expressions/equality.md index 58bf9360dc638..76914fba02875 100644 --- a/docs/csharp/fundamentals/expressions/equality.md +++ b/docs/csharp/fundamentals/expressions/equality.md @@ -20,13 +20,13 @@ helpviewer_keywords: > [!TIP] > This article is part of the **Fundamentals** section for developers who already know at least one programming language and are learning C#. If you're new to programming, start with the [Get started](../../tour-of-csharp/tutorials/index.md) tutorials first. > -> **Coming from another language?** In Java, `==` on objects and JavaScript `===` on objects test identity, not content. C# classes work the same way by default. In Python, `==` calls `__eq__` and tests content by default , similar to how C# [records](../types/records.md) compare. C# [structs](../types/structs.md) also compare by value when you call `Equals`. +> **Coming from another language?** In Java, `==` on objects and JavaScript `===` on objects test identity, not content. C# classes work the same way by default. In Python, `==` calls `__eq__` and tests content by default, similar to how C# [records](../types/records.md) compare. C# [structs](../types/structs.md) also compare by value when you call `Equals`. -C# distinguishes two kinds of equality. *Value equality* means two instances are equal when their data matches. *Reference equality* means two variables are equal only when they point to the same object in memory. This condition is also called *identity*. The kind of type gives you the best first clue about the default equality behavior: value types usually compare data, and reference types usually compare identity. Defaults aren't destiny, but that mental model prevents subtle bugs where two objects that look identical aren't considered equal, or where a mutation through one variable silently changes what another variable sees. +C# distinguishes two kinds of equality. *Value equality* means two instances are equal when their data matches. *Reference equality* means two variables are equal only when they point to the same object in memory. This condition is also called *identity*. Value types usually compare data, and reference types usually compare identity. Type authors can change those defaults, but that mental model prevents subtle bugs where two objects that look identical aren't considered equal, or where a mutation through one variable silently changes what another variable sees. ## Value types, reference types, and equality defaults -Every type in C# is either a *value type* or a *reference type*. A *value type* holds its data directly in the variable. A *reference type* holds a reference to an object. When you assign a reference-type variable to another variable, both variables refer to the same object. This article uses that distinction as a quick refresher. For more information about value types and reference types, see [Type system overview](../types/index.md#value-types-and-reference-types). +Every type in C# is either a *value type* or a *reference type*. A *value type* holds its data directly in the variable. A *reference type* holds a reference to an object. When you assign a reference-type variable to another variable, both variables refer to the same object. For more information about value types and reference types, see [Type system overview](../types/index.md#value-types-and-reference-types). The default equality behavior usually follows the kind of type: @@ -35,11 +35,11 @@ The default equality behavior usually follows the kind of type: - **[Tuples](../types/tuples.md)** are value types. Two tuples are equal when all their element values match. - **[Classes](../types/classes.md)** are reference types. A plain class uses reference equality, so `==` and test whether two variables point to the same object. -A plain class shows reference equality. Two separate objects with the same data aren't equal, but two variables that refer to the same object are equal: +A class uses reference equality. Two separate objects with the same data aren't equal, but two variables that refer to the same object are equal: :::code language="csharp" source="snippets/equality/Program.cs" ID="ClassEquality"::: -A plain `struct` shows value equality through . Two struct instances are equal when their fields match: +A `struct` shows value equality through . Two struct instances are equal when their fields match: :::code language="csharp" source="snippets/equality/Program.cs" ID="StructEquality"::: @@ -60,24 +60,19 @@ For more information about tuple syntax and deconstruction, see [Tuples and deco A common use is inside an `Equals` override to short-circuit the full comparison: when both arguments are the same reference, they're always equal without checking individual fields. > [!NOTE] -> Advanced detail: when variables are typed as an [interface](../types/interfaces.md), `==` checks whether the interface variables refer to the same object. A call to `Equals` still runs the underlying object's implementation. +> When variables are typed as an [interface](../types/interfaces.md), `==` checks whether the interface variables refer to the same object. A call to `Equals` still runs the underlying object's implementation. > [!NOTE] -> always returns `false` when comparing value types, even if both arguments contain the same values. This is because each value-type argument is independently *boxed* into a separate heap object when passed to `ReferenceEquals`. +> always returns `false` when comparing value types, even if both arguments contain the same values. This behavior occurs because each value-type argument is independently *boxed* into a separate heap object when passed to `ReferenceEquals`. ## Types can define different equality semantics -Defaults aren't destiny. Some types define equality semantics that differ from the type-kind default, and your own types can do the same when their data should determine equality. +Types *can* define equality semantics that differ from the default behavior. The most common reason is to implement value equality. If you create a type that represents data, such as a bank account, a product in inventory, or a user in a system, consider instances with the same values as equal. *Choose [record types](../types/records.md) for implementing value equality*, and the compiler generates all the necessary equality members for you. -Common exceptions and customizations include: - -- **[Records](../types/records.md)** generate value equality and include `==`/`!=` operators. The next section shows how the `record` modifier gives value equality to both record classes and record structs. -- **Strings** are classes, but `==` and compare string content, not identity. -- **Your own classes and structs** can define value equality when their data should determine equality. - -Equality is woven through these related members: +> [!NOTE] +> **Strings** are classes, but `==` and compare string content, not identity. -- `==`: the equality operator. Most types use this as the primary equality check. Its behavior depends on whether the type has a built-in or user-defined `==` operator. +- `==`: the equality operator. Most types use this operator as the primary equality check. Its behavior depends on whether the type has a built-in or user-defined `==` operator. - `!=`: the inequality operator. When a type defines a user-defined `==` operator, it must also define `!=`. - : a virtual method inherited by every type. You can override it to change equality semantics for a type. - : a virtual method used by hash-based collections. When two values are equal, their hash codes must also be equal. @@ -99,9 +94,9 @@ The same compiler generation applies to `record struct` types: Record types generate the whole equality set for their own type. Both `record class` and `record struct` types override and . They also generate `==` and `!=` operators, plus a typed `Equals` method for the record type. Unlike a plain `struct`, a `record struct` therefore supports `==` and `!=` automatically. For more information about record types and their equality semantics, see [Records](../types/records.md#value-equality). -## Records with reference-type members +### Records with reference-type members -Record equality is synthesized from the members' own equality. Each property or field is compared using its own `Equals` method. For most scalar values—`int`, `string`, `DateTime`, and similar types—that works exactly as you'd expect. The subtlety arises with common mutable collections such as `List` or `T[]`: these types compare by reference, so two record instances that contain *different list objects with the same content* are **not** considered equal by the synthesized record equality. +Record equality uses the members' own equality semantics. Each property or field is compared by using its own `Equals` method. For most scalar values, such as `int`, `string`, or `DateTime`, this approach compares the values of the record members. The subtlety arises with common mutable collections such as `List` or `T[]`: these types compare by reference, so two record instances that contain *different list objects with the same content* are **not** considered equal by the synthesized record equality. :::code language="csharp" source="snippets/equality/Program.cs" ID="RecordWithCollectionProblem"::: @@ -111,42 +106,10 @@ When you need record equality to reflect collection *contents*, you have a few o - **Implement `IEquatable`** on the record and override `Equals` to use for the collection members. - **Use a collection type with value equality** — for example, a custom `IEqualityComparer` or a type whose own `Equals` compares elements. -- **Design around identity**: if the record represents an entity rather than a pure value, reference equality for its collection members may be intentional. - -## Implement equality yourself when a type can't be a record +- **Design around identity**: if the record represents an entity rather than a pure value, reference equality for its collection members might be intentional. > [!IMPORTANT] -> Use `record` whenever possible — the compiler generates all required equality members for you. Manual implementation is only needed when your type must derive from a non-record class or has other constraints that prevent `record`. - -Here is a minimal manual implementation for a value type that can't be a record: - -:::code language="csharp" source="snippets/equality/Program.cs" ID="ColorDefinition"::: - -The implementation provides three required members: `Equals(Color?)` as the core comparison, `override Equals(object?)` for object-level calls, and `override GetHashCode()` so hash-based collections work correctly. `HashCode.Combine` is a library helper that builds one hash from the same values used by `Equals`. Implementing (the `Equals(Color?)` overload) is optional but avoids boxing when callers already have the concrete type. - -When you also define `==` and `!=`, the language requires them as a pair; warnings [CS0660](../../language-reference/compiler-messages/overloaded-operator-errors.md#equality-operators) and [CS0661](../../language-reference/compiler-messages/overloaded-operator-errors.md#equality-operators) remind you to keep all four members consistent. - -With the three members above in place, `Equals` reflects value equality, but `==` still tests identity because no `==` operator has been declared yet: - -:::code language="csharp" source="snippets/equality/Program.cs" ID="IEquatableUsage"::: - -A correct implementation must also satisfy the *equivalence contract* (assume `x`, `y`, and `z` are non-null): - -1. **Reflexive**: `x.Equals(x)` returns `true`. -2. **Symmetric**: `x.Equals(y)` returns the same value as `y.Equals(x)`. -3. **Transitive**: if `x.Equals(y)` and `y.Equals(z)` are both `true`, then `x.Equals(z)` must be `true`. -4. **Consistent**: successive calls to `x.Equals(y)` return the same value as long as neither object changes. -5. **Null behavior**: `x.Equals(null)` returns `false`; `x.Equals(y)` must not throw when called on a non-null `x`. - -The symmetric and transitive rules require extra care in unsealed hierarchies — see [Equality in class hierarchies](../../language-reference/operators/equality-operators.md#equality-in-class-hierarchies) in the language reference. - -For the complete `==` and `!=` operator syntax, see [Equality operators](../../language-reference/operators/equality-operators.md) in the language reference. - -## Polymorphic equality in unsealed class hierarchies - -Implementing value equality in an unsealed class hierarchy is error-prone. The key hazard: `IEquatable.Equals(T?)` dispatches on the *declared* type of the variable, not the runtime type, so a base-class implementation can silently ignore fields added by derived classes. The fix requires a `virtual` typed `Equals` and a `GetType() == other.GetType()` guard. Records handle this correctly out of the box — prefer `record` whenever value equality is the goal. - -For the complete pattern, hazard explanation, and worked examples, see [Equality in class hierarchies](../../language-reference/operators/equality-operators.md#equality-in-class-hierarchies) in the language reference. +> Manual implementation of equality is rare today in C#. Records handle the common scenario of value equality automatically. If you need to implement equality manually - for example, because your type must derive from a non-record base class - see [Implement equality yourself when a type can't be a record](../../language-reference/operators/equality-operators.md#implement-equality-yourself-when-a-type-cant-be-a-record) in the language reference. ## See also @@ -154,7 +117,7 @@ For the complete pattern, hazard explanation, and worked examples, see [Equality - [Classes](../types/classes.md) - [Structs](../types/structs.md) - [Records](../types/records.md) -- [Tuples and deconstruction](../types/tuples.md) -- [Equality operators (language reference)](../../language-reference/operators/equality-operators.md) -- [Equality in class hierarchies](../../language-reference/operators/equality-operators.md#equality-in-class-hierarchies) — advanced guidance on polymorphic equality -- [Arithmetic, comparison, logical, and assignment operators](operators.md) — the equality operator survey alongside arithmetic, logical, and assignment operators +- [Tuples and deconstruction](../types/tuples.md). +- [Equality operators (language reference)](../../language-reference/operators/equality-operators.md). +- [Equality in class hierarchies](../../language-reference/operators/equality-operators.md#equality-in-class-hierarchies) — advanced guidance on polymorphic equality. +- [Arithmetic, comparison, logical, and assignment operators](operators.md) — the equality operator survey alongside arithmetic, logical, and assignment operators. diff --git a/docs/csharp/fundamentals/expressions/operators.md b/docs/csharp/fundamentals/expressions/operators.md index f859e6e2594f8..70deeda36e76d 100644 --- a/docs/csharp/fundamentals/expressions/operators.md +++ b/docs/csharp/fundamentals/expressions/operators.md @@ -58,12 +58,12 @@ When `++` or `--` appears as a standalone statement (not part of a larger expres Relational operators compare two values and return a `bool`. -| Operator | Meaning | Example | -|----------|---------|---------| -| `<` | Less than | `speed < limit` | -| `>` | Greater than | `speed > limit` | -| `<=` | Less than or equal | `score <= 100` | -| `>=` | Greater than or equal | `score >= 0` | +| Operator | Meaning | Example | +|----------|-----------------------|-----------------| +| `<` | Less than | `speed < limit` | +| `>` | Greater than | `speed > limit` | +| `<=` | Less than or equal | `score <= 100` | +| `>=` | Greater than or equal | `score >= 0` | :::code language="csharp" source="snippets/operators/Program.cs" ID="RelationalOps"::: diff --git a/docs/csharp/fundamentals/expressions/snippets/equality/Program.cs b/docs/csharp/fundamentals/expressions/snippets/equality/Program.cs index 2e10b33a99d16..f1855e61cc859 100644 --- a/docs/csharp/fundamentals/expressions/snippets/equality/Program.cs +++ b/docs/csharp/fundamentals/expressions/snippets/equality/Program.cs @@ -41,14 +41,6 @@ Console.WriteLine(t1 == t2); // => True // -// -var red1 = new Color(255, 0, 0); -var red2 = new Color(255, 0, 0); - -Console.WriteLine(red1.Equals(red2)); // => True -Console.WriteLine(red1 == red2); // => False (no == overload; identity check) -// - // var doc1 = new Document("Report"); var doc2 = new Document("Report"); @@ -85,28 +77,6 @@ record Person(string First, string Last); record struct Dimension(double Width, double Height); -// -class Color : IEquatable -{ - public Color(int r, int g, int b) - { - R = r; - G = g; - B = b; - } - - public int R { get; } - public int G { get; } - public int B { get; } - - public bool Equals(Color? other) => - other is not null && R == other.R && G == other.G && B == other.B; - - public override bool Equals(object? obj) => obj is Color other && Equals(other); - public override int GetHashCode() => HashCode.Combine(R, G, B); -} -// - class Document(string title) { public string Title { get; } = title; diff --git a/docs/csharp/fundamentals/object-oriented/objects.md b/docs/csharp/fundamentals/object-oriented/objects.md index 33de713cf229c..93719dd42adcb 100644 --- a/docs/csharp/fundamentals/object-oriented/objects.md +++ b/docs/csharp/fundamentals/object-oriented/objects.md @@ -8,34 +8,34 @@ helpviewer_keywords: --- # Objects - create instances of types -A class or struct definition is like a blueprint that specifies what the type can do. An object is basically a block of memory that is allocated and configured according to the blueprint. A program might create many objects of the same class. Objects are also called instances, and they can be stored in either a named variable or in an array or collection. Client code is the code that uses these variables to call the methods and access the public properties of the object. In an object-oriented language such as C#, a typical program consists of multiple objects interacting dynamically. +A class or struct definition is like a blueprint that specifies what the type can do. An object is a block of memory that the program allocates and configures according to the blueprint. A program might create many objects of the same class. You can also call objects instances. You can store them in a named variable or in an array or collection. Client code uses these variables to call the methods and access the public properties of the object. In an object-oriented language such as C#, a typical program consists of multiple objects interacting dynamically. > [!NOTE] -> Static types behave differently than what is described here. For more information, see [Static Classes and Static Class Members](../../programming-guide/classes-and-structs/static-classes-and-static-class-members.md). +> Static types behave differently than what is described in this article. For more information, see [Static Classes and Static Class Members](../../programming-guide/classes-and-structs/static-classes-and-static-class-members.md). -## Struct Instances vs. Class Instances +## Struct instances vs. class instances -Because classes are reference types, a variable of a class object holds a reference to the address of the object on the managed heap. If a second variable of the same type is assigned to the first variable, then both variables refer to the object at that address. This point is discussed in more detail later in this article. +Because classes are reference types, a variable of a class object holds a reference to the address of the object on the managed heap. If you assign a second variable of the same type to the first variable, both variables refer to the object at that address. This article discusses this point in more detail later. -Instances of classes are created by using the [`new` operator](../../language-reference/operators/new-operator.md). In the following example, `Person` is the type and `person1` and `person2` are instances, or objects, of that type. +You create instances of classes by using the [`new` operator](../../language-reference/operators/new-operator.md). In the following example, `Person` is the type and `person1` and `person2` are instances, or objects, of that type. :::code language="csharp" source="./snippets/objects/Program.cs"::: -Because structs are value types, a variable of a struct object holds a copy of the entire object. Instances of structs can also be created by using the `new` operator, but this isn't required, as shown in the following example: +Because structs are value types, a variable of a struct object holds a copy of the entire object. You can also create instances of structs by using the `new` operator, but you don't need to use it, as shown in the following example: :::code language="csharp" source="./snippets/objects/Application.cs"::: -The memory for both `p1` and `p2` is allocated on the thread stack. That memory is reclaimed along with the type or method in which it's declared. This is one reason why structs are copied on assignment. By contrast, the memory that is allocated for a class instance is automatically reclaimed (garbage collected) by the common language runtime when all references to the object are out of scope. It isn't possible to deterministically destroy a class object like you can in C++. For more information about garbage collection in .NET, see [Garbage Collection](../../../standard/garbage-collection/index.md). +The thread stack allocates memory for both `p1` and `p2`. The program reclaims that memory along with the type or method in which you declare it. This memory management is one reason why structs are copied on assignment. By contrast, the common language runtime automatically reclaims (garbage collects) the memory it allocates for a class instance when all references to the object go out of scope. You can't deterministically destroy a class object like you can in C++. For more information about garbage collection in .NET, see [Garbage Collection](../../../standard/garbage-collection/index.md). > [!NOTE] -> The allocation and deallocation of memory on the managed heap is highly optimized in the common language runtime. In most cases, there's no significant difference in the performance cost of allocating a class instance on the heap versus allocating a struct instance on the stack. +> The common language runtime highly optimizes the allocation and deallocation of memory on the managed heap. In most cases, there's no significant difference in the performance cost of allocating a class instance on the heap versus allocating a struct instance on the stack. -## Object Identity vs. Value Equality +## Object identity vs. value equality -When you compare two objects for equality, you must first distinguish whether you want to know whether the two variables represent the same object in memory, or whether the values of one or more of their fields are equivalent. If you're intending to compare values, you must consider whether the objects are instances of value types (structs) or reference types (classes, delegates, arrays). +When you compare two objects for equality, first decide whether you want to know if the two variables represent the same object in memory or if the values of one or more of their fields are equivalent. If you want to compare values, consider whether the objects are instances of value types (structs) or reference types (classes, delegates, arrays). -- To determine whether two class instances refer to the same location in memory (which means that they have the same *identity*), use the static method. ( is the implicit base class for all value types and reference types, including user-defined structs and classes.) -- The method, by default, determines whether the instance fields in two struct instances have the same values. Because all structs implicitly inherit from , you call the method directly on your object as shown in the following example: +- Use the static method to determine whether two class instances refer to the same location in memory (which means that they have the same *identity*). ( is the implicit base class for all value types and reference types, including user-defined structs and classes.) +- By default, the method determines whether the instance fields in two struct instances have the same values. Because all structs implicitly inherit from , you call the method directly on your object as shown in the following example: :::code language="csharp" source="./snippets/objects/Equality.cs" ID="Snippet32"::: @@ -43,9 +43,9 @@ When you compare two objects for equality, you must first distinguish whether yo - To determine whether the values of the fields in two class instances are equal, you might be able to use the method or the [== operator](../../language-reference/operators/equality-operators.md#equality-operator-). However, only use them if the class has overridden or overloaded them to provide a custom definition of what "equality" means for objects of that type. The class might also implement the interface or the interface. Both interfaces provide methods that can be used to test value equality. When designing your own classes that override `Equals`, make sure to follow the guidelines stated in [Implement equality yourself when a type can't be a record](../expressions/equality.md#implement-equality-yourself-when-a-type-cant-be-a-record) and . -## Related Sections +## Related sections -For more information: +For more information, see: - [Classes](../types/classes.md) - [Constructors](../../programming-guide/classes-and-structs/constructors.md) diff --git a/docs/csharp/language-reference/compiler-messages/overloaded-operator-errors.md b/docs/csharp/language-reference/compiler-messages/overloaded-operator-errors.md index efb8bf7917df3..b6154c1fcfb8c 100644 --- a/docs/csharp/language-reference/compiler-messages/overloaded-operator-errors.md +++ b/docs/csharp/language-reference/compiler-messages/overloaded-operator-errors.md @@ -107,8 +107,8 @@ ai-usage: ai-assisted This article covers the following compiler errors and warnings: - - [**CS0031**](#overflow-and-underflow-errors): *Constant value 'value' cannot be converted to a 'type'* - [**CS0056**](#inconsistent-accessibility): *Inconsistent accessibility: return type 'type' is less accessible than operator 'operator'* @@ -231,7 +231,7 @@ All types used in a public operator's signature must be at least as accessible a The C# language restricts which types can participate in user-defined conversions. For the full rules, see [User-defined conversion operators](../operators/user-defined-conversion-operators.md) and [Conversion operators](~/_csharpstandard/standard/classes.md#15104-conversion-operators) in the C# specification. -- Remove the conversion operator that converts to or from an interface type (**CS0552**). The language prohibits user-defined conversions involving interface types because interface conversions are handled through the type system's reference conversions and boxing. Use explicit interface implementations or helper methods instead. +- Remove the conversion operator that converts to or from an interface type (**CS0552**). The language prohibits user-defined conversions involving interface types because the type system handles interface conversions through reference conversions and boxing. Use explicit interface implementations or helper methods instead. - Remove the conversion operator that converts to or from a base class (**CS0553**). Conversions between a type and its base class already exist through implicit reference conversions (upcast) and explicit reference conversions (downcast), so a user-defined conversion would create ambiguity. - Remove the conversion operator that converts to or from a derived class (**CS0554**). Like base class conversions, conversions between a type and its derived types are built into the language through inheritance, and user-defined conversions would conflict with them. - Remove the conversion operator that converts the enclosing type to itself (**CS0555**). Every type already has an implicit identity conversion to itself, so a user-defined conversion from a type to the same type is redundant and not permitted. @@ -275,7 +275,7 @@ The compiler enforces strict matching between operator declarations and the inte - Change the implementing member to an operator declaration that matches the interface's operator member, or change the interface member to a method if the implementing member is a method (**CS9311**). An operator can only implement an interface member that's also declared as an operator—you can't satisfy an operator contract with a regular method, or vice versa. - Change the overriding member to an operator declaration that matches the base class's operator member, or change the base class member to a method if the derived class member is a method (**CS9312**). Like interface implementation, an override must match the kind of member being overridden—an operator can't override a non-operator member. -- Change the compound assignment operator declaration to accept exactly one parameter (**CS9313**). Compound assignment operators are instance members where the left operand is implicitly `this`, so only the right-hand operand is declared as a parameter. +- Change the compound assignment operator declaration to accept exactly one parameter (**CS9313**). Compound assignment operators are instance members where the left operand is implicitly `this`, so you only declare the right-hand operand as a parameter. ## Equality operators diff --git a/docs/csharp/language-reference/compiler-messages/record-declaration-errors.md b/docs/csharp/language-reference/compiler-messages/record-declaration-errors.md index f1e0dbb31e978..5242650ad9098 100644 --- a/docs/csharp/language-reference/compiler-messages/record-declaration-errors.md +++ b/docs/csharp/language-reference/compiler-messages/record-declaration-errors.md @@ -54,7 +54,7 @@ ai-usage: ai-assisted The C# compiler generates errors and warnings when you misuse [record types](../builtin-types/record.md). Record types provide built-in members that implement value-based equality. These diagnostics help you follow the rules for declaring and using record types. - - [**CS8851**](#equality-members): *'type' defines 'Equals' but not 'GetHashCode'* diff --git a/docs/csharp/language-reference/operators/equality-operators.md b/docs/csharp/language-reference/operators/equality-operators.md index 00793f5ef5512..7f6306d441498 100644 --- a/docs/csharp/language-reference/operators/equality-operators.md +++ b/docs/csharp/language-reference/operators/equality-operators.md @@ -51,7 +51,7 @@ By default, reference-type operands, excluding records, are equal if they refer :::code language="csharp" source="snippets/shared/EqualityOperators.cs" id="ReferenceTypesEquality"::: -As the example shows, user-defined reference types support the `==` operator by default. However, a reference type can overload the `==` operator. If a reference type overloads the `==` operator, use the method to check if two references of that type refer to the same object. +As the preceding example shows, user-defined reference types support the `==` operator by default. However, a reference type can overload the `==` operator. If a reference type overloads the `==` operator, use the method to check if two references of that type refer to the same object. ### Record types equality @@ -96,7 +96,32 @@ The following example demonstrates how to use the `!=` operator: ## Equality in class hierarchies -Value equality in an unsealed class hierarchy requires more care than in a sealed class. The hazard is that `IEquatable.Equals(T? other)` dispatch follows the *declared type* (the type written in the variable declaration) of the variable, not its runtime type. If `Shape` declares a non-`virtual` `Equals(Shape? other)`, a variable typed as `Shape` that holds a `Circle` at runtime invokes `Shape.Equals`—silently ignoring `Circle`-specific fields. Two `Circle` objects with different radii can compare as equal when accessed through a `Shape` variable. +Records handle inheritance correctly without manual work. The compiler-generated equality checks both runtime type and all declared properties, so it automatically satisfies the symmetry and transitivity requirements. Prefer `record` over a manual unsealed hierarchy when value equality is the goal. + +> [!IMPORTANT] +> Use `record` whenever possible — the compiler generates all required equality members for you. Manual implementation is only needed when your type must derive from a non-record class or has other constraints that prevent `record`. + +Here is a minimal manual implementation for a value type that can't be a record: + +:::code language="csharp" source="snippets/EqualityHierarchies/Program.cs" id="ColorDefinition"::: + +The implementation provides three required members: `Equals(T?)` as the core comparison, `override Equals(object?)` for object-level calls, and `override GetHashCode()` so hash-based collections work correctly. `HashCode.Combine` is a library helper that builds one hash from the same values used by `Equals`. Implementing (the `Equals(T?)` overload) is optional but avoids boxing when callers already have the concrete type. + +When you also define `==` and `!=`, the language requires them as a pair; warnings [CS0660](../../language-reference/compiler-messages/overloaded-operator-errors.md#equality-operators) and [CS0661](../../language-reference/compiler-messages/overloaded-operator-errors.md#equality-operators) remind you to keep all four members consistent. + +With the three members above in place, `Equals` reflects value equality, but `==` still tests identity because no `==` operator has been declared yet: + +:::code language="csharp" source="snippets/EqualityHierarchies/Program.cs" id="IEquatableUsage"::: + +A correct implementation must also satisfy the *equivalence contract* (assume `x`, `y`, and `z` are non-null): + +1. **Reflexive**: `x.Equals(x)` returns `true`. +2. **Symmetric**: `x.Equals(y)` returns the same value as `y.Equals(x)`. +3. **Transitive**: if `x.Equals(y)` and `y.Equals(z)` are both `true`, then `x.Equals(z)` must be `true`. +4. **Consistent**: successive calls to `x.Equals(y)` return the same value as long as neither object changes. +5. **Null behavior**: `x.Equals(null)` returns `false`; `x.Equals(y)` must not throw when called on a non-null `x`. + +Value equality in an unsealed class hierarchy requires more care than in a sealed class to satisfy the symmetric and transitive rules. The hazard is that `IEquatable.Equals(T? other)` dispatch follows the *declared type* (the type written in the variable declaration) of the variable, not its runtime type. If `Shape` declares a non-`virtual` `Equals(Shape? other)`, a variable typed as `Shape` that holds a `Circle` at runtime invokes `Shape.Equals`—silently ignoring `Circle`-specific fields. Two `Circle` objects with different radii can compare as equal when accessed through a `Shape` variable. The correct pattern requires two cooperating requirements: make the typed `Equals` method `virtual` so each derived class can extend the comparison, and add a `GetType() == other.GetType()` guard in the base-class implementation so objects of different runtime types are never considered equal. @@ -125,13 +150,7 @@ A derived class that adds fields overrides the typed `Equals`, casts to its own ### Sealed classes are simpler -A `sealed` class cannot be subclassed, so compile-time and runtime types always agree. The `GetType()` guard and `virtual` dispatch are unnecessary. The `IEquatable` pattern shown in [Implement equality yourself](../../fundamentals/expressions/equality.md#implement-equality-yourself-when-a-type-cant-be-a-record) is correct and complete for a sealed class. - -### Prefer records for value equality in hierarchies - -Records handle inheritance correctly without manual work. The compiler-generated equality checks both runtime type and all declared properties, satisfying the symmetry and transitivity requirements automatically. Prefer `record` over a manual unsealed hierarchy when value equality is the goal. - -For an introduction to equality semantics across C# type kinds, see [Equality comparisons](../../fundamentals/expressions/equality.md). +You can't subclass a `sealed` class, so compile-time and runtime types always agree. You don't need the `GetType()` guard or `virtual` dispatch. The `IEquatable` pattern shown in [Implement equality yourself when a type can't be a record](#implement-equality-yourself-when-a-type-cant-be-a-record) is correct and complete for a sealed class. ## Operator overloadability diff --git a/docs/csharp/language-reference/operators/snippets/EqualityHierarchies/Program.cs b/docs/csharp/language-reference/operators/snippets/EqualityHierarchies/Program.cs index 71f49ebbc2e01..7cf089c22128b 100644 --- a/docs/csharp/language-reference/operators/snippets/EqualityHierarchies/Program.cs +++ b/docs/csharp/language-reference/operators/snippets/EqualityHierarchies/Program.cs @@ -1,3 +1,11 @@ +// +var red1 = new Color(255, 0, 0); +var red2 = new Color(255, 0, 0); + +Console.WriteLine(red1.Equals(red2)); // => True +Console.WriteLine(red1 == red2); // => False (no == overload; identity check) +// + // Shape circle1 = new Circle("red", 5.0); Shape circle2 = new Circle("red", 7.0); @@ -49,4 +57,26 @@ public override bool Equals(Shape? other) => public override int GetHashCode() => HashCode.Combine(GetType(), Color, Radius); } -// \ No newline at end of file +// + +// +class Color : IEquatable +{ + public Color(int r, int g, int b) + { + R = r; + G = g; + B = b; + } + + public int R { get; } + public int G { get; } + public int B { get; } + + public bool Equals(Color? other) => + other is not null && R == other.R && G == other.G && B == other.B; + + public override bool Equals(object? obj) => obj is Color other && Equals(other); + public override int GetHashCode() => HashCode.Combine(R, G, B); +} +// \ No newline at end of file From d72fbd5c0355988f9221376cd0dec5eb33766c00 Mon Sep 17 00:00:00 2001 From: Bill Wagner Date: Wed, 19 Aug 2026 16:58:11 -0400 Subject: [PATCH 11/11] Fix build warnings. --- docs/csharp/fundamentals/object-oriented/objects.md | 4 ++-- docs/csharp/how-to/index.md | 2 +- .../compiler-messages/overloaded-operator-errors.md | 2 +- .../compiler-messages/record-declaration-errors.md | 2 +- .../csharp/language-reference/operators/equality-operators.md | 2 ++ 5 files changed, 7 insertions(+), 5 deletions(-) diff --git a/docs/csharp/fundamentals/object-oriented/objects.md b/docs/csharp/fundamentals/object-oriented/objects.md index 93719dd42adcb..cd2e20e561a84 100644 --- a/docs/csharp/fundamentals/object-oriented/objects.md +++ b/docs/csharp/fundamentals/object-oriented/objects.md @@ -39,9 +39,9 @@ When you compare two objects for equality, first decide whether you want to know :::code language="csharp" source="./snippets/objects/Equality.cs" ID="Snippet32"::: - The default implementation of `Equals` uses boxing and reflection in some cases. For information about how to provide an efficient equality algorithm that's specific to your type, see [Implement equality yourself when a type can't be a record](../expressions/equality.md#implement-equality-yourself-when-a-type-cant-be-a-record). Records are reference types that use value semantics for equality. + The default implementation of `Equals` uses boxing and reflection in some cases. For information about how to provide an efficient equality algorithm that's specific to your type, see [Implement equality yourself when a type can't be a record](../../language-reference/operators/equality-operators.md#implement-equality-yourself-when-a-type-cant-be-a-record). Records are reference types that use value semantics for equality. -- To determine whether the values of the fields in two class instances are equal, you might be able to use the method or the [== operator](../../language-reference/operators/equality-operators.md#equality-operator-). However, only use them if the class has overridden or overloaded them to provide a custom definition of what "equality" means for objects of that type. The class might also implement the interface or the interface. Both interfaces provide methods that can be used to test value equality. When designing your own classes that override `Equals`, make sure to follow the guidelines stated in [Implement equality yourself when a type can't be a record](../expressions/equality.md#implement-equality-yourself-when-a-type-cant-be-a-record) and . +- To determine whether the values of the fields in two class instances are equal, you might be able to use the method or the [== operator](../../language-reference/operators/equality-operators.md#equality-operator-). However, only use them if the class has overridden or overloaded them to provide a custom definition of what "equality" means for objects of that type. The class might also implement the interface or the interface. Both interfaces provide methods that can be used to test value equality. When designing your own classes that override `Equals`, make sure to follow the guidelines stated in [Implement equality yourself when a type can't be a record](../../language-reference/operators/equality-operators.md#implement-equality-yourself-when-a-type-cant-be-a-record) and . ## Related sections diff --git a/docs/csharp/how-to/index.md b/docs/csharp/how-to/index.md index 78854d1f07a03..342638eab8fb0 100644 --- a/docs/csharp/how-to/index.md +++ b/docs/csharp/how-to/index.md @@ -68,7 +68,7 @@ You may create types that define their own rules for equality or define a natural ordering among objects of that type. - [Test for reference-based equality](../fundamentals/expressions/equality.md#use-objectreferenceequals-to-test-identity-directly). -- [Define value-based equality for a type](../fundamentals/expressions/equality.md#implement-equality-yourself-when-a-type-cant-be-a-record). +- [Define value-based equality for a type](../language-reference/operators/equality-operators.md#implement-equality-yourself-when-a-type-cant-be-a-record). ## Exception handling diff --git a/docs/csharp/language-reference/compiler-messages/overloaded-operator-errors.md b/docs/csharp/language-reference/compiler-messages/overloaded-operator-errors.md index b6154c1fcfb8c..9e4953d6c799d 100644 --- a/docs/csharp/language-reference/compiler-messages/overloaded-operator-errors.md +++ b/docs/csharp/language-reference/compiler-messages/overloaded-operator-errors.md @@ -283,7 +283,7 @@ The compiler enforces strict matching between operator declarations and the inte - **CS0660**: *Type defines operator == or operator != but doesn't override Object.Equals(object o)* - **CS0661**: *Type defines operator == or operator != but doesn't override Object.GetHashCode()* -The compiler requires that equality-related overrides and operator definitions stay in sync. When you override or define `operator ==` / `operator !=`, you must also provide the related overrides. For the full rules, see [Implement equality yourself when a type can't be a record](../../fundamentals/expressions/equality.md#implement-equality-yourself-when-a-type-cant-be-a-record) and [Equality operators](../operators/equality-operators.md). +The compiler requires that equality-related overrides and operator definitions stay in sync. When you override or define `operator ==` / `operator !=`, you must also provide the related overrides. For the full rules, see [Implement equality yourself when a type can't be a record](../operators/equality-operators.md#implement-equality-yourself-when-a-type-cant-be-a-record) and [Equality operators](../operators/equality-operators.md). - Add an override of when you override (**CS0659**). Hash-based collections like and rely on the contract that two objects that are equal must return the same hash code. Without a matching `GetHashCode` override, objects that compare as equal might hash to different buckets, causing lookups and deduplication to fail silently. - Add an override of when you define `operator ==` or `operator !=` (**CS0660**). Code that calls `Equals` directly—including many framework APIs, LINQ methods, and collection operations—won't use your custom operator. Without a consistent `Equals` override, the same two objects might be considered equal by `==` but not by `Equals`, leading to unpredictable behavior. diff --git a/docs/csharp/language-reference/compiler-messages/record-declaration-errors.md b/docs/csharp/language-reference/compiler-messages/record-declaration-errors.md index 5242650ad9098..4f4fb86a11542 100644 --- a/docs/csharp/language-reference/compiler-messages/record-declaration-errors.md +++ b/docs/csharp/language-reference/compiler-messages/record-declaration-errors.md @@ -138,7 +138,7 @@ To correct these errors, apply the following changes to your positional record d To correct these errors, apply the following changes: -- Add a `GetHashCode` method whenever you define an `Equals` method. The [equivalence contract](../../fundamentals/expressions/equality.md#implement-equality-yourself-when-a-type-cant-be-a-record) requires that objects considered equal produce the same hash code, so the compiler enforces that these two methods are always defined together (**CS8851**). +- Add a `GetHashCode` method whenever you define an `Equals` method. The [equivalence contract](../operators/equality-operators.md#implement-equality-yourself-when-a-type-cant-be-a-record) requires that objects considered equal produce the same hash code, so the compiler enforces that these two methods are always defined together (**CS8851**). - Change the receiver of a `with` expression so that it's a [record type](../builtin-types/record.md) or a [struct type](../builtin-types/struct.md). The `with` expression creates a modified copy by using the `record` copy constructor, or value copy semantics for `struct` types (**CS8858**). - Ensure the receiver of a [`with` expression](../operators/with-expression.md) has a non-void type. The `with` expression produces a new copy of the receiver, so the receiver must evaluate to a value that can be copied (**CS8857**). diff --git a/docs/csharp/language-reference/operators/equality-operators.md b/docs/csharp/language-reference/operators/equality-operators.md index 7f6306d441498..e8edfb54fadf9 100644 --- a/docs/csharp/language-reference/operators/equality-operators.md +++ b/docs/csharp/language-reference/operators/equality-operators.md @@ -101,6 +101,8 @@ Records handle inheritance correctly without manual work. The compiler-generated > [!IMPORTANT] > Use `record` whenever possible — the compiler generates all required equality members for you. Manual implementation is only needed when your type must derive from a non-record class or has other constraints that prevent `record`. +### Implement equality yourself when a type can't be a record + Here is a minimal manual implementation for a value type that can't be a record: :::code language="csharp" source="snippets/EqualityHierarchies/Program.cs" id="ColorDefinition":::