Beginner

Operators in C#

Learn how arithmetic, comparison, logical, assignment, and pattern operators work in C#, with runnable Docker examples

Operators are the verbs of any programming language — they transform values, compare them, and combine them into expressions. C# inherits the familiar infix operator set from the C family (so +, -, *, /, ==, && will look immediately recognizable), but as a modern multi-paradigm language it adds quite a bit on top: null-aware operators, pattern matching expressions, range and index operators, and a strongly-typed evaluation model that makes many subtle bugs into compile-time errors.

Because C# is statically and strongly typed, the compiler checks operator types ahead of time. You cannot accidentally add a string to an int and get silent coercion — the operator overload set determines what is legal, and ambiguous cases must be made explicit through casts or conversions. This is a deliberate trade-off: a little more typing in exchange for catching whole categories of bugs before the program ever runs.

In this tutorial we will walk through arithmetic, comparison, logical, bitwise, and assignment operators, then look at some of C#’s more distinctive operators: the null-coalescing ??, the null-conditional ?., the conditional ?:, and pattern matching with is. Each example is a complete, runnable program.

Arithmetic and Assignment Operators

C# supports the standard arithmetic operators (+, -, *, /, %) plus increment (++) and decrement (--). A key gotcha: integer division truncates toward zero, so 7 / 2 is 3, not 3.5. To get a floating-point result, at least one operand must be floating-point.

Compound assignment operators (+=, -=, *=, /=, %=) update a variable in place and return the new value.

Create a file named Program.cs:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
int a = 17;
int b = 5;

Console.WriteLine($"a + b = {a + b}");
Console.WriteLine($"a - b = {a - b}");
Console.WriteLine($"a * b = {a * b}");
Console.WriteLine($"a / b = {a / b}        (integer division truncates)");
Console.WriteLine($"a % b = {a % b}        (remainder)");

double x = 17.0;
double y = 5.0;
Console.WriteLine($"x / y = {x / y}  (floating-point division)");

int counter = 10;
counter += 3;
Console.WriteLine($"counter after += 3: {counter}");

counter *= 2;
Console.WriteLine($"counter after *= 2: {counter}");

int post = counter++;
int pre  = ++counter;
Console.WriteLine($"post-increment captured {post}, pre-increment captured {pre}, counter is now {counter}");

Run it:

1
docker run --rm -v $(pwd):/app -w /app mcr.microsoft.com/dotnet/sdk:9.0 sh -c "cp Program.cs Program.cs.bak && dotnet new console -o . --force >/dev/null 2>&1 && cp Program.cs.bak Program.cs && rm Program.cs.bak && dotnet run"

Expected output:

a + b = 22
a - b = 12
a * b = 85
a / b = 3        (integer division truncates)
a % b = 2        (remainder)
x / y = 3.4  (floating-point division)
counter after += 3: 13
counter after *= 2: 26
post-increment captured 26, pre-increment captured 28, counter is now 28

Comparison and Logical Operators

Comparison operators (==, !=, <, >, <=, >=) return a bool. Logical operators (&&, ||, !) work on booleans and short-circuit&& stops evaluating once it finds a false, and || stops at the first true. The non-short-circuiting forms (& and |) also exist but force both sides to be evaluated.

For reference types, == uses reference equality by default (unless overridden, as string does for value equality).

Create a file named Program.cs:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
int age = 30;
bool hasLicense = true;
bool hasInsurance = false;

Console.WriteLine($"age == 30 : {age == 30}");
Console.WriteLine($"age != 18 : {age != 18}");
Console.WriteLine($"age >= 21 : {age >= 21}");

bool canDrive    = hasLicense && age >= 16;
bool canRentCar  = hasLicense && hasInsurance && age >= 25;
bool isRestricted = !hasInsurance || age < 18;

Console.WriteLine($"canDrive     = {canDrive}");
Console.WriteLine($"canRentCar   = {canRentCar}");
Console.WriteLine($"isRestricted = {isRestricted}");

// Short-circuit evaluation: the right side of && is never called when the left is false.
string? name = null;
bool isShortName = name != null && name.Length < 5;
Console.WriteLine($"isShortName (with null guard) = {isShortName}");

// String equality compares character contents, not references.
string greeting = "hello";
string echoed   = "hel" + "lo";
Console.WriteLine($"greeting == echoed : {greeting == echoed}");

Run it with the same Docker command shown above. Expected output:

age == 30 : True
age != 18 : True
age >= 21 : True
canDrive     = True
canRentCar   = False
isRestricted = True
isShortName (with null guard) = False
greeting == echoed : True

Null-Aware, Conditional, and Pattern Operators

This is where modern C# really diverges from the older C family. The null-conditional operator ?. returns null instead of throwing a NullReferenceException when the left side is null. The null-coalescing operator ?? returns its right operand when the left is null, and ??= assigns only if the variable is null. The conditional operator ?: is C#’s ternary expression. And the is operator combined with patterns lets you test type and shape in a single expression.

Create a file named Program.cs:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
string? userInput = null;

// Null-coalescing: provide a default
string display = userInput ?? "(no input)";
Console.WriteLine($"display = {display}");

// Null-conditional chain: safely access Length on a possibly-null string
int? length = userInput?.Length;
Console.WriteLine($"length  = {(length?.ToString() ?? "null")}");

// Null-coalescing assignment: only assigns if userInput is null
userInput ??= "default value";
Console.WriteLine($"userInput after ??= : {userInput}");

// Conditional (ternary) operator
int score = 72;
string grade = score >= 90 ? "A"
             : score >= 80 ? "B"
             : score >= 70 ? "C"
             : score >= 60 ? "D"
                           : "F";
Console.WriteLine($"score {score} -> grade {grade}");

// Pattern matching with `is`
object value = 42;
if (value is int n && n > 0)
{
    Console.WriteLine($"value is a positive int: {n}");
}

// switch expression — operator-style pattern matching
string Describe(object o) => o switch
{
    null               => "nothing",
    int i when i < 0   => $"negative int {i}",
    int i              => $"int {i}",
    string s           => $"string of length {s.Length}",
    _                  => "something else"
};

Console.WriteLine(Describe(-7));
Console.WriteLine(Describe("hello"));
Console.WriteLine(Describe(3.14));

Run it with the same Docker command. Expected output:

display = (no input)
length  = null
userInput after ??= : default value
score 72 -> grade C
value is a positive int: 42
negative int -7
string of length 5
something else

Operator Precedence

When operators are combined, C# evaluates them according to a fixed precedence table — * and / bind tighter than + and -, comparison binds tighter than &&, and && binds tighter than ||. When in doubt, parentheses cost nothing and make intent obvious to the next reader.

1
2
3
4
5
6
7
// These two are equivalent:
int r1 = 2 + 3 * 4;       // 14, because * binds tighter
int r2 = 2 + (3 * 4);     // 14, explicit

// And these are equivalent — but the explicit form is much easier to scan:
bool ok = age >= 18 && hasLicense || isOverride;
bool okExplicit = ((age >= 18) && hasLicense) || isOverride;

Running with Docker

Each example in this tutorial is a complete program. Save it as Program.cs and run it with:

1
2
3
4
5
# Pull the official .NET SDK image (one-time)
docker pull mcr.microsoft.com/dotnet/sdk:9.0

# Compile and run Program.cs
docker run --rm -v $(pwd):/app -w /app mcr.microsoft.com/dotnet/sdk:9.0 sh -c "cp Program.cs Program.cs.bak && dotnet new console -o . --force >/dev/null 2>&1 && cp Program.cs.bak Program.cs && rm Program.cs.bak && dotnet run"

The command preserves your Program.cs, scaffolds a temporary project around it, and runs it. No local .NET SDK required.

Key Concepts

  • Integer division truncates toward zero. Use a double (or cast one operand) when you need a fractional result.
  • Compound assignments (+=, -=, etc.) update in place and return the new value — they are expressions, not just statements.
  • Logical && and || short-circuit, which makes them safe for null-guards like obj != null && obj.Value > 0.
  • string equality with == compares character contents, not references — unlike most other reference types in C#.
  • ?. and ?? make null-handling expressive and concise; ??= assigns only when the target is null.
  • Pattern matching with is and switch expressions turns type-and-shape checks into single expressions, blending OOP and functional styles.
  • Operator precedence follows C-family conventions; reach for parentheses when an expression mixes more than two operator families.
  • The compiler enforces types on every operator — accidental cross-type arithmetic is a build error, not a silent coercion.

Running Today

All examples can be run using Docker:

docker pull mcr.microsoft.com/dotnet/sdk:9.0
Last updated:

Comments

Loading comments...

Leave a Comment

2000 characters remaining