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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions .openpublishing.redirection.csharp.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
]
}
92 changes: 47 additions & 45 deletions docs/csharp/fundamentals/expressions/equality.md

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions docs/csharp/fundamentals/expressions/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
163 changes: 163 additions & 0 deletions docs/csharp/fundamentals/expressions/operators.md
Original file line number Diff line number Diff line change
@@ -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
41 changes: 11 additions & 30 deletions docs/csharp/fundamentals/expressions/snippets/equality/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -41,14 +41,6 @@
Console.WriteLine(t1 == t2); // => True
// </TupleEquality>

// <IEquatableUsage>
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)
// </IEquatableUsage>

// <ReferenceEqualsDemo>
var doc1 = new Document("Report");
var doc2 = new Document("Report");
Expand All @@ -58,6 +50,15 @@
Console.WriteLine(ReferenceEquals(doc1, doc3)); // => True
// </ReferenceEqualsDemo>

// <RecordWithCollectionProblem>
var playlist1 = new Playlist("Chill", new List<string> { "Song A", "Song B" });
var playlist2 = new Playlist("Chill", new List<string> { "Song A", "Song B" });

Console.WriteLine(playlist1.Equals(playlist2)); // => False (different List instances)
Console.WriteLine(playlist1.Tracks.SequenceEqual(playlist2.Tracks)); // => True
// </RecordWithCollectionProblem>


// ── Type declarations ────────────────────────────────────────────────────────

class Order(int id, string name)
Expand All @@ -76,30 +77,10 @@ record Person(string First, string Last);

record struct Dimension(double Width, double Height);

// <ColorDefinition>
class Color : IEquatable<Color>
{
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);
}
// </ColorDefinition>

class Document(string title)
{
public string Title { get; } = title;
}


record Playlist(string Name, List<string> Tracks);
Loading
Loading