为什么ushort + ushort等于int?

在此之前,我正在试图增加两个ushorts,而且我注意到我必须把结果回传给ushort。 我以为它可能会成为一个uint(防止可能的意外溢出?),但令我惊讶的是它是一个int(System.Int32)。

有没有一些聪明的理由,或者这可能是因为int被视为“基本”整数types?

例:

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值进行操作?

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

显然你不满意这个答案,想知道“为什么这么说”。 你正在寻找“可信和/或官方消息来源”,这将是有点困难。 这些devise决定是很久以前做出来的,13年来软件工程中的狗狗生活很多。 他们是由Eric Lippert所称的“旧时代”制作的,他们已经开始做更大更好的事情,不要在这里发表答复来提供官方消息。

但是可以推断,仅仅是可信的风险。 任何托pipe编译器(如C#)都有一个约束,即它需要为.NET虚拟机生成代码。 CLI规范中对这些规则进行了仔细(而且相当可读)的描述。 这是Ecma-335规格,你可以从这里免费下载。

转到分区III,第3.1和3.2章。 它们描述了可用于执行添加, addadd add.ovf的两个IL指令。 点击表2“二进制数字操作”的链接,它描述了那些IL指令允许的操作数。 请注意,这里只列出了几种types。 字节和短以及所有的无符号types丢失。 只有int,long,IntPtr和浮点数(float和double)是允许的。 如果用x标记附加的约束条件,例如你不能将一个int添加到long。 这些约束并不完全是人为的,它们基于可以在可用硬件上合理高效地执行的事情。

任何托pipe的编译器都必须处理这个以生成有效的IL。 这并不困难,只需将ushort转换为表格中的较大值types,这种转换总是有效的。 C#编译器选取int,表中出现的下一个更大的types。 或者一般来说,将任何操作数转换为下一个最大值types,使它们都具有相同的types并满足表中的约束条件。

但是现在又出现了一个新问题,这个问题推动了C#程序员的疯狂。 增加的结果是促进型。 在你的情况下,将是int。 因此,添加两个ushort值,例如0x9000和0x9000,就会得到一个完全有效的int结果:0x12000。 问题是:这是一个价值,不适合一个ushort。 值溢出 。 但是在IL计算中它并没有溢出,只有当编译器试图把它塞进ushort时才会溢出。 0x12000被截断为0x2000。 一个令人眼花缭乱的不同的价值,只有当你用2或16个手指计数时才有意义,而不是10。

值得注意的是,add.ovf指令不处理这个问题。 这是用于自动生成溢出exception的指令。 但事实并非如此,转换后的实际计算没有溢出。

这是真正的devise决策的起点。 老人们显然决定把简单的结果截断为一个bug工厂。 当然是。 他们决定,你必须承认,你知道增加可以溢出,如果它发生,这是好的。 他们成为你的问题,主要是因为他们不知道如何使他们成为他们,并仍然生成高效的代码。 你必须投。 是的,那真让人生气,我相信你也不想要这个问题。

值得注意的是,VB.NETdevise者采取了不同的解决scheme来解决这个问题。 他们实际上成了他们的问题,并没有推卸责任。 您可以添加两个USHORT,并将其分配给USHORT而不需要投射。 不同的是,VB.NET编译器实际上会生成额外的 IL来检查溢出情况。 这不是便宜的代码,使每一个短的加法约3倍。 但是否则就解释了为什么微软维护两种语言有其他相似function的原因。

长话短说:你付出的代价是因为你使用的types与现代cpu架构不太匹配。 这本身是一个真正的好理由使用uint而不是ushort。 为了避免这种情况的发生是非常困难的,你需要大量的操作来节省内存。 不仅仅是由于CLI规范的限制,由于机器码中的操作数前缀字节,x86内核需要额外的cpu周期来加载16位值。 实际上还不确定今天是否还是这样,当我还在注意计数周期的时候,它已经回来了。 一年前的一只狗。


请注意,通过让C#编译器生成与VB.NET编译器生成的代码相同的代码,您可以对这些丑陋和危险的转换感到更好。 所以当剧组结果是不明智的时候,你会得到一个OverflowException。 使用“项目”>“属性”>“生成”选项卡>“高级”button>勾选“检查算术溢出/下溢”checkbox。 只为debugging版本。 为什么这个checkbox没有被项目模板自动打开,这又是一个很神秘的问题,因为这个决定太早了。

 ushort x = 5, y = 12; 

下面的赋值语句会产生编译错误, 因为赋值运算符右侧的算术expression式默认情况下为int

 ushort z = x + y; // Error: conversion from int to ushort 

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

编辑:
在ushort的算术运算的情况下,操作数被转换为可以保存所有值的types。 所以可以避免溢出。 操作数可以按照int,uint,long和ulong的顺序进行更改。 请参阅C#语言规范在本文档中,请参阅4.1.5集成types(在Word文档的第80页左右)。 在这里你会发现:

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

Eric Lipper在一个问题中表示

在C#中,算术从来就不是简单的。 算术可以用int,uint,longs和ulong来完成,但算术从来就不是简单的。 Shorts推广到int,算术是用int整理的,因为就像我之前说过的那样,绝大多数的算术计算都适合int。 绝大多数不适合于短期。 短的算术在现代硬件上可能会比较慢,而这个硬件是针对整数进行优化的,短算术不占用更less的空间; 它将在芯片上以整数或长整数完成。

从C#语言规范:

7.3.6.2二进制数字升级二进制数字升级发生在预定义的+, – ,*,/,%,&,|,^,==,!=,>,<,> =和<=二元运算符。 二进制数字提升隐含地将两个操作数转换为一个公共types,在非关系运算符的情况下,它也成为操作的结果types。 二进制数字提升包括按照以下顺序应用以下规则:

·如果其中一个操作数的types为decimal,另一个操作数转换为decimaltypes,或者如果另一个操作数的types为float或double,则会发生绑定时错误。

·否则,如果其中一个操作数的types是double,另一个操作数被转换为doubletypes。

·否则,如果任一操作数的types为float,则另一个操作数将转换为floattypes。

·否则,如果其中一个操作数是ulongtypes,另一个操作数将转换为ulongtypes,或者如果另一个操作数的types是sbyte,short,int或long,则会发生绑定时错误。

·否则,如果任一操作数的types为long,则另一个操作数将转换为longtypes。

·否则,如果任一操作数的types为uint,另一个操作数的types为sbyte,short或int,则两个操作数均转换为longtypes。

·否则,如果任一操作数是uinttypes,则另一个操作数转换为uinttypes。

·否则,两个操作数都转换为inttypes。

没有理由是有意的。 这只是一个效果或应用重载parsing的规则,它声明第一个重载的参数有一个隐含的转换符合参数,将使用重载。

这在C#规范的第7.3.6节中陈述如下:

数字提升不是一个独特的机制,而是将重载parsing应用于预定义的运算符的效果。

它继续举例说明:

作为数字提升的一个例子,考虑二进制*运算符的预定义实现:

int运算符*(int x,int y);

uint运算符*(uint x,uint y);

长操作符*(long x,long y);

ulong运算符*(ulong x,ulong y);

float运算符*(float x,float y);

双运算符*(double x,double y);

十进制运算符*(十进制x,十进制y);

当重载parsing规则(第7.5.3节)应用于这组操作符时,其效果是从操作数types中select隐式转换所存在的第一个操作符。 例如,对于操作b * s,其中b是一个字节,s是一个短的,重载决议select运算符*(int,int)作为最好的运算符。

事实上,你的问题有点棘手。 这个规范是语言的一部分的原因是…因为他们在创build语言的时候做出了这个决定。 我知道这听起来像一个令人失望的答案,但这是如何。


然而,真正的答案可能涉及到1999 – 2000年当天的许多背景决定。 我确信C#团队对于所有这些语言细节都有非常强大的争论。

  • C#旨在成为一种简单的,现代的,通用的,面向对象的编程语言。
  • 源代码的可移植性是非常重要的,程序员的可移植性也是非常重要的,尤其是那些已经熟悉C和C ++的程序员。
  • 支持国际化非常重要。

以上引用来自维基百科C#

所有这些devise目标可能影响了他们的决定。 例如,在2000年,系统的大部分已经是原生的32位,所以他们可能已经决定限制小于这个数的variables的数量,因为在执行算术运算时它将被转换为32位。 这通常比较慢。

那时候,你可能会问我; 如果这些types存在隐式转换,那他们为什么要包含它们呢? 那么他们的devise目标之一,如上所述,是可移植性。

因此,如果您需要在旧的C或C ++程序中编写C#包装器,则可能需要这些types来存储一些值。 在这种情况下,这些types非常方便。

这是Java没有做出的决定。 例如,如果你编写一个与C ++程序交互的Java程序,那么你的Java程序只有短的(有签名的),所以你不能很容易地把一个指派给另一个,期待正确的值。

我让你打赌,下一个可以在Java中获得这样的值的可用types是int(当然是32位)。 你在这里增加了一倍的记忆。 这可能不是什么大问题,而是必须实例化一个由10万个元素组成的数组。

实际上,我们必须记住,这些决定是通过观察过去和未来来做出的,以便顺利地从一个转移到另一个。

但是现在我觉得我最初的问题是分歧的。


所以你的问题是一个很好的问题,希望我能给你一些答案,即使我知道这可能不是你想听到的。

如果你愿意,你甚至可以阅读更多关于C#规范,下面的链接。 有一些有趣的文档可能对你很有意思。

整体types

选中和未选中的运算符

隐式数字转换表

顺便说一下,我相信你也许应该给予habib-osu奖励,因为他提供了一个相当好的答案来解决最初的问题。 🙂

问候