Создает ли модификатор «только для чтения» скрытую копию поля? - PullRequest
31 голосов
/ 01 июля 2019

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

1 Ответ

32 голосов
/ 01 июля 2019

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

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

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

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

Классифицирует доступ членов, включая:

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

И

  • Если T является структурным типом, и я идентифицирую поле экземпляра этого структурного типа:
    • Если E является значением или если поле доступно только для чтения и ссылка находится за пределами конструктора экземпляра структуры, в которой объявлено поле, то результатом является значение, а именно значение поля I в структуре экземпляр, данный Е.
    • В противном случае результатом является переменная, а именно поле I в экземпляре структуры, заданном 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()

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

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

...