Edit

Share via


12 Expressions

12.1 General

An expression is a sequence of operators and operands. This clause defines the syntax, order of evaluation of operands and operators, and meaning of expressions.

12.2 Expression classifications

12.2.1 General

The result of an expression is classified as one of the following:

  • A value. Every value has an associated type.
  • A variable. Unless otherwise specified, a variable is explicitly typed and has an associated type, namely the declared type of the variable. An implicitly typed variable has no associated type.
  • A null literal. An expression with this classification can be implicitly converted to a reference type or nullable value type.
  • An anonymous function. An expression with this classification can be implicitly converted to a compatible delegate type or expression tree type.
  • A tuple. Every tuple has a fixed number of elements, each with an expression and an optional tuple element name.
  • A property access. Every property access has an associated type, namely the type of the property. Furthermore, a property access may have an associated instance expression. When an accessor of an instance property access is invoked, the result of evaluating the instance expression becomes the instance represented by this (§12.8.14).
  • An indexer access. Every indexer access has an associated type, namely the element type of the indexer. Furthermore, an indexer access has an associated instance expression and an associated argument list. When an accessor of an indexer access is invoked, the result of evaluating the instance expression becomes the instance represented by this (§12.8.14), and the result of evaluating the argument list becomes the parameter list of the invocation.
  • Nothing. This occurs when the expression is an invocation of a method with a return type of void. An expression classified as nothing is only valid in the context of a statement_expression (§13.7) or as the body of a lambda_expression (§12.19).

For expressions which occur as subexpressions of larger expressions, with the noted restrictions, the result can also be classified as one of the following:

  • A namespace. An expression with this classification can only appear as the left-hand side of a member_access (§12.8.7). In any other context, an expression classified as a namespace causes a compile-time error.
  • A type. An expression with this classification can only appear as the left-hand side of a member_access (§12.8.7). In any other context, an expression classified as a type causes a compile-time error.
  • A method group, which is a set of overloaded methods resulting from a member lookup (§12.5). A method group may have an associated instance expression and an associated type argument list. When an instance method is invoked, the result of evaluating the instance expression becomes the instance represented by this (§12.8.14). A method group is permitted in an invocation_expression (§12.8.10) or a delegate_creation_expression (§12.8.17.5), and can be implicitly converted to a compatible delegate type (§10.8). In any other context, an expression classified as a method group causes a compile-time error.
  • An event access. Every event access has an associated type, namely the type of the event. Furthermore, an event access may have an associated instance expression. An event access may appear as the left operand of the += and -= operators (§12.21.5). In any other context, an expression classified as an event access causes a compile-time error. When an accessor of an instance event access is invoked, the result of evaluating the instance expression becomes the instance represented by this (§12.8.14).
  • A throw expression, which may be used in several contexts to throw an exception in an expression. A throw expression may be converted by an implicit conversion to any type.

A property access or indexer access is always reclassified as a value by performing an invocation of the get accessor or the set accessor. The particular accessor is determined by the context of the property or indexer access: If the access is the target of an assignment, the set accessor is invoked to assign a new value (§12.21.2). Otherwise, the get accessor is invoked to obtain the current value (§12.2.2).

An instance accessor is a property access on an instance, an event access on an instance, or an indexer access.

12.2.2 Values of expressions

Most of the constructs that involve an expression ultimately require the expression to denote a value. In such cases, if the actual expression denotes a namespace, a type, a method group, or nothing, a compile-time error occurs. However, if the expression denotes a property access, an indexer access, or a variable, the value of the property, indexer, or variable is implicitly substituted:

  • The value of a variable is simply the value currently stored in the storage location identified by the variable. A variable shall be considered definitely assigned (§9.4) before its value can be obtained, or otherwise a compile-time error occurs.
  • The value of a property access expression is obtained by invoking the get accessor of the property. If the property has no get accessor, a compile-time error occurs. Otherwise, a function member invocation (§12.6.6) is performed, and the result of the invocation becomes the value of the property access expression.
  • The value of an indexer access expression is obtained by invoking the get accessor of the indexer. If the indexer has no get accessor, a compile-time error occurs. Otherwise, a function member invocation (§12.6.6) is performed with the argument list associated with the indexer access expression, and the result of the invocation becomes the value of the indexer access expression.
  • The value of a tuple expression is obtained by applying an implicit tuple conversion (§10.2.13) to the type of the tuple expression. It is an error to obtain the value of a tuple expression that does not have a type.

12.3 Static and Dynamic Binding

12.3.1 General

Binding is the process of determining what an operation refers to, based on the type or value of expressions (arguments, operands, receivers). For instance, the binding of a method call is determined based on the type of the receiver and arguments. The binding of an operator is determined based on the type of its operands.

In C# the binding of an operation is usually determined at compile-time, based on the compile-time type of its subexpressions. Likewise, if an expression contains an error, the error is detected and reported at compile time. This approach is known as static binding.

However, if an expression is a dynamic expression (i.e., has the type dynamic) this indicates that any binding that it participates in should be based on its run-time type rather than the type it has at compile-time. The binding of such an operation is therefore deferred until the time where the operation is to be executed during the running of the program. This is referred to as dynamic binding.

When an operation is dynamically bound, little or no checking is performed by at compile time. Instead if the run-time binding fails, errors are reported as exceptions at run-time.

The following operations in C# are subject to binding:

  • Member access: e.M
  • Method invocation: e.M(e₁,...,eᵥ)
  • Delegate invocation: e(e₁,...,eᵥ)
  • Element access: e[e₁,...,eᵥ]
  • Object creation: new C(e₁,...,eᵥ)
  • Overloaded unary operators: +, -, ! (logical negation only), ~, ++, --, true, false
  • Overloaded binary operators: +, -, *, /, %, &, &&, |, ||, ??, ^, <<, >>, ==, !=, >, <, >=, <=
  • Assignment operators: =, = ref, +=, -=, *=, /=, %=, &=, |=, ^=, <<=, >>=, ??=
  • Implicit and explicit conversions

When no dynamic expressions are involved, C# defaults to static binding, which means that the compile-time types of subexpressions are used in the selection process. However, when one of the subexpressions in the operations listed above is a dynamic expression, the operation is instead dynamically bound.

It is a compile time error if a method invocation is dynamically bound and any of the parameters, including the receiver, are input parameters.

12.3.2 Binding-time

Static binding takes place at compile-time, whereas dynamic binding takes place at run-time. In the following subclauses, the term binding-time refers to either compile-time or run-time, depending on when the binding takes place.

Example: The following illustrates the notions of static and dynamic binding and of binding-time:

object o = 5;
dynamic d = 5;
Console.WriteLine(5); // static binding to Console.WriteLine(int)
Console.WriteLine(o); // static binding to Console.WriteLine(object)
Console.WriteLine(d); // dynamic binding to Console.WriteLine(int)

The first two calls are statically bound: the overload of Console.WriteLine is picked based on the compile-time type of their argument. Thus, the binding-time is compile-time.

The third call is dynamically bound: the overload of Console.WriteLine is picked based on the run-time type of its argument. This happens because the argument is a dynamic expression – its compile-time type is dynamic. Thus, the binding-time for the third call is run-time.

end example

12.3.3 Dynamic binding

This subclause is informative.

Dynamic binding allows C# programs to interact with dynamic objects, i.e., objects that do not follow the normal rules of the C# type system. Dynamic objects may be objects from other programming languages with different types systems, or they may be objects that are programmatically set up to implement their own binding semantics for different operations.

The mechanism by which a dynamic object implements its own semantics is implementation-defined. A given interface – again implementation-defined – is implemented by dynamic objects to signal to the C# run-time that they have special semantics. Thus, whenever operations on a dynamic object are dynamically bound, their own binding semantics, rather than those of C# as specified in this specification, take over.

While the purpose of dynamic binding is to allow interoperation with dynamic objects, C# allows dynamic binding on all objects, whether they are dynamic or not. This allows for a smoother integration of dynamic objects, as the results of operations on them are not always dynamic objects, but are still of a type unknown to the programmer at compile-time. Also, dynamic binding can help eliminate error-prone reflection-based code even when no objects involved are dynamic objects.

12.3.4 Types of subexpressions

When an operation is statically bound, the type of a subexpression (e.g., a receiver, and argument, an index or an operand) is always considered to be the compile-time type of that expression.

When an operation is dynamically bound, the type of a subexpression is determined in different ways depending on the compile-time type of the subexpression:

  • A subexpression of compile-time type dynamic is considered to have the type of the actual value that the expression evaluates to at run-time
  • A subexpression whose compile-time type is a type parameter is considered to have the type which the type parameter is bound to at run-time
  • Otherwise, the subexpression is considered to have its compile-time type.

12.4 Operators

12.4.1 General

Expressions are constructed from operands and operators. The operators of an expression indicate which operations to apply to the operands.

Example: Examples of operators include +, -, *, /, and new. Examples of operands include literals, fields, local variables, and expressions. end example

There are three kinds of operators:

  • Unary operators. The unary operators take one operand and use either prefix notation (such as –x) or postfix notation (such as x++).
  • Binary operators. The binary operators take two operands and all use infix notation (such as x + y).
  • Ternary operator. Only one ternary operator, ?:, exists; it takes three operands and uses infix notation (c ? x : y).

The order of evaluation of operators in an expression is determined by the precedence and associativity of the operators (§12.4.2).

Operands in an expression are evaluated from left to right.

Example: In F(i) + G(i++) * H(i), method F is called using the old value of i, then method G is called with the old value of i, and, finally, method H is called with the new value of i. This is separate from and unrelated to operator precedence. end example

Certain operators can be overloaded. Operator overloading (§12.4.3) permits user-defined operator implementations to be specified for operations where one or both of the operands are of a user-defined class or struct type.

12.4.2 Operator precedence and associativity

When an expression contains multiple operators, the precedence of the operators controls the order in which the individual operators are evaluated.

Note: For example, the expression x + y * z is evaluated as x + (y * z) because the * operator has higher precedence than the binary + operator. end note

The precedence of an operator is established by the definition of its associated grammar production.

Note: For example, an additive_expression consists of a sequence of multiplicative_expressions separated by + or - operators, thus giving the + and - operators lower precedence than the *, /, and % operators. end note

Note: The following table summarizes all operators in order of precedence from highest to lowest:

Subclause Category Operators
§12.8 Primary x.y x?.y f(x) a[x] a?[x] x++ x-- x! new typeof default checked unchecked delegate stackalloc
§12.9 Unary + - !x ~ ++x --x (T)x await x
§12.10 Multiplicative * / %
§12.10 Additive + -
§12.11 Shift << >>
§12.12 Relational and type-testing < > <= >= is as
§12.12 Equality == !=
§12.13 Logical AND &
§12.13 Logical XOR ^
§12.13 Logical OR \|
§12.14 Conditional AND &&
§12.14 Conditional OR \|\|
§12.15 and §12.16 Null coalescing and throw expression ?? throw x
§12.18 Conditional ?:
§12.21 and §12.19 Assignment and lambda expression = = ref *= /= %= += -= <<= >>= &= ^= \|= => ??=

end note

When an operand occurs between two operators with the same precedence, the associativity of the operators controls the order in which the operations are performed:

  • Except for the assignment operators and the null coalescing operator, all binary operators are left-associative, meaning that operations are performed from left to right.

    Example: x + y + z is evaluated as (x + y) + z. end example

  • The assignment operators, the null coalescing operator and the conditional operator (?:) are right-associative, meaning that operations are performed from right to left.

    Example: x = y = z is evaluated as x = (y = z). end example

Precedence and associativity can be controlled using parentheses.

Example: x + y * z first multiplies y by z and then adds the result to x, but (x + y) * z first adds x and y and then multiplies the result by z. end example

12.4.3 Operator overloading

All unary and binary operators have predefined implementations. In addition, user-defined implementations can be introduced by including operator declarations (§15.10) in classes and structs. User-defined operator implementations always take precedence over predefined operator implementations: Only when no applicable user-defined operator implementations exist will the predefined operator implementations be considered, as described in §12.4.4 and §12.4.5.

The overloadable unary operators are:

+ - ! (logical negation only) ~ ++ -- true false

Note: Although true and false are not used explicitly in expressions (and therefore are not included in the precedence table in §12.4.2), they are considered operators because they are invoked in several expression contexts: Boolean expressions (§12.24) and expressions involving the conditional (§12.18) and conditional logical operators (§12.14). end note

Note: The null-forgiving operator (postfix !, §12.8.9) is not an overloadable operator. end note

The overloadable binary operators are:

+  -  *  /  %  &  |  ^  <<  >>  ==  !=  >  <  <=  >=

Only the operators listed above can be overloaded. In particular, it is not possible to overload member access, method invocation, or the =, &&, ||, ??, ?:, =>, checked, unchecked, new, typeof, default, as, and is operators.

When a binary operator is overloaded, the corresponding compound assignment operator, if any, is also implicitly overloaded.

Example: An overload of operator * is also an overload of operator *=. This is described further in §12.21. end example

The assignment operator itself (=) cannot be overloaded. An assignment always performs a simple store of a value into a variable (§12.21.2).

Cast operations, such as (T)x, are overloaded by providing user-defined conversions (§10.5).

Note: User-defined conversions do not affect the behavior of the is or as operators. end note

Element access, such as a[x], is not considered an overloadable operator. Instead, user-defined indexing is supported through indexers (§15.9).

In expressions, operators are referenced using operator notation, and in declarations, operators are referenced using functional notation. The following table shows the relationship between operator and functional notations for unary and binary operators. In the first entry, «op» denotes any overloadable unary prefix operator. In the second entry, «op» denotes the unary postfix ++ and -- operators. In the third entry, «op» denotes any overloadable binary operator.

Note: For an example of overloading the ++ and -- operators see §15.10.2. end note

Operator notation Functional notation
«op» x operator «op»(x)
x «op» operator «op»(x)
x «op» y operator «op»(x, y)

User-defined operator declarations always require at least one of the parameters to be of the class or struct type that contains the operator declaration.

Note: Thus, it is not possible for a user-defined operator to have the same signature as a predefined operator. end note

User-defined operator declarations cannot modify the syntax, precedence, or associativity of an operator.

Example: The / operator is always a binary operator, always has the precedence level specified in §12.4.2, and is always left-associative. end example

Note: While it is possible for a user-defined operator to perform any computation it pleases, implementations that produce results other than those that are intuitively expected are strongly discouraged. For example, an implementation of operator == should compare the two operands for equality and return an appropriate bool result. end note

The descriptions of individual operators in §12.9 through §12.21 specify the predefined implementations of the operators and any additional rules that apply to each operator. The descriptions make use of the terms unary operator overload resolution, binary operator overload resolution, numeric promotion, and lifted operator definitions of which are found in the following subclauses.

12.4.4 Unary operator overload resolution

An operation of the form «op» x or x «op», where «op» is an overloadable unary operator, and x is an expression of type X, is processed as follows:

  • The set of candidate user-defined operators provided by X for the operation operator «op»(x) is determined using the rules of §12.4.6.
  • If the set of candidate user-defined operators is not empty, then this becomes the set of candidate operators for the operation. Otherwise, the predefined binary operator «op» implementations, including their lifted forms, become the set of candidate operators for the operation. The predefined implementations of a given operator are specified in the description of the operator. The predefined operators provided by an enum or delegate type are only included in this set when the binding-time type—or the underlying type if it is a nullable type—of either operand is the enum or delegate type.
  • The overload resolution rules of §12.6.4 are applied to the set of candidate operators to select the best operator with respect to the argument list (x), and this operator becomes the result of the overload resolution process. If overload resolution fails to select a single best operator, a binding-time error occurs.

12.4.5 Binary operator overload resolution

An operation of the form x «op» y, where «op» is an overloadable binary operator, x is an expression of type X, and y is an expression of type Y, is processed as follows:

  • The set of candidate user-defined operators provided by X and Y for the operation operator «op»(x, y) is determined. The set consists of the union of the candidate operators provided by X and the candidate operators provided by Y, each determined using the rules of §12.4.6. For the combined set, candidates are merged as follows:
    • If X and Y are identity convertible, or if X and Y are derived from a common base type, then shared candidate operators only occur in the combined set once.
    • If there is an identity conversion between X and Y, an operator «op»Y provided by Y has the same return type as an «op»X provided by X and the operand types of «op»Y have an identity conversion to the corresponding operand types of «op»X then only «op»X occurs in the set.
  • If the set of candidate user-defined operators is not empty, then this becomes the set of candidate operators for the operation. Otherwise, the predefined binary operator «op» implementations, including their lifted forms, become the set of candidate operators for the operation. The predefined implementations of a given operator are specified in the description of the operator. For predefined enum and delegate operators, the only operators considered are those provided by an enum or delegate type that is the binding-time type of one of the operands.
  • The overload resolution rules of §12.6.4 are applied to the set of candidate operators to select the best operator with respect to the argument list (x, y), and this operator becomes the result of the overload resolution process. If overload resolution fails to select a single best operator, a binding-time error occurs.

12.4.6 Candidate user-defined operators

Given a type T and an operation operator «op»(A), where «op» is an overloadable operator and A is an argument list, the set of candidate user-defined operators provided by T for operator «op»(A) is determined as follows:

  • Determine the type T₀. If T is a nullable value type, T₀ is its underlying type; otherwise, T₀ is equal to T.
  • For all operator «op» declarations in T₀ and all lifted forms of such operators, if at least one operator is applicable (§12.6.4.2) with respect to the argument list A, then the set of candidate operators consists of all such applicable operators in T₀.
  • Otherwise, if T₀ is object, the set of candidate operators is empty.
  • Otherwise, the set of candidate operators provided by T₀ is the set of candidate operators provided by the direct base class of T₀, or the effective base class of T₀ if T₀ is a type parameter.

12.4.7 Numeric promotions

12.4.7.1 General

This subclause is informative.

§12.4.7 and its subclauses are a summary of the combined effect of:

  • the rules for implicit numeric conversions (§10.2.3);
  • the rules for better conversion (§12.6.4.7); and
  • the available arithmetic (§12.10), relational (§12.12), and integral logical (§12.13.2) operators.

Numeric promotion consists of automatically performing certain implicit conversions of the operands of the predefined unary and binary numeric operators. Numeric promotion is not a distinct mechanism, but rather an effect of applying overload resolution to the predefined operators. Numeric promotion specifically does not affect evaluation of user-defined operators, although user-defined operators can be implemented to exhibit similar effects.

As an example of numeric promotion, consider the predefined implementations of the binary * operator:

int operator *(int x, int y);
uint operator *(uint x, uint y);
long operator *(long x, long y);
ulong operator *(ulong x, ulong y);
float operator *(float x, float y);
double operator *(double x, double y);
decimal operator *(decimal x, decimal y);

When overload resolution rules (§12.6.4) are applied to this set of operators, the effect is to select the first of the operators for which implicit conversions exist from the operand types.

Example: For the operation b * s, where b is a byte and s is a short, overload resolution selects operator *(int, int) as the best operator. Thus, the effect is that b and s are converted to int, and the type of the result is int. Likewise, for the operation i * d, where i is an int and d is a double, overload resolution selects operator *(double, double) as the best operator. end example

End of informative text.

12.4.7.2 Unary numeric promotions

This subclause is informative.

Unary numeric promotion occurs for the operands of the predefined +, -, and ~ unary operators. Unary numeric promotion simply consists of converting operands of type sbyte, byte, short, ushort, or char to type int. Additionally, for the unary – operator, unary numeric promotion converts operands of type uint to type long.

End of informative text.

12.4.7.3 Binary numeric promotions

This subclause is informative.

Binary numeric promotion occurs for the operands of the predefined +, -, *, /, %, &, |, ^, ==, !=, >, <, >=, and <= binary operators. Binary numeric promotion implicitly converts both operands to a common type which, in case of the non-relational operators, also becomes the result type of the operation. Binary numeric promotion consists of applying the following rules, in the order they appear here:

  • If either operand is of type decimal, the other operand is converted to type decimal, or a binding-time error occurs if the other operand is of type float or double.
  • Otherwise, if either operand is of type double, the other operand is converted to type double.
  • Otherwise, if either operand is of type float, the other operand is converted to type float.
  • Otherwise, if either operand is of type ulong, the other operand is converted to type ulong, or a binding-time error occurs if the other operand is of type sbyte, short, int, or long.
  • Otherwise, if either operand is of type long, the other operand is converted to type long.
  • Otherwise, if either operand is of type uint and the other operand is of type sbyte, short, or int, both operands are converted to type long.
  • Otherwise, if either operand is of type uint, the other operand is converted to type uint.
  • Otherwise, both operands are converted to type int.

Note: The first rule disallows any operations that mix the decimal type with the double and float types. The rule follows from the fact that there are no implicit conversions between the decimal type and the double and float types. end note

Note: Also note that it is not possible for an operand to be of type ulong when the other operand is of a signed integral type. The reason is that no integral type exists that can represent the full range of ulong as well as the signed integral types. end note

In both of the above cases, a cast expression can be used to explicitly convert one operand to a type that is compatible with the other operand.

Example: In the following code

decimal AddPercent(decimal x, double percent) =>
    x * (1.0 + percent / 100.0);

a binding-time error occurs because a decimal cannot be multiplied by a double. The error is resolved by explicitly converting the second operand to decimal, as follows:

decimal AddPercent(decimal x, double percent) =>
    x * (decimal)(1.0 + percent / 100.0);

end example

End of informative text.

12.4.8 Lifted operators

Lifted operators permit predefined and user-defined operators that operate on non-nullable value types to also be used with nullable forms of those types. Lifted operators are constructed from predefined and user-defined operators that meet certain requirements, as described in the following:

  • For the unary operators +, ++, -, --, !(logical negation), and ~, a lifted form of an operator exists if the operand and result types are both non-nullable value types. The lifted form is constructed by adding a single ? modifier to the operand and result types. The lifted operator produces a null value if the operand is null. Otherwise, the lifted operator unwraps the operand, applies the underlying operator, and wraps the result.
  • For the binary operators +, -, *, /, %, &, |, ^, <<, and >>, a lifted form of an operator exists if the operand and result types are all non-nullable value types. The lifted form is constructed by adding a single ? modifier to each operand and result type. The lifted operator produces a null value if one or both operands are null (an exception being the & and | operators of the bool? type, as described in §12.13.5). Otherwise, the lifted operator unwraps the operands, applies the underlying operator, and wraps the result.
  • For the equality operators == and !=, a lifted form of an operator exists if the operand types are both non-nullable value types and if the result type is bool. The lifted form is constructed by adding a single ? modifier to each operand type. The lifted operator considers two null values equal, and a null value unequal to any non-null value. If both operands are non-null, the lifted operator unwraps the operands and applies the underlying operator to produce the bool result.
  • For the relational operators <, >, <=, and >=, a lifted form of an operator exists if the operand types are both non-nullable value types and if the result type is bool. The lifted form is constructed by adding a single ? modifier to each operand type. The lifted operator produces the value false if one or both operands are null. Otherwise, the lifted operator unwraps the operands and applies the underlying operator to produce the bool result.

12.5 Member lookup

12.5.1 General

A member lookup is the process whereby the meaning of a name in the context of a type is determined. A member lookup can occur as part of evaluating a simple_name (§12.8.4) or a member_access (§12.8.7) in an expression. If the simple_name or member_access occurs as the primary_expression of an invocation_expression (§12.8.10.2), the member is said to be invoked.

If a member is a method or event, or if it is a constant, field or property of either a delegate type (§20) or the type dynamic (§8.2.4), then the member is said to be invocable.

Member lookup considers not only the name of a member but also the number of type parameters the member has and whether the member is accessible. For the purposes of member lookup, generic methods and nested generic types have the number of type parameters indicated in their respective declarations and all other members have zero type parameters.

A member lookup of a name N with K type arguments in a type T is processed as follows:

  • First, a set of accessible members named N is determined:
    • If T is a type parameter, then the set is the union of the sets of accessible members named N in each of the types specified as a primary constraint or secondary constraint (§15.2.5) for T, along with the set of accessible members named N in object.
    • Otherwise, the set consists of all accessible (§7.5) members named N in T, including inherited members and the accessible members named N in object. If T is a constructed type, the set of members is obtained by substituting type arguments as described in §15.3.3. Members that include an override modifier are excluded from the set.
  • Next, if K is zero, all nested types whose declarations include type parameters are removed. If K is not zero, all members with a different number of type parameters are removed. When K is zero, methods having type parameters are not removed, since the type inference process (§12.6.3) might be able to infer the type arguments.
  • Next, if the member is invoked, all non-invocable members are removed from the set.
  • Next, members that are hidden by other members are removed from the set. For every member S.M in the set, where S is the type in which the member M is declared, the following rules are applied:
    • If M is a constant, field, property, event, or enumeration member, then all members declared in a base type of S are removed from the set.
    • If M is a type declaration, then all non-types declared in a base type of S are removed from the set, and all type declarations with the same number of type parameters as M declared in a base type of S are removed from the set.
    • If M is a method, then all non-method members declared in a base type of S are removed from the set.
  • Next, interface members that are hidden by class members are removed from the set. This step only has an effect if T is a type parameter and T has both an effective base class other than object and a non-empty effective interface set (§15.2.5). For every member S.M in the set, where S is the type in which the member M is declared, the following rules are applied if S is a class declaration other than object:
    • If M is a constant, field, property, event, enumeration member, or type declaration, then all members declared in an interface declaration are removed from the set.
    • If M is a method, then all non-method members declared in an interface declaration are removed from the set, and all methods with the same signature as M declared in an interface declaration are removed from the set.
  • Finally, having removed hidden members, the result of the lookup is determined:
    • If the set consists of a single member that is not a method, then this member is the result of the lookup.
    • Otherwise, if the set contains only methods, then this group of methods is the result of the lookup.
    • Otherwise, the lookup is ambiguous, and a binding-time error occurs.

For member lookups in types other than type parameters and interfaces, and member lookups in interfaces that are strictly single-inheritance (each interface in the inheritance chain has exactly zero or one direct base interface), the effect of the lookup rules is simply that derived members hide base members with the same name or signature. Such single-inheritance lookups are never ambiguous. The ambiguities that can possibly arise from member lookups in multiple-inheritance interfaces are described in §18.4.6.

Note: This phase only accounts for one kind of ambiguity. If the member lookup results in a method group, further uses of method group may fail due to ambiguity, for example as described in §12.6.4.1 and §12.6.6.2. end note

12.5.2 Base types

For purposes of member lookup, a type T is considered to have the following base types:

  • If T is object or dynamic, then T has no base type.
  • If T is an enum_type, the base types of T are the class types System.Enum, System.ValueType, and object.
  • If T is a struct_type, the base types of T are the class types System.ValueType and object.

    Note: A nullable_value_type is a struct_type (§8.3.1). end note

  • If T is a class_type, the base types of T are the base classes of T, including the class type object.
  • If T is an interface_type, the base types of T are the base interfaces of T and the class type object.
  • If T is an array_type, the base types of T are the class types System.Array and object.
  • If T is a delegate_type, the base types of T are the class types System.Delegate and object.

12.6 Function members

12.6.1 General

Function members are members that contain executable statements. Function members are always members of types and cannot be members of namespaces. C# defines the following categories of function members:

  • Methods
  • Properties
  • Events
  • Indexers
  • User-defined operators
  • Instance constructors
  • Static constructors
  • Finalizers

Except for finalizers and static constructors (which cannot be invoked explicitly), the statements contained in function members are executed through function member invocations. The actual syntax for writing a function member invocation depends on the particular function member category.

The argument list (§12.6.2) of a function member invocation provides actual values or variable references for the parameters of the function member.

Invocations of generic methods may employ type inference to determine the set of type arguments to pass to the method. This process is described in §12.6.3.

Invocations of methods, indexers, operators, and instance constructors employ overload resolution to determine which of a candidate set of function members to invoke. This process is described in §12.6.4.

Once a particular function member has been identified at binding-time, possibly through overload resolution, the actual run-time process of invoking the function member is described in §12.6.6.

Note: The following table summarizes the processing that takes place in constructs involving the six categories of function members that can be explicitly invoked. In the table, e, x, y, and value indicate expressions classified as variables or values, T indicates an expression classified as a type, F is the simple name of a method, and P is the simple name of a property.

Construct Example Description
Method invocation F(x, y) Overload resolution is applied to select the best method F in the containing class or struct. The method is invoked with the argument list (x, y). If the method is not static, the instance expression is this.
T.F(x, y) Overload resolution is applied to select the best method F in the class or struct T. A binding-time error occurs if the method is not static. The method is invoked with the argument list (x, y).
e.F(x, y) Overload resolution is applied to select the best method F in the class, struct, or interface given by the type of e. A binding-time error occurs if the method is static. The method is invoked with the instance expression e and the argument list (x, y).
Property access P The get accessor of the property P in the containing class or struct is invoked. A compile-time error occurs if P is write-only. If P is not static, the instance expression is this.
P = value The set accessor of the property P in the containing class or struct is invoked with the argument list (value). A compile-time error occurs if P is read-only. If P is not static, the instance expression is this.
T.P The get accessor of the property P in the class or struct T is invoked. A compile-time error occurs if P is not static or if P is write-only.
T.P = value The set accessor of the property P in the class or struct T is invoked with the argument list (value). A compile-time error occurs if P is not static or if P is read-only.
e.P The get accessor of the property P in the class, struct, or interface given by the type of E is invoked with the instance expression e. A binding-time error occurs if P is static or if P is write-only.
e.P = value The set accessor of the property P in the class, struct, or interface given by the type of E is invoked with the instance expression e and the argument list (value). A binding-time error occurs if P is static or if P is read-only.
Event access E += value The add accessor of the event E in the containing class or struct is invoked. If E is not static, the instance expression is this.
E -= value The remove accessor of the event E in the containing class or struct is invoked. If E is not static, the instance expression is this.
T.E += value The add accessor of the event E in the class or struct T is invoked. A binding-time error occurs if E is not static.
T.E -= value The remove accessor of the event E in the class or struct T is invoked. A binding-time error occurs if E is not static.
e.E += value The add accessor of the event E in the class, struct, or interface given by the type of E is invoked with the instance expression e. A binding-time error occurs if E is static.
e.E -= value The remove accessor of the event E in the class, struct, or interface given by the type of E is invoked with the instance expression e. A binding-time error occurs if E is static.
Indexer access e[x, y] Overload resolution is applied to select the best indexer in the class, struct, or interface given by the type of e. The get accessor of the indexer is invoked with the instance expression e and the argument list (x, y). A binding-time error occurs if the indexer is write-only.
e[x, y] = value Overload resolution is applied to select the best indexer in the class, struct, or interface given by the type of e. The set accessor of the indexer is invoked with the instance expression e and the argument list (x, y, value). A binding-time error occurs if the indexer is read-only.
Operator invocation -x Overload resolution is applied to select the best unary operator in the class or struct given by the type of x. The selected operator is invoked with the argument list (x).
x + y Overload resolution is applied to select the best binary operator in the classes or structs given by the types of x and y. The selected operator is invoked with the argument list (x, y).
Instance constructor invocation new T(x, y) Overload resolution is applied to select the best instance constructor in the class or struct T. The instance constructor is invoked with the argument list (x, y).

end note

12.6.2 Argument lists

12.6.2.1 General

Every function member and delegate invocation includes an argument list, which provides actual values or variable references for the parameters of the function member. The syntax for specifying the argument list of a function member invocation depends on the function member category:

  • For instance constructors, methods, indexers and delegates, the arguments are specified as an argument_list, as described below. For indexers, when invoking the set accessor, the argument list additionally includes the expression specified as the right operand of the assignment operator.

    Note: This additional argument is not used for overload resolution, just during invocation of the set accessor. end note

  • For properties, the argument list is empty when invoking the get accessor, and consists of the expression specified as the right operand of the assignment operator when invoking the set accessor.
  • For events, the argument list consists of the expression specified as the right operand of the += or -= operator.
  • For user-defined operators, the argument list consists of the single operand of the unary operator or the two operands of the binary operator.

The arguments of properties (§15.7) and events (§15.8) are always passed as value parameters (§15.6.2.2). The arguments of user-defined operators (§15.10) are always passed as value parameters (§15.6.2.2) or input parameters (§9.2.8). The arguments of indexers (§15.9) are always passed as value parameters (§15.6.2.2), input parameters (§9.2.8), or parameter arrays (§15.6.2.4). Output and reference parameters are not supported for these categories of function members.

The arguments of an instance constructor, method, indexer, or delegate invocation are specified as an argument_list:

argument_list
    : argument (',' argument)*
    ;

argument
    : argument_name? argument_value
    ;

argument_name
    : identifier ':'
    ;

argument_value
    : expression
    | 'in' variable_reference
    | 'ref' variable_reference
    | 'out' variable_reference
    ;

An argument_list consists of one or more arguments, separated by commas. Each argument consists of an optional argument_name followed by an argument_value. An argument with an argument_name is referred to as a named argument, whereas an argument without an argument_name is a positional argument.

The argument_value can take one of the following forms:

  • An expression, indicating that the argument is passed as a value parameter or is transformed into an input parameter and then passed as that, as determined by (§12.6.4.2 and described in §12.6.2.3.
  • The keyword in followed by a variable_reference (§9.5), indicating that the argument is passed as an input parameter (§15.6.2.3.2). A variable shall be definitely assigned (§9.4) before it can be passed as an input parameter.
  • The keyword ref followed by a variable_reference (§9.5), indicating that the argument is passed as a reference parameter (§15.6.2.3.3). A variable shall be definitely assigned (§9.4) before it can be passed as a reference parameter.
  • The keyword out followed by a variable_reference (§9.5), indicating that the argument is passed as an output parameter (§15.6.2.3.4). A variable is considered definitely assigned (§9.4) following a function member invocation in which the variable is passed as an output parameter.

The form determines the parameter-passing mode of the argument: value, input, reference, or output, respectively. However, as mentioned above, an argument with value passing mode, might be transformed into one with input passing mode.

Passing a volatile field (§15.5.4) as an input, output, or reference parameter causes a warning, since the field cannot be treated as volatile by the invoked method.

12.6.2.2 Corresponding parameters

For each argument in an argument list there has to be a corresponding parameter in the function member or delegate being invoked.

The parameter list used in the following is determined as follows:

  • For virtual methods and indexers defined in classes, the parameter list is picked from the first declaration or override of the function member found when starting with the static type of the receiver, and searching through its base classes.
  • For partial methods, the parameter list of the defining partial method declaration is used.
  • For all other function members and delegates there is only a single parameter list, which is the one used.

The position of an argument or parameter is defined as the number of arguments or parameters preceding it in the argument list or parameter list.

The corresponding parameters for function member arguments are established as follows:

  • Arguments in the argument_list of instance constructors, methods, indexers and delegates:
    • A positional argument where a parameter occurs at the same position in the parameter list corresponds to that parameter, unless the parameter is a parameter array and the function member is invoked in its expanded form.
    • A positional argument of a function member with a parameter array invoked in its expanded form, which occurs at or after the position of the parameter array in the parameter list, corresponds to an element in the parameter array.
    • A named argument corresponds to the parameter of the same name in the parameter list.
    • For indexers, when invoking the set accessor, the expression specified as the right operand of the assignment operator corresponds to the implicit value parameter of the set accessor declaration.
  • For properties, when invoking the get accessor there are no arguments. When invoking the set accessor, the expression specified as the right operand of the assignment operator corresponds to the implicit value parameter of the set accessor declaration.
  • For user-defined unary operators (including conversions), the single operand corresponds to the single parameter of the operator declaration.
  • For user-defined binary operators, the left operand corresponds to the first parameter, and the right operand corresponds to the second parameter of the operator declaration.
  • An unnamed argument corresponds to no parameter when it is after an out-of-position named argument or a named argument that corresponds to a parameter array.

    Note: This prevents void M(bool a = true, bool b = true, bool c = true); being invoked by M(c: false, valueB);. The first argument is used out-of-position (the argument is used in first position, but the parameter named c is in third position), so the following arguments should be named. In other words, non-trailing named arguments are only allowed when the name and the position result in finding the same corresponding parameter. end note

12.6.2.3 Run-time evaluation of argument lists

During the run-time processing of a function member invocation (§12.6.6), the expressions or variable references of an argument list are evaluated in order, from left to right, as follows:

  • For a value argument, if the parameter’s passing mode is value

    • the argument expression is evaluated and an implicit conversion (§10.2) to the corresponding parameter type is performed. The resulting value becomes the initial value of the value parameter in the function member invocation.

    • otherwise, the parameter’s passing mode is input. If the argument is a variable reference and there exists an identity conversion (§10.2.2) between the argument’s type and the parameter’s type, the resulting storage location becomes the storage location represented by the parameter in the function member invocation. Otherwise, a storage location is created with the same type as that of the corresponding parameter. The argument expression is evaluated and an implicit conversion (§10.2) to the corresponding parameter type is performed. The resulting value is stored within that storage location. That storage location is represented by the input parameter in the function member invocation.

      Example: Given the following declarations and method calls:

      static void M1(in int p1) { ... }
      int i = 10;
      M1(i);         // i is passed as an input argument
      M1(i + 5);     // transformed to a temporary input argument
      

      In the M1(i) method call, i itself is passed as an input argument, because it is classified as a variable and has the same type int as the input parameter. In the M1(i + 5) method call, an unnamed int variable is created, initialized with the argument’s value, and then passed as an input argument. See §12.6.4.2 and §12.6.4.4.

      end example

  • For an input, output, or reference argument, the variable reference is evaluated and the resulting storage location becomes the storage location represented by the parameter in the function member invocation. For an input or reference argument, the variable shall be definitely assigned at the point of the method call. If the variable reference is given as an output argument, or is an array element of a reference_type, a run-time check is performed to ensure that the element type of the array is identical to the type of the parameter. If this check fails, a System.ArrayTypeMismatchException is thrown.

Note: this run-time check is required due to array covariance (§17.6). end note

Example: In the following code

class Test
{
    static void F(ref object x) {...}

    static void Main()
    {
        object[] a = new object[10];
        object[] b = new string[10];
        F(ref a[0]); // Ok
        F(ref b[1]); // ArrayTypeMismatchException
    }
}

the second invocation of F causes a System.ArrayTypeMismatchException to be thrown because the actual element type of b is string and not object.

end example

Methods, indexers, and instance constructors may declare their right-most parameter to be a parameter array (§15.6.2.4). Such function members are invoked either in their normal form or in their expanded form depending on which is applicable (§12.6.4.2):

  • When a function member with a parameter array is invoked in its normal form, the argument given for the parameter array shall be a single expression that is implicitly convertible (§10.2) to the parameter array type. In this case, the parameter array acts precisely like a value parameter.
  • When a function member with a parameter array is invoked in its expanded form, the invocation shall specify zero or more positional arguments for the parameter array, where each argument is an expression that is implicitly convertible (§10.2) to the element type of the parameter array. In this case, the invocation creates an instance of the parameter array type with a length corresponding to the number of arguments, initializes the elements of the array instance with the given argument values, and uses the newly created array instance as the actual argument.

The expressions of an argument list are always evaluated in textual order.

Example: Thus, the example

class Test
{
    static void F(int x, int y = -1, int z = -2) =>
        Console.WriteLine($"x = {x}, y = {y}, z = {z}");

    static void Main()
    {
        int i = 0;
        F(i++, i++, i++);
        F(z: i++, x: i++);
    }
}

produces the output

x = 0, y = 1, z = 2
x = 4, y = -1, z = 3

end example

When a function member with a parameter array is invoked in its expanded form with at least one expanded argument, the invocation is processed as if an array creation expression with an array initializer (§12.8.17.4) was inserted around the expanded arguments. An empty array is passed when there are no arguments for the parameter array; it is unspecified whether the reference passed is to a newly allocated or existing empty array.

Example: Given the declaration

void F(int x, int y, params object[] args);

the following invocations of the expanded form of the method

F(10, 20, 30, 40);
F(10, 20, 1, "hello", 3.0);

correspond exactly to

F(10, 20, new object[] { 30, 40 });
F(10, 20, new object[] { 1, "hello", 3.0 });

end example

When arguments are omitted from a function member with corresponding optional parameters, the default arguments of the function member declaration are implicitly passed. (This can involve the creation of a storage location, as described above.)

Note: Because these are always constant, their evaluation will not impact the evaluation of the remaining arguments. end note

12.6.3 Type inference

12.6.3.1 General

When a generic method is called without specifying type arguments, a type inference process attempts to infer type arguments for the call. The presence of type inference allows a more convenient syntax to be used for calling a generic method, and allows the programmer to avoid specifying redundant type information.

Example:

class Chooser
{
    static Random rand = new Random();

    public static T Choose<T>(T first, T second) =>
        rand.Next(2) == 0 ? first : second;
}

class A
{
    static void M()
    {
        int i = Chooser.Choose(5, 213); // Calls Choose<int>
        string s = Chooser.Choose("apple", "banana"); // Calls Choose<string>
    }
}

Through type inference, the type arguments int and string are determined from the arguments to the method.

end example

Type inference occurs as part of the binding-time processing of a method invocation (§12.8.10.2) and takes place before the overload resolution step of the invocation. When a particular method group is specified in a method invocation, and no type arguments are specified as part of the method invocation, type inference is applied to each generic method in the method group. If type inference succeeds, then the inferred type arguments are used to determine the types of arguments for subsequent overload resolution. If overload resolution chooses a generic method as the one to invoke, then the inferred type arguments are used as the type arguments for the invocation. If type inference for a particular method fails, that method does not participate in overload resolution. The failure of type inference, in and of itself, does not cause a binding-time error. However, it often leads to a binding-time error when overload resolution then fails to find any applicable methods.

If each supplied argument does not correspond to exactly one parameter in the method (§12.6.2.2), or there is a non-optional parameter with no corresponding argument, then inference immediately fails. Otherwise, assume that the generic method has the following signature:

Tₑ M<X₁...Xᵥ>(T₁ p₁ ... Tₓ pₓ)

With a method call of the form M(E₁ ...Eₓ) the task of type inference is to find unique type arguments S₁...Sᵥ for each of the type parameters X₁...Xᵥ so that the call M<S₁...Sᵥ>(E₁...Eₓ) becomes valid.

The process of type inference is described below as an algorithm. A conformant compiler may be implemented using an alternative approach, provided it reaches the same result in all cases.

During the process of inference each type parameter Xᵢ is either fixed to a particular type Sᵢ or unfixed with an associated set of bounds. Each of the bounds is some type T. Initially each type variable Xᵢ is unfixed with an empty set of bounds.

Type inference takes place in phases. Each phase will try to infer type arguments for more type variables based on the findings of the previous phase. The first phase makes some initial inferences of bounds, whereas the second phase fixes type variables to specific types and infers further bounds. The second phase may have to be repeated a number of times.

Note: Type inference is also used in other contexts including for conversion of method groups (§12.6.3.15) and finding the best common type of a set of expressions (§12.6.3.16). end note

12.6.3.2 The first phase

For each of the method arguments Eᵢ, an input type inference (§12.6.3.7) is made from Eᵢ to the corresponding parameter type Tⱼ.

12.6.3.3 The second phase

The second phase proceeds as follows:

  • All unfixed type variables Xᵢ which do not depend on (§12.6.3.6) any Xₑ are fixed (§12.6.3.13).
  • If no such type variables exist, all unfixed type variables Xᵢ are fixed for which all of the following hold:
    • There is at least one type variable Xₑ that depends on Xᵢ
    • Xᵢ has a non-empty set of bounds
  • If no such type variables exist and there are still unfixed type variables, type inference fails.
  • Otherwise, if no further unfixed type variables exist, type inference succeeds.
  • Otherwise, for all arguments Eᵢ with corresponding parameter type Tⱼ where the output types (§12.6.3.5) contain unfixed type variables Xₑ but the input types (§12.6.3.4) do not, an output type inference (§12.6.3.8) is made from Eᵢ to Tⱼ. Then the second phase is repeated.

12.6.3.4 Input types

If E is a method group or implicitly typed anonymous function and T is a delegate type or expression tree type then all the parameter types of T are input types of E with type T.

12.6.3.5 Output types

If E is a method group or an anonymous function and T is a delegate type or expression tree type then the return type of T is an output type of E with type T.

12.6.3.6 Dependence

An unfixed type variable Xᵢ depends directly on an unfixed type variable Xₑ if for some argument Eᵥ with type Tᵥ Xₑ occurs in an input type of Eᵥ with type Tᵥ and Xᵢ occurs in an output type of Eᵥ with type Tᵥ.

Xₑ depends on Xᵢ if Xₑ depends directly on Xᵢ or if Xᵢ depends directly on Xᵥ and Xᵥ depends on Xₑ. Thus “depends on” is the transitive but not reflexive closure of “depends directly on”.

12.6.3.7 Input type inferences

An input type inference is made from an expression E to a type T in the following way:

  • If E is a tuple expression (§12.8.6) with arity N and elements Eᵢ, and T is a tuple type with arity N with corresponding element types Tₑ or T is a nullable value type T0? and T0 is a tuple type with arity N that has a corresponding element type Tₑ, then for each Eᵢ, an input type inference is made from Eᵢ to Tₑ.
  • If E is an anonymous function, an explicit parameter type inference (§12.6.3.9) is made from E to T
  • Otherwise, if E has a type U and the corresponding parameter is a value parameter (§15.6.2.2) then a lower-bound inference (§12.6.3.11) is made from U to T.
  • Otherwise, if E has a type U and the corresponding parameter is a reference parameter (§15.6.2.3.3), or output parameter (§15.6.2.3.4) then an exact inference (§12.6.3.10) is made from U to T.
  • Otherwise, if E has a type U and the corresponding parameter is an input parameter (§15.6.2.3.2) and E is an input argument, then an exact inference (§12.6.3.10) is made from U to T.
  • Otherwise, if E has a type U and the corresponding parameter is an input parameter (§15.6.2.3.2) then a lower bound inference (§12.6.3.11) is made from U to T.
  • Otherwise, no inference is made for this argument.

12.6.3.8 Output type inferences

An output type inference is made from an expression E to a type T in the following way:

  • If E is a tuple expression with arity N and elements Eᵢ, and T is a tuple type with arity N with corresponding element types Tₑ or T is a nullable value type T0? and T0 is a tuple type with arity N that has a corresponding element type Tₑ, then for each Eᵢ an output type inference is made from Eᵢ to Tₑ.
  • If E is an anonymous function with inferred return type U (§12.6.3.14) and T is a delegate type or expression tree type with return type Tₓ, then a lower-bound inference (§12.6.3.11) is made from U to Tₓ.
  • Otherwise, if E is a method group and T is a delegate type or expression tree type with parameter types T₁...Tᵥ and return type Tₓ, and overload resolution of E with the types T₁...Tᵥ yields a single method with return type U, then a lower-bound inference is made from U to Tₓ.
  • Otherwise, if E is an expression with type U, then a lower-bound inference is made from U to T.
  • Otherwise, no inferences are made.

12.6.3.9 Explicit parameter type inferences

An explicit parameter type inference is made from an expression E to a type T in the following way:

  • If E is an explicitly typed anonymous function with parameter types U₁...Uᵥ and T is a delegate type or expression tree type with parameter types V₁...Vᵥ then for each Uᵢ an exact inference (§12.6.3.10) is made from Uᵢ to the corresponding Vᵢ.

12.6.3.10 Exact inferences

An exact inference from a type U to a type V is made as follows:

  • If V is one of the unfixed Xᵢ then U is added to the set of exact bounds for Xᵢ.
  • Otherwise, sets V₁...Vₑ and U₁...Uₑ are determined by checking if any of the following cases apply:
    • V is an array type V₁[...] and U is an array type U₁[...] of the same rank
    • V is the type V₁? and U is the type U₁
    • V is a constructed type C<V₁...Vₑ> and U is a constructed type C<U₁...Uₑ>
      If any of these cases apply then an exact inference is made from each Uᵢ to the corresponding Vᵢ.
  • Otherwise, no inferences are made.

12.6.3.11 Lower-bound inferences

A lower-bound inference from a type U to a type V is made as follows:

  • If V is one of the unfixed Xᵢ then U is added to the set of lower bounds for Xᵢ.
  • Otherwise, if V is the type V₁? and U is the type U₁? then a lower bound inference is made from U₁ to V₁.
  • Otherwise, sets U₁...Uₑ and V₁...Vₑ are determined by checking if any of the following cases apply:
    • V is an array type V₁[...]and U is an array type U₁[...]of the same rank
    • V is one of IEnumerable<V₁>, ICollection<V₁>, IReadOnlyList<V₁>>, IReadOnlyCollection<V₁> or IList<V₁> and U is a single-dimensional array type U₁[]
    • V is a constructed class, struct, interface or delegate type C<V₁...Vₑ> and there is a unique type C<U₁...Uₑ> such that U (or, if U is a type parameter, its effective base class or any member of its effective interface set) is identical to, inherits from (directly or indirectly), or implements (directly or indirectly) C<U₁...Uₑ>.
    • (The “uniqueness” restriction means that in the case interface C<T>{} class U: C<X>, C<Y>{}, then no inference is made when inferring from U to C<T> because U₁ could be X or Y.)
      If any of these cases apply then an inference is made from each Uᵢ to the corresponding Vᵢ as follows:
    • If Uᵢ is not known to be a reference type then an exact inference is made
    • Otherwise, if U is an array type then a lower-bound inference is made
    • Otherwise, if V is C<V₁...Vₑ> then inference depends on the i-th type parameter of C:
      • If it is covariant then a lower-bound inference is made.
      • If it is contravariant then an upper-bound inference is made.
      • If it is invariant then an exact inference is made.
  • Otherwise, no inferences are made.

12.6.3.12 Upper-bound inferences

An upper-bound inference from a type U to a type V is made as follows:

  • If V is one of the unfixed Xᵢ then U is added to the set of upper bounds for Xᵢ.
  • Otherwise, sets V₁...Vₑ and U₁...Uₑ are determined by checking if any of the following cases apply:
    • U is an array type U₁[...]and V is an array type V₁[...]of the same rank
    • U is one of IEnumerable<Uₑ>, ICollection<Uₑ>, IReadOnlyList<Uₑ>, IReadOnlyCollection<Uₑ> or IList<Uₑ> and V is a single-dimensional array type Vₑ[]
    • U is the type U1? and V is the type V1?
    • U is constructed class, struct, interface or delegate type C<U₁...Uₑ> and V is a class, struct, interface or delegate type which is identical to, inherits from (directly or indirectly), or implements (directly or indirectly) a unique type C<V₁...Vₑ>
    • (The “uniqueness” restriction means that given an interface C<T>{} class V<Z>: C<X<Z>>, C<Y<Z>>{}, then no inference is made when inferring from C<U₁> to V<Q>. Inferences are not made from U₁ to either X<Q> or Y<Q>.)
      If any of these cases apply then an inference is made from each Uᵢ to the corresponding Vᵢ as follows:
    • If Uᵢ is not known to be a reference type then an exact inference is made
    • Otherwise, if V is an array type then an upper-bound inference is made
    • Otherwise, if U is C<U₁...Uₑ> then inference depends on the i-th type parameter of C:
      • If it is covariant then an upper-bound inference is made.
      • If it is contravariant then a lower-bound inference is made.
      • If it is invariant then an exact inference is made.
  • Otherwise, no inferences are made.

12.6.3.13 Fixing

An unfixed type variable Xᵢ with a set of bounds is fixed as follows:

  • The set of candidate types Uₑ starts out as the set of all types in the set of bounds for Xᵢ.
  • Each bound for Xᵢ is examined in turn: For each exact bound U of Xᵢ all types Uₑ that are not identical to U are removed from the candidate set. For each lower bound U of Xᵢ all types Uₑ to which there is not an implicit conversion from U are removed from the candidate set. For each upper-bound U of Xᵢ all types Uₑ from which there is not an implicit conversion to U are removed from the candidate set.
  • If among the remaining candidate types Uₑ there is a unique type V to which there is an implicit conversion from all the other candidate types, then Xᵢ is fixed to V.
  • Otherwise, type inference fails.

12.6.3.14 Inferred return type

The inferred return type of an anonymous function F is used during type inference and overload resolution. The inferred return type can only be determined for an anonymous function where all parameter types are known, either because they are explicitly given, provided through an anonymous function conversion or inferred during type inference on an enclosing generic method invocation.

The inferred effective return type is determined as follows:

  • If the body of F is an expression that has a type, then the inferred effective return type of F is the type of that expression.
  • If the body of F is a block and the set of expressions in the block’s return statements has a best common type T (§12.6.3.16), then the inferred effective return type of F is T.
  • Otherwise, an effective return type cannot be inferred for F.

The inferred return type is determined as follows:

  • If F is async and the body of F is either an expression classified as nothing (§12.2), or a block where no return statements have expressions, the inferred return type is «TaskType» (§15.14.1).
  • If F is async and has an inferred effective return type T, the inferred return type is «TaskType»<T>»(§15.14.1).
  • If F is non-async and has an inferred effective return type T, the inferred return type is T.
  • Otherwise, a return type cannot be inferred for F.

Example: As an example of type inference involving anonymous functions, consider the Select extension method declared in the System.Linq.Enumerable class:

namespace System.Linq
{
    public static class Enumerable
    {
        public static IEnumerable<TResult> Select<TSource,TResult>(
            this IEnumerable<TSource> source,
            Func<TSource,TResult> selector)
        {
            foreach (TSource element in source)
            {
                yield return selector(element);
            }
        }
   }
}

Assuming the System.Linq namespace was imported with a using namespace directive, and given a class Customer with a Name property of type string, the Select method can be used to select the names of a list of customers:

List<Customer> customers = GetCustomerList();
IEnumerable<string> names = customers.Select(c => c.Name);

The extension method invocation (§12.8.10.3) of Select is processed by rewriting the invocation to a static method invocation:

IEnumerable<string> names = Enumerable.Select(customers, c => c.Name);

Since type arguments were not explicitly specified, type inference is used to infer the type arguments. First, the customers argument is related to the source parameter, inferring TSource to be Customer. Then, using the anonymous function type inference process described above, c is given type Customer, and the expression c.Name is related to the return type of the selector parameter, inferring TResult to be string. Thus, the invocation is equivalent to

Sequence.Select<Customer,string>(customers, (Customer c) => c.Name)

and the result is of type IEnumerable<string>.

The following example demonstrates how anonymous function type inference allows type information to “flow” between arguments in a generic method invocation. Given the following method and invocation:

class A
{
    static Z F<X,Y,Z>(X value, Func<X,Y> f1, Func<Y,Z> f2)
    {
        return f2(f1(value));
    }

    static void M()
    {
        double hours = F("1:15:30", s => TimeSpan.Parse(s), t => t.TotalHours);
    }
}

type inference for the invocation proceeds as follows: First, the argument “1:15:30” is related to the value parameter, inferring X to be string. Then, the parameter of the first anonymous function, s, is given the inferred type string, and the expression TimeSpan.Parse(s) is related to the return type of f1, inferring Y to be System.TimeSpan. Finally, the parameter of the second anonymous function, t, is given the inferred type System.TimeSpan, and the expression t.TotalHours is related to the return type of f2, inferring Z to be double. Thus, the result of the invocation is of type double.

end example

12.6.3.15 Type inference for conversion of method groups

Similar to calls of generic methods, type inference shall also be applied when a method group M containing a generic method is converted to a given delegate type D (§10.8). Given a method

Tₑ M<X₁...Xᵥ>(T₁ x₁ ... Tₑ xₑ)

and the method group M being assigned to the delegate type D the task of type inference is to find type arguments S₁...Sᵥ so that the expression:

M<S₁...Sᵥ>

becomes compatible (§20.2) with D.

Unlike the type inference algorithm for generic method calls, in this case, there are only argument types, no argument expressions. In particular, there are no anonymous functions and hence no need for multiple phases of inference.

Instead, all Xᵢ are considered unfixed, and a lower-bound inference is made from each argument type Uₑ of D to the corresponding parameter type Tₑ of M. If for any of the Xᵢ no bounds were found, type inference fails. Otherwise, all Xᵢ are fixed to corresponding Sᵢ, which are the result of type inference.

12.6.3.16 Finding the best common type of a set of expressions

In some cases, a common type needs to be inferred for a set of expressions. In particular, the element types of implicitly typed arrays and the return types of anonymous functions with block bodies are found in this way.

The best common type for a set of expressions E₁...Eᵥ is determined as follows:

  • A new unfixed type variable X is introduced.
  • For each expression Ei an output type inference (§12.6.3.8) is performed from it to X.
  • X is fixed (§12.6.3.13), if possible, and the resulting type is the best common type.
  • Otherwise inference fails.

Note: Intuitively this inference is equivalent to calling a method void M<X>(X x₁ ... X xᵥ) with the Eᵢ as arguments and inferring X. end note

12.6.4 Overload resolution

12.6.4.1 General

Overload resolution is a binding-time mechanism for selecting the best function member to invoke given an argument list and a set of candidate function members. Overload resolution selects the function member to invoke in the following distinct contexts within C#:

  • Invocation of a method named in an invocation_expression (§12.8.10).
  • Invocation of an instance constructor named in an object_creation_expression (§12.8.17.2).
  • Invocation of an indexer accessor through an element_access (§12.8.12).
  • Invocation of a predefined or user-defined operator referenced in an expression (§12.4.4 and §12.4.5).

Each of these contexts defines the set of candidate function members and the list of arguments in its own unique way. For instance, the set of candidates for a method invocation does not include methods marked override (§12.5), and methods in a base class are not candidates if any method in a derived class is applicable (§12.8.10.2).

Once the candidate function members and the argument list have been identified, the selection of the best function member is the same in all cases:

  • First, the set of candidate function members is reduced to those function members that are applicable with respect to the given argument list (§12.6.4.2). If this reduced set is empty, a compile-time error occurs.
  • Then, the best function member from the set of applicable candidate function members is located. If the set contains only one function member, then that function member is the best function member. Otherwise, the best function member is the one function member that is better than all other function members with respect to the given argument list, provided that each function member is compared to all other function members using the rules in §12.6.4.3. If there is not exactly one function member that is better than all other function members, then the function member invocation is ambiguous and a binding-time error occurs.

The following subclauses define the exact meanings of the terms applicable function member and better function member.

12.6.4.2 Applicable function member

A function member is said to be an applicable function member with respect to an argument list A when all of the following are true:

  • Each argument in A corresponds to a parameter in the function member declaration as described in §12.6.2.2, at most one argument corresponds to each parameter, and any parameter to which no argument corresponds is an optional parameter.
  • For each argument in A, the parameter-passing mode of the argument is identical to the parameter-passing mode of the corresponding parameter, and
    • for a value parameter or a parameter array, an implicit conversion (§10.2) exists from the argument expression to the type of the corresponding parameter, or
    • for a reference or output parameter, there is an identity conversion between the type of the argument expression (if any) and the type of the corresponding parameter, or
    • for an input parameter when the corresponding argument has the in modifier, there is an identity conversion between the type of the argument expression (if any) and the type of the corresponding parameter, or
    • for an input parameter when the corresponding argument omits the in modifier, an implicit conversion (§10.2) exists from the argument expression to the type of the corresponding parameter.

For a function member that includes a parameter array, if the function member is applicable by the above rules, it is said to be applicable in its normal form. If a function member that includes a parameter array is not applicable in its normal form, the function member might instead be applicable in its expanded form:

  • The expanded form is constructed by replacing the parameter array in the function member declaration with zero or more value parameters of the element type of the parameter array such that the number of arguments in the argument list A matches the total number of parameters. If A has fewer arguments than the number of fixed parameters in the function member declaration, the expanded form of the function member cannot be constructed and is thus not applicable.
  • Otherwise, the expanded form is applicable if for each argument in A, one of the following is true:
    • the parameter-passing mode of the argument is identical to the parameter-passing mode of the corresponding parameter, and:
      • for a fixed value parameter or a value parameter created by the expansion, an implicit conversion (§10.2) exists from the argument expression to the type of the corresponding parameter; or
      • for a by-reference parameter, the type of the argument expression is identical to the type of the corresponding parameter.
    • the parameter-passing mode of the argument is value, and the parameter-passing mode of the corresponding parameter is input, and an implicit conversion (§10.2) exists from the argument expression to the type of the corresponding parameter.

When the implicit conversion from the argument type to the parameter type of an input parameter is a dynamic implicit conversion (§10.2.10), the results are undefined.

Example: Given the following declarations and method calls:

public static void M1(int p1) { ... }
public static void M1(in int p1) { ... }
public static void M2(in int p1) { ... }
public static void Test()
{
    int i = 10; uint ui = 34U;

    M1(in i);   // M1(in int) is applicable
    M1(in ui);  // no exact type match, so M1(in int) is not applicable
    M1(i);      // M1(int) and M1(in int) are applicable
    M1(i + 5);  // M1(int) and M1(in int) are applicable
    M1(100u);   // no implicit conversion exists, so M1(int) is not applicable

    M2(in i);   // M2(in int) is applicable
    M2(i);      // M2(in int) is applicable
    M2(i + 5);  // M2(in int) is applicable
}

end example

  • A static method is only applicable if the method group results from a simple_name or a member_access through a type.
  • An instance method is only applicable if the method group results from a simple_name, a member_access through a variable or value, or a base_access.
    • If the method group results from a simple_name, an instance method is only applicable if this access is permitted §12.8.14.
  • When the method group results from a member_access which could be via either an instance or a type as described in §12.8.7.2, both instance and static methods are applicable.
  • A generic method whose type arguments (explicitly specified or inferred) do not all satisfy their constraints is not applicable.
  • In the context of a method group conversion, there shall exist an identity conversion (§10.2.2) or an implicit reference conversion (§10.2.8) from the method return type to the delegate’s return type. Otherwise, the candidate method is not applicable.

12.6.4.3 Better function member

For the purposes of determining the better function member, a stripped-down argument list A is constructed containing just the argument expressions themselves in the order they appear in the original argument list, and leaving out any out or ref arguments.

Parameter lists for each of the candidate function members are constructed in the following way:

  • The expanded form is used if the function member was applicable only in the expanded form.
  • Optional parameters with no corresponding arguments are removed from the parameter list
  • Reference and output parameters are removed from the parameter list
  • The parameters are reordered so that they occur at the same position as the corresponding argument in the argument list.

Given an argument list A with a set of argument expressions {E₁, E₂, ..., Eᵥ} and two applicable function members Mᵥ and Mₓ with parameter types {P₁, P₂, ..., Pᵥ} and {Q₁, Q₂, ..., Qᵥ}, Mᵥ is defined to be a better function member than Mₓ if

  • for each argument, the implicit conversion from Eᵥ to Qᵥ is not better than the implicit conversion from Eᵥ to Pᵥ, and
  • for at least one argument, the conversion from Eᵥ to Pᵥ is better than the conversion from Eᵥ to Qᵥ.

In case the parameter type sequences {P₁, P₂, ..., Pᵥ} and {Q₁, Q₂, ..., Qᵥ} are equivalent (i.e., each Pᵢ has an identity conversion to the corresponding Qᵢ), the following tie-breaking rules are applied, in order, to determine the better function member.

  • If Mᵢ is a non-generic method and Mₑ is a generic method, then Mᵢ is better than Mₑ.
  • Otherwise, if Mᵢ is applicable in its normal form and Mₑ has a params array and is applicable only in its expanded form, then Mᵢ is better than Mₑ.
  • Otherwise, if both methods have params arrays and are applicable only in their expanded forms, and if the params array of Mᵢ has fewer elements than the params array of Mₑ, then Mᵢ is better than Mₑ.
  • Otherwise, if Mᵥ has more specific parameter types than Mₓ, then Mᵥ is better than Mₓ. Let {R1, R2, ..., Rn} and {S1, S2, ..., Sn} represent the uninstantiated and unexpanded parameter types of Mᵥ and Mₓ. Mᵥ’s parameter types are more specific than Mₓs if, for each parameter, Rx is not less specific than Sx, and, for at least one parameter, Rx is more specific than Sx:
    • A type parameter is less specific than a non-type parameter.
    • Recursively, a constructed type is more specific than another constructed type (with the same number of type arguments) if at least one type argument is more specific and no type argument is less specific than the corresponding type argument in the other.
    • An array type is more specific than another array type (with the same number of dimensions) if the element type of the first is more specific than the element type of the second.
  • Otherwise if one member is a non-lifted operator and the other is a lifted operator, the non-lifted one is better.
  • If neither function member was found to be better, and all parameters of Mᵥ have a corresponding argument whereas default arguments need to be substituted for at least one optional parameter in Mₓ, then Mᵥ is better than Mₓ.
  • If for at least one parameter Mᵥ uses the better parameter-passing choice (§12.6.4.4) than the corresponding parameter in Mₓ and none of the parameters in Mₓ use the better parameter-passing choice than Mᵥ, Mᵥ is better than Mₓ.
  • Otherwise, no function member is better.

12.6.4.4 Better parameter-passing mode

It is permitted to have corresponding parameters in two overloaded methods differ only by parameter-passing mode provided one of the two parameters has value-passing mode, as follows:

public static void M1(int p1) { ... }
public static void M1(in int p1) { ... }

Given int i = 10;, according to §12.6.4.2, the calls M1(i) and M1(i + 5) result in both overloads being applicable. In such cases, the method with the parameter-passing mode of value is the better parameter-passing mode choice.

Note: No such choice need exist for arguments of input, output, or reference passing modes, as those arguments only match the exact same parameter passing modes. end note

12.6.4.5 Better conversion from expression

Given an implicit conversion C₁ that converts from an expression E to a type T₁, and an implicit conversion C₂ that converts from an expression E to a type T₂, C₁ is a better conversion than C₂ if one of the following holds:

  • E exactly matches T₁ and E does not exactly match T₂ (§12.6.4.6)
  • E exactly matches both or neither of T₁ and T₂, and T₁ is a better conversion target than T₂ (§12.6.4.7)
  • E is a method group (§12.2), T₁ is compatible (§20.4) with the single best method from the method group for conversion C₁, and T₂ is not compatible with the single best method from the method group for conversion C₂

12.6.4.6 Exactly matching expression

Given an expression E and a type T, E exactly matches T if one of the following holds:

  • E has a type S, and an identity conversion exists from S to T
  • E is an anonymous function, T is either a delegate type D or an expression tree type Expression<D> and one of the following holds:
    • An inferred return type X exists for E in the context of the parameter list of D (§12.6.3.13), and an identity conversion exists from X to the return type of D
    • E is an async lambda with no return value, and D has a return type which is a non-generic «TaskType»
    • Either E is non-async and D has a return type Y or E is async and D has a return type «TaskType»<Y>(§15.14.1), and one of the following holds:
      • The body of E is an expression that exactly matches Y
      • The body of E is a block where every return statement returns an expression that exactly matches Y

12.6.4.7 Better conversion target

Given two types T₁ and T₂, T₁ is a better conversion target than T₂ if one of the following holds:

  • An implicit conversion from T₁ to T₂ exists and no implicit conversion from T₂ to T₁ exists
  • T₁ is «TaskType»<S₁>(§15.14.1), T₂ is «TaskType»<S₂>, and S₁ is a better conversion target than S₂
  • T₁ is «TaskType»<S₁>(§15.14.1), T₂ is «TaskType»<S₂>, and T₁ is more specialized than T₂
  • T₁ is S₁ or S₁? where S₁ is a signed integral type, and T₂ is S₂ or S₂? where S₂ is an unsigned integral type. Specifically:
    • S₁ is sbyte and S₂ is byte, ushort, uint, or ulong
    • S₁ is short and S₂ is ushort, uint, or ulong
    • S₁ is int and S₂ is uint, or ulong
    • S₁ is long and S₂ is ulong

12.6.4.8 Overloading in generic classes

Note: While signatures as declared shall be unique (§8.6), it is possible that substitution of type arguments results in identical signatures. In such a situation, overload resolution will pick the most specific (§12.6.4.3) of the original signatures (before substitution of type arguments), if it exists, and otherwise report an error. end note

Example: The following examples show overloads that are valid and invalid according to this rule:

public interface I1<T> { ... }
public interface I2<T> { ... }

public abstract class G1<U>
{
    public abstract int F1(U u);           // Overload resolution for G<int>.F1
    public abstract int F1(int i);         // will pick non-generic

    public abstract void F2(I1<U> a);      // Valid overload
    public abstract void F2(I2<U> a);
}

abstract class G2<U,V>
{
    public abstract void F3(U u, V v);     // Valid, but overload resolution for
    public abstract void F3(V v, U u);     // G2<int,int>.F3 will fail

    public abstract void F4(U u, I1<V> v); // Valid, but overload resolution for
    public abstract void F4(I1<V> v, U u); // G2<I1<int>,int>.F4 will fail

    public abstract void F5(U u1, I1<V> v2);   // Valid overload
    public abstract void F5(V v1, U u2);

    public abstract void F6(ref U u);      // Valid overload
    public abstract void F6(out V v);
}

end example

12.6.5 Compile-time checking of dynamic member invocation

Even though overload resolution of a dynamically bound operation takes place at run-time, it is sometimes possible at compile-time to know the list of function members from which an overload will be chosen:

  • For a delegate invocation (§12.8.10.4), the list is a single function member with the same parameter list as the delegate_type of the invocation
  • For a method invocation (§12.8.10.2) on a type, or on a value whose static type is not dynamic, the set of accessible methods in the method group is known at compile-time.
  • For an object creation expression (§12.8.17.2) the set of accessible constructors in the type is known at compile-time.
  • For an indexer access (§12.8.12.3) the set of accessible indexers in the receiver is known at compile-time.

In these cases a limited compile-time check is performed on each member in the known set of function members, to see if it can be known for certain never to be invoked at run-time. For each function member F a modified parameter and argument list are constructed:

  • First, if F is a generic method and type arguments were provided, then those are substituted for the type parameters in the parameter list. However, if type arguments were not provided, no such substitution happens.
  • Then, any parameter whose type is open (i.e., contains a type parameter; see §8.4.3) is elided, along with its corresponding arguments.

For F to pass the check, all of the following shall hold:

  • The modified parameter list for F is applicable to the modified argument list in terms of §12.6.4.2.
  • All constructed types in the modified parameter list satisfy their constraints (§8.4.5).
  • If the type parameters of F were substituted in the step above, their constraints are satisfied.
  • If F is a static method, the method group shall not have resulted from a member_access whose receiver is known at compile-time to be a variable or value.
  • If F is an instance method, the method group shall not have resulted from a member_access whose receiver is known at compile-time to be a type.

If no candidate passes this test, a compile-time error occurs.

12.6.6 Function member invocation

12.6.6.1 General

This subclause describes the process that takes place at run-time to invoke a particular function member. It is assumed that a binding-time process has already determined the particular member to invoke, possibly by applying overload resolution to a set of candidate function members.

For purposes of describing the invocation process, function members are divided into two categories:

  • Static function members. These are static methods, static property accessors, and user-defined operators. Static function members are always non-virtual.
  • Instance function members. These are instance methods, instance constructors, instance property accessors, and indexer accessors. Instance function members are either non-virtual or virtual, and are always invoked on a particular instance. The instance is computed by an instance expression, and it becomes accessible within the function member as this (§12.8.14). For an instance constructor, the instance expression is taken to be the newly allocated object.

The run-time processing of a function member invocation consists of the following steps, where M is the function member and, if M is an instance member, E is the instance expression:

  • If M is a static function member:
    • The argument list is evaluated as described in §12.6.2.
    • M is invoked.
  • Otherwise, if the type of E is a value-type V, and M is declared or overridden in V:
    • E is evaluated. If this evaluation causes an exception, then no further steps are executed. For an instance constructor, this evaluation consists of allocating storage (typically from an execution stack) for the new object. In this case E is classified as a variable.
    • If E is not classified as a variable, or if V is not a readonly struct type (§16.2.2) and M is not a readonly function member (§16.4.12), and E is one of:
      • an input parameter (§15.6.2.3.2), or
      • a readonly field (§15.5.3), or
      • a readonly reference variable or return (§9.7), then a temporary local variable of E’s type is created and the value of E is assigned to that variable. E is then reclassified as a reference to that temporary local variable. The temporary variable is accessible as this within M, but not in any other way. Thus, only when E can be written is it possible for the caller to observe the changes that M makes to this.
    • The argument list is evaluated as described in §12.6.2.
    • M is invoked. The variable referenced by E becomes the variable referenced by this.
  • Otherwise:
    • E is evaluated. If this evaluation causes an exception, then no further steps are executed.
    • The argument list is evaluated as described in §12.6.2.
    • If the type of E is a value_type, a boxing conversion (§10.2.9) is performed to convert E to a class_type, and E is considered to be of that class_type in the following steps. If the value_type is an enum_type, the class_type is System.Enum; otherwise, it is System.ValueType.
    • The value of E is checked to be valid. If the value of E is null, a System.NullReferenceException is thrown and no further steps are executed.
    • The function member implementation to invoke is determined:
      • If the binding-time type of E is an interface, the function member to invoke is the implementation of M provided by the run-time type of the instance referenced by E. This function member is determined by applying the interface mapping rules (§18.6.5) to determine the implementation of M provided by the run-time type of the instance referenced by E.
      • Otherwise, if M is a virtual function member, the function member to invoke is the implementation of M provided by the run-time type of the instance referenced by E. This function member is determined by applying the rules for determining the most derived implementation (§15.6.4) of M with respect to the run-time type of the instance referenced by E.
      • Otherwise, M is a non-virtual function member, and the function member to invoke is M itself.
    • The function member implementation determined in the step above is invoked. The object referenced by E becomes the object referenced by this.

Note: §12.2 classifies property access as invoking the corresponding function member, either the get accessor or the set accessor. The process above is followed for invoking that accessor. end note

The result of the invocation of an instance constructor (§12.8.17.2) is the value created. The result of the invocation of any other function member is the value, if any, returned (§13.10.5) from its body.

12.6.6.2 Invocations on boxed instances

A function member implemented in a value_type can be invoked through a boxed instance of that value_type in the following situations:

  • When the function member is an override of a method inherited from type class_type and is invoked through an instance expression of that class_type.

    Note: The class_type will always be one of System.ObjectSystem.ValueType or System.Enumend note

  • When the function member is an implementation of an interface function member and is invoked through an instance expression of an interface_type.
  • When the function member is invoked through a delegate.

In these situations, the boxed instance is considered to contain a variable of the value_type, and this variable becomes the variable referenced by this within the function member invocation.

Note: In particular, this means that when a function member is invoked on a boxed instance, it is possible for the function member to modify the value contained in the boxed instance. end note

12.7 Deconstruction

Deconstruction is a process whereby an expression gets turned into a tuple of individual expressions. Deconstruction is used when the target of a simple assignment is a tuple expression, in order to obtain values to assign to each of that tuple’s elements.

An expression E is deconstructed to a tuple expression with n elements in the following way:

  • If E is a tuple expression with n elements, the result of deconstruction is the expression E itself.
  • Otherwise, if E has a tuple type (T1, ..., Tn) with n elements, then E is evaluated into a temporary variable __v, and the result of deconstruction is the expression (__v.Item1, ..., __v.Itemn).
  • Otherwise, if the expression E.Deconstruct(out var __v1, ..., out var __vn) resolves at compile-time to a unique instance or extension method, that expression is evaluated, and the result of deconstruction is the expression (__v1, ..., __vn). Such a method is referred to as a deconstructor.
  • Otherwise, E cannot be deconstructed.

Here, __v and __v1, ..., __vn refer to otherwise invisible and inaccessible temporary variables.

Note: An expression of type dynamic cannot be deconstructed. end note

12.8 Primary expressions

12.8.1 General

Primary expressions include the simplest forms of expressions.

primary_expression
    : literal
    | interpolated_string_expression
    | simple_name
    | parenthesized_expression
    | tuple_expression
    | member_access
    | null_conditional_member_access
    | invocation_expression
    | element_access
    | null_conditional_element_access
    | this_access
    | base_access
    | post_increment_expression
    | post_decrement_expression
    | null_forgiving_expression
    | array_creation_expression
    | object_creation_expression
    | delegate_creation_expression
    | anonymous_object_creation_expression
    | typeof_expression
    | sizeof_expression
    | checked_expression
    | unchecked_expression
    | default_value_expression
    | nameof_expression    
    | anonymous_method_expression
    | pointer_member_access     // unsafe code support
    | pointer_element_access    // unsafe code support
    | stackalloc_expression
    ;

Note: This grammar rule is not ANTLR-ready as it is part of a set of mutually left-recursive rules (primary_expression, member_access, invocation_expression, element_access, post_increment_expression, post_decrement_expression, null_forgiving_expression, pointer_member_access and pointer_element_access) which ANTLR does not handle. Standard techniques can be used to transform the grammar to remove the mutual left-recursion. This Standard does not do this as not all parsing strategies require it (e.g. an LALR parser would not) and doing so would obfuscate the structure and description. end note

pointer_member_access (§23.6.3) and pointer_element_access (§23.6.4) are only available in unsafe code (§23).

12.8.2 Literals

A primary_expression that consists of a literal (§6.4.5) is classified as a value.

12.8.3 Interpolated string expressions

An interpolated_string_expression consists of $, $@, or @$, immediately followed by text within " characters. Within the quoted text there are zero or more interpolations delimited by { and } characters, each of which encloses an expression and optional formatting specifications.

Interpolated string expressions have two forms; regular (interpolated_regular_string_expression) and verbatim (interpolated_verbatim_string_expression); which are lexically similar to, but differ semantically from, the two forms of string literals (§6.4.5.6).

interpolated_string_expression
    : interpolated_regular_string_expression
    | interpolated_verbatim_string_expression
    ;

// interpolated regular string expressions

interpolated_regular_string_expression
    : Interpolated_Regular_String_Start Interpolated_Regular_String_Mid?
      ('{' regular_interpolation '}' Interpolated_Regular_String_Mid?)*
      Interpolated_Regular_String_End
    ;

regular_interpolation
    : expression (',' interpolation_minimum_width)?
      Regular_Interpolation_Format?
    ;

interpolation_minimum_width
    : constant_expression
    ;

Interpolated_Regular_String_Start
    : '$"'
    ;

// the following three lexical rules are context sensitive, see details below

Interpolated_Regular_String_Mid
    : Interpolated_Regular_String_Element+
    ;

Regular_Interpolation_Format
    : ':' Interpolated_Regular_String_Element+
    ;

Interpolated_Regular_String_End
    : '"'
    ;

fragment Interpolated_Regular_String_Element
    : Interpolated_Regular_String_Character
    | Simple_Escape_Sequence
    | Hexadecimal_Escape_Sequence
    | Unicode_Escape_Sequence
    | Open_Brace_Escape_Sequence
    | Close_Brace_Escape_Sequence
    ;

fragment Interpolated_Regular_String_Character
    // Any character except " (U+0022), \\ (U+005C),
    // { (U+007B), } (U+007D), and New_Line_Character.
    : ~["\\{}\u000D\u000A\u0085\u2028\u2029]
    ;

// interpolated verbatim string expressions

interpolated_verbatim_string_expression
    : Interpolated_Verbatim_String_Start Interpolated_Verbatim_String_Mid?
      ('{' verbatim_interpolation '}' Interpolated_Verbatim_String_Mid?)*
      Interpolated_Verbatim_String_End
    ;

verbatim_interpolation
    : expression (',' interpolation_minimum_width)?
      Verbatim_Interpolation_Format?
    ;

Interpolated_Verbatim_String_Start
    : '$@"'
    | '@$"'
    ;

// the following three lexical rules are context sensitive, see details below

Interpolated_Verbatim_String_Mid
    : Interpolated_Verbatim_String_Element+
    ;

Verbatim_Interpolation_Format
    : ':' Interpolated_Verbatim_String_Element+
    ;

Interpolated_Verbatim_String_End
    : '"'
    ;

fragment Interpolated_Verbatim_String_Element
    : Interpolated_Verbatim_String_Character
    | Quote_Escape_Sequence
    | Open_Brace_Escape_Sequence
    | Close_Brace_Escape_Sequence
    ;

fragment Interpolated_Verbatim_String_Character
    : ~["{}]    // Any character except " (U+0022), { (U+007B) and } (U+007D)
    ;

// lexical fragments used by both regular and verbatim interpolated strings

fragment Open_Brace_Escape_Sequence
    : '{{'
    ;

fragment Close_Brace_Escape_Sequence
    : '}}'
    ;

Six of the lexical rules defined above are context sensitive as follows:

Rule Contextual Requirements
Interpolated_Regular_String_Mid Only recognised after an Interpolated_Regular_String_Start, between any following interpolations, and before the corresponding Interpolated_Regular_String_End.
Regular_Interpolation_Format Only recognised within a regular_interpolation and when the starting colon (:) is not nested within any kind of bracket (parentheses/braces/square).
Interpolated_Regular_String_End Only recognised after an Interpolated_Regular_String_Start and only if any intervening tokens are either Interpolated_Regular_String_Mids or tokens that can be part of regular_interpolations, including tokens for any interpolated_regular_string_expressions contained within such interpolations.
Interpolated_Verbatim_String_Mid Verbatim_Interpolation_Format Interpolated_Verbatim_String_End Recognition of these three rules follows that of the corresponding rules above with each mentioned regular grammar rule replaced by the corresponding verbatim one.

Note: The above rules are context sensitive as their definitions overlap with those of other tokens in the language. end note

Note: The above grammar is not ANTLR-ready due to the context sensitive lexical rules. As with other lexer generators ANTLR supports context sensitive lexical rules, for example using its lexical modes, but this is an implementation detail and therefore not part of this specification. end note

An interpolated_string_expression is classified as a value. If it is immediately converted to System.IFormattable or System.FormattableString with an implicit interpolated string conversion (§10.2.5), the interpolated string expression has that type. Otherwise, it has the type string.

Note: The differences between the possible types an interpolated_string_expression may be determined from the documentation for System.String (§C.2) and System.FormattableString (§C.3). end note

The meaning of an interpolation, both regular_interpolation and verbatim_interpolation, is to format the value of the expression as a string either according to the format specified by the Regular_Interpolation_Format or Verbatim_Interpolation_Format, or according to a default format for the type of expression. The formatted string is then modified by the interpolation_minimum_width, if any, to produce the final string to be interpolated into the interpolated_string_expression.

Note: How the default format for a type is determined is detailed in the documentation for System.String (§C.2) and System.FormattableString (§C.3). Descriptions of standard formats, which are identical for Regular_Interpolation_Format and Verbatim_Interpolation_Format, may be found in the documentation for System.IFormattable (§C.4) and in other types in the standard library (§C). end note

In an interpolation_minimum_width the constant_expression shall have an implicit conversion to int. Let the field width be the absolute value of this constant_expression and the alignment be the sign (positive or negative) of the value of this constant_expression:

  • If the value of field width is less than or equal to the length of the formatted string the formatted string is not modified.
  • Otherwise the formatted string is padded with white space characters so that its length is equal to field width:
    • If the alignment is positive the formatted string is right-aligned by prepending the padding,
    • Otherwise it is left-aligned by appending the padding.

The overall meaning of an interpolated_string_expression, including the above formatting and padding of interpolations, is defined by a conversion of the expression to a method invocation: if the type of the expression is System.IFormattable or System.FormattableString that method is System.Runtime.CompilerServices.FormattableStringFactory.Create (§C.3) which returns a value of type System.FormattableString; otherwise the type shall be string and the method is string.Format (§C.2) which returns a value of type string.

In both cases, the argument list of the call consists of a format string literal with format specifications for each interpolation, and an argument for each expression corresponding to the format specifications.

The format string literal is constructed as follows, where N is the number of interpolations in the interpolated_string_expression. The format string literal consists of, in order:

  • The characters of the Interpolated_Regular_String_Start or Interpolated_Verbatim_String_Start
  • The characters of the Interpolated_Regular_String_Mid or Interpolated_Verbatim_String_Mid, if any
  • Then if N ≥ 1 for each number I from 0 to N-1:
    • A placeholder specification:
      • A left brace ({) character
      • The decimal representation of I
      • Then, if the corresponding regular_interpolation or verbatim_interpolation has a interpolation_minimum_width, a comma (,) followed by the decimal representation of the value of the constant_expression
      • The characters of the Regular_Interpolation_Format or Verbatim_Interpolation_Format, if any, of the corresponding regular_interpolation or verbatim_interpolation
      • A right brace (}) character
    • The characters of the Interpolated_Regular_String_Mid or Interpolated_Verbatim_String_Mid immediately following the corresponding interpolation, if any
  • Finally the characters of the Interpolated_Regular_String_End or Interpolated_Verbatim_String_End.

The subsequent arguments are the expressions from the interpolations, if any, in order.

When an interpolated_string_expression contains multiple interpolations, the expressions in those interpolations are evaluated in textual order from the left to right.

Example:

This example uses the following format specification features:

  • the X format specification which formats integers as uppercase hexadecimal,
  • the default format for a string value is the value itself,
  • positive alignment values that right-justify within the specified minimum field width,
  • negative alignment values that left-justify within the specified minimum field width,
  • defined constants for the interpolation_minimum_width, and
  • that {{ and }} are formatted as { and } respectively.

Given:

string text = "red";
int number = 14;
const int width = -4;

Then:

Interpolated String Expression Equivalent Meaning As string Value
$"{text}" string.Format("{0}", text) "red"
$"{{text}}" string.Format("{{text}}) "{text}"
$"{ text , 4 }" string.Format("{0,4}", text) " red"
$"{ text , width }" string.Format("{0,-4}", text) "red "
$"{number:X}" string.Format("{0:X}", number) "E"
$"{text + '?'} {number % 3}" string.Format("{0} {1}", text + '?', number % 3) "red? 2"
$"{text + $"[{number}]"}" string.Format("{0}", text + string.Format("[{0}]", number)) "red[14]"
$"{(number==0?"Zero":"Non-zero")}" string.Format("{0}", (number==0?"Zero":"Non-zero")) "Non-zero"

end example

12.8.4 Simple names

A simple_name consists of an identifier, optionally followed by a type argument list:

simple_name
    : identifier type_argument_list?
    ;

A simple_name is either of the form I or of the form I<A₁, ..., Aₑ>, where I is a single identifier and I<A₁, ..., Aₑ> is an optional type_argument_list. When no type_argument_list is specified, consider e to be zero. The simple_name is evaluated and classified as follows:

  • If e is zero and the simple_name appears within a local variable declaration space (§7.3) that directly contains a local variable, parameter or constant with name I, then the simple_name refers to that local variable, parameter or constant and is classified as a variable or value.
  • If e is zero and the simple_name appears within a generic method declaration but outside the attributes of its method_declaration, and if that declaration includes a type parameter with name I, then the simple_name refers to that type parameter.
  • Otherwise, for each instance type T (§15.3.2), starting with the instance type of the immediately enclosing type declaration and continuing with the instance type of each enclosing class or struct declaration (if any):
    • If e is zero and the declaration of T includes a type parameter with name I, then the simple_name refers to that type parameter.
    • Otherwise, if a member lookup (§12.5) of I in T with e type arguments produces a match:
      • If T is the instance type of the immediately enclosing class or struct type and the lookup identifies one or more methods, the result is a method group with an associated instance expression of this. If a type argument list was specified, it is used in calling a generic method (§12.8.10.2).
      • Otherwise, if T is the instance type of the immediately enclosing class or struct type, if the lookup identifies an instance member, and if the reference occurs within the block of an instance constructor, an instance method, or an instance accessor (§12.2.1), the result is the same as a member access (§12.8.7) of the form this.I. This can only happen when e is zero.
      • Otherwise, the result is the same as a member access (§12.8.7) of the form T.I or T.I<A₁, ..., Aₑ>.
  • Otherwise, for each namespace N, starting with the namespace in which the simple_name occurs, continuing with each enclosing namespace (if any), and ending with the global namespace, the following steps are evaluated until an entity is located:
    • If e is zero and I is the name of a namespace in N, then:
      • If the location where the simple_name occurs is enclosed by a namespace declaration for N and the namespace declaration contains an extern_alias_directive or using_alias_directive that associates the name I with a namespace or type, then the simple_name is ambiguous and a compile-time error occurs.
      • Otherwise, the simple_name refers to the namespace named I in N.
    • Otherwise, if N contains an accessible type having name I and e type parameters, then:
      • If e is zero and the location where the simple_name occurs is enclosed by a namespace declaration for N and the namespace declaration contains an extern_alias_directive or using_alias_directive that associates the name I with a namespace or type, then the simple_name is ambiguous and a compile-time error occurs.
      • Otherwise, the namespace_or_type_name refers to the type constructed with the given type arguments.
    • Otherwise, if the location where the simple_name occurs is enclosed by a namespace declaration for N:
      • If e is zero and the namespace declaration contains an extern_alias_directive or using_alias_directive that associates the name I with an imported namespace or type, then the simple_name refers to that namespace or type.
      • Otherwise, if the namespaces imported by the using_namespace_directives of the namespace declaration contain exactly one type having name I and e type parameters, then the simple_name refers to that type constructed with the given type arguments.
      • Otherwise, if the namespaces imported by the using_namespace_directives of the namespace declaration contain more than one type having name I and e type parameters, then the simple_name is ambiguous and a compile-time error occurs.

    Note: This entire step is exactly parallel to the corresponding step in the processing of a namespace_or_type_name (§7.8). end note

  • Otherwise, if e is zero and I is the identifier _, the simple_name is a simple discard, which is a form of declaration expression (§12.17).
  • Otherwise, the simple_name is undefined and a compile-time error occurs.

12.8.5 Parenthesized expressions

A parenthesized_expression consists of an expression enclosed in parentheses.

parenthesized_expression
    : '(' expression ')'
    ;

A parenthesized_expression is evaluated by evaluating the expression within the parentheses. If the expression within the parentheses denotes a namespace or type, a compile-time error occurs. Otherwise, the result of the parenthesized_expression is the result of the evaluation of the contained expression.

12.8.6 Tuple expressions

A tuple_expression represents a tuple, and consists of two or more comma-separated and optionally-named expressions enclosed in parentheses. A deconstruction_expression is a shorthand syntax for a tuple containing implicitly typed declaration expressions.

tuple_expression
    : '(' tuple_element (',' tuple_element)+ ')'
    | deconstruction_expression
    ;
    
tuple_element
    : (identifier ':')? expression
    ;
    
deconstruction_expression
    : 'var' deconstruction_tuple
    ;
    
deconstruction_tuple
    : '(' deconstruction_element (',' deconstruction_element)+ ')'
    ;

deconstruction_element
    : deconstruction_tuple
    | identifier
    ;

A tuple_expression is classified as a tuple.

A deconstruction_expression var (e1, ..., en) is shorthand for the tuple_expression (var e1, ..., var en) and follows the same behavior. This applies recursively to any nested deconstruction_tuples in the deconstruction_expression. Each identifier nested within a deconstruction_expression thus introduces a declaration expression (§12.17). As a result, a deconstruction_expression can only occur on the left side of a simple assignment.

Example: The following code declares three variables: a, b, and c. Each of which is an integer and is assigned its value from the tuple on the right hand side of the assignment.

var (a, b, c) = (1, 2, 3); // a is 1, b is 2, and c is 3.
var sum = a + b + c; // sum is 6.

Any of the individual elements of the assignment can itself be a deconstruction expression. For example, the following deconstruction expression assigns six variables, a through f.

var (a, b, (c, d, (e, f))) = (1, 2, (3, 4, (5, 6)));

In this example, notice that the structure of nested tuples must match on both sides of the assignment.

If the variable(s) on the left side are implicitly typed, the corresponding expression must have a type:

(int a, string? b) = (42, null); //OK
var (c, d) = (42, null); // Invalid as type of d cannot be inferred
(int e, var f) = (42, null); // Invalid as type of f cannot be inferred

end example

A tuple expression has a type if and only if each of its element expressions Ei has a type Ti. The type shall be a tuple type of the same arity as the tuple expression, where each element is given by the following:

  • If the tuple element in the corresponding position has a name Ni, then the tuple type element shall be Ti Ni.
  • Otherwise, if Ei is of the form Ni or E.Ni or E?.Ni then the tuple type element shall be Ti Ni, unless any of the following holds:
    • Another element of the tuple expression has the name Ni, or
    • Another tuple element without a name has a tuple element expression of the form Ni or E.Ni or E?.Ni, or
    • Ni is of the form ItemX, where X is a sequence of non-0-initiated decimal digits that could represent the position of a tuple element, and X does not represent the position of the element.
  • Otherwise, the tuple type element shall be Ti.

A tuple expression is evaluated by evaluating each of its element expressions in order from left to right.

A tuple value can be obtained from a tuple expression by converting it to a tuple type (§10.2.13), by reclassifying it as a value (§12.2.2)) or by making it the target of a deconstructing assignment (§12.21.2).

Example:

(int i, string) t1 = (i: 1, "One");
(long l, string) t2 = (l: 2, null);
var t3 = (i: 3, "Three");          // (int i, string)
var t4 = (i: 4, null);             // Error: no type

In this example, all four tuple expressions are valid. The first two, t1 and t2, do not use the type of the tuple expression, but instead apply an implicit tuple conversion. In the case of t2, the implicit tuple conversion relies on the implicit conversions from 2 to long and from null to string. The third tuple expression has a type (int i, string), and can therefore be reclassified as a value of that type. The declaration of t4, on the other hand, is an error: The tuple expression has no type because its second element has no type.

if ((x, y).Equals((1, 2))) { ... };

This example shows that tuples can sometimes lead to multiple layers of parentheses, especially when the tuple expression is the sole argument to a method invocation.

end example

12.8.7 Member access

12.8.7.1 General

A member_access consists of a primary_expression, a predefined_type, or a qualified_alias_member, followed by a “.” token, followed by an identifier, optionally followed by a type_argument_list.

member_access
    : primary_expression '.' identifier type_argument_list?
    | predefined_type '.' identifier type_argument_list?
    | qualified_alias_member '.' identifier type_argument_list?
    ;

predefined_type
    : 'bool' | 'byte' | 'char' | 'decimal' | 'double' | 'float' | 'int'
    | 'long' | 'object' | 'sbyte' | 'short' | 'string' | 'uint' | 'ulong'
    | 'ushort'
    ;

The qualified_alias_member production is defined in §14.8.

A member_access is either of the form E.I or of the form E.I<A₁, ..., Aₑ>, where E is a primary_expression, predefined_type or qualified_alias_member, I is a single identifier, and <A₁, ..., Aₑ> is an optional type_argument_list. When no type_argument_list is specified, consider e to be zero.

A member_access with a primary_expression of type dynamic is dynamically bound (§12.3.3). In this case, the compiler classifies the member access as a property access of type dynamic. The rules below to determine the meaning of the member_access are then applied at run-time, using the run-time type instead of the compile-time type of the primary_expression. If this run-time classification leads to a method group, then the member access shall be the primary_expression of an invocation_expression.

The member_access is evaluated and classified as follows:

  • If e is zero and E is a namespace and E contains a nested namespace with name I, then the result is that namespace.
  • Otherwise, if E is a namespace and E contains an accessible type having name I and K type parameters, then the result is that type constructed with the given type arguments.
  • If E is classified as a type, if E is not a type parameter, and if a member lookup (§12.5) of I in E with K type parameters produces a match, then E.I is evaluated and classified as follows:

    Note: When the result of such a member lookup is a method group and K is zero, the method group can contain methods having type parameters. This allows such methods to be considered for type argument inferencing. end note

    • If I identifies a type, then the result is that type constructed with any given type arguments.
    • If I identifies one or more methods, then the result is a method group with no associated instance expression.
    • If I identifies a static property, then the result is a property access with no associated instance expression.
    • If I identifies a static field:
      • If the field is readonly and the reference occurs outside the static constructor of the class or struct in which the field is declared, then the result is a value, namely the value of the static field I in E.
      • Otherwise, the result is a variable, namely the static field I in E.
    • If I identifies a static event:
      • If the reference occurs within the class or struct in which the event is declared, and the event was declared without event_accessor_declarations (§15.8.1), then E.I is processed exactly as if I were a static field.
      • Otherwise, the result is an event access with no associated instance expression.
    • If I identifies a constant, then the result is a value, namely the value of that constant.
    • If I identifies an enumeration member, then the result is a value, namely the value of that enumeration member.
    • Otherwise, E.I is an invalid member reference, and a compile-time error occurs.
  • If E is a property access, indexer access, variable, or value, the type of which is T, and a member lookup (§12.5) of I in T with K type arguments produces a match, then E.I is evaluated and classified as follows:
    • First, if E is a property or indexer access, then the value of the property or indexer access is obtained (§12.2.2) and E is reclassified as a value.
    • If I identifies one or more methods, then the result is a method group with an associated instance expression of E.
    • If I identifies an instance property, then the result is a property access with an associated instance expression of E and an associated type that is the type of the property. If T is a class type, the associated type is picked from the first declaration or override of the property found when starting with T, and searching through its base classes.
    • If T is a class_type and I identifies an instance field of that class_type:
      • If the value of E is null, then a System.NullReferenceException is thrown.
      • Otherwise, if the field is readonly and the reference occurs outside an instance constructor of the class in which the field is declared, then the result is a value, namely the value of the field I in the object referenced by E.
      • Otherwise, the result is a variable, namely the field I in the object referenced by E.
    • If T is a struct_type and I identifies an instance field of that struct_type:
      • If E is a value, or if the field is readonly and the reference occurs outside an instance constructor of the struct in which the field is declared, then the result is a value, namely the value of the field I in the struct instance given by E.
      • Otherwise, the result is a variable, namely the field I in the struct instance given by E.
    • If I identifies an instance event:
      • If the reference occurs within the class or struct in which the event is declared, and the event was declared without event_accessor_declarations (§15.8.1), and the reference does not occur as the left-hand side of a += or -= operator, then E.I is processed exactly as if I was an instance field.
      • Otherwise, the result is an event access with an associated instance expression of E.
  • Otherwise, an attempt is made to process E.I as an extension method invocation (§12.8.10.3). If this fails, E.I is an invalid member reference, and a binding-time error occurs.

12.8.7.2 Identical simple names and type names

In a member access of the form E.I, if E is a single identifier, and if the meaning of E as a simple_name (§12.8.4) is a constant, field, property, local variable, or parameter with the same type as the meaning of E as a type_name (§7.8.1), then both possible meanings of E are permitted. The member lookup of E.I is never ambiguous, since I shall necessarily be a member of the type E in both cases. In other words, the rule simply permits access to the static members and nested types of E where a compile-time error would otherwise have occurred.

Example:

struct Color
{
    public static readonly Color White = new Color(...);
    public static readonly Color Black = new Color(...);
    public Color Complement() => new Color(...);
}

class A
{
    public «Color» Color;              // Field Color of type Color

    void F()
    {
        Color = «Color».Black;         // Refers to Color.Black static member
        Color = Color.Complement();  // Invokes Complement() on Color field
    }

    static void G()
    {
        «Color» c = «Color».White;       // Refers to Color.White static member
    }
}

For expository purposes only, within the A class, those occurrences of the Color identifier that reference the Color type are delimited by «...», and those that reference the Color field are not.

end example

12.8.8 Null Conditional Member Access

A null_conditional_member_access is a conditional version of member_access (§12.8.7) and it is a binding time error if the result type is void. For a null conditional expression where the result type may be void see (§12.8.11).

A null_conditional_member_access consists of a primary_expression followed by the two tokens “?” and “.”, followed by an identifier with an optional type_argument_list, followed by zero or more dependent_accesses any of which can be preceded by a null_forgiving_operator.

null_conditional_member_access
    : primary_expression '?' '.' identifier type_argument_list?
      (null_forgiving_operator? dependent_access)*
    ;
    
dependent_access
    : '.' identifier type_argument_list?    // member access
    | '[' argument_list ']'                 // element access
    | '(' argument_list? ')'                // invocation
    ;

null_conditional_projection_initializer
    : primary_expression '?' '.' identifier type_argument_list?
    ;

A null_conditional_member_access expression E is of the form P?.A. The meaning of E is determined as follows:

  • If the type of P is a nullable value type:

    Let T be the type of P.Value.A.

    • If T is a type parameter that is not known to be either a reference type or a non-nullable value type, a compile-time error occurs.

    • If T is a non-nullable value type, then the type of E is T?, and the meaning of E is the same as the meaning of:

      ((object)P == null) ? (T?)null : P.Value.A
      

      Except that P is evaluated only once.

    • Otherwise the type of E is T, and the meaning of E is the same as the meaning of:

      ((object)P == null) ? (T)null : P.Value.A
      

      Except that P is evaluated only once.

  • Otherwise:

    Let T be the type of the expression P.A.

    • If T is a type parameter that is not known to be either a reference type or a non-nullable value type, a compile-time error occurs.

    • If T is a non-nullable value type, then the type of E is T?, and the meaning of E is the same as the meaning of:

      ((object)P == null) ? (T?)null : P.A
      

      Except that P is evaluated only once.

    • Otherwise the type of E is T, and the meaning of E is the same as the meaning of:

      ((object)P == null) ? (T)null : P.A
      

      Except that P is evaluated only once.

Note: In an expression of the form:

P?.A₀?.A₁

then if P evaluates to null neither A₀ or A₁ are evaluated. The same is true if an expression is a sequence of null_conditional_member_access or null_conditional_element_access §12.8.13 operations.

end note

A null_conditional_projection_initializer is a restriction of null_conditional_member_access and has the same semantics. It only occurs as a projection initializer in an anonymous object creation expression (§12.8.17.3).

12.8.9 Null-forgiving expressions

12.8.9.1 General

A null-forgiving expression’s value, type, classification (§12.2) and safe-context (§16.4.15) is the value, type, classification and safe-context of its primary_expression.

null_forgiving_expression
    : primary_expression null_forgiving_operator
    ;

null_forgiving_operator
    : '!'
    ;

Note: The postfix null-forgiving and prefix logical negation operators (§12.9.4), while represented by the same lexical token (!), are distinct. Only the latter may be overridden (§15.10), the definition of the null-forgiving operator is fixed. end note

It is a compile-time error to apply the null-forgiving operator more than once to the same expression, intervening parentheses notwithstanding.

Example: the following are all invalid:

var p = q!!;            // error: applying null_forgiving_operator more than once
var s = ( ( m(t) ! ) )! // error: null_forgiving_operator applied twice to m(t)

end example

The remainder of this subclause and the following sibling subclauses are conditionally normative.

A compiler which performs static null state analysis (§8.9.5) must conform to the following specification.

The null-forgiving operator is a compile time pseudo-operation that is used to inform a compiler’s static null state analysis. It has two uses: to override a compiler’s determination that an expression maybe null; and to override a compiler issuing a warning related to nullability.

Applying the null-forgiving operator to an expression for which a compiler’s static null state analysis does not produce any warnings is not an error.

12.8.9.2 Overriding a “maybe null” determination

Under some circumstances a compiler’s static null state analysis may determine that an expression has the null state maybe null and issue a diagnostic warning when other information indicates that the expression cannot be null. Applying the null-forgiving operator to such an expression informs the compiler’s static null state analysis that the null state is in not null; which both prevents the diagnostic warning and may inform any ongoing analysis.

Example: Consider the following:

#nullable enable
public static void M()
{
    Person? p = Find("John");                  // returns Person?
    if (IsValid(p))
    {
       Console.WriteLine($"Found {p!.Name}");  // p can't be null
    }
}

public static bool IsValid(Person? person) =>
    person != null && person.Name != null;

If IsValid returns true, p can safely be dereferenced to access its Name property, and the “dereferencing of a possibly null value” warning can be suppressed using !.

end example

Example: The null-forgiving operator should be used with caution, consider:

#nullable enable
int B(int? x)
{
    int y = (int)x!; // quash warning, throw at runtime if x is null
    return y;
}

Here the null-forgiving operator is applied to a value type and quashes any warning on x. However if x is null at runtime an exception will be thrown as null cannot be cast to int.

end example

12.8.9.3 Overriding other null analysis warnings

In addition to overriding maybe null determinations as above there may be other circumstances where it is desired to override a compiler’s static null state analysis determination that an expression requires one or more warnings. Applying the null-forgiving operator to such an expression requests that a compiler does not issue any warnings for the expression. In response a compiler may choose not to issue warnings and may also modify its further analysis.

Example: Consider the following:

#nullable enable
public static void Assign(out string? lv, string? rv) { lv = rv; }

public string M(string? t)
{
    string s;
    Assign(out s!, t ?? "«argument was null»");
    return s;
}

The types of method Assign’s parameters, lv & rv, are string?, with lv being an output parameter, and it performs a simple assignment.

Method M passes the variable s, of type string, as Assign’s output parameter, the compiler used issues a warning as s is not a nullable variable. Given that Assign’s second argument cannot be null the null-forgiving operator is used to quash the warning.

end example

End of conditionally normative text.

12.8.10 Invocation expressions

12.8.10.1 General

An invocation_expression is used to invoke a method.

invocation_expression
    : primary_expression '(' argument_list? ')'
    ;

The primary_expression may be a null_forgiving_expression if and only if it has a delegate_type.

An invocation_expression is dynamically bound (§12.3.3) if at least one of the following holds:

  • The primary_expression has compile-time type dynamic.
  • At least one argument of the optional argument_list has compile-time type dynamic.

In this case, the compiler classifies the invocation_expression as a value of type dynamic. The rules below to determine the meaning of the invocation_expression are then applied at run-time, using the run-time type instead of the compile-time type of those of the primary_expression and arguments that have the compile-time type dynamic. If the primary_expression does not have compile-time type dynamic, then the method invocation undergoes a limited compile-time check as described in §12.6.5.

The primary_expression of an invocation_expression shall be a method group or a value of a delegate_type. If the primary_expression is a method group, the invocation_expression is a method invocation (§12.8.10.2). If the primary_expression is a value of a delegate_type, the invocation_expression is a delegate invocation (§12.8.10.4). If the primary_expression is neither a method group nor a value of a delegate_type, a binding-time error occurs.

The optional argument_list (§12.6.2) provides values or variable references for the parameters of the method.

The result of evaluating an invocation_expression is classified as follows:

  • If the invocation_expression invokes a returns-no-value method (§15.6.1) or a returns-no-value delegate, the result is nothing. An expression that is classified as nothing is permitted only in the context of a statement_expression (§13.7) or as the body of a lambda_expression (§12.19). Otherwise, a binding-time error occurs.
  • Otherwise, if the invocation_expression invokes a returns-by-ref method (§15.6.1) or a returns-by-ref delegate, the result is a variable with an associated type of the return type of the method or delegate. If the invocation is of an instance method, and the receiver is of a class type T, the associated type is picked from the first declaration or override of the method found when starting with T and searching through its base classes.
  • Otherwise, the invocation_expression invokes a returns-by-value method (§15.6.1) or returns-by-value delegate, and the result is a value, with an associated type of the return type of the method or delegate. If the invocation is of an instance method, and the receiver is of a class type T, the associated type is picked from the first declaration or override of the method found when starting with T and searching through its base classes.

12.8.10.2 Method invocations

For a method invocation, the primary_expression of the invocation_expression shall be a method group. The method group identifies the one method to invoke or the set of overloaded methods from which to choose a specific method to invoke. In the latter case, determination of the specific method to invoke is based on the context provided by the types of the arguments in the argument_list.

The binding-time processing of a method invocation of the form M(A), where M is a method group (possibly including a type_argument_list), and A is an optional argument_list, consists of the following steps:

  • The set of candidate methods for the method invocation is constructed. For each method F associated with the method group M:
    • If F is non-generic, F is a candidate when:
      • M has no type argument list, and
      • F is applicable with respect to A (§12.6.4.2).
    • If F is generic and M has no type argument list, F is a candidate when:
      • Type inference (§12.6.3) succeeds, inferring a list of type arguments for the call, and
      • Once the inferred type arguments are substituted for the corresponding method type parameters, all constructed types in the parameter list of F satisfy their constraints (§8.4.5), and the parameter list of F is applicable with respect to A (§12.6.4.2)
    • If F is generic and M includes a type argument list, F is a candidate when:
      • F has the same number of method type parameters as were supplied in the type argument list, and
      • Once the type arguments are substituted for the corresponding method type parameters, all constructed types in the parameter list of F satisfy their constraints (§8.4.5), and the parameter list of F is applicable with respect to A (§12.6.4.2).
  • The set of candidate methods is reduced to contain only methods from the most derived types: For each method C.F in the set, where C is the type in which the method F is declared, all methods declared in a base type of C are removed from the set. Furthermore, if C is a class type other than object, all methods declared in an interface type are removed from the set.

    Note: This latter rule only has an effect when the method group was the result of a member lookup on a type parameter having an effective base class other than object and a non-empty effective interface set. end note

  • If the resulting set of candidate methods is empty, then further processing along the following steps are abandoned, and instead an attempt is made to process the invocation as an extension method invocation (§12.8.10.3). If this fails, then no applicable methods exist, and a binding-time error occurs.
  • The best method of the set of candidate methods is identified using the overload resolution rules of §12.6.4. If a single best method cannot be identified, the method invocation is ambiguous, and a binding-time error occurs. When performing overload resolution, the parameters of a generic method are considered after substituting the type arguments (supplied or inferred) for the corresponding method type parameters.

Once a method has been selected and validated at binding-time by the above steps, the actual run-time invocation is processed according to the rules of function member invocation described in §12.6.6.

Note: The intuitive effect of the resolution rules described above is as follows: To locate the particular method invoked by a method invocation, start with the type indicated by the method invocation and proceed up the inheritance chain until at least one applicable, accessible, non-override method declaration is found. Then perform type inference and overload resolution on the set of applicable, accessible, non-override methods declared in that type and invoke the method thus selected. If no method was found, try instead to process the invocation as an extension-method invocation. end note

12.8.10.3 Extension method invocations

In a method invocation (§12.6.6.2) of one of the forms

«expr» . «identifier» ( )  
«expr» . «identifier» ( «args» )  
«expr» . «identifier» < «typeargs» > ( )  
«expr» . «identifier» < «typeargs» > ( «args» )

if the normal processing of the invocation finds no applicable methods, an attempt is made to process the construct as an extension method invocation. If «expr» or any of the «args» has compile-time type dynamic, extension methods will not apply.

The objective is to find the best type_name C, so that the corresponding static method invocation can take place:

C . «identifier» ( «expr» )  
C . «identifier» ( «expr» , «args» )  
C . «identifier» < «typeargs» > ( «expr» )  
C . «identifier» < «typeargs» > ( «expr» , «args» )

An extension method Cᵢ.Mₑ is eligible if:

  • Cᵢ is a non-generic, non-nested class
  • The name of Mₑ is identifier
  • Mₑ is accessible and applicable when applied to the arguments as a static method as shown above
  • An implicit identity, reference or boxing conversion exists from expr to the type of the first parameter of Mₑ.

The search for C proceeds as follows:

  • Starting with the closest enclosing namespace declaration, continuing with each enclosing namespace declaration, and ending with the containing compilation unit, successive attempts are made to find a candidate set of extension methods:
    • If the given namespace or compilation unit directly contains non-generic type declarations Cᵢ with eligible extension methods Mₑ, then the set of those extension methods is the candidate set.
    • If namespaces imported by using namespace directives in the given namespace or compilation unit directly contain non-generic type declarations Cᵢ with eligible extension methods Mₑ, then the set of those extension methods is the candidate set.
  • If no candidate set is found in any enclosing namespace declaration or compilation unit, a compile-time error occurs.
  • Otherwise, overload resolution is applied to the candidate set as described in §12.6.4. If no single best method is found, a compile-time error occurs.
  • C is the type within which the best method is declared as an extension method.

Using C as a target, the method call is then processed as a static method invocation (§12.6.6).

Note: Unlike an instance method invocation, no exception is thrown when expr evaluates to a null reference. Instead, this null value is passed to the extension method as it would be via a regular static method invocation. It is up to the extension method implementation to decide how to respond to such a call. end note

The preceding rules mean that instance methods take precedence over extension methods, that extension methods available in inner namespace declarations take precedence over extension methods available in outer namespace declarations, and that extension methods declared directly in a namespace take precedence over extension methods imported into that same namespace with a using namespace directive.

Example:

public static class E
{
    public static void F(this object obj, int i) { }
    public static void F(this object obj, string s) { }
}

class A { }

class B
{
    public void F(int i) { }
}

class C
{
    public void F(object obj) { }
}

class X
{
    static void Test(A a, B b, C c)
    {
        a.F(1);            // E.F(object, int)
        a.F("hello");      // E.F(object, string)
        b.F(1);            // B.F(int)
        b.F("hello");      // E.F(object, string)
        c.F(1);            // C.F(object)
        c.F("hello");      // C.F(object)
    }
}

In the example, B’s method takes precedence over the first extension method, and C’s method takes precedence over both extension methods.

public static class C
{
    public static void F(this int i) => Console.WriteLine($"C.F({i})");
    public static void G(this int i) => Console.WriteLine($"C.G({i})");
    public static void H(this int i) => Console.WriteLine($"C.H({i})");
}

namespace N1
{
    public static class D
    {
        public static void F(this int i) => Console.WriteLine($"D.F({i})");
        public static void G(this int i) => Console.WriteLine($"D.G({i})");
    }
}

namespace N2
{
    using N1;

    public static class E
    {
        public static void F(this int i) => Console.WriteLine($"E.F({i})");
    }

    class Test
    {
        static void Main(string[] args)
        {
            1.F();
            2.G();
            3.H();
        }
    }
}

The output of this example is:

E.F(1)
D.G(2)
C.H(3)

D.G takes precedence over C.G, and E.F takes precedence over both D.F and C.F.

end example

12.8.10.4 Delegate invocations

For a delegate invocation, the primary_expression of the invocation_expression shall be a value of a delegate_type. Furthermore, considering the delegate_type to be a function member with the same parameter list as the delegate_type, the delegate_type shall be applicable (§12.6.4.2) with respect to the argument_list of the invocation_expression.

The run-time processing of a delegate invocation of the form D(A), where D is a primary_expression of a delegate_type and A is an optional argument_list, consists of the following steps:

  • D is evaluated. If this evaluation causes an exception, no further steps are executed.
  • The argument list A is evaluated. If this evaluation causes an exception, no further steps are executed.
  • The value of D is checked to be valid. If the value of D is null, a System.NullReferenceException is thrown and no further steps are executed.
  • Otherwise, D is a reference to a delegate instance. Function member invocations (§12.6.6) are performed on each of the callable entities in the invocation list of the delegate. For callable entities consisting of an instance and instance method, the instance for the invocation is the instance contained in the callable entity.

See §20.6 for details of multiple invocation lists without parameters.

12.8.11 Null Conditional Invocation Expression

A null_conditional_invocation_expression is syntactically either a null_conditional_member_access (