尽管其他答案是正确的,但从它们做出真实和相关陈述的意义上说,这里还有一些语言设计的微妙点尚未表达出来。许多不同的因素促成了条件运算符的当前设计。
首先,希望尽可能多的表达式具有可以仅根据表达式的内容确定的明确类型。出于几个原因,这是合乎需要的。例如:它使构建 IntelliSense 引擎变得更加容易。您键入x.M(some-expression.
,IntelliSense 需要能够分析some-expression,确定其类型,并在 IntelliSense 知道 xM 指的是什么方法之前生成一个下拉列表。IntelliSense 在看到所有参数之前无法确定 xM 指的是什么,如果 M 已重载,但您甚至还没有输入第一个参数。
其次,我们更喜欢类型信息“从内到外”流动,因为正是我刚才提到的场景:重载解析。考虑以下:
void M(object x) {}
void M(int x) {}
void M(string x) {}
...
M(b ? 1 : "hello");
这应该怎么做?它应该调用对象重载吗?它是否应该有时调用字符串重载,有时调用 int 重载?如果你有另一个超载怎么办,比如说M(IComparable x)
- 你什么时候选择它?
当类型信息“双向流动”时,事情变得非常复杂。说“我将这个东西分配给对象类型的变量,因此编译器应该知道选择对象作为类型是可以的”并没有洗牌;通常情况下,我们不知道您要分配的变量的类型,因为这就是我们正在尝试找出的过程。重载解析正是从参数类型中计算出参数类型的过程,参数类型是您分配参数的变量。如果参数的类型取决于它们被分配到的类型,那么我们的推理就有循环性。
对于 lambda 表达式,类型信息确实“双向流动”;有效地实施这一点花了我一年的大部分时间。我写了一系列文章,描述了设计和实现编译器的一些困难,该编译器可以根据可能使用表达式的上下文来分析类型信息流入复杂表达式的情况;第一部分在这里:
http://blogs.msdn.com/ericlippert/archive/2007/01/10/lambda-expressions-vs-anonymous-methods-part-one.aspx
你可能会说“好吧,我明白为什么我分配给对象的事实不能被编译器安全地使用,我明白为什么表达式必须具有明确的类型,但为什么不是类型表达式对象,因为 int 和 string 都可以转换为对象?” 这让我想到了第三点:
第三,C# 的微妙但一贯应用的设计原则之一是“不要通过魔法产生类型”。当给定一个我们必须从中确定类型的表达式列表时,我们确定的类型总是在某个列表中。我们从不创造新的类型并为您选择;您获得的类型始终是您让我们选择的类型。如果你说在一组类型中找到最好的类型,我们在该组类型中找到最好的类型。在集合 {int, string} 中,没有最好的常见类型,例如“Animal, Turtle, Mammal, Wallaby”。此设计决策适用于条件运算符、类型推理统一场景、隐式类型数组类型的推理等。
这种设计决策的原因是,在必须确定最佳类型的任何给定情况下,它使普通人更容易计算出编译器将要做什么。如果您知道将要选择一种就在那儿,盯着您的类型,那么很容易弄清楚将要发生的事情。
它还避免了我们必须制定许多复杂的规则,当有冲突时,一组类型的最佳通用类型是什么。假设您有类型 {Foo, Bar},其中两个类都实现了 IBlah,并且两个类都继承自 Baz。哪一个是最好的通用类型,IBlah,两者都实现,或 Baz,两者都扩展?我们不想回答这个问题;我们想完全避免它。
最后,我注意到 C# 编译器实际上在某些晦涩的情况下对类型的确定存在细微的错误。我的第一篇文章在这里:
http://blogs.msdn.com/ericlippert/archive/2006/05/24/type-inference-woes-part-one.aspx
有争议的是,实际上编译器做对了,而规范是错误的;在我看来,实现设计比规范的设计更好。
无论如何,这只是设计三元运算符这一特殊方面的几个原因。这里还有其他一些微妙之处,例如,CLR 验证器如何确定给定的分支路径集是否保证在所有可能路径中的堆栈上都保留正确的类型。详细讨论这一点将把我带到相当远的地方。