Skip to main content
ANVISoftware Solutions
Lesson 12 of 62Beginner14 min

Operators in Depth

By the end of this lesson

Apply arithmetic, comparison, logical and null-coalescing operators with correct precedence.

Arithmetic and comparison work the way you would expect. The parts of this topic that produce real bugs are precedence — which operator binds tighter — and the operators C# has for dealing with missing values.

The groups you will use every day:

Arithmetic
+ - * / % — with % giving the remainder after division.
Compound assignment
+= -= *= /= — read the variable, apply the operation, store it back.
Increment and decrement
++ and -- add or subtract one. Clearest on a line of their own.
Comparison
< > <= >= produce a bool; == and != test equality.
Logical
&& and || combine conditions and stop as soon as the answer is known. ! flips a bool.
Null handling
?? supplies a fallback when a value is null, ??= assigns only if currently null, and ?. skips a member access rather than throwing.
Arithmetic and compound assignment on an order
C#
decimal subtotal = 240m;
decimal shipping = 4.99m;

subtotal += shipping;          // same as subtotal = subtotal + shipping
subtotal *= 1.20m;             // add 20 per cent VAT

int itemsToPick = 3;
int picked = 0;

while (picked < itemsToPick)
{
    picked++;                  // one clear job on its own line
}

int minutesTotal = 135;
int hours = minutesTotal / 60;      // 2
int minutes = minutesTotal % 60;    // 15

Console.WriteLine($"{subtotal:N2} due, {picked} picked, {hours}h {minutes}m spent");
  • Compound assignment reads the variable once and writes it once, which is shorter and harder to get wrong than repeating the name.
  • Division and remainder together split a value into units: 135 minutes is 2 hours and 15 minutes.
  • picked++ adds one. Used inside a bigger expression it also has a value — the old one for picked++, the new one for ++picked — which is a distinction not worth relying on. Keep it on its own line.

Precedence, from tightest to loosest

The order C# applies, with the tightest first:

  1. Unary: ! and the negative sign
  2. Multiplication, division and remainder: * / %
  3. Addition and subtraction: + -
  4. Relational comparison: < > <= >=
  5. Equality: == !=
  6. Logical and: &&
  7. Logical or: ||
  8. Null coalescing: ??
  9. Assignment: = += and the rest
The precedence bug worth memorising
C#
decimal price = 40m;
decimal? discount = null;

decimal wrong = price - discount ?? 0m;      // 0
decimal right = price - (discount ?? 0m);    // 40

Console.WriteLine($"wrong: {wrong}, right: {right}");
  • Subtraction binds tighter than ??, so the first line is read as (price - discount) ?? 0m.
  • price - discount has type decimal? and is null whenever discount is null, so the fallback replaces the whole calculation rather than the missing discount.
  • The customer is charged nothing, and no exception is raised. The test that would catch it is the one where a customer has no discount.
  • Brackets cost two characters. Add them any time ?? shares an expression with arithmetic.
Short-circuiting and the null operators
C#
Employee? manager = null;   // as though the lookup found nobody

if (manager is not null && manager.IsActive)
{
    Console.WriteLine($"Approver: {manager.Name}");
}

string displayName = manager?.Name ?? "Unassigned";
int approvals = manager?.ApprovalCount ?? 0;

string? note = null;
note ??= "No handover note supplied";

Console.WriteLine($"{displayName}, {approvals} approvals. {note}");

public class Employee
{
    public string Name { get; set; } = "";
    public bool IsActive { get; set; }
    public int ApprovalCount { get; set; }
}
  • && stops as soon as the left side is false, so manager.IsActive is never reached when manager is null. Without that guarantee this condition would throw.
  • & and | do not short-circuit: both sides always run. That matters when the right side can throw or changes something, and it is rarely what you want in a condition.
  • manager?.Name gives null when manager is null instead of throwing, and ?? then supplies the fallback. The pair replaces several lines of null checking.
  • ??= assigns only when the left side is currently null, which is a tidy way to fill in a default without an if.

Summary

  • Multiplication binds tighter than addition, comparisons tighter than && and ||, and ?? looser than all of them
  • ?? and ?: are low precedence, so brackets are needed when they share an expression with arithmetic
  • && and || stop as soon as the answer is known, which is what makes single-line null guards safe
  • ?. guards only the step it is attached to, not the rest of the chain
  • % keeps the sign of its left operand, so remainders of negative numbers are negative

Practice

Attempt each one before opening the solution. Getting it wrong first is how the idea sticks.

Try it yourself

Try it yourself

You have a price and an optional loyalty discount held as a decimal?. Work out the amount to charge, never letting it fall below zero.

Write the calculation first without brackets and print the result for a customer with no discount. Then fix it and compare.

Show solution

Without brackets, price - discount ?? 0m becomes (price - discount) ?? 0m, so a customer with no discount is charged nothing rather than the full price.

With the brackets in the right place, the fallback applies to the discount alone, which is what the sentence "no discount means zero discount" actually says.

Math.Max guards the floor. Clamping is worth doing explicitly, because a discount larger than the price otherwise produces a negative charge, and negative charges have a way of becoming refunds.

C#
decimal price = 40m;
decimal? loyaltyDiscount = null;

decimal wrong = price - loyaltyDiscount ?? 0m;             // 0
decimal charge = Math.Max(0m, price - (loyaltyDiscount ?? 0m));   // 40

Console.WriteLine($"Without brackets: {wrong:N2}");
Console.WriteLine($"With brackets:    {charge:N2}");

Think about it

Think about it

if (manager is not null && manager.IsActive) is safe. Explain why, and write what you would have to do if && evaluated both sides every time.

Show solution

&& evaluates the left side, and if it is false it stops — the right side never runs. So manager.IsActive is only reached once manager is known not to be null.

If both sides always ran, that condition would throw a NullReferenceException whenever the lookup found nobody. You would need a nested if, or manager?.IsActive == true, which works because a null on the left makes the comparison false.

This is why the order of the two halves matters. Swap them and the guarantee is gone, even though the condition looks equivalent.

Saved in this browser only.