为什么结构体不能声明为const?

52

你可以声明结构体为常量。类和结构体是可以互换的,并且对于类也适用。 - templatetypedef
1
@templatetypedef:我需要提到“除了null以外”的情况吗? - Lazlo
1
@templatetypedef:我不确定您使用的是哪个版本的C#,但在我看过的任何版本中都不是这样。 - cdhowie
糟糕!我完全误读了那个。抱歉! - templatetypedef
1
结构体必须是不可变的自从什么时候开始的? - binki
4个回答

64
因为值类型构造函数可能会做任何事情——例如,根据时间切换逻辑。在理论上,常量值类型是有意义的,但由于构造函数灵活地随意操作,它在实践中无法适用于自定义值类型。(请记住常量在编译时被评估,这意味着您的构造函数必须在编译时运行。)

1
谢谢,等S/O让我再接受。真可惜,我有一个“合法”的构造函数,如果可以的话,我很乐意把它设为const。但是没关系,我想我会找到另一个设计的。 - Lazlo
2
@Lazlo:即使如此,编译器在编译时仍然必须调用它。但是编译器并不是技术上要运行您的代码。 - cdhowie
3
我猜我在寻找类似于C++中的聚合体或POD结构体:http://en.wikipedia.org/wiki/C%2B%2B_classes的等价物。这是只是一种不同的值类型或数据组的东西。噢,好吧。 - Lazlo
1
结构体可以在没有构造函数的情况下创建。具有访问结构体字段权限的代码可以直接将其填充为所需值;编译器可以使用字段和值列表创建适当配置的结构体实例。 - supercat
3
问题并不在于性能,而在于语义,因为const值可以在静态只读字段无法使用的情况下使用(例如作为默认参数值)。 它们也被“烘焙”到编译代码中,在某些情况下这是不好的,但在其他情况下可能会有用。 - supercat
显示剩余4条评论

27

C#中的Const表示它可以在编译时确定,这就是为什么只有非常基础的类型比如intstring才能被定义为const。

如果你来自于C语言背景,也许readonly关键字更适合你。


3
这是针对有C/C++编程背景的程序员的真实答案。 - eonil
1
@Eonil 不是100%正确的; C / C ++的“const”关键字同时服务于C#的“const”和“readonly”的目的-例如,在C#中添加两个“readonly int”字段不会导致常量折叠,而在C / C ++中添加两个“const int”变量将会(如果值在编译时已知)。 C#的“const”更接近于C ++ 11的“constexpr”修饰符,而C#的“readonly”在C / C ++中没有单一的类比。 - cdhowie
@cdhowie,我认为readonly T where T:struct的类似物应该是const T&(在C ++中)。但是,大多数情况下,当人们遇到C#中的readonly关键字时,实际上是在寻找C ++对“常量正确性”的支持,而这在C#中并不存在(而新的一组Immutable类/模式则尝试提供一种类似但相当笨拙且费力的替代方法)。 - binki
@binki 不同意。const reference 是另一个存在于别处的对象的别名。在 C# 中,readonly T 创造了一个不可变的字段,该值存储在该字段本身中 -- 而不是像 C++ 引用那样存在别处。 - cdhowie
3
“这就是为什么只有像int和string这样的非常原始的类型可以成为const。”我不同意,C++也不同意(自C++11以来)。我不明白为什么我们不能在C#中拥有类似于C++ constexpr 的东西。 - Tom Lint

4
为了让C#编译器生成结构类型的const值,它必须知道所有字段中应该放置的值。C#编译器本身知道如何初始化某些类型(如Decimal)的字段,但对于大多数值类型,它没有这样的知识。
编译器可以提供一种声明结构类型常量值的方法,只要在所有struct字段都被公开的情况下。如果结构体的字段是private,那么该类型的常量只能在结构体内部声明;如果字段是internal,则可以在程序集中的任何地方声明常量;如果字段是public,则可以在任何地方声明常量。
尽管我希望看到这样的特性,但我不指望任何主流的.NET语言会实现它。编译器固有地知道的命名类型常量可以参与其他常量表达式,而static readonly变量则无法做到。例如,如果NumRows是一个等于4的常量,则像Arr[3 * NumRows + 7]这样的表达式可以替换为Arr[19],即使NumRows是在外部程序集中定义的。这使得这些常量比static readonly变量具有重要的优势。然而,如果一个常量是编译器不知道的类型,它将极度受限于参与任何常量表达式的能力,从而有效地抵消了它作为常量的优势。如果值类型常量有一个公开的字段,编译器可以使用该字段的值作为常量,但由于.NET语言的创建者在哲学上反对具有公开字段的结构体,即使他们有能力这样做,我也不指望他们允许这种用法。
将常量与static readonly变量相比较,有一些潜在的用例,但许多这样的情况可以使用现有类型来处理得到。例如,库可能会公开一个const Int64来编码关于其版本的信息,并将该值用作GetLinkedVersionInfo方法的默认参数值。当编译调用代码时,该值将被“烘焙”进去,从而允许该方法报告调用者链接的库的版本,并可能确定是否存在与运行的版本不兼容的问题。

3

我刚刚使用一个简单的可变结构体测试了readonly关键字:

struct Test
{
   public int value;

   public void setInt(int val)
   {
      value = val;
   }
}

static class Program
{
   public static readonly Test t = new Test();

   static void Main()
   {
      Console.WriteLine(t.value); // Outputs "0"
      t.setInt(10);
      //t.value = 10;  //Illegal, will not let you assign field of a static struct
      Console.WriteLine(t.value); // Still outputs "0"
   }
}

尽管readonly结构体在技术上不是编译时常量,但运行时不允许其改变。即使通过setInt()方法进行调试,它看起来像值会改变,但在Main中并没有显示出变化。 我猜测这个结构体本身被放置在“只读”内存中,禁止它们进行更改。与类不同,后者仅保持指针恒定,允许类字段自由更改。 因此,即使对于可变的结构体,static readonly实际上也相当于const

4
结构体的所有实例方法、属性和事件都接收一个结构体的引用来操作它们,相当于以ref参数的形式接收this,但在vb.net和C#中存在一个令人讨厌的怪癖:如果在只读结构体实例上调用结构体方法或属性,则编译器会默默地复制该实例并将其传递给相关代码。这在代码不修改结构体的情况下产生了缓慢但正确的语义,并在代码修改结构体的情况下产生破坏性的语义。我希望微软能够定义一个属性... - supercat
3
这段文字讲述了一种可以允许结构体属性和方法指示它们是否影响传入的方法的方法,以便编译器可以允许不修改this的方法在只读结构体上使用,但禁止使用会修改的方法。否则,最好的选择是接受annoying和ugly语法public static void setInt(ref Test it, int val)Test.setInt(ref t, 10),如果你尝试在只读变量t上使用它将正确地产生错误提示。 - supercat
@supercat,我们真的只希望微软能够将C语言中“const”的语义完全复制到C#中 :-D - binki
@binki:在C++中,常量正确性因指针未声明为“const”而引起许多头痛。也许部分原因是因为Java避免了任何“只读引用”类型的概念。然而,常量正确性的反对者未能认识到的是,在编写正确代码时,需要知道一个方法是否可以修改传入的对象,假设传入的对象永远不会被其他任何东西修改,或者两者都不是;语言没有提供表达这些内容的手段,并不意味着程序员不需要知道它们... - supercat
...或者说,关于方法可能对传入对象做什么或期望从中得到什么的错误信念不会导致错误。然而,关于 ref 参数与对象引用,我认为 .NET 如果有独立的复制输入、复制输出、复制输入输出、只读引用和读写引用参数类型会更好。 - supercat
@supercat 既然 C# 已经支持 readonly 值成员,那么像 void DoThing(readonly ref MyStruct s); 这样扩展 ref 就非常自然,因为 ref 参数已经像成员一样行为了。添加语言和运行时支持“纯”函数(或者再次使用 readonly 关键字?),你就可以拥有重要的 const 正确性语义。如果只有… - binki

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