Подтвердить что ты не робот

Const, readonly и изменяемые типы значений

Я продолжаю изучать С# и спецификацию языка. И вот другое поведение, которое я не совсем понимаю:

Спецификация языка С# четко заявляет следующее в разделе 10.4:

Тип, указанный в объявлении константы, должен быть sbyte, byte, short, ushort, int, uint, long, ulong, char, float, double, decimal, bool, string, enum-type или reference -типа.

В разделе 4.1.4 также указано следующее:

Через объявления const можно объявить константы простых типов (§10.4). Невозможно иметь константы других типов структур, но аналогичный эффект обеспечивается статическими полями readonly.

Итак, аналогичный эффект можно получить, используя статический readonly. Прочитав это, я пошел и попробовал следующий код:

static void Main()
{
    OffsetPoints();
    Console.Write("Hit a key to exit...");
    Console.ReadKey();
}

static Point staticPoint = new Point(0, 0);
static readonly Point staticReadOnlyPoint = new Point(0, 0);

public static void OffsetPoints()
{
    PrintOutPoints();
    staticPoint.Offset(1, 1);
    staticReadOnlyPoint.Offset(1, 1);
    Console.WriteLine("Offsetting...");
    Console.WriteLine();
    PrintOutPoints();
}

static void PrintOutPoints()
{
    Console.WriteLine("Static Point: X={0};Y={1}", staticPoint.X, staticPoint.Y);
    Console.WriteLine("Static readonly Point: X={0};Y={1}", staticReadOnlyPoint.X, staticReadOnlyPoint.Y);
    Console.WriteLine();
}

Вывод этого кода:

Статическая точка: X = 0; Y = 0

Статическая точка readonly: X = 0; Y = 0

Взаимозачет...

Статическая точка: X = 1; Y = 1

Статическая точка readonly: X = 0; Y = 0

Нажмите клавишу для выхода...

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

Я знаю, что изменяемые типы значений являются злыми (почему Microsoft когда-либо реализовала Point, поскольку mutable является загадкой), но не должен ли компилятор предупреждать вас каким-то образом о том, что вы пытаетесь изменить статический тип значения readonly? Или, по крайней мере, предупредите, что ваш метод Offset() не будет иметь "желаемых" побочных эффектов?

4b9b3361

Ответ 1

Эрик Липперт объясняет, что происходит на здесь:

... если поле только для чтения, а ссылка происходит вне экземпляр класса, в котором объявлено поле, тогда результатом является значение, а именно значение поля я в объекте ссылка на E.

Важным словом здесь является то, что результатом является значение поля, а не переменной, связанной с полем. Считываемые поля не являются переменные вне конструктора. (Инициализатор здесь считается внутри конструктора; см. мой предыдущий пост о том, что предмет.)

О, и просто чтобы подчеркнуть злобность изменчивых структур, вот его вывод:

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

Ответ 2

Точка readonly состоит в том, что вы не можете переназначить ссылку или значение.

Другими словами, если вы попытались выполнить этот

staticReadOnlyPoint = new Point(1, 1);

вы получите ошибку компилятора, потому что вы пытаетесь переназначить staticReadOnlyPoint. Компилятор не даст вам этого сделать.

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

[ EDIT: чтобы правильно описать описываемое поведение]

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

Итак, ваша линия

staticReadOnlyPoint.Offset(1, 1);

обращается и мутирует копию поля, а не фактическое значение в поле. Когда вы впоследствии выписываете значение, которое вы затем выписываете еще одну копию оригинала (а не мутированную копию).

Копия, которую вы сделали мутацией с вызовом Offset, отбрасывается, потому что она никогда не привязана ни к чему.

Ответ 3

У компилятора просто недостаточно информации о методе, чтобы знать, что метод мутирует структуру. Метод может иметь побочный эффект, который полезен, но не меняет других членов структуры. Если технически возможно добавить такой анализ в компилятор. Но это не будет работать для любых типов, которые живут в другой сборке.

Недопустимый компонент - это токен метаданных, который указывает, что метод не мутирует никаких членов. Подобно ключевому слову const в С++. Недоступен. Он был бы абсолютно не совместим с CLS, если бы он был добавлен в оригинальный дизайн. Существует очень мало языков, которые поддерживают это понятие. Я могу только думать о С++, но я не выхожу много.

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

staticReadOnlyPoint.Offset(1, 1);

переводится на

Point temp = staticReadOnlyPoint;   // makes a copy
temp.Offset(1, 1);

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

Ответ 4

Если вы посмотрите на IL, вы увидите, что при использовании поля readonly копия выполняется перед вызовом Offset:

IL_0014: ldsfld valuetype [System.Drawing]System.Drawing.Point 
                    Program::staticReadOnlyPoint
IL_0019: stloc.0
IL_001a: ldloca.s CS$0$0000

Почему это происходит, это вне меня.

Это может быть часть спецификации или ошибка компилятора (но она выглядит слишком преднамеренной для последнего).

Ответ 5

Эффект связан с несколькими хорошо определенными функциями, сближающимися.

readonly означает, что данное поле не может быть изменено, но не то, что цель поля не может быть изменена. Это легче понять (и чаще всего полезно на практике) с полями readonly изменяемого ссылочного типа, где вы можете сделать x.SomeMutatingMethod(), но не x = someNewObject.

Итак, первый элемент; вы можете изменить цель в поле readonly.

Второй элемент заключается в том, что при доступе к типу значения переменной не получается копия значения. Наименее запутанный пример этого - giveMeAPoint().Offset(1, 1), потому что неизвестное место для нас позже не замечает, что тип значения, возвращаемый giveMeAPoint(), может быть или не быть мутированным.

Вот почему типы ценностей не являются злыми, но в некотором смысле хуже. Поистине злой код не имеет четко определенного поведения, и все это четко определено. Это все еще запутывает, хотя (достаточно сбивает меня с толку, чтобы я ошибался в своем первом ответе), и запутывание хуже, чем зло, когда вы пытаетесь закодировать. Легко понятое зло гораздо легче избежать.

Ответ 6

Наблюдаемое поведение является неудачным последствием того факта, что ни Framework, ни С# не предоставляют никаких средств, с помощью которых объявления функций-членов могут указывать, следует ли передавать this по ref, const-ref или value. Вместо этого типы значений всегда передают this с помощью (не const const) ref, а ссылочные типы всегда передают this по значению.

"Собственное" поведение для компилятора должно было бы запретить передачу неизменяемых или временных значений с помощью ссылки, не связанной с константой. Если такое ограничение может быть наложено, то обеспечение правильной семантики для изменяемых типов значений будет означать следующее простое правило: если вы делаете неявную копию структуры, вы делаете что-то неправильно. К сожалению, тот факт, что функции-члены могут принимать только this не-const-ref ref, означает, что разработчик языка должен сделать один из трех вариантов:

  1. Угадайте, что функция-член не будет изменять `this` и просто передает неизменяемые или временные переменные` ref`. Это было бы наиболее эффективно для функций, которые фактически не изменяют "это", но могут опасно подвергать модификации вещи, которые должны быть неизменными.
  2. Не разрешать использовать функции-члены для неизменяемых или временных объектов. Это позволило бы избежать неправильной семантики, но было бы очень раздражающим ограничением, особенно учитывая, что большинство функций-членов не изменяют "это".
  3. Разрешить использование функций-членов, кроме тех, которые, скорее всего, будут модифицировать `this` (например, средства определения свойств), но вместо того, чтобы передавать неизменяемые объекты напрямую по ref, скопируйте их во временные местоположения и передайте их.

    Выбор Microsoft защищает константы от неправильной модификации, но имеет печальные последствия того, что код будет работать бесполезно медленно при вызове функций, которые не изменяют this, хотя обычно работают неправильно для тех, которые делают.

    Учитывая способ обработки this, лучше всего избегать внесения каких-либо изменений в него в функциях членства в структуре, отличных от свойств. Наличие свойств или измененных полей прекрасны, поскольку компилятор будет правильно запрещать любую попытку использовать средства определения свойств на неизменяемых или временных объектах или изменять любые его поля.