const, readonly and mutable value types
我正在继续学习C和语言规范,下面是另一个我不太理解的行为:
C语言规范在第10.4节中明确说明了以下内容:
The type specified in a constant declaration must be sbyte, byte, short, ushort, int, uint, long, ulong, char, float, double, decimal, bool, string, an enum-type, or a reference-type.
第4.1.4节还规定了以下内容:
Through const declarations it is possible to declare constants of the simple types (§10.4). It is not possible to have constants of other struct types, but a similar effect is provided by static readonly fields.
好的,所以使用静态只读可以获得类似的效果。我读了这篇文章,尝试了以下代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | 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(); } |
此代码的输出为:
Static Point: X=0;Y=0
Static readonly Point: X=0;Y=0
Offsetting...
Static Point: X=1;Y=1
Static readonly Point: X=0;Y=0
Hit a key to exit...
我真的希望编译器能给我一些关于改变静态只读字段或失败的警告,像改变引用类型那样改变字段。
我知道可变值类型是邪恶的(为什么微软会实现
埃里克·利珀特解释了这里发生的事情:
...if the field is readonly and the reference occurs outside an
instance constructor of the class in which the field is declared, then
the result is a value, namely the value of the field I in the object
referenced by E.The important word here is that the result is the value of the field,
not the variable associated with the field. Readonly fields are not
variables outside of the constructor. (The initializer here is
considered to be inside the constructor; see my earlier post on that
subject.)
哦,为了强调可变结构的邪恶,他的结论是:
This is yet another reason why mutable value types are evil. Try to
always make value types immutable.
只读的点是不能重新分配引用或值。
换句话说,如果你试图这样做
1 |
您将得到一个编译器错误,因为您正试图重新分配
但是,
[编辑:正确处理所描述的奇怪行为]
您看到
所以你的路线
1 | staticReadOnlyPoint.Offset(1, 1); |
正在访问和变异字段的副本,而不是字段中的实际值。当您随后写出值时,您将写出另一个原始副本(而不是变异副本)。
您对
编译器没有足够的关于方法的信息来知道方法会改变结构。方法可能有一个很有用的副作用,但不会改变结构的任何成员。如果技术上可以将这种分析添加到编译器中。但这对任何生活在另一个组件中的类型都不起作用。
缺少的成分是一个元数据标记,它指示一个方法不会改变任何成员。就像C++中的const关键字一样。无法使用的。如果将其添加到原始设计中,它将完全不符合CLS。很少有语言支持这个概念。我只能想到C++,但我不怎么懂。
在fwiw中,编译器会生成显式代码,以确保语句不会意外地修改readonly。本声明
1 | staticReadOnlyPoint.Offset(1, 1); |
转换为
1 2 | Point temp = staticReadOnlyPoint; // makes a copy temp.Offset(1, 1); |
添加代码,然后比较值并生成运行时错误,在技术上也是可能的。它太贵了。
所观察到的行为是一个不幸的结果,因为框架和C都没有提供任何方法,通过这些方法成员函数声明可以指定
编译器的"正确"行为是禁止非常量受限引用传递不可变或临时值。如果可以实施此类限制,则确保可变值类型的正确语义意味着遵循一个简单规则:如果您隐式复制结构,则说明您做错了什么。不幸的是,成员函数只能接受非常量限制引用的
微软的选择可以保护常量不被不当修改,但不幸的是,当调用不修改
考虑到实际处理
这种效果是由于几个定义明确的特性结合在一起造成的。
所以,第一项是:你可以改变
第二项是,当您访问非变量值类型时,您将获得该值的副本。最不令人困惑的例子是
这就是为什么价值类型不是邪恶的,但在某些方面更糟。真正邪恶的代码没有明确的行为,所有这些都是明确的。不过,它仍然令人困惑(对我来说,第一个答案就足够让人困惑了),而当你试图编码时,困惑比邪恶更糟糕。容易理解的邪恶更容易避免。
如果您查看IL,您将看到在使用
1 2 3 4 | IL_0014: ldsfld valuetype [System.Drawing]System.Drawing.Point Program::staticReadOnlyPoint IL_0019: stloc.0 IL_001a: ldloca.s CS$0$0000 |
为什么会这样,我无法理解。
它可能是规范的一部分,也可能是编译器bug(但对于后者来说,它看起来有点过于刻意)。