首页 文章

const,readonly和mutable值类型

提问于
浏览
21

我正在继续学习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 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点击一个键退出...

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

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

6 回答

  • 7

    Eric Lippert解释了here的情况:

    ...如果字段是只读的并且引用发生在声明字段的类的实例构造函数之外,则结果是一个值,即E引用的对象中字段I的值 . 这里的单词是结果是字段的值,而不是与字段关联的变量 . 只读字段不是构造函数之外的变量 . (这里的初始化程序被认为是在构造函数内部;请参阅我之前关于该主题的帖子 . )

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

    这是可变值类型为恶的另一个原因 . 尝试始终使值类型不可变 .

  • 2

    readonly的意思是你不能重新分配引用或值 .

    换句话说,如果你试图这样做

    staticReadOnlyPoint = new Point(1, 1);
    

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

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

    [ EDIT :妥善解决所描述的奇怪行为]

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

    所以你的路线

    staticReadOnlyPoint.Offset(1, 1);
    

    正在访问和变异字段的副本,而不是字段中的实际值 . 当您随后写出您正在写出的值时,则会写出另一份原始副本(而不是变异副本) .

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

  • 5

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

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

    Fwiw,编译器确实生成显式代码以确保语句不会意外地修改readonly . 这个说法

    staticReadOnlyPoint.Offset(1, 1);
    

    被翻译成

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

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

  • 2

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

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

    • 猜猜成员函数不会修改this,只需通过ref传递不可变或临时变量 . 对于实际上不修改“this”的函数来说,这将是最有效的,但是可能危险地暴露于应该是不可变的修改事物 .

    • 不允许在不可变或临时实体上使用成员函数 . 这样可以避免不正确的语义,但这将是一个非常烦人的限制,特别是考虑到大多数成员函数不修改this .

    • 允许使用除被认为最有可能修改this的成员函数(例如属性设置者),但不是直接通过ref传递不可变实体,而是将它们复制到临时位置并传递它们 .

    Microsoft 's choice protects constants from improper modification, but has the unfortunate consequences that code will run needlessly slowly when calling functions that don' t修改 this ,而通常对那些做错误的人工作不正常 .

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

  • 3

    如果您查看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
    

    为什么会这样,超出我的范围 .

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

  • 10

    这种效果是由于几个明确定义的特征汇集在一起 .

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

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

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

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

相关问题