C # и неизменяемость и поля только для чтения ... ложь? - PullRequest
44 голосов
/ 17 июля 2011

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

Как?Какие способы?

Итак, мой вопрос: когда у нас действительно может быть «настоящий» неизменяемый объект в C #, который я могу безопасно использовать в потоке?

Также анонимные типы создают неизменяемые объекты?И некоторые говорят, что LINQ использует неизменный объект внутри.Как именно?

Ответы [ 5 ]

105 голосов
/ 17 июля 2011

Вы задали там пять вопросов. Я отвечу на первый:

Наличие всех полей только для чтения в классе не обязательно делает экземпляр этого класса неизменным, поскольку существуют «способы» изменить значения полей только для чтения даже после создания. Как?

Можно ли изменить поле только для чтения после создания?

Да, , если вам достаточно доверия, чтобы нарушить правила «только для чтения» .

Как это работает?

Каждый бит пользовательской памяти в вашем процессе является изменяемым. Условные обозначения, такие как поля только для чтения, могут привести к тому, что некоторые биты будут выглядеть неизменяемыми, но если вы попытаетесь достаточно усердно, вы можете их изменить. Например, вы можете взять экземпляр неизменяемого объекта, получить его адрес и напрямую изменить необработанные биты. Для этого может потребоваться большая хитрость и знание внутренних деталей реализации диспетчера памяти, но каким-то образом диспетчеру памяти удается изменить эту память, так что вы тоже можете, если попытаетесь достаточно усердно. Вы также можете использовать «личное отражение», чтобы сломать различные части системы безопасности, если вам достаточно доверяют.

По определению, Полностью доверенному коду разрешено нарушать правила системы безопасности . Вот что означает «полностью доверенный». Если ваш полностью доверяющий код решает использовать такие инструменты, как частное отражение или небезопасный код, чтобы нарушить правила безопасности памяти, это разрешается делать полностью доверяемым кодом.

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

Итак, "только для чтения" ложь? Ну, предположим, я сказал вам, что , если все подчиняются правилам , каждый получает один кусочек торта. Является ли торт ложью? Эта претензия не претензия "вы получите кусок пирога". Это утверждение, что , если все подчиняются правилам , вы получите кусочек торта. Если кто-то обманывает и берет ваш кусок, для вас нет пирога.

Является ли поле чтения только для чтения только для чтения? Да, но , только если все подчиняются правилам . Таким образом, поля только для чтения не являются «ложью». Контракт заключается в том, что если все подчиняются правилам системы, то поле считается доступным только для чтения. Если кто-то нарушает правила, то, возможно, это не так. Это не делает утверждение «если все подчиняются правилам, поле только для чтения» - ложь!

Вопрос, который вы не задавали, но, возможно, должен был задать, заключается в том, является ли «только чтение» для полей структуры также «ложью». См. Работает ли использование открытых полей только для чтения для неизменяемых структур? для некоторых мыслей по этому вопросу. Поля только для чтения в структуре - это больше ложь, чем поля только для чтения в классе.

Что касается остальных ваших вопросов - я думаю, вы получите лучшие результаты, если зададите один вопрос на вопрос, а не пять вопросов на вопрос.

14 голосов
/ 17 июля 2011

РЕДАКТИРОВАТЬ: я сконцентрировался на коде, работающем "внутри" системы, в отличие от использования отражения и т. Д., Как упоминает Эрик.

Это зависит от типа поля. Если само поле имеет неизменяемый тип (например, String), тогда это нормально. Если это StringBuilder, тогда объект может появиться , чтобы измениться, даже если сами поля не меняют своих значений, потому что "встроенные" объекты могут измениться.

Анонимные типы точно такие же. Например:

var foo = new { Bar = new StringBuilder() };
Console.WriteLine(foo); // { Bar = }
foo.Bar.Append("Hello");
Console.WriteLine(foo); // { Bar = Hello }

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

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

Эрик Липперт написал много об неизменности - все это золото, как и следовало ожидать ... иди прочитай :) (Я не заметил, что Эрик писал ответ на этот вопрос когда я это написал. Очевидно, прочитал и его ответ!)

13 голосов
/ 21 июля 2011

Я думаю, что ответ Эрика далеко выходит за рамки первоначального вопроса, вплоть до того, что он даже не отвечает, поэтому я сделаю удар:

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

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

  • «ссылка» (переменная, указатель), которая указывает на адрес в памяти, где живет ваш объект
  • Память, содержащая данные, на которые указывает ссылка

Ссылка на объект сама по себе является типом значения. Когда вы используете readonly со ссылочным типом, вы делаете ссылку на ваш объект неизменной, не применяя неизменность к памяти, в которой находится объект.

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

8 голосов
/ 17 июля 2011

Readonly и Thread Safety - это два разных понятия, как Эрик Липперт объясняет это.

0 голосов
/ 22 июля 2011

Возможно, «Люди» слышали 3-ю руку и плохо сообщали об особой привилегии, которую конструктор получает в отношении «только для чтения» полей.Они не только для чтения.

Мало того, это не одно назначение.Конструктор класса может изменять поле столько раз, сколько пожелает.Все без нарушения каких-либо «правил».

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

Вы можете попробовать это сами:

using System;
class App
{
    class Foo 
    {
        readonly int x;
        public Foo() {  x = 1; Frob(); x = 2; Frob(); }
        void Frob() { Console.WriteLine(x); }
    }
    static void Main()
    {
        new Foo();
    }
}

Эта программа печатает 1, 2. 'x' изменяется после того, как Frob прочитал его.

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

Всевыше о классах.Структуры - это совсем другая история.

...