为什么ushort + ushort等于int?

时间:2012-04-08 18:32:17

标签: c# types integer-arithmetic

以前我今天尝试添加两个ushorts,我注意到我必须将结果反馈给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值进行操作?

5 个答案:

答案 0 :(得分:53)

简单而正确的答案是&#34;因为C#语言规范如此说明&#34;。

显然,你对这个答案并不满意,并且想知道&#34;为什么会这样说&#34;。您正在寻找可信和/或官方来源&#34;,这将会有点困难。这些设计决定是很久以前做出的,13年是很多狗生活在软件工程中。它们是由旧的计时器和#34;正如Eric Lippert所说,他们已经转向更大更好的事情,并且不会在这里发布答案以提供官方消息来源。

然而,可以推断,存在仅仅是可信的风险。任何托管编译器(如C#)都具有为.NET虚拟机生成代码所需的约束。其中的规则在CLI规范中进行了仔细(并且非常可读)的描述。这是Ecma-335规范,您可以免费下载from here

转到分区III,第3.1和3.2章。他们描述了可用于执行添加的两条IL指令addadd.ovf。单击表2,&#34;二进制数字运算&#34;的链接,它描述了那些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是一个bug工厂。必然是。他们决定你必须承认你知道添加可以溢出并且如果它发生就可以了。他们把它作为你的问题,主要是因为他们不知道如何制作它们并仍然生成有效的代码。你必须施展。是的,那令人抓狂,我确定你也不想要这个问题。

值得注意的是,VB.NET设计者采用了不同的解决方案。他们实际上已经解决了他们的问题并且没有推卸责任。您可以添加两个UShorts并将其分配给UShort而不进行强制转换。区别在于VB.NET编译器实际生成 extra IL以检查溢出情况。这不是便宜的代码,使每次短暂添加的速度慢约3倍。但其他原因解释了为什么Microsoft维护两种具有非常相似功能的语言。

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


请注意,通过让C#编译器生成与VB.NET编译器生成的代码相同的代码,您可以对这些丑陋和危险的强制转换感觉更好。因此,当演员表明不明智时,你会得到一个OverflowException。使用项目&gt;属性&gt;构建标签&gt;高级按钮&gt;勾选&#34;检查算术溢出/下溢&#34;复选框。仅适用于Debug构建。为什么这个复选框没有被项目模板自动打开,这是另一个非常神秘的问题btw,这是一个很久以前做出的决定。

答案 1 :(得分:17)

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# Language Specification在本文档中,转到4.1.5积分类型(单词文档中的第80页左右)。在这里你会发现:

  

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

Eric Lipper在question

中表示
  

算术永远不会在C#中做空。算术可以在   整数,uints,longs和ulongs,但算术从不做空。   短裤提升为int,算术是以ints完成的,因为喜欢   我之前说过,绝大多数算术计算都符合   一个int。绝大多数都不适合做空。简短的算术就是   在针对整数优化的现代硬件上可能更慢,以及   短算术不会占用更少的空间;这将是   在芯片上以整数或多头完成。

答案 2 :(得分:5)

来自C#语言规范:

7.3.6.2二进制数字促销 对于预定义的+, - ,*,/,%,&amp;,|,^,==,!=,&gt;,&lt;,&gt; =和&lt; =二元运算符的操作数,会发生二进制数字提升。二进制数字提升隐式地将两个操作数转换为公共类型,在非关系运算符的情况下,它也成为操作的结果类型。二进制数字促销包括按照它们在此处显示的顺序应用以下规则:

·如果任一操作数的类型为十进制,则另一个操作数将转换为十进制类型,或者如果另一个操作数的类型为float或double,则会发生绑定时错误。

·否则,如果任一操作数的类型为double,则另一个操作数将转换为double类型。

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

·否则,如果任一操作数的类型为ulong,则另一个操作数将转换为ulong类型,如果另一个操作数的类型为sbyte,short,int或long,则会发生绑定时错误。

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

·否则,如果任一操作数的类型为uint而另一个操作数的类型为sbyte,short或int,则两个操作数都将转换为long类型。

·否则,如果任一操作数的类型为uint,则另一个操作数将转换为uint类型。

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

答案 3 :(得分:3)

没有理由这样做。这只是一个效果或应用重载决策的规则,它规定第一个重载的参数是否存在适合参数的隐式转换,将使用该重载。

这在C#规范第7.3.6节中说明如下:

  

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

继续说明一个例子:

  

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

     

int operator *(int x,int y);

     

uint operator *(uint x,uint y);

     

长算子*(长x,长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是一个字节而s是short,重载决策选择operator *(int,int)作为最佳运算符。

答案 4 :(得分:2)

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


然而,真正的答案可能涉及1999-2000当天的许多背景决定。我确信制作C#的团队对所有这些语言细节都有相当强烈的争论。

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

The quote above is from Wikipedia C#

所有这些设计目标都可能影响了他们的决定。例如,在2000年,大多数系统已经是本机32位,因此他们可能已经决定将变量的数量限制为小于该数量,因为在执行算术运算时它将以32位进行转换。这通常较慢。

那时,你可能会问我;如果对这些类型进行隐式转换,为什么他们还包括它们呢?如上所述,他们的设计目标之一是便携性。

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

这是Java没有做出的决定。例如,如果你编写一个与C ++程序交互的Java程序,那么你接收到的是ushort值,那么Java只有short(签名),所以你不能轻易地将它们分配给另一个并期望正确的值。

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

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

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


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

如果您愿意,您甚至可以阅读有关C#规范的更多信息,请参阅下面的链接。有一些有趣的文档可能对你有用。

Integral types

The checked and unchecked operators

Implicit Numeric Conversions Table

顺便说一下,我相信你应该奖励habib-osu,因为他通过适当的链接为初始问题提供了一个相当好的答案。 :)

此致