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 efbb6325469d7..76914fba02875 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 @@ -11,13 +20,13 @@ ai-usage: ai-assisted > [!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: @@ -26,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"::: @@ -42,19 +51,28 @@ 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). -## Types can define different equality semantics +## 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. -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. +> [!NOTE] +> 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. -Common exceptions and customizations include: +> [!NOTE] +> 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`. -- **[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. +## Types can define different equality semantics -Equality is woven through these related members: +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. -- `==`: 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. +> [!NOTE] +> **Strings** are classes, but `==` and compare string content, not identity. + +- `==`: 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. @@ -76,40 +94,22 @@ 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). -## 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. - -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. - -In a complete manual implementation, provide these members: - -- `==` 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. - -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: +### Records with reference-type members -:::code language="csharp" source="snippets/equality/Program.cs" ID="IEquatableUsage"::: +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. -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. +:::code language="csharp" source="snippets/equality/Program.cs" ID="RecordWithCollectionProblem"::: -## Use `Object.ReferenceEquals` to test identity directly +`playlist1` and `playlist2` are separate `List` instances. Even though their contents match, `Equals` returns `false`. - 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: +When you need record equality to reflect collection *contents*, you have a few options: -:::code language="csharp" source="snippets/equality/Program.cs" ID="ReferenceEqualsDemo"::: +- **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 might be intentional. -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. +> [!IMPORTANT] +> 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 @@ -117,5 +117,7 @@ A common use is inside an `Equals` override to short-circuit the full comparison - [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) +- [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/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..70deeda36e76d --- /dev/null +++ b/docs/csharp/fundamentals/expressions/operators.md @@ -0,0 +1,163 @@ +--- +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`. 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. 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 + +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 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. `!=` 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] +> 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 + +`&&` (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 — 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 + +This article covers the operators you'll encounter most in everyday code. The C# language includes more operators useful in specific scenarios: + +- **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 + +- [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/equality/Program.cs b/docs/csharp/fundamentals/expressions/snippets/equality/Program.cs index 670765639cd50..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"); @@ -58,6 +50,15 @@ 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 +// + + // ── Type declarations ──────────────────────────────────────────────────────── class Order(int id, string name) @@ -76,30 +77,10 @@ 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; } + +record Playlist(string Name, List Tracks); 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..963c8f34e4615 --- /dev/null +++ b/docs/csharp/fundamentals/expressions/snippets/operators/Program.cs @@ -0,0 +1,155 @@ +// +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 + +// 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) +// + +// +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 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 ('A'=65 <= 'B'=66 <= 'C'=67) +// + +// +int expected = 42; +int actual = 42; + +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) + +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 +// + +// +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 +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 +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 is 210 +Console.WriteLine(small); // => 210 +// 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/fundamentals/expressions/snippets/operators/operators.csproj similarity index 80% rename from docs/csharp/programming-guide/statements-expressions-operators/snippets/how-to-define-value-equality-for-a-type/ValueEqualityStruct/ValueEqualityStruct.csproj rename to docs/csharp/fundamentals/expressions/snippets/operators/operators.csproj index f704bf4988fa6..dfb40caafcf9a 100644 --- a/docs/csharp/programming-guide/statements-expressions-operators/snippets/how-to-define-value-equality-for-a-type/ValueEqualityStruct/ValueEqualityStruct.csproj +++ b/docs/csharp/fundamentals/expressions/snippets/operators/operators.csproj @@ -2,9 +2,9 @@ Exe - net8.0 - enable + net10.0 enable + enable diff --git a/docs/csharp/fundamentals/object-oriented/objects.md b/docs/csharp/fundamentals/object-oriented/objects.md index ed0e19f474ef8..cd2e20e561a84 100644 --- a/docs/csharp/fundamentals/object-oriented/objects.md +++ b/docs/csharp/fundamentals/object-oriented/objects.md @@ -8,44 +8,44 @@ 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"::: - 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](../../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 [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](../../language-reference/operators/equality-operators.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/how-to/index.md b/docs/csharp/how-to/index.md index 9966fb8880776..342638eab8fb0 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](../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 ccb5aa518b072..9e4953d6c799d 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 @@ -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](../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 4ab09d24a9d5a..4f4fb86a11542 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'* @@ -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](../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 0014853addcad..e8edfb54fadf9 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 @@ -49,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 @@ -92,6 +94,66 @@ The following example demonstrates how to use the `!=` operator: :::code language="csharp" source="snippets/shared/EqualityOperators.cs" id="NonEquality"::: +## Equality in class hierarchies + +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`. + +### 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"::: + +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. + +### 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 + +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 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. @@ -114,5 +176,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/snippets/how-to-define-value-equality-for-a-type/RecordCollectionsIssue/RecordCollectionsIssue.csproj b/docs/csharp/language-reference/operators/snippets/EqualityHierarchies/EqualityHierarchies.csproj similarity index 80% rename from docs/csharp/programming-guide/statements-expressions-operators/snippets/how-to-define-value-equality-for-a-type/RecordCollectionsIssue/RecordCollectionsIssue.csproj rename to docs/csharp/language-reference/operators/snippets/EqualityHierarchies/EqualityHierarchies.csproj index fd4dd4565750e..5c0a78df5ac6b 100644 --- a/docs/csharp/programming-guide/statements-expressions-operators/snippets/how-to-define-value-equality-for-a-type/RecordCollectionsIssue/RecordCollectionsIssue.csproj +++ b/docs/csharp/language-reference/operators/snippets/EqualityHierarchies/EqualityHierarchies.csproj @@ -2,9 +2,9 @@ Exe - net8.0 - enable + 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..7cf089c22128b --- /dev/null +++ b/docs/csharp/language-reference/operators/snippets/EqualityHierarchies/Program.cs @@ -0,0 +1,82 @@ +// +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); +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); +} +// + +// +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 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/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-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 3e5ee8035ff78..194b6d7dd4156 100644 --- a/docs/csharp/toc.yml +++ b/docs/csharp/toc.yml @@ -119,6 +119,9 @@ items: href: fundamentals/expressions/index.md - 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 - name: Iteration statements @@ -541,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