Operators and Expressions
|
This section documents C# 14 on .NET 10 (LTS), as published at learn.microsoft.com/dotnet/csharp, which is the reference these pages are written and verified against. Features introduced by C# 15 / .NET 11 are still in preview and are always flagged as such — never presented as baseline. This content was generated with the assistance of AI and should be verified against learn.microsoft.com before being relied on in production. This section’s bibliography lists the reference material consulted while preparing these pages. |
An expression is anything that produces a value. This page is the reference for the operators that build them; Equality and Operator Overloading covers how to give your own types operator behaviour.
Arithmetic
int a = 17, b = 5;
Console.WriteLine(a + b); // 22
Console.WriteLine(a - b); // 12
Console.WriteLine(a * b); // 85
Console.WriteLine(a / b); // 3 -- integer division truncates toward zero
Console.WriteLine(a % b); // 2 -- remainder, sign follows the dividend
Console.WriteLine(-a / b); // -3
Console.WriteLine(-a % b); // -2
Console.WriteLine(17.0 / 5); // 3.4 -- one double operand makes it floating point
Console.WriteLine(17m / 5); // 3.4 -- decimal division
Console.WriteLine(+a); // unary plus (no-op)
Console.WriteLine(-a); // unary minus
Integer division by zero throws DivideByZeroException; floating-point division by zero yields Infinity or
NaN. Operands narrower than int (byte, short, char) are promoted to int before the operation, which
is why byte x = b1 + b2; needs a cast.
Comparison and Equality
Console.WriteLine(3 < 4); // True
Console.WriteLine(3 <= 3); // True
Console.WriteLine("a" == "a"); // True -- string == compares content
Console.WriteLine(3 != 4); // True
object x = new object();
object y = new object();
Console.WriteLine(x == y); // False -- reference identity for most classes
Console.WriteLine(x.Equals(x)); // True
// Structs and records get value equality; plain classes do not.
Console.WriteLine(new DateTime(2026, 1, 1) == new DateTime(2026, 1, 1)); // True
== on reference types means identity unless the type overloads it (string, record, many BCL value types
do). See Equality and Operator
Overloading.
Boolean Logical Operators
static bool Expensive() { Console.WriteLine("called"); return true; }
bool p = false, q = true;
Console.WriteLine(p && Expensive()); // False -- short-circuits, Expensive() not called
Console.WriteLine(p & Expensive()); // False -- but Expensive() IS called
Console.WriteLine(q || Expensive()); // True -- short-circuits
Console.WriteLine(q ^ p); // True -- exclusive or
Console.WriteLine(!p); // True
Prefer && and ||; use & and | on bool only when you genuinely need both sides evaluated.
Bitwise and Shift
uint flags = 0b_1010_1100;
Console.WriteLine(Convert.ToString(flags & 0b_0000_1111, 2)); // 1100 AND
Console.WriteLine(Convert.ToString(flags | 0b_0000_0011, 2)); // 10101111 OR
Console.WriteLine(Convert.ToString(flags ^ 0b_1111_1111, 2)); // 1010011 XOR
Console.WriteLine(Convert.ToString(~flags)); // NOT (complement)
Console.WriteLine(1 << 4); // 16 left shift
Console.WriteLine(-16 >> 2); // -4 arithmetic right shift (sign-preserving)
Console.WriteLine(-16 >>> 2); // 1073741820 unsigned right shift (C# 11)
The shift count is taken modulo the operand width (32 for int, 64 for long), so 1 << 32 is 1, not 0.
See Enums for [Flags], the usual reason to reach for these.
Assignment and Compound Assignment
int n = 10;
n += 5; // 15
n -= 3; // 12
n *= 2; // 24
n /= 4; // 6
n %= 4; // 2
n <<= 3; // 16
n &= 0b1100;
n |= 0b0011;
n ^= 0b0101;
Console.WriteLine(n);
string s = "a";
s += "b"; // "ab"
Console.WriteLine(s);
A compound assignment x op= y means x = (T)(x op y) — the cast back to x’s type is implicit, which is why
`byte b = 1; b += 300; compiles (and wraps) where b = b + 300; does not. C# 14 lets a type define its own
compound assignment operator; see
Equality and Operator Overloading.
The Conditional Operator
int score = 72;
string grade = score >= 60 ? "pass" : "fail";
Console.WriteLine(grade);
// It is an expression, so it nests -- but a switch expression usually reads better.
string band = score >= 90 ? "A" : score >= 75 ? "B" : score >= 60 ? "C" : "F";
Console.WriteLine(band);
// `ref` conditional: choose which variable to assign to.
int left = 1, right = 2;
ref int chosen = ref (score > 50 ? ref left : ref right);
chosen = 99;
Console.WriteLine($"{left} {right}"); // 99 2
Both branches must convert to a common type, or you must supply one with a cast or a target type.
Null-Coalescing and Null-Conditional
string? maybe = null;
Console.WriteLine(maybe ?? "fallback"); // ?? -- use the right side when the left is null
maybe ??= "assigned once"; // ??= -- assign only if currently null
Console.WriteLine(maybe);
string? name = null;
Console.WriteLine(name?.Length); // ?. -- null, not a NullReferenceException
Console.WriteLine(name?.Length ?? 0); // 0
int[]? numbers = null;
Console.WriteLine(numbers?[0]); // ?[] -- null, no exception
// The whole chain short-circuits: one null anywhere yields null.
Console.WriteLine(name?.Trim().ToUpperInvariant().Length);
?. on a value-returning member lifts the result to a nullable type, which is why name?.Length is int?.
Null-Conditional Assignment (C# 14)
C# 14 allows ?. and ?[] on the left of an assignment or compound assignment. The right-hand side is
evaluated only when the receiver is non-null:
public sealed class Settings
{
public string Theme { get; set; } = "light";
public List<string> Tags { get; } = [];
}
public static class NullConditionalAssignment
{
public static void Apply(Settings? settings)
{
// C# 14: no-op when `settings` is null -- previously required an `if`.
settings?.Theme = "dark";
settings?.Tags[0] = "first";
// Equivalent to:
// if (settings is not null) { settings.Theme = "dark"; }
}
}
++, -- and compound assignment work the same way. Increment and decrement are not permitted in this
position.
The Null-Forgiving Operator
#nullable enable
static int LengthOf(string? maybe)
{
// `!` asserts to the compiler that the value is not null. It generates no code
// and performs no check -- it only silences the warning. Use it sparingly.
return maybe!.Length;
}
! is a claim you are making to the compiler; if you are wrong you still get a NullReferenceException. See
Nullable Types and Null Safety.
Type Testing and Conversion
object value = "hello";
// `is` with a type pattern -- tests and declares in one step.
if (value is string text)
{
Console.WriteLine(text.Length);
}
Console.WriteLine(value is string); // True
Console.WriteLine(value is not null); // True
Console.WriteLine(value is int); // False
// `as` -- converts or yields null; never throws. Reference and nullable types only.
string? asText = value as string;
int? asNumber = value as int?; // null
Console.WriteLine($"{asText} {asNumber is null}");
// Cast -- converts or throws InvalidCastException.
string cast = (string)value;
Console.WriteLine(cast);
// typeof on a type, GetType() on an instance.
Console.WriteLine(typeof(string) == value.GetType()); // True
Use is with a pattern for a test-then-use; use as followed by a null check when you prefer that shape; use a
cast when a failure is a bug you want to hear about immediately.
Index and Range
int[] values = [10, 20, 30, 40, 50];
Console.WriteLine(values[0]); // 10
Console.WriteLine(values[^1]); // 50 -- ^n counts from the end, ^1 is the last
Console.WriteLine(values[^2]); // 40
int[] middle = values[1..4]; // 20, 30, 40 -- start inclusive, end exclusive
int[] head = values[..2]; // 10, 20
int[] tail = values[3..]; // 40, 50
int[] all = values[..]; // a copy
int[] lastTwo = values[^2..]; // 40, 50
Console.WriteLine(string.Join(",", middle));
Console.WriteLine(string.Join(",", head));
Console.WriteLine(string.Join(",", tail));
Console.WriteLine(string.Join(",", all));
Console.WriteLine(string.Join(",", lastTwo));
// Index and Range are real types you can pass around.
Index last = ^1;
Range firstThree = 0..3;
Console.WriteLine(values[last]);
Console.WriteLine(string.Join(",", values[firstThree]));
Ranging an array or string copies; ranging a Span<T> does not. That is the main reason parsers work in
spans.
default, new, with
// `default` -- target-typed since C# 7.1.
int zero = default;
List<string>? nothing = default;
// Target-typed `new` -- the type comes from the target (C# 9).
List<string> names = new();
Dictionary<string, List<int>> index = new();
Point origin = new(0, 0);
// `with` -- non-destructive mutation of a record or struct (C# 9 / C# 10).
var p1 = new Point(1, 2);
var p2 = p1 with { Y = 99 };
Console.WriteLine($"{p1} {p2} {zero} {nothing is null} {names.Count} {index.Count} {origin}");
public readonly record struct Point(int X, int Y);
switch Expressions
static string Classify(object value) => value switch
{
null => "null",
int n when n < 0 => "negative int",
int n => $"int {n}",
string { Length: 0 } => "empty string",
string s => $"string of {s.Length}",
_ => "something else",
};
public static class SwitchExpressionDemo
{
public static void Run()
{
Console.WriteLine(Classify(null));
Console.WriteLine(Classify(-3));
Console.WriteLine(Classify("abc"));
}
}
Arms are tested top to bottom and the whole thing is an expression. See
Pattern Matching for the full pattern grammar and
Control Flow for how it differs from the switch
statement.
await, stackalloc, checked
public static class OtherOperators
{
public static async Task<int> AwaitDemo()
{
int result = await Task.FromResult(42); // await is an operator
return result;
}
public static int StackallocDemo()
{
Span<int> buffer = stackalloc int[8]; // stack memory, no allocation
for (int i = 0; i < buffer.Length; i++)
{
buffer[i] = i * i;
}
return buffer[^1]; // 49
}
public static int CheckedDemo() => unchecked(int.MaxValue + 1);
}
stackalloc in a Span<T> is safe code; only pointer-typed stackalloc needs unsafe. Never stackalloc in
a loop — the memory is released only when the method returns.
Operator Precedence
Highest to lowest. Operators in the same row have equal precedence.
| Category | Operators | Associativity |
|---|---|---|
Primary |
|
left |
Unary |
|
right |
Range |
|
left |
Multiplicative |
|
left |
Additive |
|
left |
Shift |
|
left |
Relational and type-testing |
|
left |
Equality |
|
left |
Boolean logical AND |
|
left |
Boolean logical XOR |
|
left |
Boolean logical OR |
|
left |
Conditional AND |
|
left |
Conditional OR |
|
left |
Null-coalescing |
|
right |
Conditional |
|
right |
Assignment / lambda |
|
right |
Two traps worth remembering: &/^/| bind tighter than &&/|| but looser than ==, so
if (flags & Mask == Mask) does not mean what it looks like — parenthesise it. And ?? is right-associative,
so a ?? b ?? c is a ?? (b ?? c), which is what you want.
Operand evaluation is strictly left to right, independent of precedence.
See Also
-
Pattern Matching —
ispatterns andswitchexpressions in depth. -
Equality and Operator Overloading — defining operators on your own types.
-
Nullable Types and Null Safety — what
?.,??and!mean to the null-state analysis. -
Control Flow — the statements these expressions appear in.
References
-
Microsoft Learn — Type-testing operators and cast expressions.
-
Microsoft Learn — What’s new in C# 14 (null-conditional assignment,
nameofof unbound generics).