AlgoMaster Logo

Operators

Last Updated: May 22, 2026

High Priority
9 min read

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.

Arithmetic Operators

The five basic arithmetic operators match calculator semantics, with one twist around division.

OperatorNameExampleResult
+Additionsubtotal + taxsum
-Subtractiontotal - discountdifference
*Multiplicationprice * quantityproduct
/Divisionrevenue / ordersquotient
%Modulo (remainder)cartItems % 12remainder

A small program that uses each one to build a cart total:

The values flow through the operators in natural order.

Integer Division vs Floating-Point Division

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.

The Modulo Operator

% 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.

Unary Plus and Minus

+ and - also work as unary operators on a single value. Unary - negates, unary + does nothing useful and is mostly there for symmetry.

Increment and Decrement

++ 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.

  • Prefix (++count): increment first, then return the new value.
  • Postfix (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.

Assignment Operators

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:

OperatorEquivalent to
x += yx = x + y
x -= yx = x - y
x *= yx = x * y
x /= yx = x / y
x %= yx = x % y
x &= yx = x & y
x |= yx = x | y
x ^= yx = x ^ y
x <<= yx = x << y
x >>= yx = x >> y
x ??= yassign 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

Comparison operators return bool. They're the building blocks of if conditions, while loops, and any logic that branches.

OperatorMeaningExample
==Equal tostatus == "shipped"
!=Not equal toquantity != 0
<Less thanstock < 10
>Greater thantotal > 100m
<=Less than or equalrating <= 3
>=Greater than or equaltotal >= 50m

A small example that checks free shipping eligibility:

Value vs Reference Equality

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.

Floating-Point Comparison Pitfall

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

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.

Short-Circuit Evaluation

&& 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 and Shift Operators

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.

OperatorNameExampleMeaning
&ANDa & bbit is 1 where both are 1
|ORa | bbit is 1 where either is 1
^XORa ^ bbit is 1 where the bits differ
~NOT~aflips every bit
<<Left shifta << 2multiply by 2 to the power of n
>>Right shifta >> 2divide by 2 to the power of n (sign-extending)
>>>Unsigned right shifta >>> 2divide 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.

Null Operators (Recap)

Earlier chapters introduced the null operators. A quick reference follows; the full coverage lives in the chapter on nullability.

OperatorNameWhat it does
??Null-coalescinga ?? b returns a if non-null, otherwise b
??=Null-coalescing assignmenta ??= b assigns b to a only when a is null
?.Null-conditional member accesscart?.Total returns null if cart is null, else cart.Total
?[]Null-conditional indexingitems?[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.

Type-Testing Operators

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.

Conditional (Ternary) Operator

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:

Target-Typed Conditional (C# 9)

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.

When Not to Use Ternaries

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.

Range and Index Operators

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.

Checked and Unchecked Contexts

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.

Operator Precedence

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.

PrecedenceCategoryOperators
1 (highest)Primaryx.y, f(x), a[i], x?.y, x?[i], x++, x--, new, typeof, checked, unchecked, nameof
2Unary+x, -x, !x, ~x, ++x, --x, (T)x, await, &x, *x
3Multiplicative*, /, %
4Additive+, -
5Shift<<, >>, >>>
6Relational, type-testing<, >, <=, >=, is, as
7Equality==, !=
8Logical AND&
9Logical XOR^
10Logical OR|
11Conditional AND&&
12Conditional OR||
13Null-coalescing??
14Conditional?:
15 (lowest)Assignment, lambda=, +=, -=, *=, /=, %=, &=, |=, ^=, <<=, >>=, ??=, =>

The whole table isn't required. Two practical takeaways cover most cases:

  • Multiplication and division happen before addition and subtraction, like in math.
  • Equality and relational operators happen before && 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.