减去无符号整数和有符号整数以及常量折叠

8
基于这个有趣的问题:Addition of int and uint,并按照Nicholas Carey答案中提到的对于常量折叠进行试验,我发现编译器似乎表现不一致:

考虑下面的代码片段:

int i = 1;
uint j = 2;
var k = i - j;

在这里,编译器正确地将k解析为long。这种特定的行为在规范中得到了明确定义,如先前引用问题的答案所解释的那样。
令我感到惊讶的是,在处理文字常量或常量时,该行为会发生变化。阅读Nicholas Carey的答案后,我意识到行为可能是不一致的,因此我进行了检查,并且果然如此:
const int i = 1;
const uint j = 2;
var k = i - j; //Compile time error: The operation overflows at compile time in checked mode.
k = 1 - 2u; //Compile time error: The operation overflows at compile time in checked mode.

k在这种情况下被解析为Uint32

处理常量时行为不同的原因是什么?还是这是编译器中的一个小但不幸的“bug”(没有更好的术语)?


2
猜测编译器不会对常量进行隐式转换。 - Powerlord
@Powerlord 嗯,毕竟它正在将 int 隐式转换为 uint,所以必须这样做。 - InBetween
1
规范允许这样做,尽管... §6.1.9 隐式常量表达式转换: "类型为 int 的常量表达式 (§7.19) 可以转换为类型 sbytebyteshortushortuintulong,前提是常量表达式的值在目标类型的范围内。" 仍在努力查找它对字面值和/或常量变量的说明。 - Powerlord
2个回答

4
C#规范版本5,第6.1.9节,“常量表达式”只允许以下隐式转换:
* 常量表达式(§7.19)类型为int,可以转换为sbyte、byte、short、ushort、uint或ulong类型,前提是常量表达式的值在目标类型的范围内。 * 类型为long的常量表达式可以转换为ulong类型,前提是常量表达式的值不是负数。
请注意,长整数(long)不在int转换列表中。
另一半问题是仅有少量数字推广适用于二进制操作:
(来自第7.3.6.2节“二元数值提升”):
如果任意一个操作数的类型为decimal,则另一个操作数将被转换为decimal类型,或者如果另一个操作数是float或double类型,则会发生绑定时错误。 否则,如果任意一个操作数的类型为double,则另一个操作数将被转换为double类型。 否则,如果任意一个操作数的类型为float,则另一个操作数将被转换为float类型。 否则,如果任意一个操作数的类型为ulong,则另一个操作数将被转换为ulong类型,或者如果另一个操作数的类型为sbyte、short、int或long,则会发生绑定时错误。 否则,如果任意一个操作数的类型为long,则另一个操作数将被转换为long类型。 否则,如果任意一个操作数的类型为uint,且另一个操作数的类型为sbyte、short或int,则两个操作数都将被转换为long类型。 否则,如果任意一个操作数的类型为uint,则另一个操作数将被转换为uint类型。 否则,两个操作数都将被转换为int类型。
请记住:常量禁止执行从int到long的转换,这意味着两个参数都将被提升为uint。

顺便提一下,没有任何迹象表明为什么“int”常量表达式不能转换为“long”...或者是否根本无法转换“uint”常量...在这两种情况下都没有解释。 - Powerlord

3

请查看此处的答案。

问题在于您使用了const。

运行时,当有一个const时,行为与字面值完全相同,或者就像您在代码中硬编码这些数字一样,因此由于数字是1和2,它会转换为Uint32,因为1在uint32范围内。然后,当您尝试用uint32减去1-2时,它会溢出,因为1u-2u= +4,294,967,295(0xFFFFFFFF)。

编译器允许查看字面值,并将其解释为其他变量所不能的方式。由于const永远不会改变,因此它可以做出它本来无法做出的保证。在这种情况下,它可以保证1在uint范围内,因此可以隐式转换。在正常情况下(没有const),它无法做出那个保证,

有符号整数的范围从-2,147,483,648(0x80000000)到+2,147,483,647(0x7FFFFFFF)。

无符号整数的范围从0(0x00000000)到+4,294,967,295(0xFFFFFFFF)。

故事的寓意是,当混合const和var时要小心,您可能会得到意想不到的结果。


在这里使用 var 不是问题,因为它是一个编译时错误。在实际代码中,我永远不会在表达式中使用 var,除非类型一眼就很清楚。而且我知道使用 const常量字面值 会有相同的行为。我的问题是,为什么这种行为与涉及变量的一般行为不一致。 - InBetween
@Powerlord 不,var不是问题所在。long k = 1 - 2u;也是编译时错误。在C#中,返回类型从不在重载决策中起作用,那么为什么var会成为问题的一部分呢?问题在于当处理常量时,为什么1 - 2u会解析为uint - InBetween
因为当您使用常量时,编译器会将它们视为文字,这意味着编译器会查看它们并说:“那是一个uint”。这意味着当您尝试从较小的uint中减去较大的uint时,它会溢出。对于uints而言,1-2= +4,294,967,295(0xFFFFFFFF)。 - Jared Wadsworth
@JaredWadsworth:好的,如果我要减去两个uint,我会理解这一点。但是我正在减去一个int和一个uint,所以编译器将uint解释为返回类型是错误的。当不涉及常量时,它不会这样做。为什么要改变行为? - InBetween
1
@InBetween 很抱歉,但编译器从不会出错。它将1解释为uint,将2解释为uint,因为它将它们放在文字中。这仅在使用常量时发生,以确保常量永远不会更改(即保持恒定)。 1在uint范围内,因此被转换为uint,然后进行减法运算。如果它们不是const,则变成long的原因是编译器无法保证int在uint范围内。使用const修饰符可以。 - Jared Wadsworth
显示剩余4条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接