编译器值类型分辨率和硬编码“0”整数值

时间:2013-01-08 21:37:44

标签: c# .net compiler-construction

首先,有点背景知识。阅读问题并接受答案posted here,了解我的问题的具体方案。我不确定是否存在其他相似的案例,但这是我所知道的唯一案例。

上面的“怪癖”是我早已意识到的。直到最近,我才明白原因的全部范围。

微软关于SqlParameter课程的文档对这种情况有了更多了解。

  

在value参数中指定Object时,SqlDbType为   从Microsoft .NET Framework类型的Object推断。

     

使用SqlParameter构造函数的此重载时请小心   指定整数参数值。因为这个过载需要一个   类型Object的值,您必须将整数值转换为Object   键入,当值为零时,如下面的C#示例所示。

     

Parameter = new SqlParameter("@pname", Convert.ToInt32(0));

     

如果你这样做   不执行此转换,编译器假定您正在尝试   调用SqlParameter(string,SqlDbType)构造函数重载。

(emph。补充)

我的问题是为什么编译器假定当你指定硬编码“0”(并且只有值“0”)时,你试图指定枚举类型而不是整数类型?在这种情况下,它假定您声明SqlDbType值,而不是值0.

这是非直观的,更糟糕​​的是,错误是不一致的。我有一些旧的应用程序,我已经编写了多年来调用存储过程。我将对应用程序进行更改(通常甚至与我的SQL Server类无关),发布更新,这个问题会突然中断应用程序。

当包含多个方法签名的对象包含两个相似的签名,其中一个参数是对象/整数而另一个接受枚举时,为什么编译器会被值0混淆?

正如我所提到的,我从未将此视为任何其他类的任何其他构造函数或方法的问题。这对SqlParameter类来说是唯一的,还是在C#/ .Net?

中继承的bug

5 个答案:

答案 0 :(得分:19)

这是因为零整数可以隐式转换为枚举:

enum SqlDbType
{
    Zero = 0,
    One = 1
}

class TestClass
{
    public TestClass(string s, object o)
    { System.Console.WriteLine("{0} => TestClass(object)", s); } 

    public TestClass(string s, SqlDbType e)
    { System.Console.WriteLine("{0} => TestClass(Enum SqlDbType)", s); }
}

// This is perfectly valid:
SqlDbType valid = 0;
// Whilst this is not:
SqlDbType ohNoYouDont = 1;

var a1 = new TestClass("0", 0);
// 0 => TestClass(Enum SqlDbType)
var a2 = new TestClass("1", 1); 
// => 1 => TestClass(object)

(改编自Visual C# 2008 Breaking Changes - change 12

当编译器执行重载决策时,SqlDbTypeobject构造函数的Applicable function memberbetter conversion rules,因为:

  

从参数类型到相应参数

的类型存在隐式转换(第6.1节)

SqlDbType x = 0object x = 0都有效)

由于Visual C# 2008SqlDbType参数优于object参数:

  • 如果T1T2属于同一类型,则两种转换都不会更好。
    • objectSqlDbType的类型不同
  • 如果ST1,则C1是更好的转换。
    • 0不是object
  • 如果ST2,则C2是更好的转换。
    • 0不是SqlDbType
  • 如果存在从T1T2的隐式转换,并且不存在从T2T1的隐式转换,则C1是更好的转换。
    • 不存在从objectSqlDbType的隐式转换
  • 如果存在从T2T1的隐式转换,并且不存在从T1T2的隐式转换,则C2是更好的转换。
    • 存在从SqlDbTypeobject的隐式转换,因此SqlDbType是更好的转化

请注意,正如@Eric在他的回答中解释的那样,完全构成一个常数0({3}}(微软对C#规范的实现)已经(非常微妙地)改变了。

答案 1 :(得分:16)

RichardTowers的答案非常好,但我想我会补充一点。

正如其他答案所指出的那样,行为的原因是(1)零可转换为任何枚举,显然是对象,(2)任何枚举类型更具体因此,采用枚举的方法因重载决策选择为更好的方法。第二点是我希望不言自明,但是第一点解释了什么?

首先,这里有一个不幸的偏离规范。规范说任何文字零,即源代码中出现的实际字面的数字0,可以隐式转换为任何枚举类型。编译器实际上实现了可以如此转换任何常数零。原因是因为编译器有时允许允许常数为零而有时不以奇怪且不一致的方式存在的错误。解决问题的最简单方法是始终允许恒定的零。你可以在这里详细阅读:

http://blogs.msdn.com/b/ericlippert/archive/2006/03/28/the-root-of-all-evil-part-one.aspx

其次,允许零转换为任何枚举的原因是为了确保始终可以将“标志”枚举归零。良好的编程习惯是每个“标志”枚举都有一个值“无”,它等于零,但这是一个指导,而不是一个要求。 C#1.0的设计者认为你可能不得不说

看起来很奇怪
for (MyFlags f = (MyFlags)0; ...

初始化本地。我个人的观点是,这个决定造成了比它值得更多的麻烦,无论是对上述错误的悲伤还是你所发现的重载决议引入的奇怪之处。

最后,构造函数的设计者可能已经意识到这首先是一个问题,并且使得重载的签名使得开发人员可以清楚地决定调用哪个ctor而不必插入强制转换。不幸的是,这是一个相当模糊的问题,所以很多设计师都没有意识到这一点。希望任何阅读此内容的人都不会犯同样的错误; 如果您希望两个覆盖具有不同的语义,请不要在对象和任何枚举之间创建歧义。

答案 2 :(得分:1)

这显然是一种已知行为,会影响任何有枚举和对象类型的函数重载。我完全不明白,但Eric Lippert在his blog

上总结得很清楚

答案 3 :(得分:1)

这是因为整数文字0具有对任何枚举类型的隐式转换。 C#规范声明:

  

6.1.3隐式枚举转化

     

隐式枚举转换允许decimal-integer-literal   0转换为任何枚举类型和任何可以为null的类型   底层类型是枚举类型。在后一种情况下,转换是   通过转换为基础枚举类型并包装   结果

因此,在这种情况下,最具体的重载是SqlParameter(string, DbType)

这不适用于其他int值,因此SqlParameter(string, object)构造函数是最具体的。

答案 4 :(得分:0)

在解析重载方法的类型时,C#选择最具体的选项。 SqlParameter类有两个构造函数,它们只有两个参数SqlParameter(String, SqlDbType)SqlParameter(String, Object)。当您提供文字0时,它可以被解释为Object或SqlDbType。由于SqlDbType比Object更具体,因此假定它是意图。

您可以在this answer中了解有关重载解析的更多信息。