【问题标题】:Compiler Value Type Resolution and Hardcoded "0" Integer Values编译器值类型解析和硬编码“0”整数值
【发布时间】:2013-01-08 21:37:44
【问题描述】:

首先,一些背景知识。阅读问题和接受的答案posted here,了解我的问题的具体情况。我不确定是否存在其他类似的案例,但这是我知道的唯一案例。

上面的“怪癖”是我很早就意识到的。直到最近我才完全了解原因。

Microsoft 关于SqlParameter 类的文档对这种情况提供了更多说明。

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

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

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

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

(强调。添加)

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

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

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

正如我所提到的,我从未将这视为任何其他构造函数或任何其他类的方法的问题。这是 SqlParameter 类独有的还是 C#/.Net 中继承的错误?

【问题讨论】:

  • 与您的问题没有直接关系,但我认为更好的建议是使用命名参数 (new SqlParameter("@pname", value: 0)) 而不是 Convert.ToInt32()。
  • 为什么人们不总是指定 dbtype 和 length?不这样做会导致难以检测到问题并破坏计划缓存。只是为了节省一些击键还是有其他好处?
  • @adrianm,这是因为存在接受参数名称和值的构造函数。如果不能使用,为什么要在 SqlParameter 类上有构造函数?这不是关于保存击键,而是关于在类上使用构造函数,因为它们是两个构造函数之间的伪冲突,因为一个接受一个对象,另一个接受一个枚举。如果这是“糟糕的编码习惯”,那么首先不应该存在接受值的构造函数。
  • 与其在new SqlParameter("@pname", Convert.ToInt32(0)) 中进行不必要的方法调用,不如通过直接使用强制转换语法指定所需类型来解决问题,如new SqlParameter("@pname", (object)0) 中所示。当重载决议选择错误的重载(或拒绝选择任何重载)时,这是通常要做的事情。一般来说,如果像M(a, b, c, d) 这样的东西不起作用,请在需要的地方插入精确的类型,例如M((T1)a, b, (T2)c, d)。

标签: c# .net compiler-construction


【解决方案1】:

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

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)

当编译器执行重载决议时,对于SqlDbType 和object 构造函数,0 都是Applicable function member,因为:

存在从实参类型到相应形参类型的隐式转换(第 6.1 节)

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

SqlDbType 参数优于object 参数,因为better conversion rules:

  • 如果T1 和T2 是同一类型,则两种转换都不是更好。
    • object 和 SqlDbType 不是同一类型
  • 如果S 是T1,则C1 是更好的转换。
    • 0 不是 object
  • 如果S 是T2,则C2 是更好的转换。
    • 0 不是 SqlDbType
  • 如果存在从T1 到T2 的隐式转换,并且不存在从T2 到T1 的隐式转换,则C1 是更好的转换。
    • 不存在从 object 到 SqlDbType 的隐式转换
  • 如果存在从T2 到T1 的隐式转换,并且不存在从T1 到T2 的隐式转换,则C2 是更好的转换。
    • 存在从SqlDbType 到object 的隐式转换,因此SqlDbType 是更好的转换

请注意,正如@Eric 在他的回答中解释的那样,Visual C# 2008(微软对 C# 规范的实现)中 确切 构成常量 0 的内容(相当微妙地)发生了变化。

【讨论】:

  • 哇,谢谢你的周到。这无疑说明了情况。既然已经解释过了,我想我必须同意这是有道理的,而且这种方法是解释 0 的更好方法。不过,对于更随意的开发人员来说,这还是相当令人困惑的行为。
  • 会不会是您上次编译时使用了Visual C# 2005 而这次您使用了2008,而您被他们所做的重大更改所困扰?否则听起来像是一个标准的比特腐烂案例。
  • 我删除了我的问题,因为我开始怀疑这是否是问题的一部分。我上一次编译这个应用程序是在 2011 年,所以我使用的是 VS 2010。当新版本的 VS 出现时,我总是将我的项目更新到最新的 VS 版本,而不是 .Net 运行时变体。我不知道,但我不得不接受这个完全是个谜。
  • 非常彻底的回答!完成规范的工作做得很好。
【解决方案2】:

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

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

首先,不幸的是,这里与规范存在偏差。规范说,任何literal zero,即在源代码中实际literally出现的数字0,可以隐式转换为任何枚举类型。编译器实际上实现了任何常量零都可以因此被转换。原因是因为编译器会有时允许常量零,有时不允许,以一种奇怪和不一致的方式。解决问题的最简单方法是始终允许恒定零。您可以在此处详细阅读:

https://web.archive.org/web/20110308161103/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 而无需插入强制转换。不幸的是,这是一个非常模糊的问题,所以很多设计师都没有意识到这一点。希望任何阅读本文的人都不会犯同样的错误; 如果您希望这两个覆盖具有不同的语义,请不要在对象和任何枚举之间产生歧义。

【讨论】:

  • “原因是因为编译器有时会允许常量零,有时不允许的错误”感谢您承认并指出这一点!多年来,这种奇怪的事情对我来说一直是零星的讨厌!最后,它得到了验证——代码能够编译和工作(多年),并因此神奇地停止工作,这将帮助我今晚睡得更好。
  • 感谢您的解释。我在想知道将 0 隐式转换为枚举的原因。
【解决方案3】:

这显然是一种已知行为,会影响同时存在枚举和对象类型的任何函数重载。我不完全理解,但 Eric Lippert 在his blog上总结得很好

【讨论】:

  • 感谢您发布对 Lippert 博客的引用。很高兴看到做出实施此行为的决定时有些悲伤。情况似乎总是如此,简单的问题往往源于相当复杂的情况。
【解决方案4】:

这是因为整数文字 0 隐式转换为任何枚举类型。 C# 规范指出:

6.1.3 隐式枚举转换

隐式枚举转换允许十进制-整数-文字 0 被转换为任何枚举类型和任何可空类型,其 基础类型是枚举类型。在后一种情况下,转换是 通过转换为基础枚举类型并包装 结果。

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

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

【讨论】:

    【解决方案5】:

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

    您可以在this answer 中阅读有关重载解决方案的更多信息。

    【讨论】:

      猜你喜欢
      • 2017-12-30
      • 1970-01-01
      • 2012-12-05
      • 2016-08-09
      • 2019-10-24
      • 1970-01-01
      • 2013-03-29
      • 2023-03-05
      • 2017-12-06
      相关资源
      最近更新 更多