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


94

我是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
按照msdn.microsoft.com/zh-cn/library/ty67wk28.aspx,“first_expression和second_expression的类型必须相同,或者必须存在从一种类型到另一种类型的隐式转换。”
Egor,2016年

2
我有一段时间没有做C#了,但是您引用的消息不是这样吗?三元组的两个分支必须返回相同的数据类型。您正在给它一个int和一个String。C#不知道如何自动将int转换为String。您需要在其上进行显式类型转换。
周杰伦

2
@ blackr1234因为an int不是对象。这是原始的。
法学徒

3
@ Code-Apprentice是的,它们确实很大的不同 -C#具有运行时规范化,因此列表属于不相关的类型。哦,如果您愿意,也可以将通用接口协方差/协方差放入组合中,以引入某种程度的关系;)
Lucas Trzesniewski

3
为了OP的利益:Java中的类型擦除意味着ArrayList<String>并且ArrayList<Integer>ArrayList在字节码中出现。这意味着它们在运行时看起来是完全相同的类型。显然,在C#中,它们是不同的类型。
法学徒

Answers:


106

查看C#5语言规范的第7.14节:条件运算符,我们可以看到以下内容:

  • 如果x具有X类型,而y具有Y类型,则

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

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

    • 否则,无法确定表达式类型,并且会发生编译时错误

换句话说:它试图找到Y是否x和可以转化成海誓山盟并且如果不是,则产生编译错误。在我们的情况下intstring没有显式或隐式转换,因此将无法编译。

将此与Java 7语言规范的第15.25节:条件运算符进行对比:

  • 如果第二个操作数和第三个操作数具有相同的类型(可能为null类型),则这是条件表达式的类型。(没有
  • 如果第二个操作数和第三个操作数之一是原始类型T,而另一个操作数的类型是对T应用装箱转换(第5.1.7节)的结果,则条件表达式的类型为T。(NO
  • 如果第二个操作数和第三个操作数之一为空类型,而另一个的类型为引用类型,则条件表达式的类型为该引用类型。(没有
  • 否则,如果第二和第三操作数具有可转换(第5.1.8节)为数字类型的类型,则有以下几种情况:(NO
  • 否则,第二和第三操作数分别为S1和S2类型。令T1为对S1进行装箱转换所产生的类型,而T2为对S2进行装箱转换所产生的类型。
    条件表达式的类型是将捕获转换(§5.1.10)应用于lub(T1,T2)(§15.12.2.7)的结果。(

并且,请参阅第15.12.2.7节。根据实际参数推断类型参数,我们可以看到它试图找到一个共同的祖先,该祖先将用作调用它的调用的类型ObjectObject 可以接受的参数,因此该调用将起作用。


1
我读过C#规范,说必须在至少一个方向上进行隐式转换。仅当在任一方向上没有隐式转换时,才会发生编译器错误。
法学徒

1
当然,这仍然是最完整的答案,因为您直接引用了规范。
法学徒

实际上,这仅在Java 5中发生了变化(我想可能是6)。在此之前,Java的行为与C#完全相同。由于枚举的实现方式,这在Java中可能比C#引起更多惊喜,尽管一直以来都是一个很好的改变。
Voo

@Voo:仅供参考,Java的5和Java 6具有相同的规格版本(Java语言规范,第三版),所以绝对没有在Java中6变化
ruakh

86

给出的答案是好的。我要补充一点,这是C#规则是更通用的设计指南的结果。当要求从几种选择之一推断出表达式的类型时,C#会选择其中唯一的一种。也就是说,如果您给C#一些选择,例如“长颈鹿,哺乳动物,动物”,那么它可能会根据情况选择最通用的动物(动物),也可能选择最特殊的长颈鹿。但是它必须选择实际给出的选择之一。C#从不说“我的选择是在猫和狗之间,因此我会推断出动物是最好的选择”。那不是给定的选择,所以C#无法选择它。

对于三元运算符,C#会尝试选择更通用的int和string类型,但都不是最通用的类​​型。C#决定不推断任何类型,而不是选择对象之类的非首选类型。

我还注意到这与C#的另一种设计原则保持一致:如果出现问题,请告诉开发人员。该语言不会说“我会猜你的意思,如果可以的话,请继续进行下去”。语言说:“我认为您在这里写了一些令人困惑的东西,我将告诉您。”

另外,我注意到C#并不是从变量赋值,而是从另一个方向。C#不会说“您正在分配给对象变量,因此表达式必须可转换为对象,因此我将确保它是”。相反,C#说“此表达式必须具有类型,并且我必须能够推断出该类型与对象兼容”。由于表达式没有类型,因此会产生错误。


6
我跳过了第一段,想知道为什么突然出现协方差/协方差,然后我对第二段开始更多地达成共识,然后我看到谁写了答案……应该早猜。您是否曾经确切地注意到有多少人以“长颈鹿”为例?您必须非常喜欢长颈鹿。
法拉普

21
长颈鹿真棒!
埃里克·利珀特

9
@Pharap:说真的,我有这么多使用长颈鹿的教学理由。首先,长颈鹿是世界闻名的。其次,这是一个有趣的词,它们看起来很奇怪,令人难忘。第三,在类比中使用“长颈鹿”和“乌龟”来强调,“这两个类别虽然可以共享基本类别,但它们是非常不同的动物,具有不同的特征”。有关类型系统的问题通常都有一些示例,其中类型在逻辑上非常相似,从而以错误的方式驱动您的直觉。
埃里克·利珀特

7
@Pharap:问题通常是“为什么不能将客户发票提供者工厂列表用作发票提供者工厂列表?” 并且您的直觉说“是的,这些基本上是同一回事”,但是当您说“为什么不能将充满长颈鹿的房间用作充满哺乳动物的房间?” 那么说“由于充满哺乳动物的房间可以容纳老虎,而充满长颈鹿的房间则不能”,就成了一种直观的类比。
埃里克·利珀特

6
@Eric我最初遇到的问题int? b = (a != 0 ? a : (int?) null)。还有一个问题,以及旁边的所有链接问题。如果您继续关注链接,则有很多。相比之下,我从未听说有人遇到过Java的现实问题。
BlueRaja-Danny Pflughoeft

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 '02

2
完全是@ blackr1234:我不想重复已经说过的其他答案,但是三元表达式需要求出一个类型的值,并且编译器不会尝试找到两者的“最低公分母”。
Mathieu Guindon '16

2
实际上,这与类型擦除无关,而与通配符类型有关。的擦除ArrayList<String>ArrayList<Integer>ArrayList(原始类型),但推断类型此三元操作是ArrayList<?>(通配符类型)。更一般而言,Java 运行时通过类型擦除实现泛型对表达式的编译时类型没有影响。
meriton '16

1
@vaxquis谢谢!我是C#的人,Java泛型使我困惑;-)
Mathieu Guindon

2
实际上,two > six ? new ArrayList<Integer>() : new ArrayList<String>()is 的类型ArrayList<? extends Serializable&Comparable<?>>意味着您可以将其分配给type ArrayList<? extends Serializable>的变量以及type的变量ArrayList<? extends Comparable<?>>,但是当然ArrayList<?>,它也等效于ArrayList<? extends Object>
Holger

6

在Java和C#(以及大多数其他语言)中,表达式的结果都有一个类型。对于三元运算符,将为结果评估两个可能的子表达式,并且两个子表达式必须具有相同的类型。对于Java,int可以Integer通过自动装箱将变量转换为。现在,由于IntegerString都继承自Object,可以通过简单的缩小转换将它们转换为相同类型。

另一方面,在C#中,an int是基元,并且没有隐式转换为string或任何其他object


感谢你的回答。我现在正在想到自动装箱。C#不允许自动装箱吗?
blackr1234 '02

2
我不认为这是正确的,因为在C#中完全有可能对一个对象装箱一个int。我相信,这里的区别在于C#在确定是否允许使用C#时采用了非常宽松的方法。
Jeroen Vannevel,

9
这与自动装箱无关,自动装箱在C#中也会发生。区别在于C#不会尝试找到通用的超类型,而Java(仅Java 5!)可以找到。通过尝试使用2个自定义类(它们显然都继承自对象),尝试查看会发生什么,您可以轻松地进行测试。还存在从int到的隐式转换object
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

5

这很简单。字符串和整数之间没有隐式转换。三元运算符需要最后两个操作数具有相同的类型。

尝试:

Write(two > six ? two.ToString() : "6");

11
问题是什么导致Java和C#之间的差异。这不能回答问题。
Jeroen Vannevel'2
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.