Skip to main content
C# beginner Lesson 5 of 25

Operators in C#

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

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
x -= 3;
x *= 2;
x /= 4;
x %= 3;

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)

// Logical — && and || short-circuit (second operand not evaluated if unnecessary)
bool a = true, b = false;
Console.WriteLine(a && b);   // false — AND: both must be true
Console.WriteLine(a || b);   // true  — OR: at least one must be true
Console.WriteLine(!a);       // 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(Convert.ToString(~flags, 2));         // ...1010 — NOT: inverts all bits
Console.WriteLine(flags << 1);  // 10 — left shift (multiply by 2)
Console.WriteLine(flags >> 1);  // 2  — right shift (divide by 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!"

// Useful for lazy loading — only call the expensive method if cache is null
string GetCachedValue() => null;
string value = GetCachedValue() ?? LoadFromDatabase();

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

// Equivalent to:
// if (items == null) items = new List<string>();

// Classic use-case: lazy property initialization
private List<string>? _cache;
public List<string> Cache => _cache ??= LoadCache();  // loads once, reuses after

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)

// Chained access — short-circuits at the first null
Customer? customer = GetCustomer(id);
string? city = customer?.Address?.City;  // null if customer or Address is null

// With method calls — call is skipped entirely if null
customer?.Notify("Order shipped");

// With indexers
var firstOrder = customer?.Orders?[0];  // null if customer or Orders is null

// Combining with ?? — safe access with a fallback default
string city = customer?.Address?.City ?? "Unknown";

Null-Conditional with Events

A common pattern for thread-safe event invocation:

// Old way — has a race condition if another thread unsubscribes between the check and invoke
if (PropertyChanged != null)
    PropertyChanged(this, e);

// Correct way — null-conditional captures the delegate atomically
PropertyChanged?.Invoke(this, e);

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
// The compiler warns if not all inputs are covered
string label = score switch
{
    >= 90 => "Excellent",
    >= 70 => "Good",
    >= 50 => "Average",
    _     => "Needs Improvement"  // _ is the discard / default arm
};

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

Type patterns in switch

// Dispatch to different logic based on the runtime type of an object
object shape = new Circle(5.0);

double area = shape switch
{
    Circle c    => Math.PI * c.Radius * c.Radius,   // bind and use properties
    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()}")
};

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"));

// Match on nested properties — no explicit null checks or property accesses needed
string region = customer switch
{
    { Address.Country: "US" }           => "North America",
    { Address.Country: "UK" }           => "Europe",
    { Address: { City: "Tokyo" } }      => "Asia",
    _                                   => "Other"
};

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";

// Concatenation — fine for a small number of strings
string full = hello + ", " + world + "!";

// String interpolation — preferred, more readable
string full2 = $"{hello}, {world}!";

// Equality — compares content, not reference
Console.WriteLine("abc" == "abc");   // True
Console.WriteLine("abc" != "xyz");   // True

// Comparison — returns negative, zero, or positive
Console.WriteLine(string.Compare("apple", "banana"));  // negative (apple < banana)

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:

// Ambiguous to a reader — * has higher precedence, so b * c runs first
int result = a + b * c;

// Explicit — no ambiguity, no need to recall the precedence table
int result2 = (a + b) * c;

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.