C#和Java的三元运算符(?:)之间的区别

95

我是一名C#新手,最近遇到了一个问题。在使用三元运算符(? :)时,C#与Java之间存在差异。

在下面的代码片段中,为什么第四行不起作用?编译器显示一个错误消息:there is no implicit conversion between 'int' and 'string'。第五行也不起作用。这两个List、它们都是对象,不是吗?

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

然而,同样的代码在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

C#缺少哪些语言特性?如果有的话,为什么没有添加?


4
根据 https://msdn.microsoft.com/en-us/library/ty67wk28.aspx “要么第一个表达式和第二个表达式的类型必须相同,要么存在从一种类型到另一种类型的隐式转换。” - Egor
2
我已经有一段时间没有做C#了,但是你引用的消息不是说了吗?三元运算符的两个分支必须返回相同的数据类型。你给它一个int和一个String。C#不知道如何自动将int转换为String。你需要在它上面放置一个显式的类型转换。 - Jay
2
因为int不是一个对象,而是一个原始数据类型。 - Code-Apprentice
3
@Code-Apprentice 是的,它们确实非常不同- C# 具有运行时具体化,因此列表是不相关类型。哦,如果你想介绍一些关系的话,你还可以把通用接口协变性/逆变性混合在其中 ;) - Lucas Trzesniewski
3
为了帮助原始发布者:在Java中,“类型擦除”意味着ArrayList<String>ArrayList<Integer>在字节码中变成了只有ArrayList。这意味着它们在运行时看起来是完全相同的类型。显然,在C#中它们是不同的类型。 - Code-Apprentice
显示剩余7条评论
5个回答

107
浏览 C# 5 语言规范第7.14节:条件运算符,我们可以看到以下内容:
  • 如果 x 的类型为 X,y 的类型为 Y,则

    • 如果从 X 到 Y 存在隐式转换 (§6.1),但从 Y 到 X 不存在,则 Y 是条件表达式的类型。

    • 如果从 Y 到 X 存在隐式转换 (§6.1),但从 X 到 Y 不存在,则 X 是条件表达式的类型。

    • 否则,无法确定表达式类型,会出现编译时错误。

换句话说:它尝试查找 x 和 y 是否可以相互转换,如果不能,则会出现编译错误。在我们的情况下,intstring 没有显式或隐式转换,因此无法编译。

与之相对比,参考Java 7语言规范第15.25节:条件运算符

如果第二个和第三个操作数具有相同的类型(可以是null类型),那么条件表达式的类型就是它们的类型。(
如果第二个和第三个操作数中有一个是基本类型T,而另一个操作数的类型是应用了装箱转换(§5.1.7)到T的结果,则条件表达式的类型为T。(
如果第二个和第三个操作数中有一个是null类型,而另一个操作数的类型是引用类型,则条件表达式的类型就是该引用类型。(
否则,如果第二个和第三个操作数的类型可以转换(§5.1.8)为数值类型,则有几种情况:(
否则,第二个和第三个操作数分别为S1和S2类型。让T1成为将S1应用装箱转换后的类型,让T2成为将S2应用装箱转换后的类型。
条件表达式的类型是将lub(T1,T2)(§15.12.2.7)应用于捕获转换(§5.1.10)的结果。(

而且,查看第15.12.2.7节。根据实际参数推断类型参数,我们可以看到它试图找到一个公共祖先作为该调用使用的类型,这将使其具有Object类型。 Object是一个可接受的参数,因此该调用将起作用。


1
我阅读了C#规范,发现至少有一个方向上必须存在隐式转换。只有在两个方向上都不存在隐式转换时,编译器才会出现错误。 - Code-Apprentice
1
当然了,这依然是最完整的答案,因为你直接引用了规范。 - Code-Apprentice
这实际上只是在Java 5中更改的(我想,可能是6)。在此之前,Java的行为与C#完全相同。由于枚举的实现方式,这可能会在Java中引起比C#更多的意外,因此一直是一个很好的变化。 - Voo
@Voo:FYI,Java 5和Java 6具有相同的规范版本(《Java语言规范》第三版),因此它在Java 6中绝对没有更改。 - ruakh

87
给出的答案很好;我想要补充的是,这个C#规则是更一般的设计指南的结果。当从几种选择中推断表达式的类型时,C#会选择唯一最佳的一种。也就是说,如果你给C#提供了一些选项,比如“长颈鹿、哺乳动物、动物”,它可能会选择最一般的——动物——或者可能会选择最具体的——长颈鹿——这取决于具体情况。但它必须选择实际给定的选择之一。C#从不说“我的选择是猫和狗之间,因此我会推断动物是最佳选择”。这不是给出的选择之一,因此C#无法选择它。
在三元运算符的情况下,C#尝试选择int和string的更一般类型,但两者都不是更一般的类型。C#不会选择在第一次选择中不存在的类型,例如object,而是决定无法推断任何类型。
我还注意到这符合C#的另一个设计原则:如果某些东西看起来不对,告诉开发人员。语言不会说“我会猜测你的意思,如果可以的话,我会混过去”。语言会说“我认为你写的东西很混淆,我会告诉你这一点。”
此外,我注意到C#不是从变量推导赋值的值,而是相反的。C#不会说“你正在为一个对象变量赋值,因此表达式必须可以转换为object,因此我会确保它是可转换的”。相反,C#会说“这个表达式必须有一个类型,并且我必须能够推断出该类型与object兼容”。由于表达式没有类型,因此会产生错误。

6
我读完第一段后开始想知道为什么会出现协变/逆变的兴趣,然后在第二段开始更加同意作者的观点,接着看到了回答者是谁……早该猜到了。你是否曾经注意到你使用“长颈鹿”作为例子的次数?你一定非常喜欢长颈鹿。 - Pharap
21
长颈鹿太棒了! - Eric Lippert
10
认真地说,我之所以经常使用长颈鹿这个词,是出于教学的原因。首先,长颈鹿是全世界都知道的动物。其次,它是一个好玩的词,并且它们的外表非常奇特,很容易让人记住。第三,在同一比喻中使用“长颈鹿”和“海龟”等词语,强调“尽管它们可能共享一个基类,但这两个类是非常不同的动物,具有非常不同的特征”。关于类型系统的问题通常会涉及逻辑上非常相似的类型示例,这会引导你的直觉走向错误的方向。 - Eric Lippert
8
@Pharap:通常问题是“为什么不能使用客户发票提供商工厂的列表作为发票提供商工厂的列表?”,而您的直觉是“是的,它们基本上是相同的东西”,但是当您说“为什么不能把一间长满长颈鹿的房间用作充满哺乳动物的房间?”时,这变成了更多的直观类比,可以说“因为充满哺乳动物的房间可以容纳老虎,充满长颈鹿的房间则不行”。 - Eric Lippert
7
@Eric,我最初遇到这个问题是在使用int? b = (a != 0 ? a : (int?) null)时。还有这个问题,以及侧边栏中的所有链接问题。如果您继续跟随链接,会发现有相当多的问题。相比之下,我从未听说过有人在Java中的实际应用中遇到过类似的问题。 - BlueRaja - Danny Pflughoeft
显示剩余5条评论

24
关于泛型部分:
two > six ? new List<int>() : new List<string>()

在C#中,编译器试图将右侧表达式部分转换为一些公共类型;由于List<int>List<string>是两个不同的构造类型,因此一个无法转换为另一个。

在Java中,编译器尝试找到一个通用超类型而不是进行转换,因此代码的编译涉及使用通配符类型擦除的隐式使用;

two > six ? new ArrayList<Integer>() : new ArrayList<String>()

该内容的编译类型为ArrayList<?>(实际上也可以是ArrayList<? extends Serializable>ArrayList<? extends Comparable<?>>,具体取决于使用环境,因为它们都是常见的泛型超类型),而运行时类型为原始ArrayList(因为它是通用原始超类型)。

例如(自行测试)

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

实际上,“Write(two > six ? new List<object>() : new List<string>());”也不起作用。 - blackr1234
2
@blackr1234 正确:我不想重复其他答案已经说过的内容,但三元表达式需要评估为一种类型,编译器不会尝试找到这两个类型的“最低公共分母”。 - Mathieu Guindon
2
实际上,这不是关于类型擦除,而是关于通配符类型。ArrayList<String>ArrayList<Integer>的擦除将是ArrayList(原始类型),但此三元运算符的推断类型为ArrayList<?>(通配符类型)。更一般地说,Java 运行时 通过类型擦除实现泛型对表达式的编译时类型没有影响。 - meriton
1
@vaxquis 谢谢!我是一个C#程序员,Java的泛型让我感到困惑;-) - Mathieu Guindon
2
实际上,two > six ? new ArrayList<Integer>() : new ArrayList<String>() 的类型是 ArrayList<? extends Serializable&Comparable<?>>,这意味着你可以将它赋值给类型为 ArrayList<? extends Serializable>ArrayList<? extends Comparable<?>> 的变量,当然,ArrayList<?>(等同于 ArrayList<? extends Object>)也可以。 - Holger
@Holger 发现了这个问题,点赞!(顺便说一句,我完全忘记了 String 实现了 Comparable 接口!)我为你而高兴。 - user719662

6
在Java和C#(以及大多数其他语言)中,表达式的结果都有一个类型。对于三元运算符而言,有两个可能的子表达式被求值,其结果必须具有相同的类型。在Java中,可以通过自动装箱将int变量转换为Integer。由于IntegerString都继承自Object,因此它们可以通过简单的窄化转换转换为相同的类型。
另一方面,在C#中,int是一个原始类型,没有隐式转换到string或任何其他object的方法。

谢谢你的回答。自动装箱是我目前正在考虑的。C# 不允许自动装箱吗? - blackr1234
2
我认为这并不正确,因为在C#中将int强制转换为对象是完全可能的。我相信这里的区别在于,C#采取了一种非常悠闲的方法来决定是否允许这样做。 - Jeroen Vannevel
9
这与自动装箱无关,C#中也会发生同样的情况。不同之处在于,C#不会尝试找到一个公共超类型,而Java(仅限Java 5以上版本)会。您可以通过尝试使用两个自定义类(两者都显然继承自object)来测试它的结果。此外,从intobject还存在隐式转换。 - Voo
3
再挑刺一点:从 Integer/String 转换为 Object 不是缩小转换,恰恰相反 :-) - Voo
1
IntegerString也实现了SerializableComparable接口,因此对它们的赋值也是有效的,例如Comparable<?> c=condition? 6: "6";List<? extends Serializable> l = condition? new ArrayList<Integer>(): new ArrayList<String>();都是合法的Java代码。 - Holger
显示剩余3条评论

5
这很简单。字符串和整数之间没有隐式转换。三元运算符需要最后两个操作数具有相同的类型。
尝试: ```html ```
Write(two > six ? two.ToString() : "6");

11
问题是什么导致了Java和C#之间的差异。这句话并没有回答问题。 - Jeroen Vannevel

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接