34

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

4

5 回答 5

67

简单而正确的答案是“因为 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 中获得牵引力是很困难的,在操纵它们的成本超过内存节省之前,你需要很多它们。不仅仅是因为有限的 CLI 规范,x86 内核需要额外的 cpu 周期来加载 16 位值,因为机器代码中的操作数前缀字节。实际上不确定今天是否仍然如此,当我仍然注意计算周期时,它曾经回来过。一年前的狗。


请注意,通过让 C# 编译器生成与 VB.NET 编译器生成的代码相同的代码,您可以更好地了解这些丑陋和危险的强制转换。所以当演员被证明是不明智的时,你会得到一个 OverflowException。使用项目>属性>构建选项卡>高级按钮>勾选“检查算术上溢/下溢”复选框。仅用于调试版本。为什么项目模板没有自动打开此复选框是另一个非常神秘的问题,顺便说一句,这是很久以前做出的决定。

于 2012-04-14T21:28:52.540 回答
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# 中完成。算术可以用 int、uint、long 和 ulong 来完成,但算术永远不能用 short 来完成。Shorts 提升为 int 并且算术是在 int 中完成的,因为就像我之前所说的,绝大多数算术计算都适合 int。绝大多数不适合做空。在为整数优化的现代硬件上,短算术可能会更慢,而且短算术不会占用更少的空间;它将在芯片上以整数或长整数形式完成。

于 2012-04-08T18:36:56.537 回答
6

来自 C# 语言规范:

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

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

· 否则,如果任一操作数是双精度类型,则另一个操作数将转换为双精度类型。

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

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

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

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

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

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

于 2012-04-08T18:42:11.467 回答
3

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

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

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

它继续举例说明:

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

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

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

长运算符 *(long x, long y);

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

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

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

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

当重载决策规则(第 7.5.3 节)应用于这组运算符时,效果是从操作数类型中选择存在隐式转换的第一个运算符。例如,对于 b * s 操作,其中 b 是一个字节,s 是一个短字节,重载决策选择 operator *(int, int) 作为最佳运算符。

于 2012-04-08T19:03:23.343 回答
2

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


然而,真正的答案可能涉及到 1999-2000 年的许多背景决定。我确信制作 C# 的团队对所有这些语言细节进行了激烈的辩论。

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

上面的引用来自维基百科 C#

所有这些设计目标都可能影响了他们的决定。例如,在 2000 年,大多数系统已经是原生的 32 位,因此他们可能决定限制比这更小的变量的数量,因为在执行算术运算时无论如何都会在 32 位上进行转换。这通常较慢。

那时,您可能会问我;如果这些类型存在隐式转换,为什么它们仍然包含它们?如上所述,他们的设计目标之一就是便携性。

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

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

我让你打赌,在 Java 中可以接收这种值的下一个可用类型是 int(当然是 32 位)。你刚刚在这里把你的记忆加倍了。这可能没什么大不了的,相反,您必须实例化一个包含 100 000 个元素的数组。

事实上,我们必须记住,这些决定是通过回顾过去和未来做出的,以便提供从一个到另一个的平稳转移。

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


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

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

积分型

已检查和未检查的运算符

隐式数值转换表

顺便说一句,我相信您可能应该为此奖励habib-osu,因为他通过正确的链接为最初的问题提供了相当好的答案。:)

问候

于 2012-04-14T03:52:11.483 回答