“只读”修饰符是否创建字段的隐藏副本?

BAR*_*ART 32 c# struct value-type readonly-attribute

MutableSlabImmutableSlab实现之间的唯一区别是readonly应用于handle字段的修饰符:

using System;
using System.Runtime.InteropServices;

public class Program
{
    class MutableSlab : IDisposable
    {
        private GCHandle handle;

        public MutableSlab()
        {
            this.handle = GCHandle.Alloc(new byte[256], GCHandleType.Pinned);
        }

        public bool IsAllocated => this.handle.IsAllocated;

        public void Dispose()
        {
            this.handle.Free();
        }
    }

    class ImmutableSlab : IDisposable
    {
        private readonly GCHandle handle;

        public ImmutableSlab()
        {
            this.handle = GCHandle.Alloc(new byte[256], GCHandleType.Pinned);
        }

        public bool IsAllocated => this.handle.IsAllocated;

        public void Dispose()
        {
            this.handle.Free();
        }
    }

    public static void Main()
    {
        var mutableSlab = new MutableSlab();
        var immutableSlab = new ImmutableSlab();

        mutableSlab.Dispose();
        immutableSlab.Dispose();

        Console.WriteLine($"{nameof(mutableSlab)}.handle.IsAllocated = {mutableSlab.IsAllocated}");
        Console.WriteLine($"{nameof(immutableSlab)}.handle.IsAllocated = {immutableSlab.IsAllocated}");
    }
}
Run Code Online (Sandbox Code Playgroud)

但是它们产生不同的结果:

mutableSlab.handle.IsAllocated = False
immutableSlab.handle.IsAllocated = True
Run Code Online (Sandbox Code Playgroud)

GCHandle是一个可变结构,当您复制它时,它的行为与带有的情况完全相同immutableSlab

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

Jon*_*eet 33

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

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

根据C#5 ECMA规范

12.7.5.1节(普通成员)

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

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

和:

  • 如果T是一个结构类型,并且我标识了该结构类型的实例字段:
    • 如果E是一个值,或者该字段是只读的,并且引用发生在声明该字段的struct的实例构造函数之外,则结果是一个值,即由给出的struct实例中的I字段的值E.
    • 否则,结果是一个变量,即E给定的struct实例中的字段I。

我不确定为什么实例字段部分专门引用结构类型,而静态字段部分却没有。重要的是表达式是分类为变量还是值。这在函数成员调用中很重要...

12.6.6.1节(函数成员的调用,常规)

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

[...]

  • 否则,如果E的类型是值类型V,并且M在V中声明或覆盖:
    • [...]
    • 如果E未归类为变量,则会创建E类型的临时局部变量,并将E的值分配给该变量。然后,将E重新分类为对该临时局部变量的引用。可以在M中以这种方式访问​​临时变量,但不能以其他任何方式。因此,只有当E是一个真变量时,调用方才可以观察到M对此所做的更改。

这是一个独立的示例:

using System;
using System.Globalization;

struct Counter
{
    private int count;

    public int IncrementedCount => ++count;
}

class Test
{
    static readonly Counter readOnlyCounter;
    static Counter readWriteCounter;

    static 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
    }
}
Run Code Online (Sandbox Code Playgroud)

这是致电给的IL readOnlyCounter.IncrementedCount

ldsfld     valuetype Counter Test::readOnlyCounter
stloc.0
ldloca.s   V_0
call       instance int32 Counter::get_IncrementedCount()
Run Code Online (Sandbox Code Playgroud)

它将字段值复制到堆栈上,然后调用属性...,以便字段值最终不会改变;它count在副本中递增。

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

ldsflda    valuetype Counter Test::readWriteCounter
call       instance int32 Counter::get_IncrementedCount()
Run Code Online (Sandbox Code Playgroud)

这将直接在字段上进行调用,因此字段值最终会在属性中更改。

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

ldsfld     valuetype Counter Test::readOnlyCounter
stloc.0
ldloca.s   V_0
call       instance int32 Counter::get_IncrementedCount()
Run Code Online (Sandbox Code Playgroud)

使用readonly结构本身上的修饰符,该field1.NoOp()调用不会创建副本。如果删除readonly修饰符并重新编译,您将看到它创建了一个副本,就像在中所做的一样readOnlyCounter.IncrementedCount

我写了一篇2014年博客文章,发现该readonly领域导致Noda Time出现性能问题。幸运的是,现在可以使用readonly结构上的修饰符修复该问题。