An expression is a sequence of operators and operands that produces a value, or a
variable, namespace, type, method group or similar (C# specification clause 12). Patterns
and switch expressions are on the Patterns page,
and await on Async and
iterators.
Operator precedence
Highest first. Operators in one row have the same precedence (clause 12.4.2).
| Category | Operators | Clause |
|---|---|---|
| Primary | x.y f(x) a[i] x?.y x?[y] x++ x-- x! new typeof checked unchecked default nameof delegate sizeof stackalloc x->y | 12.8 |
| Unary | +x -x !x ~x ++x --x ^x (T)x await &x *x true false | 12.9 |
| Range | x..y | 12.11 |
| Switch and with | switch with | 12.12, 12.10 |
| Multiplicative | x * y x / y x % y | 12.13 |
| Additive | x + y x - y | 12.13 |
| Shift | x << y x >> y x >>> y | 12.14 |
| Relational and type-testing | x < y x > y x <= y x >= y is as | 12.15 |
| Equality | x == y x != y | 12.15 |
| Logical AND | x & y | 12.16 |
| Logical XOR | x ^ y | 12.16 |
| Logical OR | x | y | 12.16 |
| Conditional AND | x && y | 12.17 |
| Conditional OR | x || y | 12.17 |
| Null coalescing and throw | x ?? y throw x | 12.18, 12.19 |
| Conditional | c ? t : f | 12.21 |
| Assignment and lambda | = = ref += -= *= /= %= &= |= ^= <<= >>= >>>= ??= => | 12.24, 12.22 |
Associativity and order of evaluation
Binary operators are left-associative: a + b - c means
(a + b) - c. Assignment, ??, ??=,
?: and lambdas are right-associative: x = y = z means
x = (y = z). Parentheses change the grouping.
Regardless of precedence, operands are evaluated from left to right. In
a + b * c the order is a, b, c, the
multiplication, then the addition. Normally every operand is evaluated. These evaluate some
operands conditionally: &&, ||, ??,
??=, ?., ?[] and ?:.
Arithmetic and shift operators
- Integer division truncates towards zero. The remainder has the sign of the left
operand, and
a % bisa - (a / b) * b. - Integer division by zero throws
DivideByZeroException. Floating-point division by zero gives an infinity or NaN. - Smaller integer types are promoted to
intfirst (clause 12.4.7), sobyte + byteis anint. If one operand isdecimal, the other must be convertible to it; it cannot mix withfloatordouble. <<shifts left.>>is arithmetic for signed types and logical for unsigned.>>>(C# 11) is always logical. Shift counts are masked: only the low 5 bits for 32-bit operands, and the low 6 bits for 64-bit ones.+on strings concatenates, and treatsnullas empty.++and--have a prefix form that returns the new value and a postfix form that returns the old one.
Comparison and logical operators
==compares values for value types and strings, and references for other reference types, unless a type overloads it.&,|and^work bitwise on integers and enums, and logically onbool, always evaluating both operands.&&and||skip the right operand if the left one decides the result.- A
NaNcompares false with everything, including itself; only!=gives true.
x is string s; // pattern test, with a new variable
x is null; // never calls a user-defined ==
var t = x as string; // the value, or null if the type is wrong
var u = (string)x; // throws InvalidCastException if the type is wrong
is and as operators; patterns are on the Patterns pageNull operators
| Operator | Meaning | Since |
|---|---|---|
a?.b, a?[i] | Null-conditional. Result is null if a is null, and b is not evaluated | 6 |
a?.b = x | Null-conditional assignment. The right side is evaluated only if a is not null | 14 |
a ?? b | a if not null, otherwise b | 2 |
a ??= b | Assigns b to a if a is null | 8 |
a! | Null-forgiving: no run-time effect, only turns off a nullable warning | 8 |
int? len = name?.Length;
string text = input ?? "none";
list ??= new List<int>();
handler?.Invoke();
customer?.Order = GetCurrentOrder(); // C# 14
Null-conditional assignment also works with compound assignment such as
+=, but not with ++ or --. A chain stops at the
first null: in a?.b.c, nothing after a null a is evaluated
(clause 12.8.8).
Primary expressions
default
default(T) gives the default value of T (clause 12.8.21).
The bare default literal (C# 7.1) takes its type from the context:
int x = default;.
nameof
nameof(x) gives a compile-time string with the name of a variable, type
or member (C# 6, clause 12.8.23). Only the last part is kept, so
nameof(a.b.c) is "c". Since C# 14 it accepts an unbound
generic type: nameof(List<>) is "List".
typeof and sizeof
typeof(T) gives the System.Type object for a type, and also
accepts typeof(List<>) (clause 12.8.18). sizeof(T)
gives a size in bytes. It works on built-in types in safe code, and otherwise needs an
unsafe context (clause 12.8.19).
new
new Person("Ann");
new Person("Ann") { Age = 30 }; // object initialiser
new List<int> { 1, 2, 3 }; // collection initialiser
new Dictionary<string, int> { ["a"] = 1 }; // index initialiser, C# 6
new { X = 1, Y = 2 }; // anonymous type
An object initialiser sets fields and properties after the constructor. A collection
initialiser calls Add for each element.
Target-typed expressions
Some expressions take their type from where they are used:
new()(C# 9, clause 12.8.17):List<int> a = new();default(C# 7.1)c ? a : b(C# 9), when the branches have no common type- Collection expressions (C# 12)
- Lambdas and method groups, unless they have a natural type
throw expressions
Since C# 7.0, throw can be an expression in ?:, as the right
operand of ??, or in an expression body (clause 12.19):
name ?? throw new ArgumentNullException(nameof(name)).
checked and unchecked
checked(expr) and unchecked(expr) set the overflow-checking
context for one expression; see
Statements for the block form.
Collection expressions
A collection expression (C# 12, clause 12.8.25) lists elements in square brackets. It needs a target type and has no type of its own.
int[] a = [1, 2, 3];
List<string> b = ["x", "y"];
Span<int> c = [1, 2];
IEnumerable<int> d = [];
int[] e = [.. a, 4, .. a]; // spread
- Targets are arrays,
Span<T>andReadOnlySpan<T>, types with a collection initialiser, types with a builder named by[CollectionBuilder], and the interfacesIEnumerable<T>,IReadOnlyCollection<T>,IReadOnlyList<T>,ICollection<T>andIList<T>. ..xis a spread element;xmust be enumerable withforeach.- A collection expression is not a constant, and always creates a collection holding every element. It cannot be lazy.
Collection expression arguments
A with(...) element (C# 15) passes arguments to the target’s constructor
or builder. It must be the first element.
List<string> names = [with(capacity: values.Length * 2), .. values];
HashSet<string> set = [with(StringComparer.OrdinalIgnoreCase), "Hello", "HELLO"];
It is not supported for arrays or spans, and the arguments cannot be
dynamic.
Ranges and indices
^x gives an index from the end, and x..y gives a range from
x up to but not including y (C# 8, clauses 12.9.6, 12.11 and
18). They produce values of System.Index and System.Range.
int[] a = { 0, 1, 2, 3, 4 };
a[^1]; // 4, the last element
a[1..3]; // { 1, 2 }
a[..2]; // { 0, 1 }
a[3..]; // { 3, 4 }
a[..^1]; // all except the last
a[..]; // all
Range r = 1..^1;
Types get these through their members, without declaring Index or
Range overloads (clause 18.4): a Length or Count
property with an integer indexer supports ^, and adding
Slice(int, int) supports ranges. A range of an array makes a copy, and a
range of a string makes a new string. A range of a span gives a slice.
Since C# 13, ^ can also be used in object initialisers.
With expressions
x with { Member = value } copies a value and sets some members (C# 9,
clause 12.10). It works on records
and, since C# 10, on any struct and anonymous type.
Conditional expressions
c ? a : b evaluates only one branch. Branches may be refs, with
ref on each (C# 7.2). If the branches share no natural type, the expression
can take its type from the target (C# 9, clause 12.21).
Deconstruction
A deconstruction splits a tuple, or any type with a Deconstruct method, into
variables (C# 7.0, clauses 12.7 and 12.24.3).
var (a, b) = (1, 2);
(int x, string y) = GetPair();
(x, _) = GetPair();
int z;
(z, var w) = GetPair(); // mixed, C# 10
public void Deconstruct(out int x, out int y) => (x, y) = (X, Y);
Assignment
x = y stores a value and is itself an expression. x op= y is
shorthand for x = x op y with x evaluated once. Since C# 14 a
type can overload a compound assignment
directly. x = ref y makes a ref
variable refer to a different target (clause 12.24.4).
Lambda expressions
A lambda (C# 3, clause 12.22) is an anonymous function. It converts to a delegate, or to an expression tree.
Func<int, int> a = x => x * 2;
Func<int, int, int> b = (x, y) => x + y;
Action c = () => Console.WriteLine("hi");
Func<int, int> d = x => { var y = x * 2; return y; };
Func<int, Task<int>> e = async x => await Task.FromResult(x);
Func<int, int> f = static x => x + 1; // cannot capture, C# 9
Func<int, int, int> g = (_, _) => 0; // discard parameters, C# 9
| Feature | Form | Since |
|---|---|---|
| Static lambdas | static x => x | 9 |
| Discard parameters | (_, _) => 0 | 9 |
| Natural type | var f = (int x) => x; | 10 |
| Explicit return type | int (string s) => 0 | 10 |
| Attributes | [A] (x) => x | 10 |
| Default parameter values | (int x = 1) => x | 12 |
| Modifiers on simple parameters | (text, out result) => … | 14 |
- A lambda can capture local variables and parameters of the enclosing method (clause
12.22.6). The capture is of the variable, not its value. Since C# 5 the
variable of a
foreachis a new one in each iteration. - A lambda has a natural type when its parameters and return
type can be determined. It then becomes a
FuncorActiontype, or a compiler-synthesised delegate. - A
paramsmodifier still needs an explicitly typed parameter list. - An expression tree (
Expression<Func<int, int>>, clause 8.6) represents the lambda as data, so a library such as a database provider can translate it.
Query expressions
A query expression (C# 3, clause 12.23) is a syntax for filtering, ordering and
transforming data. The compiler translates it into method calls such as
Where and Select, so it works on any type with those methods.
The library methods come from System.Linq.
var names =
from p in people
where p.Age >= 18
orderby p.Name descending
select p.Name;
// the compiler translates the query above into:
var names2 = people.Where(p => p.Age >= 18)
.OrderByDescending(p => p.Name)
.Select(p => p.Name);
| Clause | Purpose |
|---|---|
from x in source | Introduces a range variable; a further from is a nested iteration |
where condition | Filters the sequence |
let y = expr | Introduces a new variable computed from others |
orderby key [ascending|descending] | Sorts; commas give more keys |
join y in s on k1 equals k2 [into g] | Joins two sequences; into makes a group join |
select expr | Projects the result; must end a query, unless group does |
group x by key | Groups into IGrouping<K, T> |
into name | Continues the query from the result of select or group |
Queries are usually lazy: they run when the result is enumerated, and they run again
each time it is. Calling ToList() or ToArray() runs a query
once.
Constant expressions
A constant expression is fully evaluated at compile time (clause 12.26). It may use
literals, const names, enum members, default,
nameof, sizeof of built-in types, and the operators on those.
String constants may be joined, and an interpolated string of constant strings is
also constant (C# 10). Overflow in a constant expression is a compile-time error, unless
it is written inside unchecked.