「readonly」修飾子はフィールドの隠しコピーを作成しますか? 質問する

「readonly」修飾子はフィールドの隠しコピーを作成しますか? 質問する

MutableSlabとの実装の唯一の違いは、フィールドに適用される修飾子ImmutableSlabです。readonlyhandle

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修飾子はフィールドの隠しコピーを作成しますか?コンパイル時のチェックだけではないということですか?この動作については何も見つけられませんでしたこここの動作は文書化されていますか?

ベストアンサー1

readonly修飾子はフィールドの隠しコピーを作成しますか?

はい、通常の構造体型の読み取り専用フィールド (コンストラクターまたは静的コンストラクターの外部) でメソッドまたはプロパティを呼び出すと、最初にフィールドがコピーされます。これは、プロパティまたはメソッド アクセスによって、呼び出した値が変更されるかがコンパイラーにわからないためです。

からC# 5 ECMA 仕様:

セクション 12.7.5.1 (メンバーアクセス、一般)

これにより、メンバーのアクセスが次のように分類されます。

  • 静的フィールドを識別する場合:
    • フィールドが読み取り専用であり、参照がフィールドが宣言されているクラスまたは構造体の静的コンストラクターの外部で発生した場合、結果は値、つまり E の静的フィールド I の値になります。
    • それ以外の場合、結果は変数、つまり E の静的フィールド I になります。

そして:

  • T が構造体型であり、I がその構造体型のインスタンス フィールドを識別する場合:
    • E が値の場合、またはフィールドが読み取り専用であり、参照がフィールドが宣言されている構造体のインスタンス コンストラクターの外部で発生した場合、結果は値、つまり E によって指定された構造体インスタンス内のフィールド I の値になります。
    • それ以外の場合、結果は変数、つまり E によって指定された構造体インスタンス内のフィールド I になります。

インスタンス フィールド部分が構造体型を具体的に参照しているのに、静的フィールド部分が参照していないのはなぜかわかりません。重要なのは、式が変数として分類されるか、値として分類されるかです。これは関数メンバーの呼び出しで重要になります...

セクション 12.6.6.1 (関数メンバーの呼び出し、一般)

関数メンバー呼び出しの実行時処理は、次の手順で構成されます。ここで、M は関数メンバーであり、M がインスタンス メンバーの場合は、E はインスタンス式です。

[...]

  • それ以外の場合、E の型が値型 V であり、M が V で宣言またはオーバーライドされている場合:
    • [...]
    • E が変数として分類されていない場合は、E の型の一時ローカル変数が作成され、E の値がその変数に割り当てられます。その後、E はその一時ローカル変数への参照として再分類されます。一時変数は M 内で this としてアクセスできますが、他の方法ではアクセスできません。したがって、E が真の変数である場合にのみ、呼び出し元は M が this に加えた変更を観察できます。

自己完結的な例を次に示します。

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

への呼び出しの IL は次のとおりですreadOnlyCounter.IncrementedCount:

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()

これにより、フィールドに対して直接呼び出しが行われるため、フィールド値はプロパティ内で変更されることになります。

構造体が大きく、メンバーがしないmutate します。そのため、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

私は2014年のブログ投稿readonlyこれは、Noda Time でフィールドがパフォーマンスの問題を引き起こしていることがわかったために書いたものです。幸いなことにreadonly、構造体の修飾子を使用することで、この問題は修正されました。

おすすめ記事