【问题标题】:Why does C# limit the set of types that can be declared as const?为什么 C# 限制可以声明为 const 的类型集?
【发布时间】:2010-10-01 06:47:30
【问题描述】:

编译器错误CS0283 表示只有基本的 POD 类型(以及字符串、枚举和空引用)可以声明为const。有人对这种限制的基本原理有理论吗?例如,如果能够声明其他类型的 const 值,例如 IntPtr,那就太好了。

我相信const 的概念实际上是 C# 中的语法糖,它只是用字面值替换了名称的任何使用。例如,给定以下声明,任何对 Foo 的引用都会在编译时替换为“foo”。

const string Foo = "foo";

这会排除任何可变类型,所以也许他们选择了这个限制,而不是必须在编译时确定给定类型是否可变?

【问题讨论】:

    标签: c# constants compiler-errors


    【解决方案1】:

    来自C# specification, chapter 10.4 - Constants
    (C# 3.0 规范中的 10.4,2.0 的在线版本中的 10.3)

    常量是表示常量值的类成员:可以在编译时计算的值。

    这基本上是说您只能使用仅由文字组成的表达式。不能使用对任何方法、构造函数(不能表示为纯 IL 文字)的任何调用,因为编译器无法在编译时执行该执行,从而计算结果。此外,由于无法将方法标记为不变(即输入和输出之间存在一对一映射),编译器执行此操作的唯一方法是分析 IL 以查看是否它取决于输入参数、特殊情况处理某些类型(如 IntPtr)或只是禁止对任何代码的每次调用。

    例如,IntPtr 虽然是值类型,但仍然是结构,而不是内置文字之一。因此,任何使用 IntPtr 的表达式都需要调用 IntPtr 结构中的代码,这对于常量声明是不合法的。

    我能想到的唯一合法的常量值类型示例是仅通过声明它就用零初始化的示例,这几乎没有用。

    至于编译器如何对待/使用常量,它将使用计算值代替代码中的常量名。

    因此,您有以下效果:

    • 没有对原始常量名称、声明它的类或命名空间的引用编译到此位置的代码中
    • 如果你反编译代码,它会在其中包含幻数,只是因为如上所述,对常量的原始“引用”不存在,只有常量的值
    • 编译器可以使用它来优化甚至删除不必要的代码。例如,if (SomeClass.Version == 1),当 SomeClass.Version 的值为 1 时,实际上会删除 if 语句,并保留正在执行的代码块。如果常量的值不是 1,则整个 if 语句及其块将被删除。
    • 由于常量的值被编译到代码中,而不是对该常量的引用,因此如果常量的值发生变化(它应该不是!)

    换句话说,有以下场景:

    1. 程序集 A,包含一个名为“Version”的常量,值为 1
    2. 程序集 B,包含一个表达式,该表达式根据该常量分析程序集 A 的版本号并将其与 1 进行比较,以确保它可以与程序集一起使用
    3. 有人修改了程序集 A,将常量的值增加到 2,并重建了 A(但不是 B)

    在这种情况下,程序集 B 的编译形式仍会将 1 的值与 1 进行比较,因为在编译 B 时,常量的值是 1。

    事实上,如果这是在程序集 B 中唯一使用程序集 A 中的任何内容,则程序集 B 将在不依赖程序集 A 的情况下编译。在程序集 B 中执行包含该表达式的代码将不会加载程序集 A。

    因此,常量应该只用于永远不会改变的事物。如果它是一个可能或将在未来某个时间更改的值,并且您不能保证同时重建所有其他程序集,则只读字段比常量更合适。

    所以没关系:

    • public const Int32 NumberOfDaysInAWeekInGregorianCalendar = 7;
    • public const Int32 NumberOfHoursInADayOnEarth = 24;

    虽然不是这样:

    • public const Int32 AgeOfProgrammer = 25;
    • public const String NameOfLastProgrammerThatModifiedAssembly = "Joe Programmer";

    2016 年 5 月 27 日编辑

    好的,刚刚获得了赞成票,所以我在这里重新阅读了我的答案,这实际上有点错误。

    现在,C# 语言规范的意图就是我上面写的所有内容。您不应该使用无法用文字表示为 const 的东西。

    但是你可以吗?嗯,是的……

    我们来看看decimal类型。

    public class Test
    {
        public const decimal Value = 10.123M;
    }
    

    让我们看看这个类在使用 ildasm 时的样子真的

    .field public static initonly valuetype [mscorlib]System.Decimal X
    .custom instance void [mscorlib]System.Runtime.CompilerServices.DecimalConstantAttribute::.ctor(int8, uint8, uint32, uint32, uint32) = ( 01 00 01 00 00 00 00 00 00 00 00 00 64 00 00 00 00 00 ) 
    

    让我为你分解一下:

    .field public static initonly
    

    对应于:

    public static readonly
    

    没错,const decimal 实际上就是readonly decimal

    真正的意义在于编译器将使用DecimalConstantAttribute 来发挥它的魔力。

    现在,这是我所知道的唯一一个使用 C# 编译器的魔法,但我认为值得一提。

    【讨论】:

    • 显然你不能这样做,但是假设性地说,假设我们有一个用 [Pure] 属性标记的返回值类型的静态方法,编译器实际上强制执行(目前没有)。只要参数只是文字,语言设计者是否至少理论上允许使用返回静态值类型的纯方法(具有实际的 [Pure] 属性强制)来初始化 const?似乎他们可以使用文字 args 运行它,得到显然不会改变的结果并在值中添加 sub。我忽略了什么吗?
    • In this case, assembly B, in its compiled form, will still compare the value of 1 to 1, because when B was compiled, the constant had the value 1 - 你仍然可以在任何 C# 版本中使用 public const int AssemblyVersion = 1 做同样的事情,所以 结构 本身没有任何问题
    【解决方案2】:

    有人对此限制的基本原理有理论吗?

    如果允许它只是一个理论,我的理论是原始类型的 const 值可以在 MSIL 中的文字操作码参数中表示......但其他非原始类型的值不能,因为 MSIL 没有t 具有将用户定义类型的值表示为文字的语法。

    【讨论】:

    • 这似乎是可能的,是的(另请参阅我对@IAmCodeMonkey 的评论)
    • MSIL 根据字节序列定义常量。给定一个类型,例如struct Vector2d {public double X,Y;},编译器可以允许声明常量const Vector2d UnitXVector = {X=1.0, Y=0.0};,因为它可以确定该结构表示的确切字节序列。但是,这只适用于没有私有字段的结构;如果结构使用公共属性和私有支持字段,编译器只能通过弄清楚属性的确切作用来构造它。
    • @supercat,您是否暗示 MSIL 可以依赖在内存中以某种方式布置的对象? StructLayoutAttribute 的存在不就意味着不能吗?
    • @binki:实际上还有一个更大的问题,即使用结构的一个版本编译常量的代码如果使用该结构的不同版本运行,可能会产生意想不到的结果。 Microsoft 可以保证它永远不会更改 Decimal 类型,因此由一个 Decimal 值封装的字节序列可能有一天会封装一个不同的值,但对于大多数类型来说,没有办法做出这样的保证。跨度>
    • @supercat 但如果结构的编译器序列化版本的 byte[] 的长度不匹配,他们可以通过确保 CLR 本身不会崩溃来解决这个问题——我想这种ABI 中断与父类从继承类正在调用的构造函数中删除参数时基本相同。 C# 并没有试图让它变得那么难。而且,更重要的是,已经有文档、讨论和理解readonly static 通常更好const(但internal API 的默认方法参数!)听起来这就是“成功的坑”
    【解决方案3】:

    我认为 const 的概念实际上是 C# 中的语法糖,它只是用字面值替换名称的任何用途

    编译器如何处理其他语言中的 const 对象?

    您可以将 readonly 用于我在运行时评估的可变类型。有关差异,请参阅this article

    【讨论】:

    • 我想你可以使用 Reflector 来检查这个。
    • 关键字 const 在不同的语言中可能有不同的效果,比如在 C++ 中,它可以用来保证方法不会改变对象的状态,例如。
    • @LasseV.Karlsen,但const 在 C(++) 和 C# 中对于 non-reference 类型的含义相同。所以这个答案在谈论这个时是正确的。其他语言有这个“常量正确性”的概念。 C# 中最接近的类似物是不可变模式与由构造函数初始化的readonly 类成员组合。
    【解决方案4】:

    在我看来,只有值类型可以表示为常量(字符串除外,它位于值和对象类型之间)。

    对我来说没问题:对象(引用)必须在堆上分配,但根本不分配常量(因为它们在编译时被替换)。

    【讨论】:

    • 可以使用字符串,因为编译器可以插入 IL 代码来加载具有给定值的字符串。由于这是专门为字符串添加的 IL 魔法,因此可以使用它们。
    【解决方案5】:

    简而言之,所有简单类型、枚举和字符串都是不可变的,但例如 Struct 不是。您可以拥有一个状态可变的结构(字段、属性,甚至对引用类型的引用)。所以编译器不能确保(在编译时)结构变量的内部状态不能改变。所以编译器需要确保一个类型在定义上是不可变的,以便在常量表达式中使用。

    【讨论】:

      【解决方案6】:

      const 仅限于 C# 中的数字和字符串,因为编译器将变量替换为 MSIL 中的文字值。换句话说,当你写的时候:

      const string myName = "Bruce Wayne";
      if (someVar == myName)
      {
         ...
      }
      

      实际上被视为

      if (someVar == "Bruce Wayne")
      {
         ...
      }
      

      是的,C# 编译器足够聪明,可以将字符串上的相等运算符 (==) 视为

      string1.Equals(string2)
      

      【讨论】:

      • 对,我不明白为什么你不能声明例如一个 const IntPtr,也可以在使用点替换。也许不同之处在于必须构造一个 IntPtr,而 POD 和字符串可以直接在 IL 中表示。
      • 编译器必须在 IntPtr 中执行代码才能获得最终值,并且这个“执行代码以获得值”部分必须在使用常量的任何地方编译到代码中,并且这个是不允许的部分。
      猜你喜欢
      • 1970-01-01
      • 2014-05-07
      • 1970-01-01
      • 1970-01-01
      • 2016-02-14
      • 2013-03-27
      • 1970-01-01
      • 2012-11-16
      • 2011-06-03
      相关资源
      最近更新 更多