const,readonly和mutable值类型

时间:2012-01-24 09:47:29

标签: c#

我正在继续研究C#和语言规范,这是另一种我不太明白的行为:

C#语言规范在10.4节中明确说明了以下内容:

  

常量声明中指定的类型必须是sbyte,byte,short,ushort,int,uint,long,ulong,char,float,double,decimal,bool,string,enum-type或reference-type

它还在4.1.4节中说明了以下内容:

  

通过const声明,可以声明简单类型的常量(第10.4节)。不可能有其他结构类型的常量,但静态只读字段提供类似的效果。

好的,使用 static readonly 可以获得类似的效果。读完这篇文章后我去尝试了以下代码:

static void Main()
{
    OffsetPoints();
    Console.Write("Hit a key to exit...");
    Console.ReadKey();
}

static Point staticPoint = new Point(0, 0);
static readonly Point staticReadOnlyPoint = new Point(0, 0);

public static void OffsetPoints()
{
    PrintOutPoints();
    staticPoint.Offset(1, 1);
    staticReadOnlyPoint.Offset(1, 1);
    Console.WriteLine("Offsetting...");
    Console.WriteLine();
    PrintOutPoints();
}

static void PrintOutPoints()
{
    Console.WriteLine("Static Point: X={0};Y={1}", staticPoint.X, staticPoint.Y);
    Console.WriteLine("Static readonly Point: X={0};Y={1}", staticReadOnlyPoint.X, staticReadOnlyPoint.Y);
    Console.WriteLine();
}

此代码的输出为:

  

静态点:X = 0; Y = 0

     

静态只读点:X = 0; Y = 0

     

...抵消

     

静态点:X = 1; Y = 1

     

静态只读点:X = 0; Y = 0

     

按一个键退出...

我真的希望编译器能够给我一些关于改变静态readonly 字段的警告,或者使它失败,以便像使用引用类型一样改变字段。

我知道可变值类型是邪恶的(为什么Microsoft实现Point因为mutable是一个谜)但是编译器不应该以某种方式警告你,你试图改变静态只读值类型?或者至少警告你,你的Offset()方法不会产生“预期的”副作用?

6 个答案:

答案 0 :(得分:11)

Eric Lippert解释here发生了什么:

  

...如果该字段是只读的,并且引用发生在   然后,声明字段的类的实例构造函数   结果是一个值,即对象中字段I的值   由E。

引用      

这里重要的一点是结果是字段的值,   不是与该字段关联的变量。只读字段不是   构造函数之外的变量。 (这里的初始化程序是   被认为是在构造函数内部;看到我之前的帖子   主题。)

哦,只是为了强调可变结构的邪恶,这是他的结论:

  

这是可变值类型为恶的另一个原因。尝试   总是使值类型不可变。

答案 1 :(得分:7)

readonly 的要点是您无法重新分配参考或值。

换句话说,如果您尝试了这个

staticReadOnlyPoint = new Point(1, 1);

您将收到编译器错误,因为您尝试重新分配staticReadOnlyPoint。编译器会阻止你这样做。

但是,readonly不强制值或引用对象本身是否可变 - 这是由创建它的人设计到类或结构中的行为。

[编辑:正确解决所描述的奇怪行为]

您看到staticReadOnlyPoint看似不可变的行为的原因不是因为它本身是不可变的,而是因为它是一个只读结构。这意味着每次访问它时,您都会获得它的完整副本。

所以你的行

staticReadOnlyPoint.Offset(1, 1);

正在访问并改变字段的副本,而不是字段中的实际值。当你随后写出你正在写出的值时,然后写出 原始的另一个副本(而不是变异副本)。

您通过调用Offset而变异的副本将被丢弃,因为它永远不会分配给任何内容。

答案 2 :(得分:5)

编译器根本没有足够的有关方法的信息来了解该方法是否会改变结构。方法可能具有有用的副作用,但不会改变结构的任何成员。如果技术上可以将这样的分析添加到编译器中。但这对于生活在另一个组件中的任何类型都不起作用。

缺少的成分是元数据标记,表示方法不会改变任何成员。就像C ++中的 const 关键字一样。无法使用。如果在原始设计中添加它,它将完全符合非CLS标准。支持这一概念的语言非常少。我只能想到C ++,但我没有多少钱。

Fwiw,编译器会生成显式代码,以确保语句不会意外地修改readonly。这句话

staticReadOnlyPoint.Offset(1, 1);

被翻译为

Point temp = staticReadOnlyPoint;   // makes a copy
temp.Offset(1, 1);

添加代码然后比较值并生成运行时错误也只是技术上可行。它的成本太高了。

答案 3 :(得分:3)

观察到的行为是一个令人遗憾的结果,即Framework和C#都没有提供任何方法,成员函数声明可以指定this是否应该通过ref,const-ref或value传递。相反,值类型总是通过(非const限制)ref传递this,引用类型总是按值传递this

编译器的“正确”行为是禁止通过非const限制的ref传递不可变或临时值。如果可以施加这样的限制,确保可变值类型的正确语义将意味着遵循一个简单的规则:如果你制作一个结构的隐式副本,那么你做错了。不幸的是,成员函数只能通过非const限制的ref接受this,这意味着语言设计者必须做出以下三种选择之一:

  1. 猜测成员函数不会修改`this`,只需通过`ref`传递不可变或临时变量。对于实际上不修改“this”的函数来说,这将是最有效的,但是可能危险地暴露于应该是不可变的修改事物。
  2. 不允许在不可变或临时实体上使用成员函数。这样可以避免不正确的语义,但这将是一个非常烦人的限制,特别是考虑到大多数成员函数不修改`this`。
  3. 允许使用成员函数,除了那些被认为最有可能修改`this`的函数(例如属性设置器),但不是直接通过ref传递不可变实体,而是将它们复制到临时位置并传递它们。

微软的选择保护常量免受不正确的修改,但是在调用不修改this的函数时,代码将不必要地慢慢运行,而通常对那些执行错误的函数工作不正常会带来不幸的后果。

鉴于实际处理this的方式,最好的办法是避免在属性设置器以外的结构成员函数中对其进行任何更改。拥有属性设置器或可变字段是很好的,因为编译器将正确禁止任何尝试在不可变或临时对象上使用属性设置器,或修改其任何字段。

答案 4 :(得分:2)

如果您查看IL,您会看到在使用readonly字段时,会在调用Offset之前制作副本:

IL_0014: ldsfld valuetype [System.Drawing]System.Drawing.Point 
                    Program::staticReadOnlyPoint
IL_0019: stloc.0
IL_001a: ldloca.s CS$0$0000

为什么会发生这种情况,超出我的范围。

它可能是规范的一部分,或者是编译器错误(但对于后者来说看起来有点过于谨慎)。

答案 5 :(得分:2)

效果是由于几个明确定义的特征汇集在一起​​。

readonly表示无法更改相关字段,但不能更改字段的目标。使用可变引用类型的readonly字段更容易理解(在实践中更常用),您可以x.SomeMutatingMethod()而不是x = someNewObject

所以,第一项是;你可以改变readonly字段的目标。

第二项是,当您访问非变量值类型时,您将获得该值的副本。最令人困惑的例子是giveMeAPoint().Offset(1, 1),因为我们之后没有一个已知位置可以观察到giveMeAPoint()返回的值类型可能已经或可能没有被变异。

这就是为什么价值类型不是邪恶的,但在某些方面更糟糕。真正邪恶的代码没有明确定义的行为,所有这些都是明确定义的。虽然它仍然令人困惑(令我感到困惑的是我在第一次回答时弄错了),当你尝试编码时,混淆比邪恶还要糟糕。容易理解邪恶是更容易避免的。