整体类型促销不一致

时间:2017-09-29 12:12:58

标签: c#

using System;

public class Tester
{
    public static void Main()
    {
        const uint x=1u;
        const int y=-1;
        Console.WriteLine((x+y).GetType());
        // Let's refactor and inline y... oops!
        Console.WriteLine((x-1).GetType());
    }
}

想象一下上面的代码用于以下情况:

public long Foo(uint x)
{
    const int y = -1;
    var ptr = anIntPtr.ToInt64() + (x + y) * 4096;
    return ptr;
}

内联y看起来非常安全,但事实并非如此。语言本身的这种不一致是违反直觉的,而且非常危险。大多数程序员只是内联y,但实际上你最终会出现整数溢出错误。事实上,如果你编写如上所述的代码,你很容易让下一个人在内联y的同一段代码上工作,甚至不用考虑两次。

我认为这是C#的一个非常适得其反的语言设计问题。

第一个问题,在C#规范中定义了这种行为,为什么这样设计?

第二个问题,1.GetType() / (-1).GetType()给出System.Int32。那么为什么它与const int y=-1的行为不同?

第三个问题,如果隐式转换为uint,那么我们如何明确地告诉编译器它是有符号整数(1i不是有效的语法!)?

最后一个问题,这不是语言设计团队所期望的行为(Eric Lippert会加入吗?),可以吗?

2 个答案:

答案 0 :(得分:9)

此行为由C#标准的第6.1.9节描述,隐式常量表达式转换

  

•如果constant-expression的值在目标类型的范围内,则int类型的常量表达式(第7.19节)可以转换为sbyte,byte,short,ushort,uint或ulong类型。

所以你有const uint x = 1u;和常量表达式(x - 1)

根据规范,x - 1的结果通常为int,但因为常量表达式的值(即0)在{{1}的范围内它将被视为uint

请注意,编译器在此处将uint视为无符号。

如果您将表达式更改为1,则会将(x + -1)视为已签名,并将结果更改为-1。 (在这种情况下,int中的-是“一元运算符”,它将-1的结果类型转换为-1,因此编译器无法再转换它可以intuint}那样。

本部分规范意味着如果我们要将常量表达式更改为1,则结果将不再是x - 2,而是转换为uint。但是,如果进行了更改,则会收到编译错误,指出结果将溢出int

这是因为C#规范的另一部分,在 7.19常量表达式部分中指出:

  

常量表达式的编译时评估使用与非常量表达式的运行时评估相同的规则,除了运行时评估会引发异常的情况,编译时评估会导致编译时错误发生。

在这种情况下,如果进行uint计算会出现溢出,因此编译器会发生故障。

关于这一点:

checked

这与此相同:

const uint x = 1u;
const int y = -1;
Console.WriteLine((x + y).GetType()); // Long

这是因为-1的类型为Console.WriteLine((1u + -1).GetType()); // Long ,而int的类型为1u

第7.3.6.2节“二进制数字促销”描述了这一点:

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

(我省略了与此特定表达无关的部分。)

附录:我只是想指出常数和非常数值之间的一元减号(又称“否定”)运算符的细微差别。

根据标准:

  

如果否定运算符的操作数是uint类型,则转换为long类型,结果类型为long。

变量确实如此:

uint

虽然对于编译时常量,如果涉及var p = -1; Console.WriteLine(p.GetType()); // int var q = -1u; Console.WriteLine(q.GetType()); // long var r = 1u; Console.WriteLine(r.GetType()); // uint 的表达式正在使用它,1的值将转换为uint,以便将整个表达式保持为{{1} {},uint实际上被视为uint

我同意OP - 这是非常微妙的东西,导致各种惊喜。

答案 1 :(得分:7)

  

第一个问题,C#规范中定义了这种行为

你的第一个问题是答案,在Matthew Watson的优秀答案中得到了答案。

  

为什么这样设计?

所有设计流程都需要在各种竞争设计目标之间进行权衡。 C#的设计目标包括对C ++开发人员熟悉的各种元素,与使用非.NET友好约定(如无符号整数类型)的非托管库进行互操作的能力,编写能够找出您的意思的编译器的能力模棱两可的情况,但在看起来你做错了什么时仍会通知你,等等。

“值可以无缝替代评估这些值的符号”是语言设计的一个很好的原则。但它不是唯一的一个。由于其中一些目标在您的情况下是矛盾的,因此需要付出一些代价。 (另外,正如我在下面提到的,你不是替换值!)

我同意你的观点,x + -1x - 1有不同类型的事实很奇怪。你想要它们是什么类型的?

让我们假设你想让它们都很长。现在我们遇到以下问题:x - x的类型是什么?如果它是uint,因为我们有两个不同的uint,那么我们有x - xx - 1是不同类型的怪异。如果它很长,那么我们就会感到奇怪,因为适合uint 的两个uint 的差异并不是一个uint。

让我们假设你想要他们两个都是uint。那么x + anySignedInt应该是不是?为什么它应该是uint而不是int?当然如果我们有uint 2和int -3,那么2 + -3应该是int -1。

无论你做什么,你最终都会遇到奇怪的情况。那是因为无符号数量不符合通常的算术规则。语言设计团队在糟糕的情况下尽其所能。

17年前这些决定是如何达成的具体细节在时间的迷雾中消失了。

  

第二个问题,1和-1的类型是System.Int32。那么为什么它与const int y = -1?

的行为不同

我认为你的问题是“为什么x + yx - 1分析时显然是等同的表达式?”,但它们相同的表达式。 x + yx + -1是相同的表达式,并对它们进行相同的分析;不适合uint的uint和int常量的总和会促使两者都变长。两个uint的区别是一个uint。

你的根本错误在于你认为增加一个负数,减去一个正数是相同的。 在无符号算术中它们不是,因为在无符号算术中没有“添加负数”之类的东西。没有负面消息!

  

如果隐式转换为uint,那么我们如何明确地告诉编译器它是有符号整数(1i不是有效的语法!)?

我不明白这个问题。你告诉编译器带有强制转换的东西的类型,但我不认为这就是你所要求的。

  

最后一个问题,这不是语言设计团队所期望的行为(Eric Lippert会加入吗?),可以吗?

我不再代表语言设计团队发言,但我可以告诉你,如果我在以下时间问你这个问题我会说些什么:

语言设计团队强烈希望你不要使用uint,尤其是永远不要在同一个表达式中混合int和uint,因为这样做会令人困惑和怪异。仅使用uint与使用uint的非托管代码进行互操作。

你会注意到uint不在公共语言子集中,并且许多逻辑上永远不会消极的数量,例如字符串或数组的长度,在.NET中始终是整数。这是有原因的。使用整数或长期。