Создает ли модификатор "только для чтения" скрытую копию поля? - программирование

Создает ли модификатор "только для чтения" скрытую копию поля?

Единственное различие между MutableSlab и ImmutableSlab реализаций является readonly для handle 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 ли модификатор readonly скрытую копию поля? Значит ли это, что это не только проверка во время компиляции? Я не мог найти ничего об этом поведении здесь. Это поведение задокументировано?

4b9b3361

Ответ 1

readonly ли модификатор readonly скрытую копию поля?

Вызов метода или свойства в доступном только для чтения поле обычного типа структуры (вне конструктора или статического конструктора) сначала копирует поле, да. Это потому, что компилятор не знает, изменит ли свойство или метод доступа значение, которое вы вызываете.

Из спецификации С# 5 ECMA:

Раздел 12.7.5.1 (Доступ членов, общий)

Это классифицирует доступ членов, в том числе:

  • Если я идентифицирую статическое поле:
    • Если поле доступно только для чтения и ссылка находится вне статического конструктора класса или структуры, в которой объявлено поле, то результатом является значение, а именно значение статического поля я в E.
    • В противном случае результатом является переменная, а именно статическое поле я в E.

А также:

  • Если T является типом структуры, и я идентифицирую поле экземпляра этого типа структуры:
    • Если E является значением или если поле доступно только для чтения, а ссылка находится за пределами конструктора экземпляра структуры, в которой объявлено поле, то результатом является значение, а именно значение поля я в экземпляре структуры, заданное E.
    • В противном случае результатом является переменная, а именно поле я в экземпляре структуры, заданном E.

Я не уверен, почему часть поля экземпляра специально относится к типам структуры, а часть статического поля - нет. Важной частью является то, классифицируется ли выражение как переменная или значение. Что тогда важно в вызове функции-члена...

Раздел 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
    }
}

Вот 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()

Это делает вызов непосредственно в поле, поэтому значение поля в конечном итоге изменяется внутри свойства.

Создание копии может быть неэффективным, когда структура велика и член не изменяет ее. Вот почему в С# 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 вместо структур.