C# 是';只读';修改器是否创建字段的隐藏副本?

C# 是';只读';修改器是否创建字段的隐藏副本?,c#,struct,value-type,readonly-attribute,C#,Struct,Value Type,Readonly Attribute,MutableSlab和immutablelab实现之间的唯一区别是readonly修饰符应用于句柄字段: 使用系统; 使用System.Runtime.InteropServices; 公共课程 { 类MutableSlab:IDisposable { 私人GCHandle; 公共可变板() { this.handle=GCHandle.Alloc(新字节[256],GCHandleType.pinted); } public bool IsAllocated=>this.handle.IsA

MutableSlab
immutablelab
实现之间的唯一区别是
readonly
修饰符应用于
句柄
字段:

使用系统;
使用System.Runtime.InteropServices;
公共课程
{
类MutableSlab:IDisposable
{
私人GCHandle;
公共可变板()
{
this.handle=GCHandle.Alloc(新字节[256],GCHandleType.pinted);
}
public bool IsAllocated=>this.handle.IsAllocated;
公共空间处置()
{
this.handle.Free();
}
}
类ImmutableLab:IDisposable
{
私有只读GCHandle句柄;
公共ImmutableLab()
{
this.handle=GCHandle.Alloc(新字节[256],GCHandleType.pinted);
}
public bool IsAllocated=>this.handle.IsAllocated;
公共空间处置()
{
this.handle.Free();
}
}
公共静态void Main()
{
var mutableSlab=新的mutableSlab();
var ImmutableLab=新的ImmutableLab();
mutableSlab.Dispose();
immutableLab.Dispose();
Console.WriteLine($“{nameof(mutableSlab)}.handle.IsAllocated={mutableSlab.IsAllocated}”);
Console.WriteLine($“{nameof(ImmutableLab)}.handle.IsAllocated={ImmutableLab.IsAllocated}”);
}
}
但它们会产生不同的结果:

mutableSlab.handle.IsAllocated = False
immutableSlab.handle.IsAllocated = True
GCHandle是一个可变结构,当您复制它时,它的行为与
ImmutableLab
场景中的行为完全相同

readonly
修饰符是否创建字段的隐藏副本?这是否意味着它不仅仅是编译时检查?我找不到关于这种行为的任何信息。这种行为有记录吗

readonly
修饰符是否创建字段的隐藏副本

在常规结构类型(构造函数或静态构造函数之外)的只读字段上调用方法或属性首先复制该字段,是的。这是因为编译器不知道属性或方法访问是否会修改您调用它的值

从:

第12.7.5.1节(会员访问,一般)

这将对成员访问进行分类,包括:

  • 如果我识别一个静态字段:
    • 如果该字段是只读的,并且引用发生在声明该字段的类或结构的静态构造函数之外,则结果是一个值,即E中静态字段I的值
    • 否则,结果是一个变量,即E中的静态场I
以及:

  • 如果T是结构类型,并且我标识了该结构类型的实例字段:
    • 如果E是一个值,或者如果该字段是只读的,并且引用发生在声明该字段的结构的实例构造函数之外,则结果是一个值,即E给出的结构实例中的字段I的值
    • 否则,结果是一个变量,即E给出的struct实例中的字段I
我不确定为什么实例字段部分特别引用结构类型,但静态字段部分没有。重要的部分是表达式是分类为变量还是值。这在函数成员调用中很重要

第12.6.6.1节(功能成员调用,概述)

函数成员调用的运行时处理包括以下步骤,其中M是函数成员,如果M是实例成员,E是实例表达式:

[……]

  • 否则,如果E的类型是值类型V,并且M在V中声明或重写:
    • [……]
    • 如果E未分类为变量,则会创建E类型的临时局部变量,并将E的值指定给该变量。然后将E重新分类为该临时局部变量的参考。临时变量在M中是可以访问的,但不能以任何其他方式访问。因此,只有当E是真变量时,调用者才可能观察到M对此所做的更改
下面是一个独立的示例:

使用系统;
利用制度全球化;
结构计数器
{
私人整数计数;
公共整数递增计数=>++计数;
}
课堂测试
{
静态只读计数器只读计数器;
静态计数器读写计数器;
静态void Main()
{
Console.WriteLine(readOnlyCounter.IncrementedCount);//1
Console.WriteLine(readOnlyCounter.IncrementedCount);//1
Console.WriteLine(readOnlyCounter.IncrementedCount);//1
Console.WriteLine(readWriteCounter.IncrementedCount);//1
Console.WriteLine(readWriteCounter.IncrementedCount);//2
Console.WriteLine(readWriteCounter.IncrementedCount);//3
}
}
这是调用
readOnlyCounter.IncrementedCount
的IL:

ldsfld     valuetype Counter Test::readOnlyCounter
stloc.0
ldloca.s   V_0
call       instance int32 Counter::get_IncrementedCount()
将字段值复制到堆栈上,然后调用属性。。。因此字段的值最终不会改变;它在副本中递增
count

将其与读写字段的IL进行比较:

ldsflda    valuetype Counter Test::readWriteCounter
call       instance int32 Counter::get_IncrementedCount()
这使得直接对字段进行调用,因此字段值最终会在属性中发生更改

当结构较大且成员未对其进行变异时,创建副本可能效率低下。这就是为什么在C#7.2及更高版本中,
readonly
修饰符可以应用于结构。下面是另一个例子:

使用系统;
利用制度全球化;
只读结构ReadOnlyStruct
{
公共void NoOp(){}
}
课堂测试
{
静态只读只读结构字段1;
静态只读结构字段2;
静态void Main()
{
字段1.NoOp();
字段2.NoOp();
}
}
使用<代码