Difference between C# and Java's ternary operator (? :)
I am a C# newbie and I just encounter a problem. There is a difference between C# and Java when dealing with the ternary operator (? :
).
In the following code segment, why does the 4th line not work? The compiler shows an error message of there is no implicit conversion between 'int' and 'string'
. The 5th line does not work as well. Both List
s are objects, aren't they?
int two = 2;
double six = 6.0;
Write(two > six ? two : six); //param: double
Write(two > six ? two : "6"); //param: not object
Write(two > six ? new List<int>() : new List<string>()); //param: not object
However, the same code works in Java:
int two = 2;
double six = 6.0;
System.out.println(two > six ? two : six); //param: double
System.out.println(two > six ? two : "6"); //param: Object
System.out.println(two > six ? new ArrayList<Integer>()
: new ArrayList<String>()); //param: Object
What language feature in C# is missing? If any, why is it not added?
Looking through the C# 5 Language Specification section 7.14: Conditional Operator we can see the following:
If x has type X and y has type Y then
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.
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
In other words: it tries to find whether or not x and y can be converted to eachother and if not, a compilation error occurs. In our case int
and string
have no explicit or implicit conversion so it won't compile.
Contrast this with the Java 7 Language Specification section 15.25: Conditional Operator:
- If the second and third operands have the same type (which may be the null type), then that is the type of the conditional expression. (NO)
- If one of the second and third operands is of primitive type T, and the type of the other is the result of applying boxing conversion (§5.1.7) to T, then the type of the conditional expression is T. (NO)
- If one of the second and third operands is of the null type and the type of the other is a reference type, then the type of the conditional expression is that reference type. (NO)
- Otherwise, if the second and third operands have types that are convertible (§5.1.8) to numeric types, then there are several cases: (NO)
- Otherwise, the second and third operands are of types S1 and S2 respectively. Let T1 be the type that results from applying boxing conversion to S1, and let T2 be the type that results from applying boxing conversion to S2.
The type of the conditional expression is the result of applying capture conversion (§5.1.10) to lub(T1, T2) (§15.12.2.7). (YES)
And, looking at section 15.12.2.7. Inferring Type Arguments Based on Actual Arguments we can see it tries to find a common ancestor that will serve as the type used for the call which lands it with Object
. Object
is an acceptable argument so the call will work.
The given answers are good; I would add to them that this rule of C# is a consequence of a more general design guideline. When asked to infer the type of an expression from one of several choices, C# chooses the unique best of them. That is, if you give C# some choices like "Giraffe, Mammal, Animal" then it might choose the most general -- Animal -- or it might choose the most specific -- Giraffe -- depending on the circumstances. But it must choose one of the choices it was actually given. C# never says "my choices are between Cat and Dog, therefore I will deduce that Animal is the best choice". That wasn't a choice given, so C# cannot choose it.
In the case of the ternary operator C# tries to choose the more general type of int and string, but neither is the more general type. Rather than picking a type that was not a choice in the first place, like object, C# decides that no type can be inferred.
I note also that this is in keeping with another design principle of C#: if something looks wrong, tell the developer. The language does not say "I'm going to guess what you meant and muddle on through if I can". The language says "I think you've written something confusing here, and I'm going to tell you about that."
Also, I note that C# does not reason from the variable to the assigned value, but rather the other direction. C# does not say "you're assigning to an object variable therefore the expression must be convertible to object, therefore I will make sure that it is". Rather, C# says "this expression must have a type, and I must be able to deduce that the type is compatible with object". Since the expression does not have a type, an error is produced.
Regarding the generics part:
two > six ? new List<int>() : new List<string>()
In C#, the compiler tries to convert the right-hand expression parts to some common type; since List<int>
and List<string>
are two distinct constructed types, one can't be converted to the other.
In Java, the compiler tries to find a common supertype instead of converting, so the compilation of the code involves the implicit use of wildcards and type erasure;
two > six ? new ArrayList<Integer>() : new ArrayList<String>()
has the compile type of ArrayList<?>
(actually, it can be also ArrayList<? extends Serializable>
or ArrayList<? extends Comparable<?>>
, depending on use context, since they are both common generic supertypes) and runtime type of raw ArrayList
(since it's the common raw supertype).
For example (test it yourself),
void test( List<?> list ) {
System.out.println("foo");
}
void test( ArrayList<Integer> list ) { // note: can't use List<Integer> here
// since both test() methods would clash after the erasure
System.out.println("bar");
}
void test() {
test( true ? new ArrayList<Object>() : new ArrayList<Object>() ); // foo
test( true ? new ArrayList<Integer>() : new ArrayList<Object>() ); // foo
test( true ? new ArrayList<Integer>() : new ArrayList<Integer>() ); // bar
} // compiler automagically binds the correct generic QED
In both Java and C# (and most other languages), the result of an expression has a type. In the case of the ternary operator, there are two possible subexpressions evaluated for the result and both must have the same type. In the case of Java, an int
variable can be converted to an Integer
by autoboxing. Now since both Integer
and String
inherit from Object
, they can be converted to the same type by a simple narrowing conversion.
On the other hand, in C#, an int
is a primitive and there is not implicit conversion to string
or any other object
.