Operators and Expressions
🎯 What You'll Learn
- What operators and expressions are
- Arithmetic operators for calculations (+, -, *, /, %)
- Comparison operators for making decisions (==, !=, <, >, etc.)
- Logical operators for combining conditions (&&, ||, !)
- Assignment operators and compound assignment (+=, -=, etc.)
- The powerful null-handling operators (?., ??, ??=)
- The ternary conditional operator (?:)
- Operator precedence and why parentheses matter
- Common string operations
What Are Operators and Expressions?
In the previous section, you learned how to store data in variables. But storing data is only useful if you can do something with it—calculate totals, compare values, make decisions. That's where operators come in.
An operator is a symbol that tells the compiler to perform a specific
operation (like addition or comparison).
An expression is a combination of values, variables, and operators
that evaluates to a single result.
// These are all expressions:
5 + 3 // Evaluates to 8
price * quantity // Evaluates to a decimal
age >= 18 // Evaluates to true or false
firstName + " " + lastName // Evaluates to a string
isActive && hasPermission // Evaluates to true or false
Every expression produces a value. That value can be stored in a variable, passed to a method, used in a condition, or combined with other expressions.
Arithmetic Operators
Arithmetic operators perform mathematical calculations. These are the operators you'd expect from basic math class.
Basic Arithmetic
| Operator | Name | Example | Result |
|---|---|---|---|
+ |
Addition | 10 + 3 |
13 |
- |
Subtraction | 10 - 3 |
7 |
* |
Multiplication | 10 * 3 |
30 |
/ |
Division | 10 / 3 |
3 (integer division!) |
% |
Modulus (remainder) | 10 % 3 |
1 |
int a = 10;
int b = 3;
int sum = a + b; // 13
int difference = a - b; // 7
int product = a * b; // 30
int quotient = a / b; // 3 (not 3.33!)
int remainder = a % b; // 1
// Unary operators (operate on a single value)
int positive = +a; // 10 (rarely used)
int negative = -a; // -10 (negation)
When you divide two integers, C# performs integer division—the result
is truncated (not rounded). 10 / 3 equals 3, not 3.33.
If you need decimal results, at least one operand must be a floating-point type.
// Integer division - decimal part is LOST
int result1 = 10 / 3; // 3 (truncated)
int result2 = 7 / 2; // 3 (not 3.5)
// Floating-point division - preserves decimals
double result3 = 10.0 / 3; // 3.3333...
double result4 = 10 / 3.0; // 3.3333...
double result5 = (double)10 / 3; // 3.3333... (cast to force floating-point)
// For money calculations, use decimal
decimal total = 100.00m;
decimal perPerson = total / 3; // 33.3333...
The Modulus Operator (%)
The modulus operator returns the remainder after division. It's incredibly useful for many programming tasks:
// Basic modulus - what's left over?
int remainder = 17 % 5; // 2 (17 = 5*3 + 2)
// Check if a number is even or odd
int number = 42;
bool isEven = number % 2 == 0; // true (42 is even)
bool isOdd = number % 2 != 0; // false
// Check if divisible by something
int year = 2024;
bool divisibleBy4 = year % 4 == 0; // true (useful for leap years)
bool divisibleBy100 = year % 100 == 0; // false
// Wrap around (cycling through values)
int hour = 25;
int hour24 = hour % 24; // 1 (wraps around after 24)
// Get the last digit of a number
int lastDigit = 12345 % 10; // 5
// Alternating rows in a table (for styling)
for (int row = 0; row < 10; row++)
{
bool isAlternateRow = row % 2 == 0; // true for 0, 2, 4, 6, 8
}
Increment and Decrement
The ++ and -- operators add or subtract 1 from a variable.
They can be placed before (prefix) or after (postfix) the variable, with a subtle difference.
int count = 5;
count++; // count is now 6 (same as count = count + 1)
count--; // count is now 5 (same as count = count - 1)
// Prefix vs Postfix - the difference matters when used in expressions
int a = 5;
int b = a++; // b = 5, THEN a becomes 6 (postfix: use, then increment)
int c = 5;
int d = ++c; // c becomes 6 FIRST, then d = 6 (prefix: increment, then use)
// Most common usage: standalone statements (prefix vs postfix doesn't matter)
int counter = 0;
counter++; // Simple increment
// Common in loops (we'll cover loops in Control Flow)
for (int i = 0; i < 10; i++)
{
// i increments each iteration
}
The prefix/postfix difference can be confusing. As a best practice, avoid using
++ or -- inside larger expressions. Use them as standalone
statements (count++;) where the difference doesn't matter.
Comparison Operators
Comparison operators compare two values and return a bool (true or false).
They're essential for making decisions in your code.
| Operator | Name | Example | Result |
|---|---|---|---|
== |
Equal to | 5 == 5 |
true |
!= |
Not equal to | 5 != 3 |
true |
> |
Greater than | 5 > 3 |
true |
< |
Less than | 5 < 3 |
false |
>= |
Greater than or equal | 5 >= 5 |
true |
<= |
Less than or equal | 5 <= 3 |
false |
int age = 25;
int minimumAge = 18;
int retirementAge = 65;
bool isAdult = age >= minimumAge; // true
bool canRetire = age >= retirementAge; // false
bool isExactly25 = age == 25; // true
bool isNot25 = age != 25; // false
// Works with other types too
decimal price = 99.99m;
decimal budget = 100.00m;
bool canAfford = price <= budget; // true
// String comparison (case-sensitive by default)
string input = "yes";
bool isYes = input == "yes"; // true
bool isYES = input == "YES"; // false (case matters!)
// Case-insensitive string comparison
bool isYesIgnoreCase = input.Equals("YES", StringComparison.OrdinalIgnoreCase); // true
= is assignment (put a value into a variable).
== is comparison (check if two values are equal).
Writing if (x = 5) instead of if (x == 5) is a common bug.
Fortunately, C# usually catches this with a compiler error.
Logical Operators
Logical operators combine multiple boolean expressions. They're how you express conditions like "is active AND has permission" or "is admin OR is owner."
| Operator | Name | Description | Example |
|---|---|---|---|
&& |
Logical AND | True if BOTH are true | true && false → false |
|| |
Logical OR | True if EITHER is true | true || false → true |
! |
Logical NOT | Inverts the value | !true → false |
Truth Tables
These tables show the result of each logical operator for all possible inputs:
| AND (&&) | ||
|---|---|---|
| A | B | A && B |
| true | true | true |
| true | false | false |
| false | true | false |
| false | false | false |
| OR (||) | ||
|---|---|---|
| A | B | A || B |
| true | true | true |
| true | false | true |
| false | true | true |
| false | false | false |
| NOT (!) | |
|---|---|
| A | !A |
| true | false |
| false | true |
// AND - both conditions must be true
bool isActive = true;
bool hasPermission = true;
bool canAccess = isActive && hasPermission; // true (both are true)
// OR - at least one condition must be true
bool isAdmin = false;
bool isOwner = true;
bool canEdit = isAdmin || isOwner; // true (owner is true)
// NOT - inverts the value
bool isLoggedIn = true;
bool isGuest = !isLoggedIn; // false
// Real-world examples
int age = 25;
bool hasLicense = true;
bool canDrive = age >= 18 && hasLicense; // Must be adult AND have license
decimal orderTotal = 150m;
bool isPremiumMember = false;
bool hasFreeShipping = orderTotal >= 100m || isPremiumMember; // true
// Combining multiple conditions
int quantity = 50;
bool inStock = true;
bool canFulfillOrder = inStock && quantity > 0 && quantity <= 100;
// Using parentheses for clarity (and correctness)
bool complexCondition = (isAdmin || isOwner) && isActive;
// Different from: isAdmin || (isOwner && isActive)
Short-Circuit Evaluation
C#'s && and || operators use short-circuit
evaluation—they stop as soon as the result is determined:
// AND short-circuits: if the first is false, the second is never evaluated
bool result1 = false && SomeExpensiveFunction(); // Function never called!
// OR short-circuits: if the first is true, the second is never evaluated
bool result2 = true || SomeExpensiveFunction(); // Function never called!
// This is useful for null checks!
string name = null;
// Without short-circuit: would crash with NullReferenceException
// bool valid = name.Length > 0; // CRASH!
// With short-circuit: safe because Length is never accessed if name is null
bool valid = name != null && name.Length > 0; // false (no crash)
Always put null checks before accessing properties:
obj != null && obj.Property. The property access only happens if the
null check passes.
Assignment Operators
Assignment operators store values in variables. Beyond the basic =,
there are compound operators that combine assignment with arithmetic.
| Operator | Example | Equivalent To |
|---|---|---|
= |
x = 5 |
Assign 5 to x |
+= |
x += 3 |
x = x + 3 |
-= |
x -= 3 |
x = x - 3 |
*= |
x *= 3 |
x = x * 3 |
/= |
x /= 3 |
x = x / 3 |
%= |
x %= 3 |
x = x % 3 |
int score = 100;
score += 10; // score is now 110
score -= 5; // score is now 105
score *= 2; // score is now 210
score /= 3; // score is now 70
// String concatenation with +=
string message = "Hello";
message += " World"; // "Hello World"
// Common pattern: accumulating a total
decimal total = 0m;
total += 19.99m; // Add first item
total += 29.99m; // Add second item
total += 9.99m; // Add third item
// total is now 59.97
Null-Handling Operators
Null references are a common source of bugs. C# provides special operators to handle null values elegantly. These are extremely useful in real-world code.
Null-Conditional Operator (?. and ?[])
The ?. operator only accesses a member if the object is not null.
If the object is null, the entire expression returns null instead of crashing.
string name = null;
// Without null-conditional: crashes with NullReferenceException!
// int length = name.Length; // CRASH!
// With null-conditional: returns null instead of crashing
int? length = name?.Length; // null (no crash)
// Chaining multiple levels
string city = customer?.Address?.City; // Safe even if customer or Address is null
// With array indexer
int[]? numbers = null;
int? firstNumber = numbers?[0]; // null (no crash)
// With method calls
string upper = name?.ToUpper(); // null (no crash)
Null-Coalescing Operator (??)
The ?? operator provides a default value when something is null.
Think of it as "use this value, or if it's null, use that value instead."
string? input = null;
string result = input ?? "default"; // "default" (because input is null)
string? input2 = "hello";
string result2 = input2 ?? "default"; // "hello" (input2 is not null)
// Common use: providing default values
string displayName = user.Nickname ?? user.FullName ?? "Anonymous";
// With nullable value types
int? quantity = null;
int actualQuantity = quantity ?? 0; // 0
// Combining with null-conditional
int nameLength = name?.Length ?? 0; // 0 if name is null
Null-Coalescing Assignment (??=)
The ??= operator assigns a value only if the variable is currently null.
string? name = null;
name ??= "Unknown"; // name is now "Unknown"
string? name2 = "Akwasi";
name2 ??= "Unknown"; // name2 is still "Akwasi" (not null, so no assignment)
// Equivalent to:
if (name == null)
{
name = "Unknown";
}
// Useful for lazy initialization
private List<string>? _items;
public List<string> Items => _items ??= new List<string>();
?. — Safe member access (returns null if object is null)
?? — Provide default value if null
??= — Assign only if currently null
Master these three operators—you'll use them constantly in real C# code.
The Ternary Conditional Operator (?:)
The ternary operator is a compact way to choose between two values based on a condition. It's like an inline if-else.
// Syntax: condition ? valueIfTrue : valueIfFalse
int age = 20;
string status = age >= 18 ? "Adult" : "Minor"; // "Adult"
// Equivalent if-else:
string status2;
if (age >= 18)
status2 = "Adult";
else
status2 = "Minor";
// Common use cases
int quantity = 5;
string itemText = quantity == 1 ? "item" : "items"; // "items"
Console.WriteLine($"You have {quantity} {itemText}"); // "You have 5 items"
// Finding the larger of two numbers
int a = 10, b = 20;
int max = a > b ? a : b; // 20
// Nested ternary (use sparingly - can be hard to read)
int score = 85;
string grade = score >= 90 ? "A"
: score >= 80 ? "B"
: score >= 70 ? "C"
: score >= 60 ? "D"
: "F"; // "B"
// With null-coalescing for even more power
string? userName = null;
string greeting = $"Hello, {userName ?? "Guest"}!"; // "Hello, Guest!"
Use ternary for simple, single-line choices. For complex logic or multiple statements, use regular if-else—it's more readable. Avoid deeply nested ternaries.
String Operations
Strings have special behavior with operators and many useful methods. Let's explore the most common operations.
String Concatenation
// Concatenation with +
string firstName = "Akwasi";
string lastName = "Asante";
string fullName = firstName + " " + lastName; // "Akwasi Asante"
// String interpolation (preferred)
string fullName2 = $"{firstName} {lastName}"; // "Akwasi Asante"
// Interpolation with formatting
decimal price = 1234.567m;
string formatted = $"Price: {price:C}"; // "Price: $1,234.57" (currency)
string twoDecimals = $"Price: {price:F2}"; // "Price: 1234.57" (2 decimals)
string padded = $"ID: {42:D5}"; // "ID: 00042" (padded to 5 digits)
DateTime now = DateTime.Now;
string dateStr = $"Today: {now:yyyy-MM-dd}"; // "Today: 2024-01-15"
String Comparison
string a = "hello";
string b = "HELLO";
// Case-sensitive comparison (default)
bool equal1 = a == b; // false
bool equal2 = a.Equals(b); // false
// Case-insensitive comparison
bool equal3 = a.Equals(b, StringComparison.OrdinalIgnoreCase); // true
bool equal4 = string.Equals(a, b, StringComparison.OrdinalIgnoreCase); // true
// Comparing with ToLower/ToUpper (works but less efficient)
bool equal5 = a.ToLower() == b.ToLower(); // true
Common String Methods
string text = " Hello, World! ";
// Length
int length = text.Length; // 17
// Trimming whitespace
string trimmed = text.Trim(); // "Hello, World!"
string trimStart = text.TrimStart(); // "Hello, World! "
string trimEnd = text.TrimEnd(); // " Hello, World!"
// Case conversion
string upper = text.ToUpper(); // " HELLO, WORLD! "
string lower = text.ToLower(); // " hello, world! "
// Searching
bool contains = text.Contains("World"); // true
bool startsWith = text.StartsWith(" H"); // true
bool endsWith = text.EndsWith("!"); // false (trailing spaces!)
int index = text.IndexOf("World"); // 9
// Substring
string sub1 = text.Substring(2); // "Hello, World! " (from index 2)
string sub2 = text.Substring(2, 5); // "Hello" (5 chars from index 2)
// Replace
string replaced = text.Replace("World", "C#"); // " Hello, C#! "
// Split and Join
string csv = "apple,banana,cherry";
string[] fruits = csv.Split(','); // ["apple", "banana", "cherry"]
string joined = string.Join(" | ", fruits); // "apple | banana | cherry"
// Check for empty or null
string empty = "";
string? nullStr = null;
bool isEmpty = string.IsNullOrEmpty(empty); // true
bool isNull = string.IsNullOrEmpty(nullStr); // true
bool isWhitespace = string.IsNullOrWhiteSpace(" "); // true
In C#, strings cannot be changed after creation. Methods like ToUpper()
or Replace() return a new string—they don't modify the original.
This is why you need to assign the result: text = text.ToUpper();
Operator Precedence
When an expression has multiple operators, C# evaluates them in a specific order called precedence. This is like math's "order of operations" (PEMDAS/BODMAS).
| Priority | Operators | Description |
|---|---|---|
| 1 (highest) | () [] ?. ! |
Parentheses, member access, null-conditional, not |
| 2 | ++ -- (unary) + - |
Increment, decrement, unary plus/minus |
| 3 | * / % |
Multiplication, division, modulus |
| 4 | + - |
Addition, subtraction |
| 5 | < > <= >= |
Comparison |
| 6 | == != |
Equality |
| 7 | && |
Logical AND |
| 8 | || |
Logical OR |
| 9 | ?? |
Null-coalescing |
| 10 | ?: |
Ternary conditional |
| 11 (lowest) | = += -= *= /= |
Assignment |
// Multiplication before addition
int result1 = 2 + 3 * 4; // 14, not 20 (3*4 first, then +2)
int result2 = (2 + 3) * 4; // 20 (parentheses force addition first)
// Comparison before logical operators
bool result3 = 5 > 3 && 2 < 4; // true (comparisons first, then &&)
// AND before OR (common source of bugs!)
bool a = true, b = false, c = true;
bool result4 = a || b && c; // true (b && c first, then a ||)
bool result5 = (a || b) && c; // true (parentheses change meaning)
Don't rely on memorizing precedence. Use parentheses to make your intent clear.
(a && b) || c is easier to read than a && b || c, even
though they mean the same thing. Clear code beats clever code.
Putting It All Together: InvenTrack Example
Let's apply what we've learned to a realistic scenario—calculating order totals in our InvenTrack system:
// Product data
string productName = "Premium Widget";
decimal unitPrice = 79.99m;
int quantity = 5;
decimal? discountPercent = 10m; // 10% discount, could be null
// Customer data
string? customerName = "Kofi Mensah";
bool isPremiumMember = true;
bool isFirstOrder = false;
// Constants
const decimal VatRate = 0.125m; // 12.5%
const decimal FreeShippingThreshold = 200m;
const decimal ShippingCost = 15m;
// Calculations
decimal subtotal = unitPrice * quantity; // 399.95
// Apply discount (use 0 if no discount)
decimal discountRate = (discountPercent ?? 0m) / 100m; // 0.10
decimal discountAmount = subtotal * discountRate; // 39.995
decimal afterDiscount = subtotal - discountAmount; // 359.955
// Calculate VAT
decimal vatAmount = afterDiscount * VatRate; // 44.994375
decimal totalWithVat = afterDiscount + vatAmount; // 404.949375
// Determine shipping (free for premium members, high orders, or first order)
bool qualifiesForFreeShipping = isPremiumMember
|| totalWithVat >= FreeShippingThreshold
|| isFirstOrder;
decimal shipping = qualifiesForFreeShipping ? 0m : ShippingCost; // 0
// Final total
decimal grandTotal = totalWithVat + shipping;
// Format for display
string displayName = customerName ?? "Guest";
string itemLabel = quantity == 1 ? "item" : "items";
string shippingText = shipping == 0m ? "FREE" : $"{shipping:C}";
// Output
Console.WriteLine($"Order for: {displayName}");
Console.WriteLine($"{quantity} {itemLabel} of {productName}");
Console.WriteLine($"Subtotal: {subtotal:C}");
Console.WriteLine($"Discount: -{discountAmount:C}");
Console.WriteLine($"VAT (12.5%): {vatAmount:C}");
Console.WriteLine($"Shipping: {shippingText}");
Console.WriteLine($"─────────────────────");
Console.WriteLine($"Total: {grandTotal:C}");
Key Takeaways
- Expressions combine values, variables, and operators to produce a result
- Arithmetic operators:
+ - * / %— watch for integer division truncation! - Comparison operators:
== != < > <= >=— return bool - Logical operators:
&& || !— use short-circuit evaluation - Compound assignment:
+= -= *= /=— shorthand for common patterns - Null operators:
?.(safe access),??(default value),??=(assign if null) - Ternary operator:
condition ? ifTrue : ifFalse— inline conditionals - Precedence: When in doubt, use parentheses for clarity
You can now perform calculations and comparisons. In the next section, we'll learn
how to use these comparisons to control what your program does—making decisions
with if statements and repeating actions with loops.
What's Next?
In the next section, Control Flow, we'll learn how to make your
programs smart—branching based on conditions (if, switch)
and repeating actions (for, while, foreach).