С#: Почему мутации в readonly structs не ломаются?

В С#, если у вас есть struct, например:

struct Counter
{
    private int _count;

    public int Value
    {
        get { return _count; }
    }

    public int Increment()
    {
        return ++_count;
    }
}

И у вас есть такая программа:

static readonly Counter counter = new Counter();

static void Main()
{
    // print the new value from the increment function
    Console.WriteLine(counter.Increment());
    // print off the value stored in the item
    Console.WriteLine(counter.Value);
}

Выходной сигнал программы будет:

1
0

Это кажется совершенно неправильным. Я ожидал бы, что результат будет равен двум 1s (как если бы Counter был class, или if struct Counter : ICounter и Counter является ICounter), или быть ошибкой компиляции. Я понимаю, что обнаружение этого во время компиляции является довольно сложным делом, но это поведение, по-видимому, нарушает логику.

Есть ли причина для такого поведения, выходящего за пределы сложности выполнения?

Ответы

Ответ 1

structs являются типами значений и, следовательно, имеют тип значений типа. Это означает, что каждый раз, когда вы обращаетесь к структуре, вы в основном работаете с копией значения struct.

В вашем примере вы не меняете оригинал struct, а только его временную копию.

См. здесь дополнительные пояснения:

Почему изменчивые структуры злы

Ответ 2

В .net метод экземпляра struct семантически эквивалентен статическому структурному методу с дополнительным параметром ref типа struct. Таким образом, учитывая объявления:

struct Blah { 
   public int value;
   public void Add(int Amount) { value += Amount; }
   public static void Add(ref Blah it; int Amount; it.value += Amount;}
}

Метод вызывает:

someBlah.Add(5);
Blah.Add(ref someBlah, 5);

являются семантически эквивалентными, за исключением одного отличия: последний вызов разрешен только в том случае, если someBlah является изменчивым местом хранения (переменная, поле и т.д.), а не если это место хранения только для чтения или временное значение (результат считывания свойства и т.д.).

Это столкнулось с разработчиками .net-языков с проблемой: запрет использования каких-либо функций-членов в структурах только для чтения будет раздражать, но они не хотят позволять функциям-членам писать переменные только для чтения. Они решили "пунтировать" и сделать так, чтобы вызов метода экземпляра в структуре только для чтения создавал копию структуры, вызывал эту функцию и затем отбрасывал ее. Это приводит к замедлению вызовов методам экземпляра, которые не записывают основную структуру, и делают это так, что попытка использовать метод, который обновляет базовую структуру в структуре только для чтения, приведет к разной разбитой семантике из того, что будет если она была передана непосредственно структурой. Обратите внимание, что дополнительное время, затраченное копией, почти никогда не даст правильной семантики в случаях, которые не были бы правильными без копии.

Один из моих главных моментов в .net заключается в том, что до сих пор (по крайней мере, 4.0 и, возможно, 4.5) все еще нет атрибута, через который функция-член структуры может указывать, изменяет ли она this. Люди релят о том, как структуры должны быть неизменными, а не предоставлять инструменты, позволяющие структурам безопасно предлагать мутирующие методы. Это, несмотря на то, что так называемые "неизменные" структуры являются ложью. Все нетривиальные типы значений в изменяемых местах хранения изменяемы, как и все типы значений в штучной упаковке. Создание "неизменяемой" структуры может заставить переписать целую структуру, когда нужно только изменить одно поле, но поскольку struct1 = struct2 мутирует struct1, копируя все общедоступные и частные поля из struct2, и нет ничего такого определения типа для структуры может сделать это, чтобы исключить (кроме полей) никаких действий, чтобы предотвратить непредвиденную мутацию членов структуры. Кроме того, из-за проблем с потоками структуры очень ограничены в своей способности применять любые инвариантные отношения между их полями. ИМХО, было бы лучше, если бы структура имела возможность разрешить произвольный доступ к полям, давая понять, что любой код, получающий структуру, должен проверять, соответствуют ли его поля всем требуемым условиям, чем пытаться предотвратить формирование структур, которые не соответствуют условиям.