Note
Access to this page requires authorization. You can try signing in or changing directories.
Access to this page requires authorization. You can try changing directories.
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 bythis
(§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
+
,-
,*
,/
, andnew
. 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 asx++
). - 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)
, methodF
is called using the old value ofi
, then methodG
is called with the old value ofi
, and, finally, methodH
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 asx + (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 asx = (y = z)
. end example
Precedence and associativity can be controlled using parentheses.
Example:
x + y * z
first multipliesy
byz
and then adds the result tox
, but(x + y) * z
first addsx
andy
and then multiplies the result byz
. 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
andfalse
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
oras
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 appropriatebool
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 operationoperator «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
andY
for the operationoperator «op»(x, y)
is determined. The set consists of the union of the candidate operators provided byX
and the candidate operators provided byY
, each determined using the rules of §12.4.6. For the combined set, candidates are merged as follows:- If
X
andY
are identity convertible, or ifX
andY
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
andY
, an operator«op»Y
provided byY
has the same return type as an«op»X
provided byX
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
- 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₀
. IfT
is a nullable value type,T₀
is its underlying type; otherwise,T₀
is equal toT
. - For all
operator «op»
declarations inT₀
and all lifted forms of such operators, if at least one operator is applicable (§12.6.4.2) with respect to the argument listA
, then the set of candidate operators consists of all such applicable operators inT₀
. - Otherwise, if
T₀
isobject
, 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 ofT₀
, or the effective base class ofT₀
ifT₀
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
, whereb
is abyte
ands
is ashort
, overload resolution selectsoperator *(int, int)
as the best operator. Thus, the effect is thatb
ands
are converted toint
, and the type of the result isint
. Likewise, for the operationi * d
, wherei
is anint
andd
is adouble
,overload
resolution selectsoperator *(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 typedecimal
, or a binding-time error occurs if the other operand is of typefloat
ordouble
. - Otherwise, if either operand is of type
double
, the other operand is converted to typedouble
. - Otherwise, if either operand is of type
float
, the other operand is converted to typefloat
. - Otherwise, if either operand is of type
ulong
, the other operand is converted to typeulong
, or a binding-time error occurs if the other operand is oftype sbyte
,short
,int
, orlong
. - Otherwise, if either operand is of type
long
, the other operand is converted to typelong
. - Otherwise, if either operand is of type
uint
and the other operand is of typesbyte
,short
, orint
, both operands are converted to typelong
. - Otherwise, if either operand is of type
uint
, the other operand is converted to typeuint
. - Otherwise, both operands are converted to type
int
.
Note: The first rule disallows any operations that mix the
decimal
type with thedouble
andfloat
types. The rule follows from the fact that there are no implicit conversions between thedecimal
type and thedouble
andfloat
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 ofulong
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 adouble
. The error is resolved by explicitly converting the second operand todecimal
, 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 anull
value if the operand isnull
. 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 anull
value if one or both operands arenull
(an exception being the&
and|
operators of thebool?
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 isbool
. The lifted form is constructed by adding a single?
modifier to each operand type. The lifted operator considers twonull
values equal, and anull
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 thebool
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 isbool
. The lifted form is constructed by adding a single?
modifier to each operand type. The lifted operator produces the valuefalse
if one or both operands arenull
. Otherwise, the lifted operator unwraps the operands and applies the underlying operator to produce thebool
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 namedN
in each of the types specified as a primary constraint or secondary constraint (§15.2.5) forT
, along with the set of accessible members namedN
inobject
. - Otherwise, the set consists of all accessible (§7.5) members named
N
inT
, including inherited members and the accessible members namedN
inobject
. IfT
is a constructed type, the set of members is obtained by substituting type arguments as described in §15.3.3. Members that include anoverride
modifier are excluded from the set.
- If
- Next, if
K
is zero, all nested types whose declarations include type parameters are removed. IfK
is not zero, all members with a different number of type parameters are removed. WhenK
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, whereS
is the type in which the memberM
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 ofS
are removed from the set. - If
M
is a type declaration, then all non-types declared in a base type ofS
are removed from the set, and all type declarations with the same number of type parameters asM
declared in a base type ofS
are removed from the set. - If
M
is a method, then all non-method members declared in a base type ofS
are removed from the set.
- If
- 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 andT
has both an effective base class other thanobject
and a non-empty effective interface set (§15.2.5). For every memberS.M
in the set, whereS
is the type in which the memberM
is declared, the following rules are applied ifS
is a class declaration other thanobject
:- 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 asM
declared in an interface declaration are removed from the set.
- If
- 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
isobject
ordynamic
, thenT
has no base type. - If
T
is an enum_type, the base types ofT
are the class typesSystem.Enum
,System.ValueType
, andobject
. - If
T
is a struct_type, the base types ofT
are the class typesSystem.ValueType
andobject
.Note: A nullable_value_type is a struct_type (§8.3.1). end note
- If
T
is a class_type, the base types ofT
are the base classes ofT
, including the class typeobject
. - If
T
is an interface_type, the base types ofT
are the base interfaces ofT
and the class typeobject
. - If
T
is an array_type, the base types ofT
are the class typesSystem.Array
andobject
. - If
T
is a delegate_type, the base types ofT
are the class typesSystem.Delegate
andobject
.
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
, andvalue
indicate expressions classified as variables or values,T
indicates an expression classified as a type,F
is the simple name of a method, andP
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 notstatic
, the instance expression isthis
.T.F(x, y)
Overload resolution is applied to select the best method F
in the class or structT
. A binding-time error occurs if the method is notstatic
. 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 ofe
. A binding-time error occurs if the method isstatic
. The method is invoked with the instance expressione
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 ifP
is write-only. IfP
is notstatic
, the instance expression isthis
.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 ifP
is read-only. IfP
is notstatic
, the instance expression isthis
.T.P
The get accessor of the property P
in the class or structT
is invoked. A compile-time error occurs ifP
is notstatic
or ifP
is write-only.T.P = value
The set accessor of the property P
in the class or structT
is invoked with the argument list(value)
. A compile-time error occurs ifP
is notstatic
or ifP
is read-only.e.P
The get accessor of the property P
in the class, struct, or interface given by the type ofE
is invoked with the instance expressione
. A binding-time error occurs ifP
isstatic
or ifP
is write-only.e.P = value
The set accessor of the property P
in the class, struct, or interface given by the type ofE
is invoked with the instance expressione
and the argument list(value)
. A binding-time error occurs ifP
isstatic
or ifP
is read-only.Event access E += value
The add accessor of the event E
in the containing class or struct is invoked. IfE
is notstatic
, the instance expression isthis
.E -= value
The remove accessor of the event E
in the containing class or struct is invoked. IfE
is notstatic
, the instance expression isthis
.T.E += value
The add accessor of the event E
in the class or structT
is invoked. A binding-time error occurs ifE
is notstatic
.T.E -= value
The remove accessor of the event E
in the class or structT
is invoked. A binding-time error occurs ifE
is notstatic
.e.E += value
The add accessor of the event E
in the class, struct, or interface given by the type ofE
is invoked with the instance expressione
. A binding-time error occurs ifE
isstatic
.e.E -= value
The remove accessor of the event E
in the class, struct, or interface given by the type ofE
is invoked with the instance expressione
. A binding-time error occurs ifE
isstatic
.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 expressione
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 expressione
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
andy
. 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 byM(c: false, valueB);
. The first argument is used out-of-position (the argument is used in first position, but the parameter namedc
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 typeint
as the input parameter. In theM1(i + 5)
method call, an unnamedint
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 aSystem.ArrayTypeMismatchException
to be thrown because the actual element type ofb
isstring
and notobject
.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
andstring
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) anyXₑ
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 onXᵢ
Xᵢ
has a non-empty set of bounds
- There is at least one type variable
- 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 typeTⱼ
where the output types (§12.6.3.5) contain unfixed type variablesXₑ
but the input types (§12.6.3.4) do not, an output type inference (§12.6.3.8) is made fromEᵢ
toTⱼ
. 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 arityN
and elementsEᵢ
, andT
is a tuple type with arityN
with corresponding element typesTₑ
orT
is a nullable value typeT0?
andT0
is a tuple type with arityN
that has a corresponding element typeTₑ
, then for eachEᵢ
, an input type inference is made fromEᵢ
toTₑ
. - If
E
is an anonymous function, an explicit parameter type inference (§12.6.3.9) is made fromE
toT
- Otherwise, if
E
has a typeU
and the corresponding parameter is a value parameter (§15.6.2.2) then a lower-bound inference (§12.6.3.11) is made fromU
toT
. - Otherwise, if
E
has a typeU
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 fromU
toT
. - Otherwise, if
E
has a typeU
and the corresponding parameter is an input parameter (§15.6.2.3.2) andE
is an input argument, then an exact inference (§12.6.3.10) is made fromU
toT
. - Otherwise, if
E
has a typeU
and the corresponding parameter is an input parameter (§15.6.2.3.2) then a lower bound inference (§12.6.3.11) is made fromU
toT
. - 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 arityN
and elementsEᵢ
, andT
is a tuple type with arityN
with corresponding element typesTₑ
orT
is a nullable value typeT0?
andT0
is a tuple type with arityN
that has a corresponding element typeTₑ
, then for eachEᵢ
an output type inference is made fromEᵢ
toTₑ
. - If
E
is an anonymous function with inferred return typeU
(§12.6.3.14) andT
is a delegate type or expression tree type with return typeTₓ
, then a lower-bound inference (§12.6.3.11) is made fromU
toTₓ
. - Otherwise, if
E
is a method group andT
is a delegate type or expression tree type with parameter typesT₁...Tᵥ
and return typeTₓ
, and overload resolution ofE
with the typesT₁...Tᵥ
yields a single method with return typeU
, then a lower-bound inference is made fromU
toTₓ
. - Otherwise, if
E
is an expression with typeU
, then a lower-bound inference is made fromU
toT
. - 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 typesU₁...Uᵥ
andT
is a delegate type or expression tree type with parameter typesV₁...Vᵥ
then for eachUᵢ
an exact inference (§12.6.3.10) is made fromUᵢ
to the correspondingVᵢ
.
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 unfixedXᵢ
thenU
is added to the set of exact bounds forXᵢ
. - Otherwise, sets
V₁...Vₑ
andU₁...Uₑ
are determined by checking if any of the following cases apply:V
is an array typeV₁[...]
andU
is an array typeU₁[...]
of the same rankV
is the typeV₁?
andU
is the typeU₁
V
is a constructed typeC<V₁...Vₑ>
andU
is a constructed typeC<U₁...Uₑ>
If any of these cases apply then an exact inference is made from eachUᵢ
to the correspondingVᵢ
.
- 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 unfixedXᵢ
thenU
is added to the set of lower bounds forXᵢ
. - Otherwise, if
V
is the typeV₁?
andU
is the typeU₁?
then a lower bound inference is made fromU₁
toV₁
. - Otherwise, sets
U₁...Uₑ
andV₁...Vₑ
are determined by checking if any of the following cases apply:V
is an array typeV₁[...]
andU
is an array typeU₁[...]
of the same rankV
is one ofIEnumerable<V₁>
,ICollection<V₁>
,IReadOnlyList<V₁>>
,IReadOnlyCollection<V₁>
orIList<V₁>
andU
is a single-dimensional array typeU₁[]
V
is a constructedclass
,struct
,interface
ordelegate
typeC<V₁...Vₑ>
and there is a unique typeC<U₁...Uₑ>
such thatU
(or, ifU
is a typeparameter
, 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 fromU
toC<T>
becauseU₁
could beX
orY
.)
If any of these cases apply then an inference is made from eachUᵢ
to the correspondingVᵢ
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
isC<V₁...Vₑ>
then inference depends on thei-th
type parameter ofC
:- 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 unfixedXᵢ
thenU
is added to the set of upper bounds forXᵢ
. - Otherwise, sets
V₁...Vₑ
andU₁...Uₑ
are determined by checking if any of the following cases apply:U
is an array typeU₁[...]
andV
is an array typeV₁[...]
of the same rankU
is one ofIEnumerable<Uₑ>
,ICollection<Uₑ>
,IReadOnlyList<Uₑ>
,IReadOnlyCollection<Uₑ>
orIList<Uₑ>
andV
is a single-dimensional array typeVₑ[]
U
is the typeU1?
andV
is the typeV1?
U
is constructed class, struct, interface or delegate typeC<U₁...Uₑ>
andV
is aclass, struct, interface
ordelegate
type which isidentical
to,inherits
from (directly or indirectly), or implements (directly or indirectly) a unique typeC<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 fromC<U₁>
toV<Q>
. Inferences are not made fromU₁
to eitherX<Q>
orY<Q>
.)
If any of these cases apply then an inference is made from eachUᵢ
to the correspondingVᵢ
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
isC<U₁...Uₑ>
then inference depends on thei-th
type parameter ofC
:- 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 forXᵢ
. - Each bound for
Xᵢ
is examined in turn: For each exact bound U ofXᵢ
all typesUₑ
that are not identical toU
are removed from the candidate set. For each lower boundU
ofXᵢ
all typesUₑ
to which there is not an implicit conversion fromU
are removed from the candidate set. For each upper-bound U ofXᵢ
all typesUₑ
from which there is not an implicit conversion toU
are removed from the candidate set. - If among the remaining candidate types
Uₑ
there is a unique typeV
to which there is an implicit conversion from all the other candidate types, thenXᵢ
is fixed toV
. - 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 ofF
is the type of that expression. - If the body of
F
is a block and the set of expressions in the block’sreturn
statements has a best common typeT
(§12.6.3.16), then the inferred effective return type ofF
isT
. - 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 ofF
is either an expression classified as nothing (§12.2), or a block where noreturn
statements have expressions, the inferred return type is«TaskType»
(§15.14.1). - If
F
is async and has an inferred effective return typeT
, the inferred return type is«TaskType»<T>»
(§15.14.1). - If
F
is non-async and has an inferred effective return typeT
, the inferred return type isT
. - 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 theSystem.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 ausing namespace
directive, and given a classCustomer
with aName
property of typestring
, theSelect
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 beCustomer
. Then, using the anonymous function type inference process described above,c
is given typeCustomer
, and the expressionc.Name
is related to the return type of the selector parameter, inferringTResult
to bestring
. Thus, the invocation is equivalent toSequence.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 bestring
. Then, the parameter of the first anonymous function,s
, is given the inferred typestring
, and the expressionTimeSpan.Parse(s)
is related to the return type off1
, inferringY
to beSystem.TimeSpan
. Finally, the parameter of the second anonymous function,t
, is given the inferred typeSystem.TimeSpan
, and the expressiont.TotalHours
is related to the return type off2
, inferringZ
to bedouble
. Thus, the result of the invocation is of typedouble
.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 toX
. 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 theEᵢ
as arguments and inferringX
. 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. IfA
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.
- the parameter-passing mode of the argument is identical to the parameter-passing mode of the corresponding parameter, and:
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.
- If the method group results from a simple_name, an instance method is only applicable if
- 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ᵥ
toQᵥ
is not better than the implicit conversion fromEᵥ
toPᵥ
, and - for at least one argument, the conversion from
Eᵥ
toPᵥ
is better than the conversion fromEᵥ
toQᵥ
.
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 andMₑ
is a generic method, thenMᵢ
is better thanMₑ
. - Otherwise, if
Mᵢ
is applicable in its normal form andMₑ
has a params array and is applicable only in its expanded form, thenMᵢ
is better thanMₑ
. - 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 ofMₑ
, thenMᵢ
is better thanMₑ
. - Otherwise, if
Mᵥ
has more specific parameter types thanMₓ
, thenMᵥ
is better thanMₓ
. Let{R1, R2, ..., Rn}
and{S1, S2, ..., Sn}
represent the uninstantiated and unexpanded parameter types ofMᵥ
andMₓ
.Mᵥ
’s parameter types are more specific thanMₓ
s if, for each parameter,Rx
is not less specific thanSx
, and, for at least one parameter,Rx
is more specific thanSx
:- 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 inMₓ
, thenMᵥ
is better thanMₓ
. - If for at least one parameter
Mᵥ
uses the better parameter-passing choice (§12.6.4.4) than the corresponding parameter inMₓ
and none of the parameters inMₓ
use the better parameter-passing choice thanMᵥ
,Mᵥ
is better thanMₓ
. - 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 matchesT₁
andE
does not exactly matchT₂
(§12.6.4.6)E
exactly matches both or neither ofT₁
andT₂
, andT₁
is a better conversion target thanT₂
(§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 conversionC₁
, andT₂
is not compatible with the single best method from the method group for conversionC₂
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 typeS
, and an identity conversion exists fromS
toT
E
is an anonymous function,T
is either a delegate typeD
or an expression tree typeExpression<D>
and one of the following holds:- An inferred return type
X
exists forE
in the context of the parameter list ofD
(§12.6.3.13), and an identity conversion exists fromX
to the return type ofD
E
is anasync
lambda with no return value, andD
has a return type which is a non-generic«TaskType»
- Either
E
is non-async andD
has a return typeY
orE
is async andD
has a return type«TaskType»<Y>
(§15.14.1), and one of the following holds:- The body of
E
is an expression that exactly matchesY
- The body of
E
is a block where every return statement returns an expression that exactly matchesY
- The body of
- An inferred return type
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₁
toT₂
exists and no implicit conversion fromT₂
toT₁
exists T₁
is«TaskType»<S₁>
(§15.14.1),T₂
is«TaskType»<S₂>
, andS₁
is a better conversion target thanS₂
T₁
is«TaskType»<S₁>
(§15.14.1),T₂
is«TaskType»<S₂>
, andT₁
is more specialized thanT₂
T₁
isS₁
orS₁?
whereS₁
is a signed integral type, andT₂
isS₂
orS₂?
whereS₂
is an unsigned integral type. Specifically:S₁
issbyte
andS₂
isbyte
,ushort
,uint
, orulong
S₁
isshort
andS₂
isushort
,uint
, orulong
S₁
isint
andS₂
isuint
, orulong
S₁
islong
andS₂
isulong
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-typeV
, andM
is declared or overridden inV
: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 caseE
is classified as a variable.- If
E
is not classified as a variable, or ifV
is not a readonly struct type (§16.2.2) andM
is not a readonly function member (§16.4.12), andE
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 ofE
’s type is created and the value ofE
is assigned to that variable.E
is then reclassified as a reference to that temporary local variable. The temporary variable is accessible asthis
withinM
, but not in any other way. Thus, only whenE
can be written is it possible for the caller to observe the changes thatM
makes tothis
.
- The argument list is evaluated as described in §12.6.2.
M
is invoked. The variable referenced byE
becomes the variable referenced bythis
.
- 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 convertE
to a class_type, andE
is considered to be of that class_type in the following steps. If the value_type is an enum_type, the class_type isSystem.Enum;
otherwise, it isSystem.ValueType
. - The value of
E
is checked to be valid. If the value ofE
is null, aSystem.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 ofM
provided by the run-time type of the instance referenced byE
. This function member is determined by applying the interface mapping rules (§18.6.5) to determine the implementation ofM
provided by the run-time type of the instance referenced byE
. - Otherwise, if
M
is a virtual function member, the function member to invoke is the implementation ofM
provided by the run-time type of the instance referenced byE
. This function member is determined by applying the rules for determining the most derived implementation (§15.6.4) ofM
with respect to the run-time type of the instance referenced byE
. - Otherwise,
M
is a non-virtual function member, and the function member to invoke isM
itself.
- If the binding-time type of
- 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 theset
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.Object
,System.ValueType
orSystem.Enum
. end 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 withn
elements, the result of deconstruction is the expressionE
itself. - Otherwise, if
E
has a tuple type(T1, ..., Tn)
withn
elements, thenE
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
andpointer_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) andSystem.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) andSystem.FormattableString
(§C.3). Descriptions of standard formats, which are identical for Regular_Interpolation_Format and Verbatim_Interpolation_Format, may be found in the documentation forSystem.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 numberI
from0
toN-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
- A left brace (
- The characters of the Interpolated_Regular_String_Mid or Interpolated_Verbatim_String_Mid immediately following the corresponding interpolation, if any
- A placeholder specification:
- 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 nameI
, 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 nameI
, 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 ofT
includes a type parameter with nameI
, then the simple_name refers to that type parameter. - Otherwise, if a member lookup (§12.5) of
I
inT
withe
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 ofthis
. 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 formthis.I
. This can only happen whene
is zero. - Otherwise, the result is the same as a member access (§12.8.7) of the form
T.I
orT.I<A₁, ..., Aₑ>
.
- If
- If
- 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 andI
is the name of a namespace inN
, 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 nameI
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
inN
.
- If the location where the simple_name occurs is enclosed by a namespace declaration for
- Otherwise, if
N
contains an accessible type having nameI
ande
type parameters, then:- If
e
is zero and the location where the simple_name occurs is enclosed by a namespace declaration forN
and the namespace declaration contains an extern_alias_directive or using_alias_directive that associates the nameI
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.
- If
- 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 nameI
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
ande
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
ande
type parameters, then the simple_name is ambiguous and a compile-time error occurs.
- If
Note: This entire step is exactly parallel to the corresponding step in the processing of a namespace_or_type_name (§7.8). end note
- If
- Otherwise, if
e
is zero andI
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
throughf
.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 beTi Ni
. - Otherwise, if
Ei
is of the formNi
orE.Ni
orE?.Ni
then the tuple type element shall beTi 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
orE.Ni
orE?.Ni
, or Ni
is of the formItemX
, whereX
is a sequence of non-0
-initiated decimal digits that could represent the position of a tuple element, andX
does not represent the position of the element.
- Another element of the tuple expression has the name
- 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
andt2
, do not use the type of the tuple expression, but instead apply an implicit tuple conversion. In the case oft2
, the implicit tuple conversion relies on the implicit conversions from2
tolong
and fromnull
tostring
. The third tuple expression has a type(int i, string)
, and can therefore be reclassified as a value of that type. The declaration oft4
, 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 andE
is a namespace andE
contains a nested namespace with nameI
, then the result is that namespace. - Otherwise, if
E
is a namespace andE
contains an accessible type having nameI
andK
type parameters, then the result is that type constructed with the given type arguments. - If
E
is classified as a type, ifE
is not a type parameter, and if a member lookup (§12.5) ofI
inE
withK
type parameters produces a match, thenE.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
inE
. - Otherwise, the result is a variable, namely the static field
I
inE
.
- 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
- 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 ifI
were a static field. - Otherwise, the result is an event access with no associated instance expression.
- 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
- 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
- If
E
is a property access, indexer access, variable, or value, the type of which isT
, and a member lookup (§12.5) ofI
inT
withK
type arguments produces a match, thenE.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 ofE
. - If
I
identifies an instance property, then the result is a property access with an associated instance expression ofE
and an associated type that is the type of the property. IfT
is a class type, the associated type is picked from the first declaration or override of the property found when starting withT
, and searching through its base classes. - If
T
is a class_type andI
identifies an instance field of that class_type:- If the value of
E
isnull
, then aSystem.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 byE
. - Otherwise, the result is a variable, namely the field
I
in the object referenced byE
.
- If the value of
- If
T
is a struct_type andI
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 fieldI
in the struct instance given byE
. - Otherwise, the result is a variable, namely the field
I
in the struct instance given byE
.
- If
- 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, thenE.I
is processed exactly as ifI
was an instance field. - Otherwise, the result is an event access with an associated instance expression of
E
.
- 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
- First, if
- 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 theColor
identifier that reference theColor
type are delimited by«...»
, and those that reference theColor
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 ofP.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 ofE
isT?
, and the meaning ofE
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
isT
, and the meaning ofE
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 expressionP.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 ofE
isT?
, and the meaning ofE
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
isT
, and the meaning ofE
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 tonull
neitherA₀
orA₁
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
returnstrue
,p
can safely be dereferenced to access itsName
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 ifx
isnull
at runtime an exception will be thrown asnull
cannot be cast toint
.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
, arestring?
, withlv
being an output parameter, and it performs a simple assignment.Method
M
passes the variables
, of typestring
, asAssign
’s output parameter, the compiler used issues a warning ass
is not a nullable variable. Given thatAssign
’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 withT
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 withT
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 groupM
:- If
F
is non-generic,F
is a candidate when:M
has no type argument list, andF
is applicable with respect toA
(§12.6.4.2).
- If
F
is generic andM
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 ofF
is applicable with respect toA
(§12.6.4.2)
- If
F
is generic andM
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 ofF
is applicable with respect toA
(§12.6.4.2).
- If
- The set of candidate methods is reduced to contain only methods from the most derived types: For each method
C.F
in the set, whereC
is the type in which the methodF
is declared, all methods declared in a base type ofC
are removed from the set. Furthermore, ifC
is a class type other thanobject
, 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 methodsMₑ
, 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 methodsMₑ
, then the set of those extension methods is the candidate set.
- If the given namespace or compilation unit directly contains non-generic type declarations
- 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, andC
’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 overC.G
, andE.F
takes precedence over bothD.F
andC.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 ofD
isnull
, aSystem.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 (§12.8.8) or null_conditional_element_access (§12.8.13) where the final dependent_access is an invocation expression (§12.8.10).
A null_conditional_invocation_expression occurs within the context of a statement_expression (§13.7), anonymous_function_body (§12.19.1), or method_body (§15.6.1).
Unlike the syntactically equivalent null_conditional_member_access or null_conditional_element_access, a null_conditional_invocation_expression may be classified as nothing.
null_conditional_invocation_expression
: null_conditional_member_access null_forgiving_operator? '(' argument_list? ')'
| null_conditional_element_access null_forgiving_operator? '(' argument_list? ')'
;
The optional null_forgiving_operator may be included if and only if the null_conditional_member_access or null_conditional_element_access has a delegate_type.
A null_conditional_invocation_expression expression E
is of the form P?A
; where A
is the remainder of the syntactically equivalent null_conditional_member_access or null_conditional_element_access, A
will therefore start with .
or [
. Let PA
signify the concatention of P
and A
.
When E
occurs as a statement_expression the meaning of E
is the same as the meaning of the statement:
if ((object)P != null) PA
except that P
is evaluated only once.
When E
occurs as a anonymous_function_body or method_body the meaning of E
depends on its classification:
If
E
is classified as nothing then its meaning is the same as the meaning of the block:{ if ((object)P != null) PA; }
except that
P
is evaluated only once.Otherwise the meaning of
E
is the same as the meaning of the block:{ return E; }
and in turn the meaning of this block depends on whether
E
is syntactically equivalent to a null_conditional_member_access (§12.8.8) or null_conditional_element_access (§12.8.13).
12.8.12 Element access
12.8.12.1 General
An element_access consists of a primary_expression, followed by a “[
” token, followed by an argument_list, followed by a “]
” token. The argument_list consists of one or more arguments, separated by commas.
element_access
: primary_expression '[' argument_list ']'
;
When recognising a primary_expression if both the element_access and pointer_element_access (