【问题标题】:Does the 'readonly' modifier create a hidden copy of a field?'readonly' 修饰符是否会创建字段的隐藏副本?
【发布时间】:2019-11-11 20:47:01
【问题描述】:

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。是否记录了这种行为?

【问题讨论】:

  • 我不会将此作为答案发布,因为我不能 100% 确定 GC 的行为。但是不,readonly 关键字不会引入新字段。它按照它在锡上说的做。您观察到的行为可能是由于 GC 没有按照您的意愿行事。尝试运行 GC.Collect()。 GC 接受提示,通常不接受命令。
  • 我现在正在写一个答案......但是对于那些没有耐心的人,这是我之前写的一篇博文:codeblog.jonskeet.uk/2014/07/16/…
  • 通过只读字段的成员调用会创建一个副本。并不是有一个额外的字段 - 而是在调用之前复制了该字段。
  • 请注意,Resharper 实际上对此发出了警告;对于ImmutableSlab 中的this.handle.Free();,它给出警告:“值类型的只读字段调用了不纯方法。”

标签: c# struct value-type readonly-attribute


【解决方案1】:

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

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

来自C# 5 ECMA specification

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

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

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

还有:

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

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

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

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

[...]

  • 否则,如果 E 的类型是值类型 V,并且在 V 中声明或覆盖了 M:
    • [...]
    • 如果 E 未被分类为变量,则创建 E 类型的临时局部变量并将 E 的值分配给该变量。然后将 E 重新分类为对该临时局部变量的引用。临时变量可以在 M 中作为 this 访问,但不能以任何其他方式访问。因此,只有当 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 修饰符解决了这个问题。

【讨论】:

  • Calling a method or property on a read-only field of a regular struct type first copies the field。我在文档中找不到这个声明,但我认为这是它的隐含版本:Because value types directly contain their data, a field that is a readonly value type is immutable。因此,如果该字段不是只读结构,那么当我调用更改该字段状态的不纯方法时,它必须创建一个副本。我说的对吗?
  • @BART:是的。你在看哪个文档?它在某个地方的 C# 规范中,但它可能不是很容易找到。
  • @Jon Skeet 我在看微软网站readonly keyword,但没有像你这样明确的声明。我将更深入地了解 C# 规范。谢谢解释
  • @BART:我现在已经引用了规范的相关部分,我将看看文本中令人困惑的部分。
  • 感谢乔恩的全面解释!在此之前,我认为 readonly 只是指引用变量,而不是对象本身。我学到了新东西!为那个答案竖起大拇指:-)
猜你喜欢
  • 2011-10-15
  • 2011-08-26
  • 2011-10-19
  • 1970-01-01
  • 1970-01-01
  • 2014-05-25
  • 1970-01-01
  • 2016-05-12
  • 1970-01-01
相关资源
最近更新 更多