'readonly' 修饰符是否创建字段的隐藏副本?
Does the 'readonly' modifier create a hidden copy of a field?
MutableSlab
和 ImmutableSlab
实现之间的唯一区别是 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 字段上调用方法或 属性 首先复制该字段,是的。那是因为编译器不知道 属性 或方法访问是否会修改您调用它的值。
第 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
修饰符来解决这个问题。
MutableSlab
和 ImmutableSlab
实现之间的唯一区别是 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 字段上调用方法或 属性 首先复制该字段,是的。那是因为编译器不知道 属性 或方法访问是否会修改您调用它的值。
第 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
修饰符来解决这个问题。