95

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 Lists 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?

Quill
  • 2,729
  • 1
  • 33
  • 44
blackr1234
  • 1,420
  • 12
  • 23
  • What is the result of the Java version? – Code-Apprentice Feb 05 '16 at 18:17
  • @Code-Apprentice The compiler does not show any problem. ``println(Object x)`` is used in the 4th line. – blackr1234 Feb 05 '16 at 18:18
  • That only says what the result is **not**...which doesn't answer my question. – Code-Apprentice Feb 05 '16 at 18:19
  • 4
    As per https://msdn.microsoft.com/en-us/library/ty67wk28.aspx "Either the type of first_expression and second_expression must be the same, or an implicit conversion must exist from one type to the other." – Egor Feb 05 '16 at 18:19
  • 2
    I haven't done C# in a while, but doesn't the message you quote say it? The two branches of the ternary have to return the same data type. You're giving it an int and a String. C# doesn't know how to automagically convert an int to a String. You need to put an explicit type conversion on it. – Jay Feb 05 '16 at 18:20
  • @Egor I actually read that before asking the question. But I do not know why C# does not use Write(object) as Java does. – blackr1234 Feb 05 '16 at 18:21
  • 2
    @blackr1234 Because an `int` is not an object. It's a primitive. – Code-Apprentice Feb 05 '16 at 18:22
  • I am just guessing, but perhaps C# does not have auto-boxing. – RaminS Feb 05 '16 at 18:24
  • Now you have introduced another complexity: generics. I'm willing to bet that the rules governing generics are very different between C# and Java. In Java, we have type erasure which probably plays a role here. – Code-Apprentice Feb 05 '16 at 18:52
  • 3
    @Code-Apprentice yes they are [very different indeed](http://stackoverflow.com/a/34833268/3764814) - C# has runtime reification, so the lists are of unrelated types. Oh, and you could also throw generic interface covariance/contravariance into the mix if you wanted to, to introduce some level of relationship ;) – Lucas Trzesniewski Feb 05 '16 at 23:34
  • 3
    For the OP's benefit: type erasure in Java means that `ArrayList` and `ArrayList` become just `ArrayList` in the bytecode. This means that they appear to be **the exact same type** at run-time. Apparently in C# they are different types. – Code-Apprentice Feb 05 '16 at 23:38
  • 1
    As someone who is reasonably adept with both C# and Java I much prefer the way C# handles generics (i.e. with reification). Working with (read: against) Java's type erasure was a real pain. I kept trying to refer to the type parameter and not being able to do what I wanted because the type parameter gets erased so you can't ply it for information. – Pharap Feb 06 '16 at 06:29

5 Answers5

107

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.

Jeroen Vannevel
  • 43,651
  • 22
  • 107
  • 170
  • 1
    I read the C# specification as saying there must be an implicit conversion in at least one direction. The compiler error occurs only when there is no implicit conversion in either direction. – Code-Apprentice Feb 05 '16 at 18:56
  • 1
    Of course, this is still the most complete answer since you quote directly from the specifications. – Code-Apprentice Feb 05 '16 at 18:56
  • This was actually only changed with Java 5 (I think, might have been 6). Before that Java had exactly the same behavior as C#. Due to the way enums are implemented this probably caused even more surprises in Java than C# though so a good change all along. – Voo Feb 05 '16 at 19:34
  • @Voo: FYI, Java 5 and Java 6 have the same spec version (*The Java Language Specification*, Third Edition), so it definitely didn't change in Java 6. – ruakh Feb 07 '16 at 21:16
87

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.

Eric Lippert
  • 647,829
  • 179
  • 1,238
  • 2,067
  • 6
    I got past the first paragraph wondering why the sudden covariance/contravariance interest, then I started to agree more on the second paragraph, then I saw who wrote the answer... Should have guessed sooner. Have you ever noticed [exactly how much you use 'Giraffe' as an example](https://www.google.co.uk/search?q=Eric+Lippert+Giraffe&ie=utf-8&oe=utf-8&gws_rd=cr&ei=vpO1VuTJCMGra4mqobgN)? You must really like Giraffes. – Pharap Feb 06 '16 at 06:36
  • 21
    Giraffes are awesome! – Eric Lippert Feb 06 '16 at 06:39
  • Can't argue with that. Ask O'Reilly to reserve it for you just in case to decide to add to their [programming animal menagerie](http://www.oreilly.com/animals.html). It may never happen, *but just in case*. – Pharap Feb 06 '16 at 06:51
  • 10
    @Pharap: In all seriousness, there are pedagogical reasons why I use giraffe so much. First of all, giraffes are well-known around the world. Second, it's a kind of fun-to-say word, and they are so odd looking that it's a memorable image. Third, using, say, "giraffe" and "turtle" in the same analogy emphasizes strongly "these two classes, though they may share a base class, are very different animals with very different characteristics". Questions about type systems often have examples where the types are logically very similar, which drives your intuition the wrong way. – Eric Lippert Feb 06 '16 at 14:56
  • 8
    @Pharap: That is, the question is usually something like "why can't a list of customer invoice provider factories be used as a list of invoice provider factories?" and your intuition says "yeah, those are basically the same thing", but when you say "why can't a room full of giraffes be used as a room full of mammals?" then it becomes much more of an intuitive analogy to say "because a room full of mammals can contain a tiger, a room full of giraffes cannot". – Eric Lippert Feb 06 '16 at 14:58
  • Good explanation of the reasoning behind it. The people writing the Java spec probably agreed with this too since that's what they implemented up to Java 1.4. The change with Java 5 was probably due to the way enums were implemented. It would be highly unintuitive that `MyEnum x = foo ? MyEnum.Bar : MyEnum.Baz` should fail to compile, particularly with a error message about there being different types. Since C# does not have this particular problem, the issue comes up rarely and in those cases it's probably a good idea to be explicit anyhow. – Voo Feb 06 '16 at 21:23
  • 2
    Honestly I consider this a design mistake. It makes the ternery operator annoying because I constantly have to explicitly write conversions the compiler could easily imply. I can't think of a case where this saves the developer from shooting themselves in the foot, it's just annoying. – BlueRaja - Danny Pflughoeft Feb 07 '16 at 00:19
  • @BlueRaja-DannyPflughoeft: Can you give an example of a typical realistic scenario where you have to insert a conversion? – Eric Lippert Feb 07 '16 at 00:24
  • 7
    @Eric I originally ran into this problem with `int? b = (a != 0 ? a : (int?) null)`. There's also [this question](http://stackoverflow.com/questions/2215745), along with all the linked questions on the side. If you continue following the links, there's quite a few. In comparison, I've never heard of someone running into a real-world issue with the way Java does it. – BlueRaja - Danny Pflughoeft Feb 07 '16 at 00:38
  • 5
    @BlueRaja-DannyPflughoeft: I agree that your scenario is both common and irritating. It might be worthwhile to add a special case to the language that says that in cases where types are being inferred, if one of the expressions is null then nullable versions of any value types involved in the inference automatically become candidates. – Eric Lippert Feb 07 '16 at 14:10
24

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
Mathieu Guindon
  • 69,817
  • 8
  • 107
  • 235
  • Actually ``Write(two > six ? new List() : new List());`` does not work either. – blackr1234 Feb 05 '16 at 19:06
  • 2
    @blackr1234 exactly: I didn't want to repeat what other answers already said, but the ternary expression needs to evaluate to a type, and the compiler won't attempt to find the "lowest common denominator" of the two. – Mathieu Guindon Feb 05 '16 at 19:08
  • 2
    Actually, this is not about type erasure, but about wildcard types. The erasures of `ArrayList` and `ArrayList` would be `ArrayList` (the raw type), but the inferred type for this ternary operator is `ArrayList>` (the wildcard type). More generally, that the Java *runtime* implements generics through type erasure has no effect on the *compile time* type of an expression. – meriton Feb 06 '16 at 00:43
  • 1
    @vaxquis thanks! I'm a C# guy, Java generics confuse me ;-) – Mathieu Guindon Feb 06 '16 at 01:14
  • 2
    Actually, the type of `two > six ? new ArrayList() : new ArrayList()` is `ArrayList extends Serializable&Comparable>>` which means you may assign it to a variable of type `ArrayList extends Serializable>` as well as a variable of type `ArrayList extends Comparable>>`, but of course `ArrayList>`, which is equivalent to `ArrayList extends Object>`, works as well. – Holger Feb 06 '16 at 19:02
  • @Holger +1 for spotting that (I, for one, completely forgotten that `String` implements `Comparable` BTW!), IFTFY. –  Feb 07 '16 at 16:52
6

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.

Code-Apprentice
  • 81,660
  • 23
  • 145
  • 268
  • Thanks for the answer. Autoboxing is what I am currently thinking of. Doesn't C# allow autoboxing? – blackr1234 Feb 05 '16 at 18:23
  • @blackr1234 I'm more familiar with Java than C#. From the results of your code, I deduce that C# doesn't autobox in this case. – Code-Apprentice Feb 05 '16 at 18:27
  • I see. I have edited the question to include the 5th line. Lists are objects, aren't they? Why won't this work? – blackr1234 Feb 05 '16 at 18:36
  • 2
    I don't think this is correct as boxing an int to an object is perfectly possible in C#. The difference here is, I believe, that C# just takes a very laidback approach at determining whether it is allowed or not. – Jeroen Vannevel Feb 05 '16 at 18:40
  • @JeroenVannevel Thanks for pointing that out. I'm quite confident that my description of Java's semantics are correct. Obviously the C# semantics are different, but I don't know the details. – Code-Apprentice Feb 05 '16 at 18:54
  • 9
    This has nothing to do with auto boxing, which would happen just as much in C#. The difference is that C# does not try to find a common supertype while Java (since Java 5 only!) does. You can easily test that by trying to see what happens if you try it with 2 custom classes (which both inherit from object obviously). Also there is an implicit conversion from `int` to `object`. – Voo Feb 05 '16 at 19:35
  • 3
    And to nitpick just a bit more: The conversion from `Integer/String` to `Object` is not a narrowing conversion, it's the exact opposite :-) – Voo Feb 05 '16 at 19:44
  • 1
    `Integer` and `String` also implement `Serializable` and `Comparable` so assignments to either of them will work as well, e.g. `Comparable> c=condition? 6: "6";` or `List extends Serializable> l = condition? new ArrayList(): new ArrayList();` are legal Java code. – Holger Feb 06 '16 at 19:07
5

This is pretty straightforward. There is no implicit conversion between string and int. the ternary operator needs the last two operands to have the same type.

Try:

Write(two > six ? two.ToString() : "6");
Mathieu Guindon
  • 69,817
  • 8
  • 107
  • 235
ChiralMichael
  • 1,214
  • 9
  • 20