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

时间:2019-07-01 07:26:29

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

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

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

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

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

1 个答案:

答案 0 :(得分:32)

  

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

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

来自C# 5 ECMA specification

第12.7.5.1节(普通成员)

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

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

并且:

  
      
  • 如果T是一个结构类型,并且我标识了该结构类型的实例字段:      
        
    • 如果E是一个值,或者字段是只读的并且引用发生在声明该字段的结构的实例构造函数之外,则结果是一个值,即结构中字段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
    }
}

以下是呼叫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修饰符可以应用于结构的原因。这是另一个示例:

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修饰符来解决此问题。