For an operation of the form -x
, unary operator overload resolution (§7.2.3) is applied to select a specific operator implementation. The operand is converted to the parameter type of the selected operator, and the type of the result is the return type of the operator. The predefined negation operators are given here:
• Integer negation:
int operator -(int x);
long operator -(long x);
The result is computed by subtracting x
from zero. If the value of x
is the smallest representable value of the operand type (−231 for int
or −263 for long
), then the mathematical negation of x
is not representable within the operand type. If this occurs within a checked
context, a System.OverflowException
is thrown; if it occurs within an unchecked
con-text, the result is the value of the operand and the overflow is not reported.
If the operand of the negation operator is of type uint
, it is converted to type long
, and the type of the result is long
. An exception is the rule that permits the int
value −2147483648 (−231) to be written as a decimal integer literal (§2.4.4.2).
If the operand of the negation operator is of type ulong
, a compile-time error occurs. An exception is the rule that permits the long
value −9223372036854775808 (−263) to be written as a decimal integer literal (§2.4.4.2).
• Floating point negation:
float operator -(float x);
double operator -(double x);
The result is the value of x
with its sign inverted. If x
is NaN, the result is also NaN.
• Decimal negation:
decimal operator -(decimal x);
The result is computed by subtracting x
from zero. Decimal negation is equivalent to using the unary minus operator of type System.Decimal
.
For an operation of the form !x
, unary operator overload resolution (§7.2.3) is applied to select a specific operator implementation. The operand is converted to the parameter type of the selected operator, and the type of the result is the return type of the operator. Only one predefined logical negation operator exists:
bool operator !(bool x);
This operator computes the logical negation of the operand: If the operand is true
, the result is false
. If the operand is false
, the result is true
.
For an operation of the form ~x
, unary operator overload resolution (§7.2.3) is applied to select a specific operator implementation. The operand is converted to the parameter type of the selected operator, and the type of the result is the return type of the operator. The predefined bitwise complement operators are listed here:
int operator ~(int x);
uint operator ~(uint x);
long operator ~(long x);
ulong operator ~(ulong x);
For each of these operators, the result of the operation is the bitwise complement of x
.
Every enumeration type E
implicitly provides the following bitwise complement operator:
E operator ~(E x);
The result of evaluating ~x
, where x
is an expression of an enumeration type E
with an underlying type U
, is exactly the same as evaluating (E)(~(U)x)
.
pre-increment-expression:
++
unary-expression
pre-decrement-expression:
--
unary-expression
The operand of a prefix increment or decrement operation must be an expression classified as a variable, a property access, or an indexer access. The result of the operation is a value of the same type as the operand.
If the operand of a prefix increment or decrement operation is a property or indexer access, the property or indexer must have both a get
accessor and a set
accessor. If this is not the case, a compile-time error occurs.
Unary operator overload resolution (§7.2.3) is applied to select a specific operator implementation. Predefined ++
and --
operators exist for the following types: sbyte
, byte
, short
, ushort
, int
, uint
, long
, ulong
, char
, float
, double
, decimal
, and any enum type. The predefined ++
operators return the value produced by adding 1 to the operand, and the predefined --
operators return the value produced by subtracting 1 from the operand. In a checked
context, if the result of this addition or subtraction is outside the range of the result type and the result type is an integral type or enum type, a System.OverflowException
is thrown.
The runtime processing of a prefix increment or decrement operation of the form ++x
or --x
consists of the following steps:
• If x
is classified as a variable:
- x
is evaluated to produce the variable.
- The selected operator is invoked with the value of x
as its argument.
- The value returned by the operator is stored in the location given by the evalua-tion of x
.
- The value returned by the operator becomes the result of the operation.
• If x
is classified as a property or indexer access:
- The instance expression (if x
is not static
) and the argument list (if x
is an indexer access) associated with x
are evaluated, and the results are used in the subsequent get
and set
accessor invocations.
- The get
accessor of x
is invoked.
- The selected operator is invoked with the value returned by the get
accessor as its argument.
- The set
accessor of x
is invoked with the value returned by the operator as its value
argument.
- The value returned by the operator becomes the result of the operation.
The ++
and --
operators also support postfix notation (§7.5.9). The result of x++
or x--
is the value of x
before the operation, whereas the result of ++x
or --x
is the value stored in x
after the operation. In either case, x
itself has the same value after the operation.
An operator
++
or operator
--
implementation can be invoked using either postfix or prefix notation. It is not possible to have separate operator implementations for the two notations.
A cast-expression is used to explicitly convert an expression to a given type.
cast-expression:
(
type )
unary-expression
A cast-expression of the form (T)E
, where T
is a type and E
is a unary-expression, performs an explicit conversion (§6.2) of the value of E
to type T
. If no explicit conversion exists from E
to T
, a compile-time error occurs. Otherwise, the result is the value produced by the explicit conversion. The result is always classified as a value, even if E
denotes a variable.
The grammar for a cast-expression leads to certain syntactic ambiguities. For example, the expression (x)-y
could be interpreted either as a cast-expression (a cast of -y
to type x
) or as an additive-expression combined with a parenthesized-expression (which computes the value x
- y
).
To resolve cast-expression ambiguities, the following rule exists: A sequence of one or more tokens (§2.3.3) enclosed in parentheses is considered the start of a cast-expression only if at least one of the following criteria is met:
• The sequence of tokens is correct grammar for a type, but not for an expression.
• The sequence of tokens is correct grammar for a type, and the token immediately following the closing parentheses is the token “~”
, the token “!”
, the token “(”
, an identifier (§2.4.1), a literal (§2.4.4), or any keyword (§2.4.3) except as
or is
.
The term “correct grammar” simply means that the sequence of tokens must conform to the particular grammatical production. It specifically does not consider the actual meaning of any constituent identifiers. For example, if x
and y
are identifiers, then x.y
is correct grammar for a type, even if x.y
doesn’t actually denote a type.
From the disambiguation rule it follows that, if x
and y
are identifiers, (x)y
, (x)(y)
, and (x)(-y)
are cast-expressions, but (x)-y
is not, even if x
identifies a type. However, if x
is a keyword that identifies a predefined type (such as int
), then all four forms are cast-expressions (because such a keyword could not possibly be an expression by itself).
The *
, /
, %
, +
, and -
operators are called the arithmetic operators.
multiplicative-expression:
unary-expression
multiplicative-expression *
unary-expression
multiplicative-expression /
unary-expression
multiplicative-expression %
unary-expression
additive-expression:
multiplicative-expression
additive-expression +
multiplicative-expression
additive-expression multiplicative-expression
For an operation of the form x
*
y
, binary operator overload resolution (§7.2.4) is applied to select a specific operator implementation. The operands are converted to the parameter types of the selected operator, and the type of the result is the return type of the operator.
The predefined multiplication operators are listed below. All of the operators shown here compute the product of x
and y
.
• Integer multiplication:
int operator *(int x, int y);
uint operator *(uint x, uint y);
long operator *(long x, long y);
ulong operator *(ulong x, ulong y);
In a checked
context, if the product is outside the range of the result type, a System. OverflowException
is thrown. In an unchecked
context, overflows are not reported and any significant high-order bits outside the range of the result type are discarded.
• Floating point multiplication:
float operator *(float x, float y);
double operator *(double x, double y);
The product is computed according to the rules of IEEE 754 arithmetic. The following table lists the results of all possible combinations of non-zero finite values, zeros, infini-ties, and NaNs. In the table, x
and y
are positive finite values, and z
is the result of x * y
. If the result is too large for the destination type, z
is infinity. If the result is too small for the destination type, z
is zero.
decimal operator *(decimal x, decimal y);
If the resulting value is too large to represent in the decimal
format, a System. Overflow-Exception
is thrown. If the result value is too small to represent in the decimal
format, the result is zero. The scale of the result, before any rounding, is the sum of the scales of the two operands.
Decimal multiplication is equivalent to using the multiplication operator of type System.Decimal
.
For an operation of the form x / y
, binary operator overload resolution (§7.2.4) is applied to select a specific operator implementation. The operands are converted to the parameter types of the selected operator, and the type of the result is the return type of the operator.
The predefined division operators are listed below. The operators all compute the quotient of x
and y
.
• Integer division:
int operator /(int x, int y);
uint operator /(uint x, uint y);
long operator /(long x, long y);
ulong operator /(ulong x, ulong y);
If the value of the right operand is zero, a System.DivideByZeroException
is thrown.
The division rounds the result toward zero, and the absolute value of the result is the largest possible integer that is less than the absolute value of the quotient of the two operands. The result is zero or positive when the two operands have the same sign; it is zero or negative when the two operands have opposite signs.
If the left operand is the smallest representable int
or long
value and the right operand is -1
, an overflow occurs. In a checked
context, this overflow causes a System. Arithmetic Exception
(or a subclass thereof) to be thrown. In an unchecked
context, the implementation determines whether a System.ArithmeticException
(or a subclass thereof) is thrown or the overflow goes unreported with the resulting value being that of the left operand.
• Floating point division:
float operator /(float x, float y);
double operator /(double x, double y);
The quotient is computed according to the rules of IEEE 754 arithmetic. The following table lists the results of all possible combinations of non-zero finite values, zeros, infinities, and NaNs. In the table, x
and y
are positive finite values, and z
is the result of x / y
. If the result is too large for the destination type, z
is infinity. If the result is too small for the destination type, z
is zero.
• Decimal division:
decimal operator /(decimal x, decimal y);
If the value of the right operand is zero, a System.DivideByZeroException
is thrown. If the resulting value is too large to represent in the decimal
format, a System.Overflow-Exception
is thrown. If the result value is too small to represent in the decimal
format, the result is zero. The scale of the result is the smallest scale that will preserve a result equal to the nearest representable decimal value to the true mathematical result.
Decimal division is equivalent to using the division operator of type System.Decimal
.
For an operation of the form x
%
y
, binary operator overload resolution (§7.2.4) is applied to select a specific operator implementation. The operands are converted to the parameter types of the selected operator, and the type of the result is the return type of the operator.
The predefined remainder operators are listed below. The operators all compute the remainder of the division between x
and y
.
int operator %(int x, int y);
uint operator %(uint x, uint y);
long operator %(long x, long y);
ulong operator %(ulong x, ulong y);
The result of x
%
y
is the value produced by x
- (x / y) * y
. If y
is zero, a System.Divide-ByZeroException
is thrown.
If the left operand is the smallest int
or long
value and the right operand is -1
, a System.OverflowException
is thrown. In no case does x
%
y
throw an exception where x / y
would not throw an exception.
• Floating point remainder:
float operator %(float x, float y);
double operator %(double x, double y);
The following table lists the results of all possible combinations of non-zero finite values, zeros, infinities, and NaNs. In the table, x
and y
are positive finite values, and z
is the result of x % y
and is computed as x - n * y
where n y
is the largest possible integer that is less than or equal to x / y
. This method of computing the remainder is analogous to that used for integer operands, but differs from the IEEE 754 definition (in which n
is the integer closest to x / y
).
decimal operator %(decimal x, decimal y);
If the value of the right operand is zero, a System.DivideByZeroException
is thrown. The scale of the result, before any rounding, is the larger of the scales of the two operands, and the sign of the result, if non-zero, is the same as that of x
.
Decimal remainder is equivalent to using the remainder operator of type System. Decimal
.
For an operation of the form x + y
, binary operator overload resolution (§7.2.4) is applied to select a specific operator implementation. The operands are converted to the parameter types of the selected operator, and the type of the result is the return type of the operator.
The predefined addition operators are listed below. For numeric and enumeration types, the predefined addition operators compute the sum of the two operands. When one or both operands are of type string, the predefined addition operators concatenate the string representation of the operands.
• Integer addition:
int operator +(int x, int y);
uint operator +(uint x, uint y);
long operator +(long x, long y);
ulong operator +(ulong x, ulong y);
In a checked
context, if the sum is outside the range of the result type, a System. OverflowException
is thrown. In an unchecked
context, overflows are not reported and any significant high-order bits outside the range of the result type are discarded.
• Floating point addition:
float operator +(float x, float y);
double operator +(double x, double y);
The sum is computed according to the rules of IEEE 754 arithmetic. The following table lists the results of all possible combinations of non-zero finite values, zeros, infinities, and NaNs. In the table, x
and y
are nonzero finite values, and z
is the result of x
+ y
. If x
and y
have the same magnitude but opposite signs, z
is positive zero. If x + y
is too large to represent in the destination type, z
is an infinity with the same sign as x + y
.
• Decimal addition:
decimal operator +(decimal x, decimal y);
If the resulting value is too large to represent in the decimal
format, a System. OverflowException
is thrown. The scale of the result, before any rounding, is the larger of the scales of the two operands.
Decimal addition is equivalent to using the addition operator of type System.Decimal
.
• Enumeration addition. Every enumeration type implicitly provides the following predefined operators, where E
is the enum type and U
is the underlying type of E:
E operator +(E x, U y);
E operator +(U x, E y);
• The operators are evaluated exactly as (E)((U)x + (U)y)
.
• String concatenation:
string operator +(string x, string y);
string operator +(string x, object y);
string operator +(object x, string y);
The binary +
operator performs string concatenation when one or both operands are of type string
. If an operand of string concatenation is null
, an empty string is substituted. Otherwise, any nonstring argument is converted to its string representation by invoking the virtual ToString
method inherited from type object
. If T
oString returns null
, an empty string is substituted.
using System;
class Test
{
static void Main ( ) {
string s = null;
Console.WriteLine("s = >" + s + "<"); // Displays s = ><
int i = 1;
Console.WriteLine("i = " + i); // Displays i = 1
float f = 1.2300E+15F;
Console.WriteLine("f = " + f); // Displays f = 1.23E+15
decimal d = 2.900m;
Console.WriteLine("d = " + d); // Displays d = 2.900
}
}
The result of the string concatenation operator is a string that consists of the characters of the left operand followed by the characters of the right operand. The string concatenation operator never returns a null
value. A System.OutOfMemoryException
may be thrown if there is not enough memory available to allocate the resulting string.
• Delegate combination. Every delegate type implicitly provides the following predefined operator, where D
is the delegate type:
D operator +(D x, D y);
The binary +
operator performs delegate combination when both operands are of some delegate type D
. (If the operands have different delegate types, a compile-time error occurs.) If the first operand is null
, the result of the operation is the value of the second operand (even if that is also null
). Otherwise, if the second operand is null
, then the result of the operation is the value of the first operand. Otherwise, the result of the operation is a new delegate instance that, when invoked, invokes the first operand and then invokes the second operand. For examples of delegate combination, see §7.7.5 and §15.4. Because System.Delegate
is not a delegate type, operator +
is not defined for it.
For an operation of the form x
- y
, binary operator overload resolution (§7.2.4) is applied to select a specific operator implementation. The operands are converted to the parameter types of the selected operator, and the type of the result is the return type of the operator.
The predefined subtraction operators are listed below. The operators all subtract y
from x
.
• Integer subtraction:
int operator -(int x, int y);
uint operator -(uint x, uint y);
long operator -(long x, long y);
ulong operator -(ulong x, ulong y);
In a checked
context, if the difference is outside the range of the result type, a System. OverflowException
is thrown. In an unchecked
context, overflows are not reported and any significant high-order bits outside the range of the result type are discarded.
• Floating point subtraction:
float operator -(float x, float y);
double operator -(double x, double y);
The difference is computed according to the rules of IEEE 754 arithmetic. The following table lists the results of all possible combinations of non-zero finite values, zeros, infini-ties, and NaNs. In the table, x
and y
are non-zero finite values, and z
is the result of x
y
. If x
and y
are equal, z
is positive zero. If x - y
is too large to represent in the destination type, z
is an infinity with the same sign as x - y
.
• Decimal subtraction:
decimal operator -(decimal x, decimal y);
If the resulting value is too large to represent in the decimal
format, a System. Overflow-Exception
is thrown. The scale of the result, before any rounding, is the larger of the scales of the two operands.
Decimal subtraction is equivalent to using the subtraction operator of type System. Decimal.
• Enumeration subtraction. Every enumeration type implicitly provides the following predefined operator, where E
is the enum type and U is the underlying type of E:
U operator -(E x, E y);
This operator is evaluated exactly as (U)((U)x - (U)y)
. In other words, the operator computes the difference between the ordinal values of x
and y
, and the type of the result is the underlying type of the enumeration.
E operator -(E x, U y);
This operator is evaluated exactly as (E)((U)x - y
). In other words, the operator subtracts a value from the underlying type of the enumeration, yielding a value of the enumeration.
• Delegate removal. Every delegate type implicitly provides the following predefined operator, where D
is the delegate type:
D operator -(D x, D y);
The binary -
operator performs delegate removal when both operands are of some delegate type D
. If the operands have different delegate types, a compile-time error occurs. If the first operand is null
, the result of the operation is null
. Otherwise, if the second operand is null
, then the result of the operation is the value of the first operand. Otherwise, both operands represent invocation lists (§15.1) having one or more entries, and the result is a new invocation list consisting of the first operand’s list with the second operand’s entries removed from it, provided the second operand’s list is a proper contiguous sublist of the first operand’s list. [To determine sublist equality, corresponding entries are compared as for the delegate equality operator (§7.9.8).] Otherwise, the result is the value of the left operand. Neither of the operands’ lists is changed in the process. If the second operand’s list matches multiple sublists of contiguous entries in the first operand’s list, the rightmost matching sublist of contiguous entries is removed. If removal results in an empty list, the result is null
. For example,
delegate void D(int x);
class C
{
public static void M1(int i) { /* … */ }
public static void M2(int i) { /* … */ }
}
class Test
{
static void Main( ) {
D cd1 = new D(C.M1);
D cd2 = new D(C.M2);
D cd3 = cd1 + cd2 + cd2 + cd1; // M1 + M2 + M2 + M1
cd3 -= cd1; // => M1 + M2 + M2
cd3 = cd1 + cd2 + cd2 + cd1; // M1 + M2 + M2 + M1
cd3 -= cd1 + cd2; // => M2 + M1
cd3 = cd1 + cd2 + cd2 + cd1; // M1 + M2 + M2 + M1
cd3 -= cd2 + cd2; // => M1 + M1
cd3 = cd1 + cd2 + cd2 + cd1; // M1 + M2 + M2 + M1
cd3 -= cd2 + cd1; // => M1 + M2
cd3 = cd1 + cd2 + cd2 + cd1; // M1 + M2 + M2 + M1
cd3 -= cd1 + cd1; // => M1 + M2 + M2 + M1
}
}
The <<
and >>
operators are used to perform bit-shifting operations.
shift-expression:
additive-expression
shift-expression <<
additive-expression
shift-expression right-shift additive-expression
For an operation of the form x
<<
count
or x
>>
count
, binary operator overload resolution (§7.2.4) is applied to select a specific operator implementation. The operands are converted to the parameter types of the selected operator, and the type of the result is the return type of the operator.
When declaring an overloaded shift operator, the type of the first operand must always be the class or struct containing the operator declaration, and the type of the second operand must always be int
.
The predefined shift operators are listed below.
• Shift left:
int operator <<(int x, int count);
uint operator <<(uint x, int count);
long operator <<(long x, int count);
ulong operator <<(ulong x, int count);
The <<
operator shifts x
left by a number of bits computed as described below.
The high-order bits outside the range of the result type of x
are discarded, the remaining bits are shifted left, and the low-order empty bit positions are set to zero.
• Shift right:
int operator >>(int x, int count);
uint operator >>(uint x, int count);
long operator >>(long x, int count);
ulong operator >>(ulong x, int count);
The >>
operator shifts x
right by a number of bits computed as described below.
When x
is of type int
or long
, the low-order bits of x
are discarded, the remaining bits are shifted right, and the high-order empty bit positions are set to zero if x
is non-negative and set to one if x
is negative.
When x
is of type uint
or ulong
, the low-order bits of x
are discarded, the remaining bits are shifted right, and the high-order empty bit positions are set to zero.
For the predefined operators, the number of bits to shift is computed as follows:
• When the type of x
is int
or uint
, the shift count is given by the low-order five bits of count
. In other words, the shift count is computed from count & 0x1F
.
• When the type of x
is long
or ulong
, the shift count is given by the low-order six bits of count
. In other words, the shift count is computed from count & 0x3F
.
If the resulting shift count is zero, the shift operators simply return the value of x
.
Shift operations never cause overflows and produce the same results in checked
and unchecked
contexts.
When the left operand of the >>
operator is of a signed integral type, the operator performs an arithmetic shift right, wherein the value of the most significant bit (the sign bit) of the operand is propagated to the high-order empty bit positions. When the left operand of the >>
operator is of an unsigned integral type, the operator performs a logical shift right, wherein the high-order empty bit positions are always set to zero. To perform the opposite operation of that inferred from the operand type, explicit casts can be used. For example, if x
is a variable of type int
, the operation unchecked((int)((uint)x >> y))
performs a logical shift right of x
.
The ==
, !=
, <
, >
, <=
, >=
, is
, and as
operators are called the relational and type-testing operators.
relational-expression:
shift-expression
relational-expression <
shift-expression
relational-expression >
shift-expression
relational-expression <=
shift-expression
relational-expression >=
shift-expression
relational-expression is
type
relational-expression as
type
equality-expression:
relational-expression
equality-expression ==
relational-expression
equality-expression !=
relational-expression
The is
operator is described in §7.9.10. The as
operator is described in §7.9.11.
The ==
, !=
, <
, >
, <=
, and >=
operators are comparison operators. For an operation of the form x
op y
, where op is a comparison operator, overload resolution (§7.2.4) is applied to select a specific operator implementation. The operands are converted to the parameter types of the selected operator, and the type of the result is the return type of the operator.
The predefined comparison operators are described in the following sections. All predefined comparison operators return a result of type bool
, as described in the following table.
The predefined integer comparison operators are given here:
bool operator ==(int x, int y);
bool operator ==(uint x, uint y);
bool operator ==(long x, long y);
bool operator ==(ulong x, ulong y);
bool operator !=(int x, int y);
bool operator !=(uint x, uint y);
bool operator !=(long x, long y);
bool operator !=(ulong x, ulong y);
bool operator <(int x, int y);
bool operator <(uint x, uint y);
bool operator <(long x, long y);
bool operator <(ulong x, ulong y);
bool operator >(int x, int y);
bool operator >(uint x, uint y);
bool operator >(long x, long y);
bool operator >(ulong x, ulong y);
bool operator <=(int x, int y);
bool operator <=(uint x, uint y);
bool operator <=(long x, long y);
bool operator <=(ulong x, ulong y);
bool operator >=(int x, int y);
bool operator >=(uint x, uint y);
bool operator >=(long x, long y);
bool operator >=(ulong x, ulong y);
Each of these operators compares the numeric values of the two integer operands and returns a bool
value that indicates whether the particular relation is true
or false
.
The predefined floating point comparison operators are given here:
bool operator ==(float x, float y);
bool operator ==(double x, double y);
bool operator !=(float x, float y);
bool operator !=(double x, double y);
bool operator <(float x, float y);
bool operator <(double x, double y);
bool operator >(float x, float y);
bool operator >(double x, double y);
bool operator <=(float x, float y);
bool operator <=(double x, double y);
bool operator >=(float x, float y);
bool operator >=(double x, double y);
These operators compare the operands according to the rules of the IEEE 754 standard:
• If either operand is NaN, the result is false
for all operators except !=
, for which the result is true
. For any two operands, x
!=
y
always produces the same result as !(x
==
y)
. However, when one or both operands are NaNs, the <
, >
, <=
, and >=
operators do not produce the same results as the logical negation of the opposite operator. For example, if either of x
and y
is NaN, then x
<
y
is false
, but !(x
>=
y)
is true
.
• When neither operand is NaN, the operators compare the values of the two floating point operands with respect to the ordering
-∞ < -max < … < -min < 0.0 == +0.0 < +min < … < +max < +∞
where min
and max
are, respectively, the smallest and largest positive finite values that can be represented in the given floating point format. This ordering has several notable effects:
- Negative and positive zeros are considered equal.
- A negative infinity is considered less than all other values, but equal to another nega-tive infinity.
- A positive infinity is considered greater than all other values, but equal to another -positive infinity.
The predefined decimal comparison operators are shown here:
bool operator ==(decimal x, decimal y);
bool operator !=(decimal x, decimal y);
bool operator <(decimal x, decimal y);
bool operator >(decimal x, decimal y);
bool operator <=(decimal x, decimal y);
bool operator >=(decimal x, decimal y);
Each of these operators compares the numeric values of the two decimal operands and returns a bool
value that indicates whether the particular relation is true
or false
. Each decimal comparison is equivalent to using the corresponding relational or equality operator of type System.Decimal
.
There are two predefined boolean equality operators:
bool operator ==(bool x, bool y);
bool operator !=(bool x, bool y);
The result of ==
is true
if both x
and y
are true
or if both x
and y
are false
. Otherwise, the result is false
.
The result of !=
is false
if both x
and y
are true
or if both x
and y
are false
. Otherwise, the result is true
. When the operands are of type bool
, the !=
operator produces the same result as the ^
operator.
Every enumeration type implicitly provides the following predefined comparison operators:
bool operator ==(E x, E y);
bool operator !=(E x, E y);
bool operator <(E x, E y);
bool operator >(E x, E y);
bool operator <=(E x, E y);
bool operator >=(E x, E y);
The result of evaluating x
op y
, where x
and y
are expressions of an enumeration type E
with an underlying type U
, and op is one of the comparison operators, is exactly the same as evaluating((U)x)
op ((U)y)
. In other words, the enumeration type comparison operators simply compare the underlying integral values of the two operands.
There are two predefined reference type equality operators:
bool operator ==(object x, object y);
bool operator !=(object x, object y);
These operators return the result of comparing the two references for equality or non-equality.
Because the predefined reference type equality operators accept operands of type object
, they apply to all types that do not declare applicable operator ==
and operator !=
members. Conversely, any applicable user-defined equality operators effectively hide the predefined reference type equality operators.
The predefined reference type equality operators require one of the following:
• Both operands are reference-type values or the literal null
. Furthermore, a standard implicit conversion (§6.3.1) exists from the type of either operand to the type of the other operand.
• One operand is a value of type T
, where T
is a type-parameter, and the other operand is the literal null
. Furthermore, T
does not have the value type constraint.
Unless one of these conditions is true, a compile-time error occurs. These rules have several notable implications:
• It is a compile-time error to use the predefined reference type equality operators to compare two references that are known to be different at compile time. For example, if the compile-time types of the operands are two class types A
and B
, and if neither A
nor B
derives from the other, then it would be impossible for the two operands to reference the same object. Thus the operation is considered a compile-time error.
• The predefined reference type equality operators do not permit value type operands to be compared. Therefore, unless a struct type declares its own equality operators, it is not possible to compare values of that struct type.
• The predefined reference type equality operators never cause boxing operations to occur for their operands. It would be meaningless to perform such boxing operations, because references to the newly allocated boxed instances would necessarily differ from all other references.
• If an operand of a type parameter type T
is compared to null
, and the runtime type of T
is a value type, the result of the comparison is false
.
The following example checks whether an argument of an unconstrained type parameter type is null
.
class C<T>
{
void F(T x) {
if (x == null) throw new ArgumentNullException();
…
}
}
The x
==
null
construct is permitted even though T
could represent a value type, and the result is simply defined to be false
when T
is a value type.
For an operation of the form x
==
y
or x
!=
y
, if any applicable operator
==
or operator
!=
exists, the operator overload resolution (§7.2.4) rules will select that operator instead of the predefined reference type equality operator. Nevertheless, it is always possible to select the predefined reference type equality operator by explicitly casting one or both of the operands to type object
. The example
using System;
class Test
{
static void Main() {
string s = "Test";
string t = string.Copy(s);
Console.WriteLine(s == t);
Console.WriteLine((object)s == t);
Console.WriteLine(s == (object)t);
Console.WriteLine((object)s == (object)t);
}
}
produces the following output:
True
False
False
False
In this example, the s
and t
variables refer to two distinct string
instances containing the same characters. The first comparison outputs True
because the predefined string equality operator (§7.9.7) is selected when both operands are of type string
. The remaining comparisons all output False
because the predefined reference type equality operator is selected when one or both of the operands are of type object
.
This technique is not meaningful for value types, however. The example
class Test
{
static void Main() {
int i = 123;
int j = 123;
System.Console.WriteLine((object)i == (object)j);
}
}
outputs False
because the casts create references to two separate instances of boxed int
values.
There are two predefined string equality operators:
bool operator ==(string x, string y);
bool operator !=(string x, string y);
Two string
values are considered equal when one of the following is true:
• Both values are null
.
• Both values are non-null references to string instances that have identical lengths and identical characters in each character position.
The string equality operators compare string values rather than string references. When two separate string instances contain the exact same sequence of characters, the values of the strings are equal, but the references are different. As described in §7.9.6, the reference type equality operators can be used to compare string references instead of string values.
Every delegate type implicitly provides the following predefined comparison operators:
bool operator ==(System.Delegate x, System.Delegate y);
bool operator !=(System.Delegate x, System.Delegate y);
Two delegate instances are considered equal as follows:
• If either of the delegate instances is null
, they are equal if and only if both are null
.
• If the delegates have different runtime types, they are never equal.
• If both of the delegate instances have an invocation list (§15.1), those instances are equal if and only if their invocation lists are the same length, and each entry in one’s invocation list is equal (as defined below) to the corresponding entry, in order, in the other’s invocation list.
The following rules govern the equality of invocation list entries:
• If two invocation list entries both refer to the same static method, then the entries are equal
• If two invocation list entries both refer to the same nonstatic method on the same target object (as defined by the reference equality operators), then the entries are equal.
• Invocation list entries produced from evaluation of semantically identical anonymous-function-expression s with the same (possibly empty) set of captured outer variable instances are permitted (but not required) to be equal.
The ==
and !=
operators permit one operand to be a value of a nullable type and the other to be the null
literal, even if no predefined or user-defined operator (in nonlifted or lifted form) exists for the operation.
For an operation of one of the forms
x == null null == x x != null null != x
where x
is an expression of a nullable type, if operator overload resolution (§7.2.4) fails to find an applicable operator, the result is instead computed from the HasValue
property of x
. Specifically, the first two forms are translated into !x.HasValue
, and the last two forms are translated into x.HasValue
.
The is
operator is used to dynamically check if the runtime type of an object is compatible with a given type. The result of the operation E
is
T
, where E
is an expression and T
is a type, is a boolean value indicating whether E
can successfully be converted to type T
by a reference conversion, a boxing conversion, or an unboxing conversion. The operation is evaluated as follows, after type arguments have been substituted for all type parameters:
• If E
is an anonymous function, a compile-time error occurs.
• If E
is a method group or the null
literal, of if the type of E
is a reference type or a nullable type and the value of E
is null, the result is false.
• Otherwise, let D
represent the dynamic type of E
as follows:
- If the type of E
is a reference type, D
is the runtime type of the instance reference by E
.
- If the type of E
is a nullable type, D
is the underlying type of that nullable type.
- If the type of E
is a non-nullable value type, D
is the type of E
.
• The result of the operation depends on D
and T
as follows:
- If T
is a reference type, the result is true
if D
and T
are the same type, if D
is a reference type and an implicit reference conversion from D
to T
exists, or if D
is a value type and a boxing conversion from D
to T
exists.
- If T
is a nullable type, the result is true
if D
is the underlying type of T
.
- If T
is a non-nullable value type, the result is true
if D
and T
are the same type.
- Otherwise, the result is false
.
The is
operator does not consider user-defined conversions.
The as
operator is used to explicitly convert a value to a given reference type or nullable type. Unlike a cast expression (§7.6.6), the as
operator never throws an exception. Instead, if the indicated conversion is not possible, the resulting value is null
.
In an operation of the form E
as
T
, E
must be an expression and T
must be a reference type, a type parameter known to be a reference type, or a nullable type. Furthermore, at least one of the following must be true, or otherwise a compile-time error occurs:
• An identity (§6.1.1), implicit nullable (§6.1.4), implicit reference (§6.1.6), boxing (§6.1.7), explicit nullable (§6.2.3), explicit reference (§6.2.4), or unboxing (§6.2.5) conversion exists from E
to T
.
• The type of E
or T
is an open type.
• E
is the null
literal.
The operation E
as
T
produces the same result as
E is T ? (T)(E) : (T)null
except that E
is evaluated only once. The compiler can be expected to optimize E
as
T
to perform at most one dynamic type check, as opposed to the two dynamic type checks implied by the expansion above.
Some conversions, such as user-defined conversions, are not possible with the as
operator and should instead be performed using cast expressions.
In the example
class X
{
public string F(object o) {
return o as string; // Okay, string is a reference type
}
public T G<T>(object o) where T: Attribute {
return o as T; // Okay, T has a class constraint
}
public U H<U>(object o) {
return o as U; // Error, U is unconstrained
}
}
the type parameter T
of G
is known to be a reference type, because it has the class constraint. The type parameter U
of H
is not, however; hence, the use of the as
operator in H
is disallowed.
The &
, ^
, and |
operators are called the logical operators.
and-expression:
equality-expression and-expression &
equality-expression
exclusive-or-expression:
and-expression
exclusive-or-expression ^
and-expression
inclusive-or-expression:
exclusive-or-expression
inclusive-or-expression |
exclusive-or-expression
For an operation of the form x
op y
, where op is one of the logical operators, overload resolution (§7.2.4) is applied to select a specific operator implementation. The operands are converted to the parameter types of the selected operator, and the type of the result is the return type of the operator.
The predefined logical operators are described in the following sections.
The predefined integer logical operators are listed here:
int operator &(int x, int y);
uint operator &(uint x, uint y);
long operator &(long x, long y);
ulong operator &(ulong x, ulong y);
int operator |(int x, int y);
uint operator |(uint x, uint y);
long operator |(long x, long y);
ulong operator |(ulong x, ulong y);
int operator ^(int x, int y);
uint operator ^(uint x, uint y);
long operator ^(long x, long y);
ulong operator ^(ulong x, ulong y);
The &
operator computes the bitwise logical AND
of the two operands, the |
operator computes the bitwise logical OR
of the two operands, and the ^
operator computes the bitwise logical exclusive OR
of the two operands. No overflows are possible from these operations.
Every enumeration type E
implicitly provides the following predefined logical operators:
E operator &(E x, E y);
E operator |(E x, E y);
E operator ^(E x, E y);
The result of evaluating x
op y
, where x
and y
are expressions of an enumeration type E
with an underlying type U
, and op is one of the logical operators, is exactly the same as evaluating(E)((U)x
op (U)y)
. In other words, the enumeration type logical operators simply perform the logical operation on the underlying type of the two operands.
There are three predefined boolean logical operators:
bool operator &(bool x, bool y);
bool operator |(bool x, bool y);
bool operator ^(bool x, bool y);
The result of x
&
y
is true
if both x
and y
are true
. Otherwise, the result is false
.
The result of x
|
y
is true
if either x
or y
is true
. Otherwise, the result is false
.
The result of x
^
y
is true
if x
is true
and y
is false
, or if x
is false
and y
is true
. Otherwise, the result is false
. When the operands are of type bool
, the ^
operator computes the same result as the !=
operator.
The nullable boolean type bool?
can represent three values: true
, false
, and null
. It is conceptually similar to the three-valued type used for boolean expressions in SQL. To ensure that the results produced by the &
and |
operators for bool?
operands are consistent with SQL’s three-valued logic, two predefined operators are provided:
bool? operator &(bool? x, bool? y);
bool? operator |(bool? x, bool? y);
The following table lists the results produced by these operators for all combinations of the values true
, false
, and null
.
The &&
and ||
operators are called the conditional logical operators. They are also called the “short-circuiting” logical operators.
conditional-and-expression:
inclusive-or-expression
conditional-and-expression &&
inclusive-or-expression
conditional-or-expression:
conditional-and-expression
conditional-or-expression ||
conditional-and-expression
The &&
and ||
operators are conditional versions of the &
and |
operators:
• The operation x
&&
y
corresponds to the operation x
&
y
, except that y
is evaluated only if x
is not false
.
• The operation x
||
y
corresponds to the operation x
|
y
, except that y
is evaluated only if x
is not true
.
An operation of the form x
&&
y
or x
||
y
is processed by applying overload resolution (§7.2.4) as if the operation was written x
&
y
or x
|
y
. Then,
• If overload resolution fails to find a single best operator, or if overload resolution selects one of the predefined integer logical operators, a compile-time error occurs.
• Otherwise, if the selected operator is one of the predefined boolean logical operators (§7.10.3) or nullable boolean logical operators (§7.10.4), the operation is processed as described in §7.11.1.
• Otherwise, the selected operator is a user-defined operator, and the operation is processed as described in §7.11.2.
It is not possible to directly overload the conditional logical operators. However, because the conditional logical operators are evaluated in terms of the regular logical operators, overloads of the regular logical operators are, with certain restrictions, also considered overloads of the conditional logical operators. This issue is described further in §7.11.2.
When the operands of &&
or ||
are of type bool
, or when the operands are of types that do not define an applicable operator
&
or operator
|
, but do define implicit conversions to bool
, the operation is processed as follows:
• The operation x
&&
y
is evaluated as x
?
y
:
false
. In other words, x
is first evaluated and converted to type bool
. Then, if x
is true
, y
is evaluated and converted to type bool
, and this becomes the result of the operation. Otherwise, the result of the operation is false
.
• The operation x
||
y
is evaluated as x
?
true
:
y
. In other words, x
is first evaluated and converted to type bool
. Then, if x
is true
, the result of the operation is true
. Otherwise, y
is evaluated and converted to type bool
, and this becomes the result of the operation.
When the operands of &&
or ||
are of types that declare an applicable user-defined operator
&
or operator
|
, both of the following conditions must hold, where T
is the type in which the selected operator is declared:
• The return type and the type of each parameter of the selected operator must be T
. In other words, the operator must compute the logical AND
or the logical OR
of two operands of type T
, and must return a result of type T
.
• T
must contain declarations of operator
true
and operator
false
.
A compile-time error occurs if either of these requirements is not satisfied. Otherwise, the &&
or ||
operation is evaluated by combining the user-defined operator
true
or operator
false
with the selected user-defined operator:
• The operation x
&&
y
is evaluated as T.false(x)
?
x
:
T.&(x,
y)
, where T.false(x)
is an invocation of the operator
false
declared in T
, and T.&(x,
y)
is an invocation of the selected operator
&
. In other words, x
is first evaluated, and operator
false
is invoked on the result to determine if x
is definitely false. Then, if x
is definitely false, the result of the operation is the value previously computed for x
. Otherwise, y
is evaluated, and the selected operator
&
is invoked on the value previously computed for x
and the value computed for y
to produce the result of the operation.
• The operation x
||
y
is evaluated as T.true(x)
?
x
:
T.|(x,
y)
, where T.true(x)
is an invocation of the operator
true
declared in T
, and T.|(x,
y)
is an invocation of the selected operator
|
. In other words, x
is first evaluated, and operator
true
is invoked on the result to determine if x
is definitely true. Then, if x
is definitely true, the result of the operation is the value previously computed for x
. Otherwise, y
is evaluated, and the selected operator
|
is invoked on the value previously computed for x
and the value computed for y
to produce the result of the operation.
In either of these operations, the expression given by x
is evaluated only once, and the expression given by y
either is not evaluated or is evaluated exactly once.
For an example of a type that implements operator
true
and operator
false
, see §11.4.2.
The ??
operator is called the null coalescing operator.
null-coalescing-expression:
conditional-or-expression
conditional-or-expression ??
null-coalescing-expression
A null coalescing expression of the form a
??
b
requires a
to be of a nullable type or reference type. If a
is non-null, the result of a
??
b
is a
; otherwise, the result is b
. The operation evaluates b
only if a
is null.
The null coalescing operator is right-associative, meaning that operations are grouped from right to left. For example, an expression of the form a
??
b
??
c
is evaluated as a
??
(b
??
c)
. In general terms, an expression of the form E1
??
E2
??
… ??
EN
returns the first of the operands that is non-null, or null
if all operands are null.
The type of the expression a
??
b
depends on which implicit conversions are available between the types of the operands. In order of preference, the type of a
??
b
is A0
, A
, or B
, where A
is the type of a
, B
is the type of b
(provided that b
has a type), and A0
is the underlying type of A
if A
is a nullable type, or A
otherwise. Specifically, a
??
b
is processed as follows:
• If A
is not a nullable type or a reference type, a compile-time error occurs.
• If A
is a nullable type and an implicit conversion exists from b
to A0
, the result type is A0
. At runtime, a
is evaluated first. If a
is not null, a
is unwrapped to type A0
, and this becomes the result. Otherwise, b
is evaluated and converted to type A0
, and this becomes the result.
• Otherwise, if an implicit conversion exists from b
to A
, the result type is A
. At runtime, a
is evaluated first. If a
is not null, a
becomes the result. Otherwise, b
is evaluated and converted to type A
, and this becomes the result.
• Otherwise, if b
has a type B
and an implicit conversion exists from A0
to B
, the result type is B
. At runtime, a
is evaluated first. If a
is not null, a
is unwrapped to type A0
(unless A
and A0
are the same type) and converted to type B
, and this becomes the result. Otherwise, b
is evaluated and becomes the result.
• Otherwise, a
and b
are incompatible, and a compile-time error occurs.
The ?:
operator is called the conditional operator (or sometimes the ternary operator).
conditional-expression:
null-coalescing-expression
null-coalescing-expression ?
expression :
expression
A conditional expression of the form b
?
x
:
y
first evaluates the condition b
. Then, if b
is true
, x
is evaluated and becomes the result of the operation. Otherwise, y
is evaluated and becomes the result of the operation. A conditional expression never evaluates both x
and y
.
The conditional operator is right-associative, meaning that operations are grouped from right to left. For example, an expression of the form a ? b : c ? d : e
is evaluated as a ? b : (c ? d : e)
.
The first operand of the ?:
operator must be an expression of a type that can be implicitly converted to bool
, or an expression of a type that implements operator true
. If neither of these requirements is satisfied, a compile-time error occurs.
The second and third operands of the ?:
operator control the type of the conditional expression. Let X
and Y
be the types of the second and third operands. Then,
• If X
and Y
are the same type, then this is the type of the conditional expression.
• Otherwise, if an implicit conversion (§6.1) exists from X
to Y
, but not from Y
to X
, then Y
is the type of the conditional expression.
• Otherwise, if an implicit conversion (§6.1) exists from Y
to X
, but not from X
to Y
, then X
is the type of the conditional expression.
• Otherwise, no expression type can be determined, and a compile-time error occurs.
The runtime processing of a conditional expression of the form b ? x : y
consists of the following steps:
• First, b
is evaluated, and the bool
value of b
is determined:
- If an implicit conversion from the type of b
to bool
exists, then this implicit conversion is performed to produce a bool
value.
- Otherwise, the operator true
defined by the type of b
is invoked to produce a bool
value.
• If the bool
value produced by the previous step is true
, then x
is evaluated and converted to the type of the conditional expression, and this becomes the result of the conditional expression.
• Otherwise, y
is evaluated and converted to the type of the conditional expression, and this becomes the result of the conditional expression.
An anonymous function is an expression that represents an “in-line” method definition. An anonymous function does not have a value in and of itself, but rather is convertible to a compatible delegate or expression tree type. The evaluation of an anonymous function conversion depends on the target type of the conversion: If it is a delegate type, the conversion evaluates to a delegate value referencing the method that the anonymous function defines. If it is an expression tree type, the conversion evaluates to an expression tree that represents the structure of the method as an object structure.
For historical reasons, two syntactic flavors of anonymous functions exist—namely, lambda-expression s and anonymous-method-expression s. For almost all purposes, lambda-expression s are more concise and expressive than anonymous-method-expression s, which remain in the language for backward compatibility.
lambda-expression:
anonymous-function-signature =>
anonymous-function-body
anonymous-method-expression:
delegate
explicit-anonymous-function-signatureopt block
anonymous-function-signature:
explicit-anonymous-function-signature
implicit-anonymous-function-signature
explicit-anonymous-function-signature:
(
explicit-anonymous-function-parameter-listopt )
explicit-anonymous-function-parameter-list
explicit-anonymous-function-parameter
explicit-anonymous-function-parameter-list ,
explicit-anonymous-function-parameter
explicit-anonymous-function-parameter:
anonymous-function-parameter-modifieropt type identifier
anonymous-function-parameter-modifier:
ref
out
implicit-anonymous-function-signature:
(
implicit-anonymous-function-parameter-listopt )
implicit-anonymous-function-parameter
implicit-anonymous-function-parameter-list
implicit-anonymous-function-parameter
implicit-anonymous-function-parameter-list ,
implicit-anonymous-function-parameter
implicit-anonymous-function-parameter:
identifier
anonymous-function-body:
expression
block
The =>
operator has the same precedence as assignment (=
) and is right-associative.
The parameters of an anonymous function in the form of a lambda-expression can be explicitly or implicitly typed. In an explicitly typed parameter list, the type of each parameter is explicitly stated. In an implicitly typed parameter list, the types of the parameters are inferred from the context in which the anonymous function occurs—specifically, when the anonymous function is converted to a compatible delegate type or expression tree type, that type provides the parameter types (§6.5).
In an anonymous function with a single, implicitly typed parameter, the parentheses may be omitted from the parameter list. In other words, an anonymous function of the form
(
param )
=> expr
can be abbreviated to
param =>
expr
The parameter list of an anonymous function in the form of an anonymous-method-expression is optional. If given, the parameters must be explicitly typed. If not, the anonymous function is convertible to a delegate with any parameter list not containing out
parameters.
Some examples of anonymous functions follow:
x => x + 1 // Implicitly typed, expression body
x => { return x + 1; } // Implicitly typed, statement body
(int x) => x + 1 // Explicitly typed, expression body
(int x) => { return x + 1; } // Explicitly typed, statement body
(x, y) => x * y // Multiple parameters
() => Console.WriteLine() // No parameters
delegate (int x) { return x + 1; } // Anonymous method expression
delegate { return 1 + 1; } // Parameter list omitted
The behavior of lambda-expression s and anonymous-method-expressions is the same except for the following points:
• anonymous-method-expression s permit the parameter list to be omitted entirely, yielding convertibility to delegate types of any list of value parameters.
• lambda-expressions permit parameter types to be omitted and inferred, whereas anonymous-method-expression s require parameter types to be explicitly stated.
• The body of a lambda-expression can be an expression or a statement block, whereas the body of an anonymous-method-expression must be a statement block.
• Because only lambda-expression s can have expression bodies, no anonymous-method-expression can be successfully converted to an expression tree type (§4.6).
The optional anonymous-function-signature of an anonymous function defines the names and optionally the types of the formal parameters for the anonymous function. The scope of the parameters of the anonymous function is the anonymous-function-body (§3.7). Together with the parameter list (if given), the anonymous-method-body constitutes a declaration space (§3.3). For this reason, it is a compile-time error for the name of a parameter of the anonymous function to match the name of a local variable, local constant, or parameter whose scope includes the anonymous-method-expression or lambda-expression.
If an anonymous function has an explicit-anonymous-function-signature, then the set of compatible delegate types and expression tree types is restricted to those that have the same parameter types and modifiers in the same order. In contrast to method group conversions (§6.6), contravariance of anonymous function parameter types is not supported. If an anonymous function does not have an anonymous-function-signature, then the set of compatible delegate types and expression tree types is restricted to those that have no out
parameters.
An anonymous-function-signature cannot include attributes or a parameter array. Nevertheless, an anonymous-function-signature may be compatible with a delegate type whose parameter list contains a parameter array.
Note that conversion to an expression tree type, even if compatible, may still fail at compile time (§4.6).
The body ( expression or block ) of an anonymous function is subject to the following rules:
• If the anonymous function includes a signature, the parameters specified in the signature are available in the body. If the anonymous function has no signature, it can be converted to a delegate type or expression type having parameters (§6.5), but the parameters cannot be accessed in the body.
• Except for ref
or out
parameters specified in the signature (if any) of the nearest enclosing anonymous function, it is a compile-time error for the body to access a ref
or out
parameter.
• When the type of this
is a struct type, it is a compile-time error for the body to accessthis
. This is true whether the access is explicit (as in this.x
) or implicit (as in x
where x
is an instance member of the struct). This rule simply prohibits such access and does not affect whether member lookup returns a member of the struct.
• The body has access to the outer variables (§7.14.4) of the anonymous function. Access of an outer variable will reference the instance of the variable that is active at the time the lambda-expression or anonymous-method-expression is evaluated (§7.14.5).
• It is a compile-time error for the body to contain a goto
statement, break
statement, or continue
statement whose target is outside the body or within the body of a contained anonymous function.
• Areturn
statement in the body returns control from an invocation of the nearest enclosing anonymous function, not from the enclosing function member. An expression specified in a return
statement must be compatible with the delegate type or expression tree type to which the nearest enclosing lambda-expression or anonymous-method-expression is converted (§6.5).
It is explicitly unspecified whether there is any way to execute the block of an anonymous function other than through evaluation and invocation of the lambda-expression or anonymous-method-expression. In particular, the compiler may choose to implement an anonymous function by synthesizing one or more named methods or types. The names of any such synthesized elements must be of a form reserved for compiler use.
Anonymous functions in an argument list participate in type inference and overload resolution. Refer to §7.4.2.3 for the exact rules governing their behavior.
The following example illustrates the effect of anonymous functions on overload resolution.
class ItemList<T>: List<T>
{
public int Sum(Func<T, int> selector) {
int sum = 0;
foreach (T item in this) sum += selector(item);
return sum;
}
public double Sum(Func<T, double> selector) {
double sum = 0;
foreach (T item in this) sum += selector(item);
return sum;
}
}
The ItemList<T>
class has two Sum
methods. Each takes a selector
argument, which extracts the value to sum over from a list item. The extracted value can be either an int
or a double
, and the resulting sum is likewise either an int
or a double
.
The Sum
methods could, for example, be used to compute sums from a list of detail lines in some order.
class Detail
{
public int UnitCount;
public double UnitPrice;
…
}
void ComputeSums() {
ItemList<Detail> orderDetails = GetOrderDetails(…);
int totalUnits = orderDetails.Sum(d => d.UnitCount);
double orderTotal = orderDetails.Sum(d => d.UnitPrice * d.UnitCount);
…
}
In the first invocation of orderDetails.Sum
, both Sum
methods are applicable because the anonymous function d => d.UnitCount
is compatible with both Func<Detail, int>
and Func<Detail, double>
. However, overload resolution picks the first Sum
method because the conversion to Func<Detail, int>
is better than the conversion to Func<Detail, double>
.
In the second invocation of orderDetails.Sum
, only the second Sum
method is applicable because the anonymous function d => d.UnitPrice * d.UnitCount
produces a value of type double
. Thus overload resolution picks the second Sum
method for that invocation.
Any local variable, value parameter, or parameter array whose scope includes the lambda-expression or anonymous-method-expression is called an outer variable of the anonymous function. In an instance function member of a class, thethis
value is considered a value parameter and is an outer variable of any anonymous function contained within the function member.
When an outer variable is referenced by an anonymous function, the outer variable is said to have been captured by the anonymous function. Ordinarily, the lifetime of a local variable is limited to execution of the block or statement with which it is associated (§5.1.7). However, the lifetime of a captured outer variable is extended at least until the delegate or expression tree created from the anonymous function becomes eligible for garbage collection.
In the example
using System;
delegate int D();
class Test
{
static D F() {
int x = 0;
D result = () => ++x;
return result;
}
static void Main() {
D d = F();
Console.WriteLine(d());
Console.WriteLine(d());
Console.WriteLine(d());
}
}
the local variable x
is captured by the anonymous function, and the lifetime of x
is extended at least until the delegate returned from F
becomes eligible for garbage collection (which doesn’t happen until the very end of the program). Because each invocation of the anonymous function operates on the same instance of x
, the example produces the following output:
1
2
3
When a local variable or a value parameter is captured by an anonymous function, the local variable or parameter is no longer considered to be a fixed variable (§18.3), but is instead considered to be a moveable variable. Thus any unsafe
code that takes the address of a captured outer variable must first use the fixed
statement to fix the variable.
A local variable is considered to be instantiated when execution enters the scope of the variable. For example, when the following method is invoked, the local variable x
is instantiated and initialized three times—once for each iteration of the loop.
static void F() {
for (int i = 0; i < 3; i++) {
int x = i * 2 + 1;
…
}
}
By comparison, moving the declaration of x
outside the loop results in a single instantiation of x
:
static void F() {
int x;
for (int i = 0; i < 3; i++) {
x = i * 2 + 1;
…
}
}
When not captured, there is no way to observe exactly how often a local variable is instantiated—because the lifetimes of the instantiations are disjoint, it is possible for each instantiation to simply use the same storage location. However, when an anonymous function captures a local variable, the effects of instantiation become apparent.
The example
using System;
delegate void D();
class Test
{
static D[] F() {
D[] result = new D[3];
for (int i = 0; i < 3; i++) {
int x = i * 2 + 1;
result[i] = () => { Console.WriteLine(x); };
}
return result;
}
static void Main() {
foreach (D d in F()) d();
}
}
produces the following output:
1
3
5
When the declaration of x
is moved outside the loop,
static D[] F() {
D[] result = new D[3];
int x;
for (int i = 0; i < 3; i++) {
x = i * 2 + 1;
result[i] = () => { Console.WriteLine(x); };
}
return result;
}
the output changes as follows:
5
5
5
If a for-statement declares an iteration variable, that variable itself is considered to be declared outside of the loop. Thus, if the example is changed to capture the iteration variable itself,
static D[] F() {
D[] result = new D[3];
for (int i = 0; i < 3; i++) {
result[i] = () => { Console.WriteLine(i); };
}
return result;
}
only one instance of the iteration variable is captured, which produces the following output:
3
3
3
It is possible for anonymous function delegates to share some captured variables, yet have separate instances of others. For example, if F
is changed to
static D[] F() {
D[] result = new D[3];
int x = 0;
for (int i = 0; i < 3; i++) {
int y = 0;
result[i] = () => { Console.WriteLine("{0} {1}", ++x, ++y); };
}
return result;
}
the three delegates capture the same instance of x
but separate instances of y
, and the output is as follows:
1 1
2 1
3 1
Separate anonymous functions can capture the same instance of an outer variable. In the example
using System;
delegate void Setter(int value);
delegate int Getter();
class Test
{
static void Main() {
int x = 0;
Setter s = (int value) => { x = value; };
Getter g = () => { return x; };
s(5);
Console.WriteLine(g());
s(10);
Console.WriteLine(g());
}
}
the two anonymous functions capture the same instance of the local variable x
, and they can “communicate” through that variable. This example results in the following output:
5
10
An anonymous function F
must always be converted to a delegate type D
or an expression tree type E
, either directly or through the execution of a delegate creation expression new D(F)
. This conversion determines the result of the anonymous function, as described in §6.5.
Query expressions provide a language-integrated syntax for queries that is similar to relational and hierarchical query languages such as SQL and XQuery.
query-expression:
from-clause query-body
from-clause:
from
typeopt identifier in
expression
query-body:
query-body-clausesopt select-or-group-clause query-continuationopt
query-body-clauses:
query-body-clause
query-body-clauses query-body-clause
query-body-clause:
from-clause
let-clause
where-clause
join-clause
join-into-clause
orderby-clause
let-clause:
let
identifier =
expression
where-clause:
where
boolean-expression
join-clause:
join
typeopt identifier in
expression on
expression equals
expression
join-into-clause:
join
typeopt identifier in
expression on
expression equals
expression into
identifier
orderby-clause:
orderby
orderings
orderings:
ordering
orderings ,
ordering
ordering:
expression ordering-directionopt
ordering-direction:
ascending
descending
select-or-group-clause:
select-clause
group-clause
select-clause:
select
expression
group-clause:
group
expression by
expression
query-continuation:
into
identifier query-body
A query expression begins with a from
clause and ends with either a select
or group
clause. The initial from
clause can be followed by zero or more from
, let
, where
, join
, or orderby
clauses. Each from
clause is a generator introducing a range variable, which ranges over the elements of a sequence. Each let
clause introduces a range variable representing a value computed by means of previous range variables. Each where
clause is a filter that excludes items from the result. Eachjoin
clause compares specified keys of the source sequence with keys of another sequence, yielding matching pairs. Each orderby
clause reorders items according to specified criteria. The finalselect
or group
clause specifies the shape of the result in terms of the range variables. Finally, an into
clause can be used to “splice” queries by treating the results of one query as a generator in a subsequent query.
Query expressions contain a number of “contextual keywords”—that is, identifiers that have special meaning in a given context. Specifically, these contextual keywords are from
, where
, join
, on
, equals
, into
, let
, orderby
, ascending
, descending
, select
, group
, and by
. To avoid ambiguities in query expressions caused by mixed use of these identifiers as keywords and simple names, the identifiers are always considered keywords when they occur anywhere within a query expression.
For this purpose, a query expression is any expression that starts with “from
identifier” followed by any token except “;
”, “=
” , or “,
”.
To use these words as identifiers within a query expression, prefix them with “@
” (§2.4.2).
The C# language does not directly specify the execution semantics of query expressions. Rather, query expressions are translated into invocations of methods that adhere to the query expression pattern (§7.15.3). Specifically, query expressions are translated into invocations of methods named Where
, Select
, SelectMany
, Join
, GroupJoin
, OrderBy
, OrderByDescending
, ThenBy
, ThenByDescending
, GroupBy
, and Cast
. These methods are expected to have particular signatures and result types, as described in §7.15.3. They can be instance methods of the object being queried or extension methods that are external to the object, and they implement the actual execution of the query.
The translation from query expressions to method invocations is a syntactic mapping that occurs before any type binding or overload resolution has been performed. The translation is guaranteed to be syntactically correct, but it is not guaranteed to produce semantically correct C# code. Following translation of query expressions, the resulting method invocations are processed as regular method invocations. This processing may, in turn, uncover errors—for example, if the methods do not exist, if arguments have wrong types, or if the methods are generic and type inference fails.
A query expression is processed by repeatedly applying the following translations until no further reductions are possible. The translations are listed in order of application: Each section assumes that the translations in the preceding sections have been performed exhaustively, and once exhausted, a section will not be revisited later in the processing of the same query expression.
Assignment to range variables is not allowed in query expressions. However, a C# implementation is permitted to not always enforce this restriction, because satisfying this constraint may sometimes not be possible with the syntactic translation scheme presented here.
Certain translations inject range variables with transparent identifiers denoted by *
. The special properties of transparent identifiers are discussed further in §7.15.2.7.
A query expression with a continuation
from … into x…
is translated into
from x in ( from … ) …
The translations in the following sections assume that queries have no into
continuations.
The example
from c in customers
group c by c.Country into g
select new { Country = g.Key, CustCount = g.Count() }
is translated into
from g in
from c in customers
group c by c.Country
select new { Country = g.Key, CustCount = g.Count() }
Its final translation is
customers.
GroupBy(c => c.Country).
Select(g => new { Country = g.Key, CustCount = g.Count() })
A from
clause that explicitly specifies a range variable type
from T x in e
is translated into
from x in ( e ) . Cast < T > ( )
Ajoin
clause that explicitly specifies a range variable type
join T x in e on k1 equals k2
is translated into
join x in ( e ) . Cast < T > ( ) on k1 equals k2
The translations in the following sections assume that queries have no explicit range variable types.
The example
from Customer c in customers
where c.City == "London"
select c
is translated into
from c in customers.Cast<Customer>()
where c.City == "London"
select c
The final translation is
customers.
Cast<Customer>().
Where(c => c.City == "London")
Explicit range variable types are useful for querying collections that implement the non-generic IEnumerable
interface, but not the generic IEnumerable<T>
interface. In the preceding example, this would be the case ifcustomers
were of type ArrayList
.
A query expression of the form
from x in e select x
is translated into
( e ) . Select ( x => x )
The example
from c in customers
select c
is translated into
customers.Select(c => c)
A degenerate query expression is one that trivially selects the elements of the source. A later phase of the translation removes degenerate queries introduced by other translation steps by replacing those queries with their source. In this situation, it is important to ensure that the result of a query expression is never the source object itself, as that would reveal the type and identity of the source to the client of the query. As a consequence, this step protects degenerate queries written directly in source code by explicitly calling Select
on the source. It is then up to the implementers of Select
and other query operators to ensure that these methods never return the source object itself.
A query expression with a second from
clause followed by a select
clause
from x1 in e1
from x2 in e2
select v
is translated into
( e1 ) . SelectMany( x1 => e2 , ( x1 , x2 ) => v )
A query expression with a second from
clause followed by something other than a select
clause
from x1 in e1
from x2 in e2
…
is translated into
from * in ( e1 ) . SelectMany( x1 => e2 , ( x1 , x2 ) => new { x1 , x2 } )
…
A query expression with a let
clause
from x in e
let y = f
…
is translated into
from * in ( e ) . Select ( x => new { x , y = f } )
…
A query expression with a where
clause
from x in e
where f
…
is translated into
from x in ( e ) . Where ( x => f )
…
A query expression with a join
clause without an into
followed by a select
clause
from x1 in e1
join x2 in e2 on k1 equals k2
select v
is translated into
( e1 ) . Join( e2 , x1 => k1 , x2 => k2 , ( x1 , x2 ) => v )
A query expression with a join
clause without an into
followed by something other than a select
clause
from x1 in e1
join x2 in e2 on k1 equals k2
…
is translated into
from * in ( e1 ) . Join(
e2 , x1 => k1 , x2 => k2 , ( x1 , x2 ) => new { x1 , x2 })
…
A query expression with a join
clause with an into
followed by a select
clause
from x1 in e1
join x2 in e2 on k1 equals k2 into g
select v
is translated into
( e1 ) . GroupJoin( e2 , x1 => k1 , x2 => k2 , ( x1 , g ) => v )
A query expression with a join
clause with an into
followed by something other than a select
clause
from x1 in e1
join x2 in e2 on k1 equals k2 into g
…
is translated into
from * in ( e1 ) . GroupJoin(
e2 , x1 => k1 , x2 => k2 , ( x1 , g ) => new { x1 , g })
…
A query expression with an orderby
clause
from x in e
orderby k1 , k2 , … , kn
…
is translated into
from x in ( e ) .
OrderBy ( x => k1 ) .
ThenBy ( x => k2 ) .
….
ThenBy ( x => kn )
…
If an ordering clause specifies a descending
direction indicator, an invocation of OrderBy-Descending
or ThenByDescending
is produced instead.
The following translations assume that there are no let
, where
, join
, or orderby
clauses, and no more than the one initial from
clause in each query expression.
The example
from c in customers
from o in c.Orders
select new { c.Name, o.OrderID, o.Total }
is translated into
customers.
SelectMany(c => c.Orders,
(c, o) => new { c.Name, o.OrderID, o.Total }
)
The example
from c in customers
from o in c.Orders
orderby o.Total descending
select new { c.Name, o.OrderID, o.Total }
is translated into
from * in customers.
SelectMany(c => c.Orders, (c, o) => new { c, o })
orderby o.Total descending
select new { c.Name, o.OrderID, o.Total }
The final translation is
customers.
SelectMany(c => c.Orders, (c, o) => new { c, o }).
OrderByDescending(x => x.o.Total).
Select(x => new { x.c.Name, x.o.OrderID, x.o.Total })
wherex
is a compiler-generated identifier that is otherwise invisible and inaccessible.
The example
from o in orders
let t = o.Details.Sum(d => d.UnitPrice * d.Quantity)
where t >= 1000
select new { o.OrderID, Total = t }
is translated into
from * in orders.
Select(o => new { o, t = o.Details.Sum(d => d.UnitPrice * d.Quantity) })
where t >= 1000
select new { o.OrderID, Total = t }
The final translation is
orders.
Select(o => new { o, t = o.Details.Sum(d => d.UnitPrice * d.Quantity) }).
Where(x => x.t >= 1000).
Select(x => new { x.o.OrderID, Total = x.t })
wherex
is a compiler-generated identifier that is otherwise invisible and inaccessible.
The example
from c in customers
join o in orders on c.CustomerID equals o.CustomerID
select new { c.Name, o.OrderDate, o.Total }
is translated into
customers.Join(orders, c => c.CustomerID, o => o.CustomerID,
(c, o) => new { c.Name, o.OrderDate, o.Total })
The example
from c in customers
join o in orders on c.CustomerID equals o.CustomerID into co
let n = co.Count()
where n >= 10
select new { c.Name, OrderCount = n }
is translated into
from * in customers.
GroupJoin(orders, c => c.CustomerID, o => o.CustomerID,
(c, co) => new { c, co })
let n = co.Count()
where n >= 10
select new { c.Name, OrderCount = n }
The final translation is
customers.
GroupJoin(orders, c => c.CustomerID, o => o.CustomerID,
(c, co) => new { c, co }).
Select(x => new { x, n = x.co.Count() }).
Where(y => y.n >= 10).
Select(y => new { y.x.c.Name, OrderCount = y.n)
wherex
and y
are compiler-generated identifiers that are otherwise invisible and inaccessible.
from o in orders
orderby o.Customer.Name, o.Total descending
select o
has the final translation
orders.
OrderBy(o => o.Customer.Name).
ThenByDescending(o => o.Total)
A query expression of the form
from x in e select v
is translated into
( e ) . Select ( x => v )
except when v is the identifier x. In the latter case, the translation is simply
( e )
For example,
from c in customers.Where(c => c.City == "London")
select c
is simply translated into
customers.Where(c => c.City == "London")
A query expression of the form
from x in e group v by k
is translated into
( e ) . GroupBy ( x => k , x => v )
except when v is the identifier x. In the latter case, the translation is
( e ) . GroupBy ( x => k )
The example
from c in customers
group c.Name by c.Country
customers.
GroupBy(c => c.Country, c => c.Name)
Certain translations inject range variables with transparent identifiers denoted by *
. Transparent identifiers are not a proper language feature; they exist only as an intermediate step in the query expression translation process.
When a query translation injects a transparent identifier, further translation steps propagate the transparent identifier into anonymous functions and anonymous object initializers. In those contexts, transparent identifiers have the following behavior:
• When a transparent identifier occurs as a parameter in an anonymous function, the members of the associated anonymous type are automatically in scope in the body of the anonymous function.
• When a member with a transparent identifier is in scope, the members of that member are in scope as well.
• When a transparent identifier occurs as a member declarator in an anonymous object initializer, it introduces a member with a transparent identifier.
In the translation steps described earlier, transparent identifiers are always introduced together with anonymous types, with the intent of capturing multiple range variables as members of a single object. An implementation of C# is permitted to use a different mechanism than anonymous types to group together multiple range variables. The following translation examples assume that anonymous types are used, and show how transparent identifiers can be translated away.
The example
from c in customers
from o in c.Orders
orderby o.Total descending
select new { c.Name, o.Total }
is translated into
from * in customers.
SelectMany(c => c.Orders, (c, o) => new { c, o })
orderby o.Total descending
select new { c.Name, o.Total }
which is further translated into
customers.
SelectMany(c => c.Orders, (c, o) => new { c, o }).
OrderByDescending(* => o.Total).
Select(* => new { c.Name, o.Total })
When transparent identifiers are erased, the final translation is equivalent to
customers.
SelectMany(c => c.Orders, (c, o) => new { c, o }).
OrderByDescending(x => x.o.Total).
Select(x => new { x.c.Name, x.o.Total })
wherex
is a compiler-generated identifier that is otherwise invisible and inaccessible.
The example
from c in customers
join o in orders on c.CustomerID equals o.CustomerID
join d in details on o.OrderID equals d.OrderID
join p in products on d.ProductID equals p.ProductID
select new { c.Name, o.OrderDate, p.ProductName }
is translated into
from * in customers.
Join(orders, c => c.CustomerID, o => o.CustomerID,
(c, o) => new { c, o })
join d in details on o.OrderID equals d.OrderID
join p in products on d.ProductID equals p.ProductID
select new { c.Name, o.OrderDate, p.ProductName }
which is further reduced to
customers.
Join(orders, c => c.CustomerID, o => o.CustomerID, (c, o) => new { c, o }).
Join(details, * => o.OrderID, d => d.OrderID, (*, d) => new { *, d }).
Join(products, * => d.ProductID, p => p.ProductID, (*, p) => new { y, p }).
Select(* => new { c.Name, o.OrderDate, p.ProductName })
The final translation is
customers.
Join(orders, c => c.CustomerID, o => o.CustomerID,
(c, o) => new { c, o }).
Join(details, x => x.o.OrderID, d => d.OrderID,
(x, d) => new { x, d }).
Join(products, y => y.d.ProductID, p => p.ProductID,
(y, p) => new { y, p }).
Select(z => new { z.y.x.c.Name, z.y.x.o.OrderDate, z.p.ProductName })
wherex
, y
, and z
are compiler-generated identifiers that are otherwise invisible and inaccessible.
The query expression pattern establishes a pattern of methods that types can implement to support query expressions. Because query expressions are translated to method invocations by means of a syntactic mapping, types have considerable flexibility in how they implement the query expression pattern. For example, the methods of the pattern can be implemented as instance methods or as extension methods because both kinds of methods have the same invocation syntax. Likewise, the methods can request delegates or expression trees because anonymous functions are convertible to both.
The recommended shape of a generic type C<T>
that supports the query expression pattern is shown below. A generic type is used to illustrate the proper relationships between parameter and result types, but it is possible to implement the pattern for nongeneric types as well.
delegate R Func<T1,R>(T1 arg1);
delegate R Func<T1,T2,R>(T1 arg1, T2 arg2);
class C
{
public C<T> Cast<T>();
}
class C<T> : C
{
public C<T> Where(Func<T, bool> predicate);
public C<U> Select<U>(Func<T, U> selector);
public C<V> SelectMany<U, V>(Func<T, C<U>> selector,
Func<T, U, V> resultSelector);
public C<V> Join<U,K,V>(C<U> inner, Func<T, K> outerKeySelector,
Func<U, K> innerKeySelector, Func<T, U, V> resultSelector);
public C<V> GroupJoin<U, K, V>(C<U> inner, Func<T, K> outerKeySelector,
Func<U, K> innerKeySelector, Func<T, C<U>,V> resultSelector);
public O<T> OrderBy<K>(Func<T, K> keySelector);
public O<T> OrderByDescending<K>(Func<T, K> keySelector);
public C<G<K, T>> GroupBy<K>(Func<T, K> keySelector);
public C<G<K, E>> GroupBy<K, E>(Func<T, K> keySelector,
Func<T, E> elementSelector);
}
class O<T> : C<T>
{
public O<T> ThenBy<K>(Func<T, K> keySelector);
public O<T> ThenByDescending<K>(Func<T, K> keySelector);
}
class G<K, T> : C<T>
{
public K Key { get; }
}
These methods use the generic delegate types Func<T1, R>
and Func<T1, T2, R>
, but they could equally well have used other delegate or expression tree types with the same relationships in parameter and result types.
Notice the recommended relationship between C<T>
and O<T>
, which ensures that the ThenBy
and ThenByDescending
methods are available only on the result of an OrderBy
or OrderByDescending
. Also notice the recommended shape of the result of GroupBy
—a sequence of sequences, where each inner sequence has an additional Key
property.
The System.Linq
namespace provides an implementation of the query operator pattern for any type that implements theSystem.Collections.Generic.IEnumerable<T>
interface.
The assignment operators assign a new value to a variable, a property, an event, or an indexer element.
assignment:
unary-expression assignment-operator expression
assignment-operator:
=
+=
-=
*=
/=
%=
&=
|=
^=
<<=
right-shift-assignment
The left operand of an assignment must be an expression classified as a variable, a property access, an indexer access, or an event access.
The =
operator is called the simple assignment operator. It assigns the value of the right operand to the variable, property, or indexer element given by the left operand. The left operand of the simple assignment operator may not be an event access (except as described in §10.8.1). The simple assignment operator is described in §7.16.1.
The assignment operators other than the =
operator are called compound assignment operators. These operators perform the indicated operation on the two operands, and then assign the resulting value to the variable, property, or indexer element given by the left operand. The compound assignment operators are described in §7.16.2.
The +=
and -=
operators with an event access expression as the left operand are called event assignment operators. No other assignment operator is valid with an event access as the left operand. The event assignment operators are described in §7.16.3.
The assignment operators are right-associative, meaning that operations are grouped from right to left. For example, an expression of the form a = b = c
is evaluated as a =
(b = c)
.
The =
operator is called the simple assignment operator. In a simple assignment, the right operand must be an expression of a type that is implicitly convertible to the type of the left operand. The operation assigns the value of the right operand to the variable, property, or indexer element given by the left operand.
The result of a simple assignment expression is the value assigned to the left operand. The result has the same type as the left operand and is always classified as a value.
If the left operand is a property or indexer access, the property or indexer must have a set
accessor. If this is not the case, a compile-time error occurs.
The runtime processing of a simple assignment of the form x = y
consists of the following steps:
• If x
is classified as a variable:
- x
is evaluated to produce the variable.
- y
is evaluated and, if required, converted to the type of x
through an implicit conversion (§6.1).
- If the variable given by x
is an array element of a reference-type, a runtime check is performed to ensure that the value computed for y
is compatible with the array instance of which x
is an element. The check succeeds if y
is null
, or if an implicit reference conversion (§6.1.6) exists from the actual type of the instance referenced by y
to the actual element type of the array instance containing x
. Otherwise, a System. ArrayTypeMismatchException
is thrown.
- The value resulting from the evaluation and conversion of y
is stored into the location given by the evaluation of x
.
• If x
is classified as a property or indexer access:
- The instance expression (if x
is not static
) and the argument list (if x
is an indexer access) associated with x
are evaluated, and the results are used in the subsequent set
accessor invocation.
- y
is evaluated and, if required, converted to the type of x
through an implicit conversion (§6.1).
- The set
accessor of x
is invoked with the value computed for y
as its value
argument.
The array covariance rules (§12.5) permit a value of an array typeA[]
to be a reference to an instance of an array type B[ ]
, provided an implicit reference conversion exists from B
to A
. Because of these rules, assignment to an array element of a reference-type requires a runtime check to ensure that the value being assigned is compatible with the array instance. In the example
string[ ] sa = new string[10];
object[ ] oa = sa;
oa[0] = null; // Okay
oa[1] = "Hello"; // Okay
oa[2] = new ArrayList(); // ArrayTypeMismatchException
the last assignment causes a System.ArrayTypeMismatchException
to be thrown because an instance of ArrayList
cannot be stored in an element of a string[ ]
.
When a property or indexer declared in a struct-type is the target of an assignment, the instance expression associated with the property or indexer access must be classified as a variable. If the instance expression is classified as a value, a compile-time error occurs. Because of the points raised in §7.5.4, the same rule also applies to fields.
Given the declarations:
struct Point
{
int x, y;
public Point(int x, int y) {
this.x = x;
this.y = y;
}
public int X {
get { return x; }
set { x = value;}
}
public int Y {
get { return y; }
set { y = value; }
}
}
struct Rectangle
{
Point a, b;
public Rectangle(Point a, Point b) {
this.a = a;
this.b = b;
}
public Point A {
get { return a; }
set { a = value; }
}
public Point B {
get { return b; }
set { b = value; }
}
}
in the example
Point p = new Point();
p.X = 100;
p.Y = 100;
Rectangle r = new Rectangle();
r.A = new Point(10, 10);
r.B = p;
the assignments to p.X
, p.Y
, r.A
, and r.B
are permitted because p
and r
are variables. However, in the example
Rectangle r = new Rectangle();
r.A.X = 10;
r.A.Y = 10;
r.B.X = 100;
r.B.Y = 100;
the assignments are all invalid, because r.A
and r.B
are not variables.
An operation of the form x
op= y
is processed by applying binary operator overload resolution (§7.2.4) as if the operation was written x
op y
. Then,
If the return type of the selected operator is implicitly convertible to the type of x
, the operation is evaluated as x = x
op y
, except that x
is evaluated only once.
• Otherwise, if the selected operator is a predefined operator, if the return type of the selected operator is explicitly convertible to the type of x
, and if y
is implicitly convertible to the type of x
or the operator is a shift operator, then the operation is evaluated as x =
(T)(x
op y)
, where T
is the type of x
, except that x
is evaluated only once.
• Otherwise, the compound assignment is invalid, and a compile-time error occurs.
The term “evaluated only once” means that in the evaluation of x
op y
, the results of any constituent expressions of x
are temporarily saved and then reused when performing the assignment to x
. For example, in the assignment A()[B()]
+= C()
, where A
is a method returning int[ ]
, and B
and C
are methods returning int
, the methods are invoked only once, in the order A
, B
, C
.
When the left operand of a compound assignment is a property access or indexer access, the property or indexer must have both a get
accessor and a set
accessor. If this is not the case, a compile-time error occurs.
The second rule permits x
op= y
to be evaluated as x = (T)(x
op y)
in certain contexts. The rule exists such that the predefined operators can be used as compound operators when the left operand is of type sbyte
, byte
, short
, ushort
, or char
. Even when both arguments are of one of those types, the predefined operators produce a result of type int
, as described in §7.2.6.2. Thus, without a cast, it would not be possible to assign the result to the left operand.
The intuitive effect of the rule for predefined operators is simply that x
op= y
is permitted if both of x
op y
and x = y
are permitted. In the example
byte b = 0;
char ch = ' ';
int i = 0;
b += 1; // Okay
b += 1000; // Error, b = 1000 not permitted
b += i; // Error, b = i not permitted
b += (byte)i; // Okay
ch += 1; // Error, ch = 1 not permitted
ch += (char)1; // Okay
the intuitive reason for each error is that a corresponding simple assignment would also have been an error.
This also means that compound assignment operations support lifted operations. In the example
int? i = 0;
i += 1; // Okay
the lifted operator +(int?,int?)
is used.
If the left operand of a +=
or -=
operator is classified as an event access, then the expression is evaluated as follows:
• The instance expression, if any, of the event access is evaluated.
• The right operand of the +=
or -=
operator is evaluated and, if required, converted to the type of the left operand through an implicit conversion (§6.1).
• An event accessor of the event is invoked, with an argument list consisting of the right operand, after evaluation and, if necessary, conversion. If the operator was+=
, the add
accessor is invoked; if the operator was -=
, the remove
accessor is invoked.
An event assignment expression does not yield a value. Thus an event assignment expression is valid only in the context of a statement-expression (§8.6).
An expression is either a non-assignment-expression or an assignment.
expression:
non-assignment-expression
assignment
non-assignment-expression:
conditional-expression
lambda-expression
query-expression
A constant-expression is an expression that can be fully evaluated at compile time.
constant-expression:
expression
A constant expression must be the null
literal or a value with one of the following types: sbyte
, byte
, short
, ushort
, int
, uint
, long
, ulong
, char
, float
, double
, decimal
, bool
, string
, or any enumeration type. Only the following constructs are permitted in constant expressions:
• Literals (including the null
literal)
• References to const
members of class and struct types
• References to members of enumeration types
• References to const
parameters or local variables
• Parenthesized subexpressions, which are themselves constant expressions
• Cast expressions, provided the target type is one of the types listed above
• checked
and unchecked
expressions
• Default value expressions
• The predefined +
, !
, and ~
unary operators
• The predefined +
, *
, /
, %
, <<
, >>
, &
, |
, ^
, &&
, ||
, ==
, !=
, <
, >
, <=
, and >=
binary operators, provided each operand is of a type listed above
• The ?:
conditional operator
The following conversions are permitted in constant expressions:
• Identity conversions
• Numeric conversions
• Enumeration conversions
• Constant expression conversions
• Implicit and explicit reference conversions, provided that the source of the conversions is a constant expression that evaluates to the null
value
Other conversions including boxing, unboxing, and implicit reference conversions of non-null values are not permitted in constant expressions. For example,
class C {
const object i = 5; // Error: boxing conversion not permitted
const object str = "hello"; // Error: implicit reference conversion
}
In this example, the initialization of i
is an error because a boxing conversion is required. The initialization of str
is an error because an implicit reference conversion from a non-null value is required.
Whenever an expression fulfills the requirements listed above, the expression is evaluated at compile time. This is true even if the expression is a subexpression of a larger expression that contains nonconstant constructs.
The compile-time evaluation of constant expressions uses the same rules as runtime evaluation of nonconstant expressions, except that where runtime evaluation would have thrown an exception, compile-time evaluation causes a compile-time error to occur.
Unless a constant expression is explicitly placed in an unchecked
context, overflows that occur in integral-type arithmetic operations and conversions during the compile-time evaluation of the expression always cause compile-time errors (§7.18).
Constant expressions occur in the contexts listed below. In these contexts, a compile-time error occurs if an expression cannot be fully evaluated at compile time.
• Constant declarations (§10.4)
• Enumeration member declarations (§14.3)
• case
labels of a switch
statement (§8.7.2)
• goto case
statements (§8.9.3)
• Dimension lengths in an array creation expression (§7.5.10.4) that includes an initializer
• Attributes (§17)
An implicit constant expression conversion (§6.1.8) permits a constant expression of type int
to be converted to sbyte
, byte
, short
, ushort
, uint
, or ulong
, provided the value of the constant expression is within the range of the destination type.
A boolean-expression is an expression that yields a result of type bool
, either directly or through application of operator true
in certain contexts as specified in the following discussion.
boolean-expression:
expression
The controlling conditional expression of an if-statement (§8.7.1), while-statement (§8.8.1), do-statement (§8.8.2), or for-statement (§8.8.3) is a boolean-expression. The controlling conditional expression of the ?:
operator (§7.13) follows the same rules as a boolean-expression, but for reasons of operator precedence is classified as a conditional-or-expression.
A boolean-expression is required to be of a type that can be implicitly converted to bool
or of a type that implements operator true
. If neither requirement is satisfied, a compile-time error occurs.
When a boolean expression is of a type that cannot be implicitly converted to bool
but does implement operator true
, then following evaluation of the expression, the operator true
implementation provided by that type is invoked to produce a bool
value.
The DBBool
struct type in §11.4.2 provides an example of a type that implements operator true
and operator false
.