添加了 int 和 uint

本文关键字:uint int 添加 | 更新日期: 2023-09-27 18:26:26

我对以下示例中的 C# 编译器行为感到惊讶:

int i = 1024;
uint x = 2048;
x = x+i;     // A error CS0266: Cannot implicitly convert type 'long' to 'uint' ...

这似乎没问题,因为int + uint可以溢出。但是,如果将uint更改为 int ,则错误会消失,就像int + int无法给出溢出一样:

int i = 1024;
int x = 2048;
x = x+i;     // OK, int

此外,uint + uint = uint

uint i = 1024;
uint x = 2048;
x = x+i;     // OK, uint

这似乎完全晦涩难懂。

为什么int + int = intuint + uint = uint,却int + uint = long

做出这个决定的动机是什么?

添加了 int 和 uint

为什么 int + int = int 和 uint + uint = uint

,但 int + uint = long? 做出这个决定的动机是什么?

问题的措辞方式暗示了这样一个前提,即设计团队希望int + uint很长,并选择类型规则来实现该目标。 这个假设是错误的。

相反,设计团队认为:

  • 人们最有可能执行哪些数学运算?
  • 哪些数学运算可以安全有效地执行?
  • 数值类型之间的哪些转换可以在不损失量级和精度的情况下执行?
  • 如何使运算符解析规则既简单又与方法重载解析规则一致?

以及许多其他考虑因素,例如设计是支持还是反对可调试、可维护、可版本的程序等。(我注意到我没有在房间里参加这个特定的设计会议,因为它早于我在设计团队的时间。但是我读过他们的笔记,知道在此期间设计团队会关心的事情。

研究这些问题导致了现在的设计:算术运算被定义为

int + int --> int,uint + uint --> uint,long + long --> long,int可以转换为long,uint可以转换为long,等等。

这些决策的结果是,当添加 uint + int 时,重载解析选择长 + 长作为最接近的匹配项,长 + 长则长,因此 uint + int 长。

让 uint + int 有一些更不同的行为,你可能认为更明智,这根本不是团队的设计目标,因为混合有符号和无符号值首先是实践中很少见的,其次,几乎总是一个错误。设计团队本可以为有符号和无符号一、二、四和八字节整数的每个组合添加特殊情况,以及字符、浮点数、双精度和十进制,或这数百种情况中的任何子集,但这与简单性的目标背道而驰。

因此,简而言之,

一方面,我们有大量的设计工作来使一个我们不希望实际使用的功能更容易使用,而代价是极其复杂的规范。另一方面,我们有一个简单的规范,在极少数情况下会产生不寻常的行为,我们希望在实践中没有人会遇到。 考虑到这些选择,你会选择哪一个? C# 设计团队选择了后者。

简短的回答是"因为标准说应该如此",参见ISO 23270的信息§14.2.5.2。规范性§13.1.2.(隐式数字转换(说:

隐式数字转换包括:

  • intlongfloatdoubledecimal
  • uintlongulongfloatdoubledecimal

intuintlongulongfloat以及从longulongdouble的转换 可能会导致精度损失,但绝不会造成量级损失。 其他隐式数值转换永远不会丢失任何信息。(我的(

[稍长]的答案是,您正在添加两种不同的类型:32 位有符号整数和 32 位无符号整数:

  • 有符号 32 位整数的域是 -2,147,483,648 (0x80000000( — +2,147,483,647 (0x7FFFFFFF(。
  • 无符号 32 位整数的域为 0 (0x00000000( — +4,294,967,295 (0xFFFFFFFF(。

因此,这些类型不兼容,因为int不能包含任何任意uint,而uint不能包含任何任意int。它们被隐式转换(根据 §13.1.2 的要求,不丢失任何信息,进行扩大转换(到可以同时包含两者的下一个最大类型:在本例中为 long,有符号 64 位整数,其域为 -9,223,372,036,854,775,808 (0x8000000000000000( — +9,223,372,036,854,775,807 (0x7FFFFFFFFFFFFFFF(。

编辑以注意:顺便说一句,执行此代码:

var x = 1024 + 2048u ;
Console.WriteLine( "'x' is an instance of `{0}`" , x.GetType().FullName ) ;

不会产生原始海报示例的long。相反,产生的是:

'x' is an instance of `System.UInt32`

这是因为不断折叠。表达式中的第一个元素1024没有后缀,因此是一个int,表达式2048u中的第二个元素是一个uint,根据规则:

  • 如果文本没有后缀,则它具有这些类型中的第一个,其值在其中 可以表示:intuintlongulong
  • 如果文字后缀为 Uu,则它具有可以表示其值的第一种类型:uintulong

由于优化器知道值是什么,因此总和被预先计算并评估为uint

一致性是小头脑的妖精。

这是数值类型重载解析的表现

形式
数值

提升包括自动执行预定义的一元和二进制数值运算符的操作数的某些隐式转换。数值提升不是一种独特的机制,而是将重载解析应用于预定义运算符的效果。数值提升具体不会影响用户定义运算符的计算,尽管可以实现用户定义运算符以表现出类似的效果。

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

如果你看看

long operator *(long x, long y);
uint operator *(uint x, uint y);

从该链接中,您可以看到这是两个可能的重载(该示例指的是operator *,但对于operator +也是如此(。

uint被隐式转换为过载解决的long,如int 所示。

从 uint 到长、ulong、float、double或十进制。

从整数到长整型、浮点数、双精度数或十进制。

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

做出这个决定的动机是什么?

可能需要设计团队的成员来回答这个问题。 埃里克·利珀特,你在哪里? :-) 请注意,尽管下面的 @Nicolas 推理非常合理,但两个操作数都转换为"最小"类型,可以包含每个操作数的完整值范围。

我认为

编译器的行为非常合乎逻辑和预期。

在下面的代码中:

int i;
int j;
var k = i + j;

此操作存在确切的重载,因此k int。同样的逻辑适用于添加两个uint,两个byte或你有什么。编译器的工作在这里很容易,它很高兴,因为重载分辨率找到了完全匹配。编写此代码的人很有可能期望kint,并且知道操作在某些情况下可能会溢出。

现在考虑您询问的情况:

uint i;
int j;
var k = i + j;

编译器看到什么?好吧,它看到一个没有匹配重载的操作;没有运算符+重载将intuint作为其两个操作数。因此,重载解析算法继续进行,并尝试找到可能有效的运算符重载。这意味着它必须找到一个重载,其中所涉及的类型可以"容纳"原始操作数;也就是说,ij都必须隐式转换为所述类型。

编译器无法将uint隐式转换为int,因为此类转换不会存在。它不能隐式地将int转换为uint,因为该转换也不存在(两者都可能导致幅度变化(。因此,它真正拥有的唯一选择是选择第一个可以"容纳"两种操作数类型的更广泛的类型,在这种情况下是 long .一旦两个操作数都隐式转换为long k long就很明显了。

IMO 这种行为的动机是选择最安全的可用选项,而不是猜测可疑编码人员的意图。编译器无法对编写此代码的人期望k是什么做出有根据的猜测。int?好吧,为什么不是uint?这两种选择似乎都同样糟糕。编译器选择唯一的逻辑路径;安全的:long。如果编码人员希望k intunit,他只需显式转换其中一个操作数。

最后但并非最不重要的一点是,C# 编译器的重载解析算法在确定最佳重载时不考虑返回类型。因此,将操作结果存储在uint中的事实与编译器完全无关,并且在重载解决过程中没有任何影响。

这都是我的猜测,我可能完全错了。但这似乎是合乎逻辑的推理。

i        int i = 1024;
uint x = 2048;
// Technique #1
    x = x + Convert.ToUInt32(i);    
// Technique #2
    x = x + checked((uint)i);
// Technique #3
    x = x + unchecked((uint) i);
// Technique #4
    x = x + (uint)i;

C# 的数字提升规则松散地基于 Java 和 C 的规则,它们的工作原理是标识两个操作数都可以转换为的类型,然后使结果成为相同的类型。 我认为这种方法在 1980 年代是合理的,但较新的语言应该将其放在一边,以支持一种研究如何使用值的语言(例如,如果我正在设计一种语言,那么给定Int32 i1,i2,i3; Int64 l;编译器将处理i4=i1+i2+i3;使用 32 位数学[在溢出的情况下抛出异常]将处理 64 位数学l=i1+i2+i3;。 但是 C# 规则是什么,不是似乎可能会改变。

应该注意的是,根据定义,C# 提升规则始终选择语言规范认为"最合适"的重载,但这并不意味着它们真的最适合任何有用的目的。 例如,double f=1111111100/11111111.0f;看起来应该产生 100.0,如果两个操作数都提升为 double,则会正确计算,但编译器将改为将整数1111111100转换为float,产生 1111111040.0f,然后执行除法产生 99.999992370605469。