Skip to main content
C# · lesson 5 of 26
C# beginner Lesson 5 of 26

Operators in C#

Master arithmetic, null-coalescing, null-conditional, and pattern matching operators in C#.

By SkillByExample · Updated

Arithmetic Operators

Arithmetic operators are the foundation of numeric computation in C#. A few behaviors are worth knowing upfront: integer division truncates toward zero (it does not round), and the modulo operator % returns the remainder. When you need a fractional result from integer inputs, cast at least one operand to a floating-point type first.

int a = 17, b = 5;

Console.WriteLine(a + b);   // 22  — addition
Console.WriteLine(a - b);   // 12  — subtraction
Console.WriteLine(a * b);   // 85  — multiplication
Console.WriteLine(a / b);   // 3   — integer division (truncates, not rounds)
Console.WriteLine(a % b);   // 2   — modulo (remainder after division)

// Cast to double to get floating-point division
Console.WriteLine((double)a / b);  // 3.4

// Increment / decrement — pre vs post matters when used in expressions
int x = 5;
Console.WriteLine(x++);  // 5 (post-increment: returns current value, then increments)
Console.WriteLine(x);    // 6
Console.WriteLine(++x);  // 7 (pre-increment: increments first, then returns)

// Compound assignment — shorthand for x = x op y
x += 10;  // x = x + 10 -> 16
x -= 3;   // x = x - 3  -> 13
x *= 2;   // x = x * 2  -> 26
x /= 4;   // x = x / 4  -> 6
x %= 3;   // x = x % 3  -> 1
Console.WriteLine($"Final x: {x}");
$ dotnet run
22
12
85
3
2
3.4
5
6
7
Final x: 1

Comparison and Logical Operators

Comparison operators produce bool results and are the building blocks of conditions. Logical operators combine booleans, with && and || short-circuiting — they stop evaluating as soon as the result is determined, which is useful for guarding against null before accessing a member.

int age = 25;
bool isAdult = age >= 18;     // true
bool isSenior = age >= 65;    // false
bool isTeenager = age is >= 13 and < 20;  // false (pattern matching syntax)
Console.WriteLine($"isAdult={isAdult}, isSenior={isSenior}, isTeenager={isTeenager}");

// Logical — && and || short-circuit (second operand not evaluated if unnecessary)
bool flagA = true, flagB = false;
Console.WriteLine(flagA && flagB);   // False — AND: both must be true
Console.WriteLine(flagA || flagB);   // True  — OR: at least one must be true
Console.WriteLine(!flagA);           // False — NOT: inverts the value

// Bitwise (on integers) — useful for flags and low-level operations
int flags = 0b_0101;
int mask  = 0b_0011;
Console.WriteLine(Convert.ToString(flags & mask, 2));   // 0001 — AND: bits set in both
Console.WriteLine(Convert.ToString(flags | mask, 2));   // 0111 — OR: bits set in either
Console.WriteLine(Convert.ToString(flags ^ mask, 2));   // 0110 — XOR: bits set in exactly one
Console.WriteLine(flags << 1);  // 10 — left shift (multiply by 2)
Console.WriteLine(flags >> 1);  // 2  — right shift (divide by 2)
$ dotnet run
isAdult=True, isSenior=False, isTeenager=False
False
True
False
0001
0111
0110
10
2

Null-Coalescing Operators

Null checks scattered throughout code quickly become noisy. The null-coalescing operators give you a compact, readable way to handle null — either by providing a fallback value or by lazily assigning a default. They are especially useful when working with optional data from databases or external APIs.

The null-coalescing operator ?? returns its left operand if not null, otherwise the right operand.

string? name = null;
string displayName = name ?? "Anonymous";   // "Anonymous"

// Chain multiple fallbacks — evaluates left to right, stops at the first non-null
string? first = null, second = null, third = "Found!";
string result = first ?? second ?? third;   // "Found!"

Console.WriteLine(displayName);
Console.WriteLine(result);
$ dotnet run
Anonymous
Found!

The null-coalescing assignment operator ??= assigns only when the variable is null. It is cleaner than an explicit null check when initializing lazily.

List<string>? items = null;
items ??= new List<string>();  // items is now an empty list; no-op if already set
Console.WriteLine($"items count: {items.Count}");
$ dotnet run
items count: 0

Null-Conditional Operators

Null-conditional operators let you safely navigate a chain of member accesses where any link might be null. Without them, you would need a null check at every step. With them, the entire chain short-circuits to null at the first null value, preventing NullReferenceException.

The null-conditional operator ?. accesses a member only if the object is not null; otherwise it returns null.

string? text = null;
int? length = text?.Length;    // null (no NullReferenceException thrown)
Console.WriteLine($"length: {length}");

// Chained access — short-circuits at the first null
string? city = text?.ToUpper().Substring(0, 3);  // null — text is null, chain stops
Console.WriteLine($"city: {city}");

// Combining with ?? — safe access with a fallback default
string display = text?.ToUpper() ?? "Empty";
Console.WriteLine($"display: {display}");
$ dotnet run
length:
city:
display: Empty

Null-Conditional with Method Chaining

The null-conditional is especially powerful when chaining multiple member accesses:

class Person { public Address? Address { get; set; } }
class Address { public string? City { get; set; } }

var person = new Person();
// Safe navigation — no NullReferenceException even if Address is null
Console.WriteLine(person.Address?.City ?? "Unknown");

person.Address = new Address { City = "London" };
Console.WriteLine(person.Address?.City ?? "Unknown");
$ dotnet run
Unknown
London

Ternary and Switch Expressions

The ternary operator is useful for simple conditional assignments in one line. Switch expressions (C# 8+) extend this to multiple branches and are exhaustiveness-checked by the compiler, meaning it warns you if a case is missing.

// Ternary — condition ? valueIfTrue : valueIfFalse
int score = 75;
string grade = score >= 90 ? "A" : score >= 70 ? "B" : "C";

// Switch expression (C# 8+) — cleaner than nested ternaries
string label = score switch
{
    >= 90 => "Excellent",
    >= 70 => "Good",
    >= 50 => "Average",
    _     => "Needs Improvement"  // _ is the discard / default arm
};

Console.WriteLine($"grade={grade}, label={label}");
$ dotnet run
grade=B, label=Good

Pattern Matching Operators

Pattern matching lets you test a value’s type, structure, and content in one expression. It reduces the need for explicit casts and null checks, and it integrates naturally with switch expressions to produce clean, readable dispatch logic.

is operator

object obj = "Hello";

// Type pattern — checks type and binds to a new variable in one step
if (obj is string s)
    Console.WriteLine(s.ToUpper());  // s is only in scope inside this block

// Null check
if (obj is not null)
    Console.WriteLine("Not null");

// Constant pattern — check for a specific value
if (obj is "Hello")
    Console.WriteLine("It's hello");

// Combined patterns with 'and' / 'or' / 'not'
int n = 42;
bool inRange    = n is >= 0 and <= 100;  // range check in one expression
bool isEdge     = n is 0 or 100;         // matches either endpoint
bool notNegative = n is not < 0;         // negated pattern
Console.WriteLine($"inRange={inRange}, isEdge={isEdge}, notNegative={notNegative}");
$ dotnet run
HELLO
Not null
It's hello
inRange=True, isEdge=False, notNegative=True

Type patterns in switch

record Circle(double Radius);
record Rectangle(double Width, double Height);
record Triangle(double Base, double Height);

object shape = new Circle(5.0);

double area = shape switch
{
    Circle c    => Math.PI * c.Radius * c.Radius,
    Rectangle r => r.Width * r.Height,
    Triangle t  => 0.5 * t.Base * t.Height,
    null        => throw new ArgumentNullException(nameof(shape)),
    _           => throw new NotSupportedException($"Unknown shape: {shape.GetType()}")
};
Console.WriteLine($"area: {area:F4}");
$ dotnet run
area: 78.5398

Property patterns

Property patterns let you match on the values of nested properties, making complex conditional logic declarative rather than imperative.

record Address(string City, string Country);
record Customer(string Name, Address Address);

var customer = new Customer("Alice", new Address("London", "UK"));

string region = customer switch
{
    { Address.Country: "US" }           => "North America",
    { Address.Country: "UK" }           => "Europe",
    { Address: { City: "Tokyo" } }      => "Asia",
    _                                   => "Other"
};
Console.WriteLine(region);
$ dotnet run
Europe

String Operators

String has its own set of operators that handle common operations like concatenation and comparison. Prefer string interpolation over + for readability, and use string.Compare with an explicit StringComparison to avoid locale-sensitive surprises.

string hello = "Hello";
string world = "World";

string full = hello + ", " + world + "!";
string full2 = $"{hello}, {world}!";

Console.WriteLine("abc" == "abc");   // True
Console.WriteLine("abc" != "xyz");   // True
Console.WriteLine(string.Compare("apple", "banana"));  // -1 (apple < banana)

Console.WriteLine(full);
Console.WriteLine(full2);
$ dotnet run
True
True
-1
Hello, World!
Hello, World!

Operator Precedence

Operators have a fixed evaluation order. Knowing the rules prevents subtle bugs, but when in doubt, parentheses always make intent explicit and are better than relying on readers knowing the precedence table by heart.

From highest to lowest (abridged):

()          — parentheses
!  ~  ++  -- — unary
*  /  %     — multiplicative
+  -        — additive
<< >>       — shift
<  >  <=  >= is  as  — relational and type
==  !=      — equality
&           — bitwise AND
^           — bitwise XOR
|           — bitwise OR
&&          — logical AND
||          — logical OR
??          — null-coalescing
?:          — ternary
=  +=  -=   — assignment

When in doubt, use parentheses to make your intent explicit:

int a = 1, b = 2, c = 3;

int result = a + b * c;       // 7  (b * c first)
int result2 = (a + b) * c;    // 9  (1 + 2) * 3
Console.WriteLine($"result={result}, result2={result2}");
$ dotnet run
result=7, result2=9

Practice

  1. Integer division pitfall. Given int x = 17, y = 5, what is x / y? What if you write (double)x / y? Explain why the results differ and when the truncation catches you by surprise.
Solution

17 / 5 gives 3 (integer division truncates toward zero). (double)17 / 5 gives 3.4. The difference is that integer division discards the remainder, while casting to double first promotes the operation to floating-point. This catches you by surprise when you expect a fractional result from two integer variables — for example, calculating an average: sum / count gives an integer average, losing the decimal part. Fix: cast at least one operand: (double)sum / count.

  1. Null-coalescing vs null-conditional. Create a string? variable set to null. Use ?? to provide a fallback, and ?. to safely access .Length. What does each return? When would you use one over the other?
Solution
string? text = null;

// ?? provides a fallback value
string safe = text ?? "default";       // "default"

// ?. safely accesses a member; returns null if source is null
int? length = text?.Length;            // null (not an exception)
int safeLength = text?.Length ?? 0;    // 0 — combine with ?? for a usable int

Use ?? when you want to guarantee a non-null value. Use ?. when you want to safely traverse a chain that might hit null, possibly followed by ?? to provide a fallback.

  1. Switch expression vs nested ternaries. Rewrite a nested ternary like score >= 90 ? "A" : score >= 80 ? "B" : "C" as a switch expression. Which is more readable? What does the switch expression give you that the ternary doesn’t?
Solution
// Ternary — hard to read with many cases
string grade = score >= 90 ? "A" : score >= 80 ? "B" : score >= 70 ? "C" : "D";

// Switch expression — clean, pattern-based, and exhaustiveness-checked
string grade = score switch
{
    >= 90 => "A",
    >= 80 => "B",
    >= 70 => "C",
    >= 60 => "D",
    _     => "F"
};

The switch expression is more readable at 3+ branches, and the compiler enforces exhaustiveness — it warns you if a case is missing. The _ discard arm makes the default explicit.

Frequently Asked Questions

What is the difference between ?? and ??=
?? is the null-coalescing operator — it returns the left side if non-null, otherwise the right side. ??= is the null-coalescing assignment — it assigns the right side only if the left is null.
When should I use ?. over a null check?
Use ?. (null-conditional) for chained member access when intermediate values might be null. It short-circuits the chain and returns null rather than throwing NullReferenceException. For simple null checks before calling a method, either style is fine.
What does the 'is' operator do in pattern matching?
'is' tests whether a value matches a pattern and optionally binds it to a new variable. It is more expressive than type-checking with 'as' or '(Type)cast' because it handles null, type, and structural checks in one expression.