为什么ushort + ushort等于int?

38

今天早些时候,我尝试添加两个ushort,并注意到必须将结果强制转换回ushort。我认为它可能会变成uint(以防止可能发生的溢出),但令我惊讶的是它是int(System.Int32)。

这是因为有巧妙的原因呢,还是因为int被视为“基本”整数类型呢?

示例:

ushort a = 1;
ushort b = 2;

ushort c = a + b; // <- "Cannot implicitly convert type 'int' to 'ushort'. An explicit conversion exists (are you missing a cast?)"
uint d = a + b; // <- "Cannot implicitly convert type 'int' to 'uint'. An explicit conversion exists (are you missing a cast?)"

int e = a + b; // <- Works!

编辑:正如GregS的答案所说,C#规范规定两个操作数(在这个例子中是'a'和'b')都应该转换为int类型。我对为什么这是规范的一部分感到好奇:为什么C#规范不允许直接对ushort值进行运算?


3
请参见J. Skeets的回答:http://bytes.com/topic/c-sharp/answers/256857-does-ushort-ushort-return-int-value 。 - Tim Schmelter
我已经根据微软的C#规范更新了我的答案。 - Habib
5个回答

74
简单而正确的答案是“因为C#语言规范是这样规定的”。
显然,你对这个答案不满意,想知道“为什么会这样”。你在寻找“可信和/或官方来源”,这可能有点困难。这些设计决策是很久以前做出的,13年在软件工程中是很长的时间。它们是由“老手”做出的,正如Eric Lippert所称,他们已经转向了更大更好的事情,不会在这里发布答案提供官方来源。
然而,可以推断出一些东西,但存在风险,仅仅是可信的。任何托管编译器,如C#的编译器,都有一个约束,即它需要为.NET虚拟机生成代码。关于这个的规则在CLI规范中精心(也相当易读)地描述。这是Ecma-335规范,你可以从这里免费下载。
打开第III部分,第3.1和3.2章。它们描述了执行加法的两个IL指令,add和add.ovf。点击链接到表格2,“二进制数值操作”,它描述了哪些操作数适用于这些IL指令。注意那里只列出了一些类型。byte和short以及所有无符号类型都不存在。只允许int、long、IntPtr和浮点数(float和double)。还有附加的限制标记为x,例如你不能将int与long相加。这些约束并不是完全人为的,它们基于可以在可用硬件上合理有效地执行的事情。
任何托管编译器都必须处理这个问题,以生成有效的IL。这并不困难,只需将ushort转换为表中的一个较大值类型,这种转换始终有效。C#编译器选择int,表中出现的下一个更大的类型。或者通常情况下,将任一操作数转换为下一个最大的值类型,使它们都具有相同的类型并满足表中的约束条件。现在有一个新的问题,这个问题让C#程序员感到很恼火。加法的结果是提升类型的结果。在您的情况下,将是int类型。因此,例如添加两个ushort值0x9000和0x9000将具有完全有效的int结果:0x12000。问题是:这是一个无法装入ushort的值。该值溢出了。但是,在IL计算中它没有溢出,只有当编译器试图将其压缩回ushort时才会溢出。0x12000被截断为0x2000。这是一个令人困惑的不同值,只有在用2或16个手指进行计数时才有一定意义,而不是10个手指。
需要注意的是,add.ovf指令无法解决这个问题。它是用于自动生成溢出异常的指令。但事实并非如此,转换后的整数实际计算没有溢出。
这就是真正的设计决策所在。老一辈的程序员显然认为,简单地截断int结果为ushort类型将产生错误的代码。当然是这样。他们认为你必须承认你知道加法可能会溢出,并且如果发生溢出也可以接受。他们把它变成了的问题,主要是因为他们不知道如何解决这个问题并且仍然生成高效的代码。你需要进行强制转换。是的,这很让人生气,我敢肯定你也不想遇到这个问题。
有趣的是VB.NET设计者采用了不同的方法来解决这个问题。他们实际上让它成为他们的问题,而不是推卸责任。您可以将两个UShort相加并将其分配给UShort而无需进行转换。区别在于VB.NET编译器实际上生成了额外的IL以检查溢出条件。这不是廉价的代码,使每个短添加变慢了3倍。但否则,这就是解释微软为什么维护了两种具有非常类似功能的语言的原因。简而言之:您因为使用了与现代CPU架构不是很匹配的类型而支付了代价。这本身就是使用uint而不是ushort的一个非常好的理由。从ushort中获得动力是困难的,您需要很多ushort,才能抵消内存节省的成本。不仅仅是由于受限的CLI规范,x86核心还需要额外的CPU周期来加载16位值,因为机器码中有操作数前缀字节。我不确定今天是否仍然是这种情况,在我还关注计算周期的时候,这是一年前的事情。
请注意,您可以通过让C#编译器生成与VB.NET编译器生成相同的代码来使这些丑陋且危险的强制转换更加安全。因此,当转换被证明是不明智的时候,您会得到OverflowException。在调试版本中使用项目>属性>构建选项卡>高级按钮>选中“检查算术溢出/下溢”复选框。顺便说一下,为什么项目模板没有自动启用此复选框是另一个非常神秘的问题,这是一个太久以前做出的决定。

这并不难,只需将ushort转换为uint,这是一种始终有效的转换。确实,我也是这么想的,但它不会被转换为uint,而是转换为int,至今没有人能够真正解释清楚。不过,这是一个非常好的答案! - lesderid
1
是的,我搞砸了,已修复。看一下我指的那个表格,它没有无符号类型。 - Hans Passant
啊,我明白了。我想那回答了我的问题!不过我还是会授予Habib.OSU赏金,因为他在我编辑问题之前就完全回答了原始问题。 - lesderid
1
非常棒的阅读;你肯定值得那个勾号!我想这又是一个为什么过早优化(使用ushort而不是uints)是邪恶的案例。 - Kyle Baran
很好,但是有一个令人烦恼的强制转换情况:ushort + ushort 的结果是 int,无论如何,即使你不能将其存储在 uint 中。现在添加两个 ushorts 永远不会导致任何负面的结果,因此我认为推断一个 int 是错误的。我知道结果可能会超出 ushort 范围; 但这就是为什么我会使用 uint 来得到结果,但是我不能(没有强制转换)。 - Piedone
2
非常棒的答案,来自 StackOverflow 大师们的详细解释正是我所喜欢的。 - JYelton

19
ushort x = 5, y = 12;

以下赋值语句会产生编译错误,因为赋值操作符右侧的算术表达式默认情况下会被计算为 int 类型
ushort z = x + y;   // Error: conversion from int to ushort

http://msdn.microsoft.com/en-us/library/cbf1574z(v=vs.71).aspx

编辑:
在对ushort进行算术运算时,操作数会被转换为一个可以容纳所有值的类型,以避免溢出。操作数可以按照int、uint、long和ulong的顺序进行更改。请参阅C#语言规范,在该文档中转到第4.1.5节整数类型(大约在Word文档的第80页左右)。这里你会发现:

对于二进制+、-、*、/、%、&、^、|、==、!=、>、<、>=和<= 运算符,操作数将被转换为类型T,其中T是int、uint、long和ulong中可以完全表示两个操作数的所有可能值的第一个类型。然后使用类型T的精度执行操作,结果的类型为T(或关系运算符的bool类型)。不允许一个操作数为long型,另一个操作数为ulong型与二元运算符结合。

Eric Lipper在一个问题中表示:

C#中从不使用short进行算术运算。算术可以在int、uint、long和ulong中进行,但从不在short中进行算术运算。short会提升为int,并在int中执行算术运算,因为正如我之前所说,绝大多数算术计算都适用于int,而不适用于short。现代硬件针对int进行了优化,因此短整型的算术可能较慢,并且短整型不会占用更少的空间。它将在芯片上使用int或long进行操作。


1
这仍然无法解释为什么它会变成 int 而不是 uint。 - lesderid
2
@lesderid,这是由于C#规范(7.8.4,第192页)中定义的+运算符重载优先级。其中第一个重载签名是int operator+(int x, int y),之后是uint operator+(uint x, uint y)。因此,它变成int而不是uint。 - Habib

6
从C#语言规范中得知:
7.3.6.2 二进制数值提升
对于预定义的+、-、*、/、%、&、|、^、==、!=、>、<、>=和<=二进制运算符的操作数进行二进制数值提升。二进制数值提升隐式将两个操作数转换为一个公共类型,该类型在非关系运算符的情况下也成为操作的结果类型。二进制数值提升由按照以下顺序应用以下规则而构成:
· 如果任意一个操作数是decimal类型,则将另一个操作数转换为decimal类型,或者如果另一个操作数是float或double类型则产生绑定时错误。
· 否则,如果任意一个操作数是double类型,则将另一个操作数转换为double类型。
· 否则,如果任意一个操作数是float类型,则将另一个操作数转换为float类型。
· 否则,如果任意一个操作数是ulong类型,则将另一个操作数转换为ulong类型,或者如果另一个操作数是sbyte、short、int或long类型则产生绑定时错误。
· 否则,如果任意一个操作数是long类型,则将另一个操作数转换为long类型。
· 否则,如果任意一个操作数是uint类型并且另一个操作数是sbyte、short或int类型,则将两个操作数都转换为long类型。
· 否则,如果任意一个操作数是uint类型,则将另一个操作数转换为uint类型。
· 否则,两个操作数都转换为int类型。

这确实证实了我的发现,但并没有真正解释为什么会发生这种情况。 - lesderid
1
@lesderid:我不明白。这是因为您正在使用符合C#规范的编译器。也许您正在询问7.3.6.2节背后的原因,以及为什么规则停在int而不继续到较小的类型? - President James K. Polk
1
是的,我对规范为什么要求将两个操作数转换为int感兴趣。我只是添加了编译器错误以澄清,并且因为我不确定这是规范还是实现决策。 - lesderid
1
我可以猜测,但我并不真正知道答案,所以您应该编辑您的问题来询问为什么语言规则是那样的。 - President James K. Polk

3

这并非出于某种特定原因,仅仅是应用了重载解析规则的效果。该规则指出,对于第一个参数存在适合于传入参数的隐式转换的重载函数,将使用该重载函数。

在C#规范7.3.6节中明确规定:

数字提升不是一种独立的机制,而是通过将预定义运算符应用于重载解析的效果来实现的。

它通过以下示例进行说明:

作为数字提升的示例,考虑二元 * 运算符的预定义实现:

int operator *(int x, int y);

uint operator *(uint x, uint y);

long operator *(long x, long y);

ulong operator *(ulong x, ulong y);

float operator *(float x, float y);

double operator *(double x, double y);

decimal operator *(decimal x, decimal y);

当应用重载解析规则(§7.5.3)到这组运算符时,其效果是从操作数类型中选择存在隐式转换的运算符的第一个运算符。例如,在执行 b * s 操作,其中 b 是 byte 类型,s 是 short 类型时,重载解析会选择 operator *(int, int) 作为最佳运算符。


2
您的问题实际上有点棘手。这个规范之所以成为语言的一部分,是因为在创建语言时他们做出了这个决定。我知道这听起来像是一个令人失望的答案,但事实就是如此。
然而,真正的答案可能涉及到1999-2000年当时许多上下文决策。我相信制作C#的团队对所有这些语言细节进行了深入的辩论。
引用如下:
  • ...
  • C#旨在成为一种简单、现代、通用的面向对象编程语言。
  • 源代码可移植性非常重要,程序员的可移植性也很重要,尤其是那些已经熟悉C和C++的程序员。
  • 支持国际化非常重要。
  • ...
以上摘自维基百科C#页面。
所有这些设计目标可能影响了他们的决策。例如,在2000年,大多数系统已经是本地32位的,因此他们可能决定限制小于32位的变量数量,因为在执行算术运算时它们将被转换为32位。这通常会更慢。
此时,你可能会问我:如果这些类型存在隐式转换,为什么他们还要包括它们呢?嗯,他们的一个设计目标,正如上面引用的那样,是可移植性。
因此,如果您需要在旧的C或C++程序周围编写C#包装器,则可能需要这些类型来存储一些值。在这种情况下,这些类型非常方便。
这是Java没有做出的决定。例如,如果您编写一个与C++程序交互的Java程序,在其中接收ushort值,那么Java只有short(带符号),因此您无法轻松地将一个值分配给另一个并期望正确的值。
我让你猜猜,在Java中可以接收这种值的下一个可用类型是什么(32位)。您在这里刚刚增加了内存量。这可能不是一个大问题,而是必须实例化100,000个元素的数组。
实际上,我们必须记住,这些决策是通过查看过去和未来来提供平稳转换而做出的。
但现在我感到我正在偏离最初的问题。
所以,你的问题是一个好问题,希望我能为你带来一些答案,尽管我知道那可能不是你想听到的。
如果你愿意,你甚至可以阅读更多关于C#规范的内容,下面是链接。有一些有趣的文档可能对你有用。 整数类型 checked和unchecked运算符 隐式数字转换表 顺便说一句,我认为你应该奖励habib-osu,因为他提供了一个相当好的答案,并附上了适当的链接。 :)
问候

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