一尘不染

为什么ushort + ushort等于int?

c#

今天以前,我试图添加两个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值进行操作?


阅读 662

收藏
2020-05-19

共1个答案

一尘不染

简单而正确的答案是“因为C#语言规范是这样说的”。

显然,您对该答案不满意,想知道“为什么这么说”。您正在寻找“可信和/或官方来源”,这将有些困难。这些设计决策是在很久以前做出的,在软件工程领域已经有13年了。它们由埃里克·利珀特(Eric
Lippert)所说的“老古董”制造,它们已经转移到更大更好的东西上,因此不在此处发布答案以提供官方消息。

但是,可以推断出它只是可信的。任何托管编译器(如C#编译器)都有其需要为.NET虚拟机生成代码的约束。CLI规范中仔细(且易于理解)描述了这些规则。它是Ecma-335规格,您可以从这里免费下载。

转到分区III,第3.1和3.2章。它们描述了可用于执行加法运算的两条IL指令,add以及add.ovf。单击表2“二进制数值运算”的链接,它描述了那些IL指令允许使用的操作数。请注意,这里仅列出了几种类型。缺少字节和短以及所有无符号类型。仅允许使用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的实际计算并未溢出。

这是真正的设计决策发挥作用的地方。以前的人显然认为,将int结果截断为ushort就是一个错误工厂。必然是。他们决定,您必须承认自己 知道
添加可能会溢出,并且如果发生这种情况也是可以的。他们把它变成了 您的 问题,主要是因为他们不知道如何使其成为自己
问题,而是仍然生成高效的代码。你必须投。是的,这令人发疯,我敢肯定您也不希望这个问题。

值得注意的是,VB.NET设计人员针对此问题采用了不同的解决方案。他们实际上解决了 他们的
问题,但并没有因此而失败。您可以添加两个UShorts并将其分配给不带强制转换的UShort。区别在于VB.NET编译器实际上会生成 额外的
IL以检查溢出条件。那不是便宜的代码,使每次添加的速度慢大约3倍。但是除此之外,这也解释了为什么Microsoft维护 两种
具有非常相似功能的语言的原因。

长话短说:您付出了代价,因为您使用的类型与现代cpu架构不太匹配。这本身就是使用uint而不是ushort的一个很好的理由。要摆脱ushort的牵引力是困难的,在操纵它们的成本超过节省的内存之前,您将需要很多这样的驱动器。不仅仅是由于CLI规范的限制,x86内核还需要一个额外的cpu周期来加载16位值,这是因为机器码中的操作数前缀字节。实际不确定今天是否仍然如此,当我仍然关注计数周期时,它曾经回到过去。一年前的狗。


请注意,通过让C#编译器生成与VB.NET编译器生成的相同的代码,您可以对这些丑陋而危险的转换感到更好。因此,当强制转换被证明是不明智的时,您将获得OverflowException。使用项目>属性>生成选项卡>高级按钮>选中“检查算术上溢/下溢”复选框。仅用于调试版本。为何项目模板没有自动打开此复选框,这是另一个非常令人迷惑的问题,顺便说一下,这个决定是在很久以前做出的。

2020-05-19