Last Updated: May 17, 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 gotchas that trip up newcomers (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 are the ones you'd expect from a calculator, with one twist around division that catches people off guard.
| 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 |
Here's a small program that uses each one to build a cart total:
Nothing surprising. The values flow through the operators the way you'd read them out loud.
This is the twist. The / operator looks the same no matter what types you give it, but the result depends on those types. If both operands are integers, C# does integer division and throws away the remainder. If at least one operand is a floating-point or decimal type, you get a real quotient.
The first division returns 3, not 3.5. The .5 doesn't round, it gets truncated. The compiler doesn't warn you about this because it's legal C#, just not always what you wanted. If you need 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 shows up more often than people expect: 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. That matters when you're using it to wrap around an index and 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 flavors: 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, you can use either. The difference only shows up when you use the result inside a larger expression. Mixing increments into complex expressions makes code harder to read. The clean 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. Straightforward.
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 as you'd expect. We'll go deep on reference equality, value equality, and Equals versus == in the section on classes and objects.
Comparing floating-point numbers with == is one of the classic traps. 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 you print them 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 your code can safely do, because the right side might be expensive, or might throw on inputs that the left side just 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 job is bitwise operations on integers.
Bitwise operators work on the individual bits of an integer. They show up most often when you're packing multiple boolean flags into a single integer (a "bitmask"), which keeps storage tight and lets you combine 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. You can represent a set of statuses 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 & ~.
You don't need bitwise operators for everyday business logic. Reach for them when you have a small fixed set of independent yes/no states and care about compact storage, or when you're working with low-level data like file permissions, network protocol fields, or hardware registers.
Earlier chapters introduced the null operators. Here's a quick reference. 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. Mastering them keeps your code free of long if (x != null) chains.
C# has two operators for asking and answering "what type is this object?" The deep coverage lives in the section on pattern matching. Here's 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, you get it. If not, you get 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 people just call it "the ternary."
Ternaries shine for short either-or assignments. They're also expression-friendly, so you can use them 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 when you had types that didn't share an obvious base.
That's the gist. You won't notice it day to day, but it explains why some old patterns suddenly compile in newer projects.
Nested ternaries get unreadable quickly. This is the kind of thing that looks clever and then traps the next person reading the code:
When you have 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 a lot of "give me the last N items" or "skip the first one" code read like a sentence.
Cost: 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. If you add 1 to int.MaxValue, you don't get an exception, you get 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.
You can also wrap a block:
The mirror image is unchecked, which forces wraparound behavior even if the surrounding context is checked. You'll mostly use it 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 silently wraps.
Cost: checked adds a small per-operation overhead. For tight inner loops on hot paths, that adds up. For ordinary business logic, you won't notice it, and the safety is worth it.
When you mix operators 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 | =, +=, -=, *=, /=, %=, &=, |=, ^=, <<=, >>=, ??=, => |
You don't need to memorize the whole table. 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 person reading the code.
All three lines compute the same value. The third one is the easiest to read because the parentheses make the order obvious without changing the math.
+, -, *, /, % work the way you'd expect, with one twist: dividing two integers truncates the remainder. Cast to double or use a decimal literal (m suffix) when you need a real quotient.++ and -- come in prefix and postfix forms. Prefix returns the new value, postfix returns the old. Avoid mixing them with other operators in the same expression.+=, -=, *=, /=, %=, &=, |=, ^=, <<=, >>=, ??=) are shorthand for "operate then assign back."bool. For value types and string, == compares contents. For other reference types, it compares references by default. Floating-point equality is unreliable due to rounding, use a tolerance or use decimal for money.&& and || short-circuit: the right side is evaluated only when the left side doesn't already settle the result. That's what makes x != null && x.Length > 0 safe.&, |, ^, ~, <<, >>, >>>) work on the bits of integers. They're handy for flag enums and low-level data. C# 11 added >>> for unsigned right shift.?: picks one of two values based on a condition. Use it for short either-or assignments. For more than two cases, use if/else or a switch expression.^ and .. operators (C# 8) make slicing arrays, strings, and lists from the end or by range concise.checked enables overflow detection on integer arithmetic, throwing OverflowException instead of silently wrapping. unchecked forces wraparound. The project setting controls the default.&&, && beats ||. When in doubt, add parentheses.