Tim said:
The rule is that the types in any part of an expression are promoted to the
widest of the participants in that part.
Sort of. That is, that's not actually the rule, but if you qualify your
statement as "the rule is, as long as no compile-time error occurs,
generally speaking the types are promoted to the widest..." then it's
reasonably close enough to have predictive value with respect to what
the eventual type of the expression will be (*).
(*) (but even there, there's an exception...see below for what happens
with "uint" when mixed with signed data types)
The _actual_ rule is that numeric promotion is done during operator
overload resolution, and types are promoted only as necessary and
possible in order to allow the operands to be used in a particular
overload of an operator.
The C# specification actually enumerates the rules in the context of
binary operators as follows (see "7.2.6.2 Binary numeric promotions"):
Binary numeric promotion consists of applying
the following rules, in the order they appear here:
• If either operand is of type decimal, the
other operand is converted to type decimal,
or a compile-time error occurs if the other
operand is of type float or double.
• Otherwise, if either operand is of type
double, the other operand is converted to
type double.
• Otherwise, if either operand is of type
float, the other operand is converted to
type float.
• Otherwise, if either operand is of type
ulong, the other operand is converted to
type ulong, or a compile time error occurs
if the other operand is of type sbyte, short,
int, or long.
• Otherwise, if either operand is of type
long, the other operand is converted to
type long.
• Otherwise, if either operand is of type
uint and the other operand is of type sbyte,
short, or int, both operands are converted
to type long.
• Otherwise, if either operand is of type
uint, the other operand is converted to type
uint.
• Otherwise, both operands are converted to
type int.
But note that these are simply the outcome of the available implicit
conversion rules, as applied in the context of operator overloading.
I.e. matching the operands to the available overloads for the operator
in question, and selecting the one that matches best according to the
implicit conversions that exist.
Of particular note: "decimal" is technically "wider than" "double" or
"float", but there is no implicit conversion from decimal to either
double or float. Mixing decimal with double or float doesn't result in
the expression being of type decimal; instead, you just get a compile
time error.
Note also that since there's no built-in type that can allow promotion
of any signed types when used with "ulong", because the compiler would
have to promote both the "ulong" and the signed type to something even
wider than the "ulong", and there is no such type in C#. (On the other
hand, if one of the operands is "uint", the compiler _can_ promote
everything to "long" and preserve the values, so "uint" does work fine
with signed types).
As you can see, it's not sufficient to look simply at the width of the
type. The promotion may actually be to something wider than _any_ of
the operands involved. And other factors may come into play that
prevent numeric promotion, depending on the exact relationship between
the types of the operands.
However, in cases where those factors do come into play, the end result
is simply a failure to compile the expression. So, at the very least
you can be assured that if it does compile, the compiler has selected
some kind of implicit conversion (which does generally involve widening
at least one of the operands, if not both) that is guaranteed to
preserve the values in question, and which matches some available
overload of the operator being used.
In that sense, with those caveats, yes...one can reinterpret the rule to
be based on the ability to widen one or both of the operands numeric
data type.
But it's important to keep in mind that that's not precisely the rule.
The rule is more explicit than that, and is dependent not specifically
on widening data types, but rather the interaction between the implicit
conversion rules and the overload resolution rules.
Note also that this means there are consistent results when dealing with
user-defined operators and implicit conversions. They will follow the
same rules as the built-in conversions and operators, and of course
user-defined conversions may or may not be tied to whether a data type
is widened or not. Depending on the conversion, it's entirely possible
that the idea of "widening" doesn't even apply, but the operator
overload resolution will still happen in a predictable, well-defined way.
For example:
Single result = 3 * 4 / 5.0 + 6;
[...]
Finally, that double result is truncated back to a single for the
assignment.
No. There's no implicit conversion from double to Single (i.e.
"float"). If you don't cast the expression, a compile time error will
occur.
Pete