An expression is a sequence of operators and operands. This chapter defines the syntax, order of evaluation of operands and operators, and meaning of expressions.
7.1 Expression Classifications
An expression is classified as one of the following:
A value. Every value has an associated type.
A variable. Every variable has an associated type-namely, the declared type of the -variable.
A namespace. An expression with this classification can appear only as the left-hand side of a member-access (§7.5.4). In any other context, an expression classified as a namespace causes a compile-time error.
A type. An expression with this classification can appear only as the left-hand side of a member-access (§7.5.4) or as an operand for the as operator (§7.9.11), the is operator (§7.9.10), or the typeof operator (§7.5.11). In any other context, an expression classified as a type causes a compile-time error.
VLADIMIR RESHETNIKOV
If an expression is classified as a type, it never denotes an array or pointer type. If an expression denotes a type parameter, it always leads to a compile-time error later.
A method group, which is a set of overloaded methods resulting from a member lookup (§7.3). 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 (§7.5.7). A method group is permitted in an invocation-expression (§7.5.5), a delegate-creation-expression (§7.5.10.5), and as the left-hand side of an is operator; it can also be implicitly converted to a -compatible delegate type (§6.6). In any other context, an expression classified as a method group causes a compile-time error.
A null literal. An expression with this classification can be implicitly converted to a reference type or a nullable type.
An anonymous function. An expression with this classification can be implicitly converted to a compatible delegate type or expression tree type.
ERIC LIPPERT
Method groups, anonymous functions, and the null literal are all expressions that have no type. These unusual expressions can be used only when the type can be figured out from the context.
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 (the get or set block) of an instance property access is invoked, the result of evaluating the instance expression becomes the instance represented by this (§7.5.7).
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-hand operand of the += and -= operators (§7.16.3). In any other context, an expression classified as an event access causes a compile-time error.
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 (the get or set block) of an indexer access is invoked, the result of evaluating the instance expression becomes the instance represented by this (§7.5.7), 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 valid only in the context of a -statement-expression (§8.6).
The final result of an expression is never a namespace, type, method group, or event access. Rather, as noted previously, these categories of expressions are intermediate constructs that are permitted only in certain contexts.
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 (§7.16.1). Otherwise, the get-accessor is invoked to obtain the current value (§7.1.1).
7.1.1 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, then 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 must be considered definitely assigned (§5.3) before its value can be obtained; 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 (§7.4.4) is performed, and the result of the invocation becomes the value of the property access expression.
VLADIMIR RESHETNIKOV
If the property has a get-accessor, but the accessor has an accessibility modifier and is not accessible in the current context, a compile-time error occurs as well.
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 (§7.4.4) 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.
VLADIMIR RESHETNIKOV
If the indexer has a get-accessor, but the accessor has an accessibility modifier and is not accessible in the current context, a compile-time error occurs as well.
7.2 Operators
Expressions are constructed from operandsand operators. The operators of an expression indicate which operations to apply to the operands. Examples of operators include +, -, *, /, and new. Examples of operands include literals, fields, local variables, and expressions.
There are three kinds of operators:
Unary operators. The unary operators take one operand and use either prefix notation (such as -x) or postfix notation (such as x++).
Binary operators. The binary operators take two operands and all use infix notation (such as x + y).
Ternary operator. Only one ternary operator (?:) exists; it takes three operands and uses infix notation (c? x: y).
The order of evaluation of operators in an expression is determined by the precedenceand associativityof the operators (§7.2.1).
Operands in an expression are evaluated from left to right. For example, in F(i) + G(i++) * H(i), method F is called using the old value of i, then method G is called with the old value of i, and finally method H is called with the new value of i. This evaluation order is separate from and unrelated to operator precedence.
Certain operators can be overloaded. Operator overloading 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 (§7.2.2).
7.2.1 Operator Precedence and Associativity
When an expression contains multiple operators, the precedenceof the operators controls the grouping of the individual expressions into operands. For example, the expression x + y * z is evaluated as x + (y * z) because the * operator has higher precedence than the binary + operator. The precedence of an operator is established by the definition of its associated grammar production. 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.
The following table summarizes all operators in order of precedence from highest to lowest.
When an operand occurs between two operators with the same precedence, the associativity of the operators controls the grouping of the expressions into operands:
Except for the assignment operators, all binary operators are left-associative, meaning that operations are grouped from left to right. For example, x + y + z is evaluated as (x + y) + z.
The assignment operators and the conditional operator (?:) are right-associative, meaning that operations are grouped from right to left. For example, x = y = z is evaluated as x = (y = z).
Precedence and associativity can be controlled using parentheses. For example, x + y * z first multiplies y by z and then adds the result to x, but (x + y) * z first adds x and y and then multiplies the result by z.
CHRIS SELLS
In general, I’m not a big fan of programs that rely on operator precendence to execute correctly. When in doubt, wrap your expressions in parentheses. The parentheses will not affect the compiled output (except that you might have fixed a bug), but they make the code much easier to understand for the human readers.
ERIC LIPPERT
The relationship between precedence, associativity, parentheses, and order of execution can be confusing. In all the cases cited here, x is evaluated before y and y is evaluated before z. At runtime, expressions are always evaluated on a strictly left-to-right basis. The way that the results of those evaluations are combined is affected by precedence, associativity, and parentheses, but the order in which the evaluation of the operands is performed at runtime is unaffected. In the x + y * z example, x is evaluated first, then y, then z. It is emphatically not the case that y and z are evaluated before x because multiplication in some sense “happens before” addition.
7.2.2 Operator Overloading
All unary and binary operators have predefined implementations that are automatically available in any expression. In addition to the predefined implementations, user-defined implementations can be introduced by including operator declarations in classes and structs (§10.10). 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 §7.2.3 and §7.2.4.
The overloadable unary operatorsare
+ - ! ~ ++ -- true false
Although true and false are not used explicitly in expressions (and, therefore, are not included in the precedence table in §7.2.1), they are considered operators because they are invoked in several expression contexts: boolean expressions (§7.19), and expressions involving the conditional (§7.13), and conditional logical operators (§7.11).
The overloadable binary operatorsare
+ - * / % & | ^ << >> == != > < >= <=
Only the operators listed here 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 assignment operator, if any, is also implicitly overloaded. For example, an overload of operator * is also an overload of operator *=. This issue is explained further in §7.16.2. Note that the assignment operator itself (=) cannot be overloaded. An assignment always performs a simple bitwise copy of a value into a variable.
Cast operations, such as (T)x, are overloaded by providing user-defined conversions (§6.4).
Element access, such as a[x], is not considered an overloadable operator. Instead, user-defined indexing is supported through indexers (§10.9).
In expressions, operators are referenced using operator notation. 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.
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. Thus it is not possible for a user-defined operator to have the same signature as a predefined operator.
User-defined operator declarations cannot modify the syntax, precedence, or associativity of an operator. For example, the / operator is always a binary operator, always has the precedence level specified in §7.2.1, and is always left-associative.
While it is possible for a user-defined operator to perform any computation it pleases, implementations that produce results other than those that are intuitively expected are strongly discouraged. For example, an implementation of operator == should compare the two operands for equality and return an appropriate bool result.
BILL WAGNER
In addition to this rule, you should limit the use of user-defined operators to those times when the operation will be obvious to the vast majority of the developers who use your code.
The descriptions of individual operators in §7.5 through §7.11 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, and numeric promotion, definitions of which are found in the following sections.
7.2.3 Unary Operator Overload Resolution
An operation of the form op x or x op, where op is an overloadable unary operator, and x is an expression of type X, is processed as follows:
The set of candidate user-defined operators provided by X for the operation operator op(x) is determined using the rules of §7.2.5.
If the set of candidate user-defined operators is not empty, then it becomes the set of candidate operators for the operation. Otherwise, the predefined unary 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 (§7.5 and §7.6).
The overload resolution rules of §7.4.3 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 compile-time error occurs.
7.2.4 Binary Operator Overload Resolution
An operation of the form x op y, where op is an overloadable binary operator, x is an expression of type X, and y is an expression of type Y, is processed as follows:
The set of candidate user-defined operators provided by X and Y for the operation -operator op(x, y) is determined. The set consists of the union of the candidate operators provided by X and the candidate operators provided by Y, each determined using the rules of §7.2.5. If X and Y are the same type, or if X and Y are derived from a common base type, then shared candidate operators occur in the combined set only once.
If the set of candidate user-defined operators is not empty, then it 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 (§7.7 through §7.11).
The overload resolution rules of §7.4.3 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 compile-time error occurs.
7.2.5 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 T0. If T is a nullable type, T0 is its underlying type; otherwise, T0 is equal to T.
For all operator op declarations in T0 and all lifted forms of such operators, if at least one operator is applicable (§7.4.3.1) with respect to the argument list A, then the set of candidate operators consists of all such applicable operators in T0.
Otherwise, if T0 is object, the set of candidate operators is empty.
Otherwise, the set of candidate operators provided by T0 is the set of candidate operators provided by the direct base class of T0, or the effective base class of T0 if T0 is a type parameter.
7.2.6 Numeric Promotions
Numeric promotion consists of automatically performing certain implicit conversions of the operands of the predefined unary and binary numeric operators. It 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 (§7.4.3) 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. For example, for the operation b * s, where b is a byte and s is a short, overload resolution selects operator *(int, int) as the best operator. Thus the effect is that b and s are converted to int, and the type of the result is int. Likewise, for the operation i * d, where i is an int and d is a double, overload resolution selects operator *(double, double) as the best operator.
7.2.6.1 Unary Numeric Promotions
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.
7.2.6.2 Binary Numeric Promotions
Binary numeric promotion occurs for the operands of the predefined +, -, *, /, %, &, |, ^, ==, !=, >, <, >=, and <= binary operators. Binary numeric promotion implicitly converts both operands to a common type that, in case of the nonrelational operators, also becomes the result type of the operation. Binary numeric promotion consists of applying the following rules, in the order they appear here:
If either operand is of type decimal, the other operand is converted to type decimal, or a compile-time error occurs if the other operand is of type float or double.
Otherwise, if either operand is of type double, the other operand is converted to type double.
Otherwise, if either operand is of type float, the other operand is converted to type float.
Otherwise, if either operand is of type ulong, the other operand is converted to type ulong, or a compile-time error occurs if the other operand is of type sbyte, short, int, or long.
Otherwise, if either operand is of type long, the other operand is converted to type long.
Otherwise, if either operand is of type uint and the other operand is of type sbyte, short, or int, both operands are converted to type long.
Otherwise, if either operand is of type uint, the other operand is converted to type uint.
Otherwise, both operands are converted to type int.
Note that the first rule disallows any operations that mix the decimal type with the double and float types. This rule follows from the fact that there are no implicit conversions between the decimal type and the double and float types.
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. No integral type exists that can represent the full range of ulong as well as the signed integral types.
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.
In the example
decimal AddPercent(decimal x, double percent)
{ return x * (1.0 + percent / 100.0);
}
a compile-time error occurs because a decimal cannot be multiplied by a double. The error is resolved by explicitly converting the second operand to decimal as follows:
decimal AddPercent(decimal x, double percent) {
return x * (decimal)(1.0 + percent / 100.0);
JOSEPH ALBAHARI The predefined operators described in this section always promote the 8- and 16-bit integral types-namely, short, ushort, sbyte, and byte. A common trap is assigning the result of a calculation on these types back to an 8- or 16-bit integral:
byte a = 1, b = 2;
byte c = a + b; // Compile-time error
In this case, the variables a and b are promoted to int, which requires an explicit cast to byte for the code to compile:
byte a = 1, b = 2;
byte c = (byte) (a + b);
}
This is just a few pages from the chapter, to purchase the book go to: http://www.informit.com/store/product.aspx?isbn=0321562992