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;