Last Updated: May 22, 2026
Operators are the symbols that combine values into new values. Adding prices, comparing stock counts, checking whether a customer qualifies for free shipping, all of it routes through a small set of operators with well-defined rules. This chapter walks through every operator category in C#, the pitfalls newcomers commonly hit (integer division, floating-point equality, overflow), and the precedence rules that decide which operation runs first when you mix them.
The five basic arithmetic operators match calculator semantics, with one twist around division.
| Operator | Name | Example | Result |
|---|---|---|---|
+ | Addition | subtotal + tax | sum |
- | Subtraction | total - discount | difference |
* | Multiplication | price * quantity | product |
/ | Division | revenue / orders | quotient |
% | Modulo (remainder) | cartItems % 12 | remainder |
A small program that uses each one to build a cart total:
The values flow through the operators in natural order.
This is the twist. The / operator looks the same no matter the operand types, but the result depends on those types. If both operands are integers, C# does integer division and discards the remainder. If at least one operand is a floating-point or decimal type, the result is a real quotient.
The first division returns 3, not 3.5. The .5 doesn't round; it gets truncated. The compiler doesn't warn about this because it's legal C#, just not always the intent. For a real quotient from two integers, cast at least one of them: (double)totalItems / 2.
Dividing an integer by zero throws DivideByZeroException at runtime. Dividing a double or float by zero doesn't throw; it returns double.PositiveInfinity, double.NegativeInfinity, or double.NaN depending on the signs. decimal division by zero throws like integers do.
% returns the remainder after division. It's useful for checking whether a number is even (n % 2 == 0), rotating through a fixed set of items (index % count), or wrapping cart sizes into pack sizes.
For negative numbers, C#'s % follows the sign of the dividend, so -7 % 3 is -1, not 2. This matters when wrapping an index where the input could be negative.
+ and - also work as unary operators on a single value. Unary - negates, unary + does nothing useful and is mostly there for symmetry.
++ and -- add or subtract one from a variable. They come in two forms: prefix (++count) and postfix (count++). Both change the variable the same way. The difference is what the expression evaluates to.
++count): increment first, then return the new value.count++): return the current value first, then increment.In standalone statements like count++;, the prefix-vs-postfix distinction doesn't matter; either form works. The difference only shows up when the result is used inside a larger expression. Mixing increments into complex expressions makes code harder to read. The conventional style in modern C# is to put count++; on its own line.
The plain = assigns the right side to the left. Every other assignment operator is shorthand for "do this operation, then assign the result back."
All the compound assignments work the same way:
| Operator | Equivalent to |
|---|---|
x += y | x = x + y |
x -= y | x = x - y |
x *= y | x = x * y |
x /= y | x = x / y |
x %= y | x = x % y |
x &= y | x = x & y |
x |= y | x = x | y |
x ^= y | x = x ^ y |
x <<= y | x = x << y |
x >>= y | x = x >> y |
x ??= y | assign y to x only if x is null |
The ??= operator is the null-coalescing assignment (C# 8). It assigns y to x only when x is currently null.
The second ??= does nothing because promoCode is no longer null.
Comparison operators return bool. They're the building blocks of if conditions, while loops, and any logic that branches.
| Operator | Meaning | Example |
|---|---|---|
== | Equal to | status == "shipped" |
!= | Not equal to | quantity != 0 |
< | Less than | stock < 10 |
> | Greater than | total > 100m |
<= | Less than or equal | rating <= 3 |
>= | Greater than or equal | total >= 50m |
A small example that checks free shipping eligibility:
For value types like int, double, decimal, and bool, == compares the actual values. Two int variables that both hold 5 are equal.
For reference types, == by default compares references, not contents. Two different Cart objects with identical items are not == to each other. They live at different addresses on the heap.
string is the well-known exception. Even though string is a reference type, the language overloads == to compare characters, so "hello" == "hello" returns true. Reference equality, value equality, and Equals versus == are covered in the section on classes and objects.
Comparing floating-point numbers with == is a classic trap. Numbers like 0.1 and 0.2 can't be represented exactly in binary double, so arithmetic on them gives results that look right when printed but aren't bit-for-bit equal to the "obvious" answer.
The fix is to compare within a small tolerance, often called epsilon:
For money, sidestep the whole problem. Use decimal instead of double. decimal represents base-10 fractions exactly, so 0.1m + 0.2m == 0.3m returns true.
Logical operators combine bool values. There are three:
&& (logical AND): true if both sides are true.|| (logical OR): true if at least one side is true.! (logical NOT): flips a bool.&& and || are short-circuit operators. They evaluate the right side only when they have to.
false && anything is false, so the right side isn't even looked at.true || anything is true, so the right side isn't even looked at.This isn't just an optimization. It changes what code can safely do, because the right side might be expensive, or might throw on inputs that the left side ruled out.
The diagram shows the decision. With &&, a false left side ends the evaluation immediately. With ||, a true left side ends it immediately. Otherwise the runtime keeps going and evaluates the right side.
A practical example: null-guarding before accessing a property.
If && evaluated both sides eagerly, couponCode.Length would throw NullReferenceException whenever couponCode was null. Short-circuiting makes the pattern safe: the left check rules out null, so the right side never runs in that case.
C# also has non-short-circuit versions, & and |, which always evaluate both sides. On bool values these are rarely useful and easy to misuse. Their main role is bitwise operations on integers.
Bitwise operators work on the individual bits of an integer. They appear most often when packing multiple boolean flags into a single integer (a "bitmask"), which keeps storage tight and combines flags with one operation.
| Operator | Name | Example | Meaning |
|---|---|---|---|
& | AND | a & b | bit is 1 where both are 1 |
| | OR | a | b | bit is 1 where either is 1 |
^ | XOR | a ^ b | bit is 1 where the bits differ |
~ | NOT | ~a | flips every bit |
<< | Left shift | a << 2 | multiply by 2 to the power of n |
>> | Right shift | a >> 2 | divide by 2 to the power of n (sign-extending) |
>>> | Unsigned right shift | a >>> 2 | divide by 2 to the power of n (zero-fill, C# 11) |
The >>> operator was added in C# 11. The plain >> operator preserves the sign bit when shifting signed integers right, which can give surprising results on negative numbers. >>> always fills the top bits with zero. For unsigned types like uint or byte, >> and >>> behave the same.
A common e-commerce use case is order status flags. A set of statuses can be represented as bits in a single int:
The [Flags] attribute tells the runtime to format combined values as a comma-separated list of names. Combining flags uses |, checking a flag uses &, clearing a flag uses & ~.
Bitwise operators aren't needed for everyday business logic. Use them for a small fixed set of independent yes/no states with a need for compact storage, or for low-level data like file permissions, network protocol fields, or hardware registers.
Earlier chapters introduced the null operators. A quick reference follows; the full coverage lives in the chapter on nullability.
| Operator | Name | What it does |
|---|---|---|
?? | Null-coalescing | a ?? b returns a if non-null, otherwise b |
??= | Null-coalescing assignment | a ??= b assigns b to a only when a is null |
?. | Null-conditional member access | cart?.Total returns null if cart is null, else cart.Total |
?[] | Null-conditional indexing | items?[0] returns null if items is null, else items[0] |
These operators are the modern, concise way to deal with values that might be null. They replace long if (x != null) chains.
C# has two operators for asking "what type is this object?" The deep coverage lives in the section on pattern matching. The preview:
is returns true if a value matches a type or pattern.
The is string productName pattern does two things at once: it checks that item is a string, and if so, declares productName of type string already cast and ready to use.
as attempts a cast. If the value is the requested type, the result is the cast value. If not, the result is null instead of an exception.
as only works with reference types and nullable value types. For value types like plain int and decimal, use pattern matching with is or an explicit cast inside a try block.
The ?: operator picks one of two values based on a condition. The form is condition ? valueIfTrue : valueIfFalse. It's the only ternary operator in C#, which is why it's often called "the ternary."
Ternaries are well-suited to short either-or assignments. They're expression-friendly, useful inside string interpolations or method calls without an extra if:
Before C# 9, the two branches of a ternary had to have a common type the compiler could resolve. This caused friction with types that didn't share an obvious base.
That's the summary. It rarely matters day to day, but it explains why some old patterns suddenly compile in newer projects.
Nested ternaries get unreadable quickly. The kind of thing that looks clever and traps the next reader:
For more than two cases, prefer an if/else if chain or a switch expression. The same logic with switch:
Cleaner, and the cases are listed top to bottom in the order they're checked.
C# 8 added two small operators that make slicing collections and strings easier.
^ is the index from end operator. ^1 means "one from the end," which is the last element. ^2 is the second-to-last, and so on. ^0 is one past the end (useful as a range boundary, not as an index by itself)... is the range operator. 1..3 means "from index 1 up to but not including index 3." Either side can be omitted: ..3 means "from the start to index 3," and 2.. means "from index 2 to the end."Ranges work on arrays, strings, Span<T>, and List<T>. They make "give me the last N items" or "skip the first one" code read clearly.
Range expressions on arrays and strings allocate a new array or string for the result. On Span<T>, slicing is allocation-free because the span is a view into the same memory.
Integer arithmetic in C# doesn't check for overflow by default. Adding 1 to int.MaxValue doesn't throw an exception; it produces int.MinValue. The bits roll over the way two's-complement arithmetic does on the underlying hardware.
For money or counters where overflow is a real bug, opt into checked arithmetic. The checked keyword forces overflow checks, and the runtime throws OverflowException if the result doesn't fit.
A block can also be wrapped:
The mirror image is unchecked, which forces wraparound behavior even if the surrounding context is checked. It's mostly used in performance-sensitive numeric code where wraparound is intentional, like hashing.
The default mode (checked or unchecked) is a project-level setting. The dotnet new template defaults to unchecked for runtime arithmetic but checked for constant expressions, so int x = int.MaxValue + 1; fails to compile but the same expression at runtime wraps silently.
checked adds a small per-operation overhead. For tight inner loops on hot paths, that adds up. For ordinary business logic, the cost is negligible, and the safety is worth it.
When operators are mixed in one expression, precedence decides the order. Higher precedence runs first. Operators with the same precedence are grouped by associativity, which for almost all operators is left-to-right.
| Precedence | Category | Operators |
|---|---|---|
| 1 (highest) | Primary | x.y, f(x), a[i], x?.y, x?[i], x++, x--, new, typeof, checked, unchecked, nameof |
| 2 | Unary | +x, -x, !x, ~x, ++x, --x, (T)x, await, &x, *x |
| 3 | Multiplicative | *, /, % |
| 4 | Additive | +, - |
| 5 | Shift | <<, >>, >>> |
| 6 | Relational, type-testing | <, >, <=, >=, is, as |
| 7 | Equality | ==, != |
| 8 | Logical AND | & |
| 9 | Logical XOR | ^ |
| 10 | Logical OR | | |
| 11 | Conditional AND | && |
| 12 | Conditional OR | || |
| 13 | Null-coalescing | ?? |
| 14 | Conditional | ?: |
| 15 (lowest) | Assignment, lambda | =, +=, -=, *=, /=, %=, &=, |=, ^=, <<=, >>=, ??=, => |
The whole table isn't required. Two practical takeaways cover most cases:
&& and ||, so a > 0 && b < 10 works without parentheses.When the expression isn't obvious, add parentheses. Parentheses cost nothing and remove ambiguity for the next reader.
All three lines compute the same value. The third is the easiest to read because the parentheses make the order obvious without changing the math.