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

Does the 'readonly' modifier create a hidden copy of a field?

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}");
    }
}

但它们产生不同的结果:

mutableSlab.handle.IsAllocated = False
immutableSlab.handle.IsAllocated = True

GCHandle 是一个可变结构,当您复制它时,它的行为与 immutableSlab.

的场景完全一样

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

Does the readonly modifier create a hidden copy of a field?

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

来自C# 5 ECMA specification:

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

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

  • If I identifies a static field:
    • If the field is readonly and the reference occurs outside the static constructor of the class or struct in which the field is declared, then the result is a value, namely the value of the static field I in E.
    • Otherwise, the result is a variable, namely the static field I in E.

并且:

  • If T is a struct-type and I identifies an instance field of that struct-type:
    • If E is a value, or if the field is readonly and the reference occurs outside an instance constructor of the struct in which the field is declared, then the result is a value, namely the value of the field I in the struct instance given by E.
    • Otherwise, the result is a variable, namely the field I in the struct instance given by E.

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

第 12.6.6.1 节(函数成员调用,一般)

The run-time processing of a function member invocation consists of the following steps, where M is the function member and, if M is an instance member, E is the instance expression:

[...]

  • Otherwise, if the type of E is a value-type V, and M is declared or overridden in V:
    • [...]
    • If E is not classified as a variable, then a temporary local variable of E's type is created and the value of E is assigned to that variable. E is then reclassified as a reference to that temporary local variable. The temporary variable is accessible as this within M, but not in any other way. Thus, only when E is a true variable is it possible for the caller to observe the changes that M makes to this.

这是一个 self-contained 示例:

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
    }
}

这是调用 readOnlyCounter.IncrementedCount 的 IL:

ldsfld     valuetype Counter Test::readOnlyCounter
stloc.0
ldloca.s   V_0
call       instance int32 Counter::get_IncrementedCount()

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

将其与 read-write 字段的 IL 进行比较:

ldsflda    valuetype Counter Test::readWriteCounter
call       instance int32 Counter::get_IncrementedCount()

直接在字段上进行调用,因此字段值最终会在 属性.

内发生变化

当结构很大并且成员改变它时,复制可能效率低下。这就是为什么在 C# 7.2 及更高版本中,可以将 readonly 修饰符应用于结构。这是另一个例子:

using System;
using System.Globalization;

readonly struct ReadOnlyStruct
{
    public void NoOp() {}
}

class Test
{
    static readonly ReadOnlyStruct field1;
    static ReadOnlyStruct field2;

    static void Main()
    {
        field1.NoOp();
        field2.NoOp();
    }
}

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

中所做的一样

我写了一篇 blog post from 2014,发现 readonly 字段在 Noda Time 中导致了性能问题。幸运的是,现在可以在结构上使用 readonly 修饰符来解决这个问题。